Test angular
This commit is contained in:
parent
39a59e7b9b
commit
7a3f62ac54
|
@ -34,7 +34,7 @@
|
|||
{
|
||||
"type": "shell",
|
||||
"label": "Start frontend",
|
||||
"command": "yarn start",
|
||||
"command": "yarn watch",
|
||||
"options": {
|
||||
"cwd": "./frontend"
|
||||
},
|
||||
|
|
|
@ -4,46 +4,46 @@ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../');
|
|||
|
||||
const Config = {
|
||||
main: {
|
||||
host: process.env.PICSUR_HOST || '0.0.0.0',
|
||||
port: process.env.PICSUR_PORT || 8080,
|
||||
host: process.env['PICSUR_HOST'] || '0.0.0.0',
|
||||
port: process.env['PICSUR_PORT'] || 8080,
|
||||
},
|
||||
database: {
|
||||
host: process.env.PICSUR_DB_HOST ?? 'localhost',
|
||||
port: process.env.PICSUR_DB_PORT
|
||||
? parseInt(process.env.PICSUR_DB_PORT)
|
||||
host: process.env['PICSUR_DB_HOST'] ?? 'localhost',
|
||||
port: process.env['PICSUR_DB_PORT']
|
||||
? parseInt(process.env['PICSUR_DB_PORT'])
|
||||
: 5432,
|
||||
username: process.env.PICSUR_DB_USERNAME ?? 'picsur',
|
||||
password: process.env.PICSUR_DB_PASSWORD ?? 'picsur',
|
||||
database: process.env.PICSUR_DB_DATABASE ?? 'picsur',
|
||||
username: process.env['PICSUR_DB_USERNAME'] ?? 'picsur',
|
||||
password: process.env['PICSUR_DB_PASSWORD'] ?? 'picsur',
|
||||
database: process.env['PICSUR_DB_DATABASE'] ?? 'picsur',
|
||||
},
|
||||
defaultAdmin: {
|
||||
username: process.env.PICSUR_ADMIN_USERNAME ?? 'admin',
|
||||
password: process.env.PICSUR_ADMIN_PASSWORD ?? 'admin',
|
||||
username: process.env['PICSUR_ADMIN_USERNAME'] ?? 'admin',
|
||||
password: process.env['PICSUR_ADMIN_PASSWORD'] ?? 'admin',
|
||||
},
|
||||
jwt: {
|
||||
secret: process.env.PICSUR_JWT_SECRET ?? 'CHANGE_ME',
|
||||
expiresIn: process.env.PICSUR_JWT_EXPIRES_IN ?? '1d',
|
||||
secret: process.env['PICSUR_JWT_SECRET'] ?? 'CHANGE_ME',
|
||||
expiresIn: process.env['PICSUR_JWT_EXPIRES_IN'] ?? '1d',
|
||||
},
|
||||
uploadLimits: {
|
||||
fieldNameSize: 128,
|
||||
fieldSize: 1024,
|
||||
fields: 16,
|
||||
files: 16,
|
||||
fileSize: process.env.PICSUR_MAX_FILE_SIZE
|
||||
? parseInt(process.env.PICSUR_MAX_FILE_SIZE)
|
||||
fileSize: process.env['PICSUR_MAX_FILE_SIZE']
|
||||
? parseInt(process.env['PICSUR_MAX_FILE_SIZE'])
|
||||
: 128000000,
|
||||
},
|
||||
static: {
|
||||
packageRoot: packageRoot,
|
||||
frontendRoot:
|
||||
process.env.PICSUR_STATIC_FRONTEND_ROOT ??
|
||||
process.env['PICSUR_STATIC_FRONTEND_ROOT'] ??
|
||||
join(packageRoot, '../frontend/dist'),
|
||||
backendRoutes: ['i', 'api'],
|
||||
},
|
||||
demo: {
|
||||
enabled: process.env.PICSUR_DEMO?.toLowerCase() === 'true',
|
||||
interval: process.env.PICSUR_DEMO_INTERVAL
|
||||
? parseInt(process.env.PICSUR_DEMO_INTERVAL)
|
||||
enabled: process.env['PICSUR_DEMO']?.toLowerCase() === 'true',
|
||||
interval: process.env['PICSUR_DEMO_INTERVAL']
|
||||
? parseInt(process.env['PICSUR_DEMO_INTERVAL'])
|
||||
: 1000 * 60 * 5,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
|
||||
# For additional information regarding the format and rule options, please see:
|
||||
# https://github.com/browserslist/browserslist#queries
|
||||
|
||||
# For the full list of supported browsers by the Angular framework, please see:
|
||||
# https://angular.io/guide/browser-support
|
||||
|
||||
# You can see what browsers were selected by your queries by running:
|
||||
# npx browserslist
|
||||
|
||||
last 1 Chrome version
|
||||
last 1 Firefox version
|
||||
last 2 Edge major versions
|
||||
last 2 Safari major versions
|
||||
last 2 iOS major versions
|
||||
Firefox ESR
|
|
@ -0,0 +1,16 @@
|
|||
# Editor configuration, see https://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.ts]
|
||||
quote_type = single
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
|
@ -1 +0,0 @@
|
|||
BUILD_PATH='./dist'
|
|
@ -1,24 +1,44 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
# Compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
/bazel-out
|
||||
|
||||
# misc
|
||||
# Node
|
||||
/node_modules
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# IDEs and editors
|
||||
.idea/
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history/*
|
||||
|
||||
# Miscellaneous
|
||||
/.angular/cache
|
||||
.sass-cache/
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# System files
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
Thumbs.db
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.angular
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
# PicsurFrontend
|
||||
|
||||
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 13.2.5.
|
||||
|
||||
## Development server
|
||||
|
||||
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
|
||||
|
||||
## Code scaffolding
|
||||
|
||||
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
|
||||
|
||||
## Build
|
||||
|
||||
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
|
||||
|
||||
## Further help
|
||||
|
||||
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
|
|
@ -0,0 +1,88 @@
|
|||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"picsur-frontend": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"style": "scss"
|
||||
},
|
||||
"@schematics/angular:application": {
|
||||
"strict": true
|
||||
}
|
||||
},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"outputPath": "dist",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": ["src/assets"],
|
||||
"styles": ["src/styles.scss"],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kb",
|
||||
"maximumError": "1mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "2kb",
|
||||
"maximumError": "4kb"
|
||||
}
|
||||
],
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"buildOptimizer": false,
|
||||
"optimization": false,
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"namedChunks": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "picsur-frontend:build:production"
|
||||
},
|
||||
"development": {
|
||||
"browserTarget": "picsur-frontend:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "picsur-frontend:build"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "picsur-frontend"
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
const scopedcss = require('craco-plugin-scoped-css');
|
||||
const webpack = require('webpack');
|
||||
|
||||
module.exports = {
|
||||
webpack: {
|
||||
plugins: {
|
||||
add: [
|
||||
new webpack.IgnorePlugin({resourceRegExp: /react-native-sqlite-storage/}),
|
||||
]
|
||||
}
|
||||
},
|
||||
devServer: {
|
||||
devMiddleware: {
|
||||
writeToDisk: true,
|
||||
},
|
||||
|
||||
port: 3300,
|
||||
liveReload: true,
|
||||
hot: false,
|
||||
open: false,
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
plugin: scopedcss,
|
||||
},
|
||||
],
|
||||
};
|
|
@ -6,57 +6,45 @@
|
|||
"license": "GPL-3.0",
|
||||
"repository": "https://github.com/rubikscraft/Picsur",
|
||||
"author": "Rubikscraft <contact@rubikscraft.nl>",
|
||||
"type": "commonjs",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve --host 0.0.0.0",
|
||||
"build": "ng build",
|
||||
"watch": "ng build --watch --configuration development"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.8.1",
|
||||
"@emotion/styled": "^11.8.1",
|
||||
"@mui/icons-material": "^5.4.2",
|
||||
"@mui/material": "^5.4.3",
|
||||
"@angular/animations": "~13.2.4",
|
||||
"@angular/cdk": "^13.2.4",
|
||||
"@angular/common": "~13.2.4",
|
||||
"@angular/compiler": "~13.2.4",
|
||||
"@angular/core": "~13.2.4",
|
||||
"@angular/forms": "~13.2.4",
|
||||
"@angular/material": "^13.2.4",
|
||||
"@angular/platform-browser": "~13.2.4",
|
||||
"@angular/platform-browser-dynamic": "~13.2.4",
|
||||
"@angular/router": "~13.2.4",
|
||||
"bootstrap": "^5.1.3",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.13.2",
|
||||
"picsur-shared": "*",
|
||||
"notistack": "^2.0.3",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-dropzone": "^12.0.4",
|
||||
"react-router-dom": "^6.2.1"
|
||||
"ngx-dropzone": "^3.1.0",
|
||||
"rxjs": "~7.5.4",
|
||||
"tslib": "^2.3.1",
|
||||
"zone.js": "~0.11.4",
|
||||
"picsur-shared": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.17.5",
|
||||
"@babel/plugin-syntax-flow": "^7.16.7",
|
||||
"@babel/plugin-transform-react-jsx": "^7.17.3",
|
||||
"@craco/craco": "7.0.0-alpha.3",
|
||||
"@angular-devkit/build-angular": "~13.2.5",
|
||||
"@angular/cli": "~13.2.5",
|
||||
"@angular/compiler-cli": "~13.2.4",
|
||||
"@types/jasmine": "~3.10.3",
|
||||
"@types/node": "^17.0.21",
|
||||
"@types/react": "^17.0.39",
|
||||
"@types/react-dom": "^17.0.11",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"craco-plugin-scoped-css": "^1.1.1",
|
||||
"postcss": "^8.4.7",
|
||||
"react-scripts": "5.0.0",
|
||||
"sass": "^1.49.9",
|
||||
"typescript": "^4.5.5"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "craco start",
|
||||
"build": "craco build"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
"@types/validator": "^13.7.1",
|
||||
"jasmine-core": "~4.0.1",
|
||||
"karma": "~6.3.16",
|
||||
"karma-chrome-launcher": "~3.1.0",
|
||||
"karma-coverage": "~2.2.0",
|
||||
"karma-jasmine": "~4.0.1",
|
||||
"karma-jasmine-html-reporter": "~1.7.0",
|
||||
"typescript": "~4.5.5"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
../../../branding/logo/
|
|
@ -1,3 +0,0 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow: /
|
|
@ -1,133 +0,0 @@
|
|||
import { AsyncFailable, Fail, HasFailed } from 'picsur-shared/dist/types';
|
||||
import {
|
||||
ApiResponse,
|
||||
ApiSuccessResponse,
|
||||
} from 'picsur-shared/dist/dto/api.dto';
|
||||
import { validate } from 'class-validator';
|
||||
import { ClassConstructor, plainToClass } from 'class-transformer';
|
||||
|
||||
export abstract class MultiPartRequest {
|
||||
public abstract createFormData(): FormData;
|
||||
}
|
||||
|
||||
class PicsurApiImplementation {
|
||||
private async fetch(
|
||||
url: RequestInfo,
|
||||
options: RequestInit,
|
||||
): AsyncFailable<Response> {
|
||||
try {
|
||||
return await window.fetch(url, options);
|
||||
} catch (e: any) {
|
||||
console.warn(e);
|
||||
return Fail('Something went wrong');
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchJsonAs<T>(
|
||||
url: RequestInfo,
|
||||
options: RequestInit,
|
||||
): AsyncFailable<T> {
|
||||
const response = await this.fetch(url, options);
|
||||
if (HasFailed(response)) {
|
||||
return response;
|
||||
}
|
||||
try {
|
||||
return await response.json();
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
return Fail('Something went wrong');
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchBuffer(
|
||||
url: RequestInfo,
|
||||
options: RequestInit,
|
||||
): AsyncFailable<ArrayBuffer> {
|
||||
const response = await this.fetch(url, options);
|
||||
if (HasFailed(response)) return response;
|
||||
|
||||
try {
|
||||
return await response.arrayBuffer();
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
return Fail('Something went wrong');
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchSafeJson<T extends Object>(
|
||||
type: ClassConstructor<T>,
|
||||
url: RequestInfo,
|
||||
options: RequestInit,
|
||||
): AsyncFailable<T> {
|
||||
let result = await this.fetchJsonAs<ApiResponse<T>>(url, options);
|
||||
if (HasFailed(result)) return result;
|
||||
|
||||
if (result.success === false) return Fail(result.data.message);
|
||||
|
||||
const resultClass = plainToClass<
|
||||
ApiSuccessResponse<T>,
|
||||
ApiSuccessResponse<T>
|
||||
>(ApiSuccessResponse, result);
|
||||
const resultErrors = await validate(resultClass);
|
||||
if (resultErrors.length > 0) {
|
||||
console.warn('result', resultErrors);
|
||||
return Fail('Something went wrong');
|
||||
}
|
||||
|
||||
const dataClass = plainToClass(type, result.data);
|
||||
const dataErrors = await validate(dataClass);
|
||||
if (dataErrors.length > 0) {
|
||||
console.warn('data', dataErrors);
|
||||
return Fail('Something went wrong');
|
||||
}
|
||||
|
||||
return result.data;
|
||||
}
|
||||
|
||||
public async get<T extends Object>(
|
||||
type: ClassConstructor<T>,
|
||||
url: string,
|
||||
): AsyncFailable<T> {
|
||||
return this.fetchSafeJson(type, url, { method: 'GET' });
|
||||
}
|
||||
|
||||
public async post<T extends Object, W extends Object>(
|
||||
sendType: ClassConstructor<T>,
|
||||
receiveType: ClassConstructor<W>,
|
||||
url: string,
|
||||
data: object,
|
||||
): AsyncFailable<W> {
|
||||
const sendClass = plainToClass(sendType, data);
|
||||
const errors = await validate(sendClass);
|
||||
if (errors.length > 0) {
|
||||
console.warn(errors);
|
||||
return Fail('Something went wrong');
|
||||
}
|
||||
|
||||
return this.fetchSafeJson(receiveType, url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(sendClass),
|
||||
});
|
||||
}
|
||||
|
||||
public async postForm<T extends Object>(
|
||||
receiveType: ClassConstructor<T>,
|
||||
url: string,
|
||||
data: MultiPartRequest,
|
||||
): AsyncFailable<T> {
|
||||
return this.fetchSafeJson(receiveType, url, {
|
||||
method: 'POST',
|
||||
body: data.createFormData(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default abstract class PicsurApi {
|
||||
private static readonly instance = new PicsurApiImplementation();
|
||||
|
||||
protected get api() {
|
||||
return PicsurApi.instance;
|
||||
}
|
||||
|
||||
protected constructor() {}
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
import { ImageUploadRequest } from '../frontenddto/imageroute.dto';
|
||||
import PicsurApi from './api';
|
||||
import { EImage } from 'picsur-shared/dist/entities/image.entity';
|
||||
import { AsyncFailable, HasFailed } from 'picsur-shared/dist/types';
|
||||
|
||||
export interface ImageLinks {
|
||||
source: string;
|
||||
markdown: string;
|
||||
html: string;
|
||||
rst: string;
|
||||
bbcode: string;
|
||||
}
|
||||
export default class ImagesApi extends PicsurApi {
|
||||
public static readonly I = new ImagesApi();
|
||||
|
||||
protected constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public async UploadImage(image: File): AsyncFailable<string> {
|
||||
const result = await this.api.postForm(
|
||||
EImage,
|
||||
'/i',
|
||||
new ImageUploadRequest(image),
|
||||
);
|
||||
|
||||
if (HasFailed(result)) return result;
|
||||
|
||||
return result.hash;
|
||||
}
|
||||
|
||||
public async GetImageMeta(image: string): AsyncFailable<EImage> {
|
||||
return await this.api.get(EImage, `/i/meta/${image}`);
|
||||
}
|
||||
|
||||
public static GetImageURL(image: string): string {
|
||||
const baseURL = window.location.protocol + '//' + window.location.host;
|
||||
|
||||
return `${baseURL}/i/${image}`;
|
||||
}
|
||||
|
||||
public static CreateImageLinks(imageURL: string) {
|
||||
return {
|
||||
source: imageURL,
|
||||
markdown: `![image](${imageURL})`,
|
||||
html: `<img src="${imageURL}" alt="image">`,
|
||||
rst: `.. image:: ${imageURL}`,
|
||||
bbcode: `[img]${imageURL}[/img]`,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
html, body {
|
||||
background: rgb(24, 24, 24);
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.contentwindow {
|
||||
border-radius: 20px;
|
||||
height: 100%;
|
||||
|
||||
border-style: solid;
|
||||
border-width: 5px;
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
import { ThemeProvider, createTheme } from '@mui/material/styles';
|
||||
import CssBaseline from '@mui/material/CssBaseline';
|
||||
import './app.scss';
|
||||
import AppRouter from './routes/router';
|
||||
import { SnackbarProvider } from 'notistack';
|
||||
import Header from './components/header/header';
|
||||
import { Container } from '@mui/material';
|
||||
import Footer from './components/footer/footer';
|
||||
|
||||
export default function App() {
|
||||
const darkTheme = createTheme({
|
||||
palette: {
|
||||
mode: 'dark',
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={darkTheme}>
|
||||
<CssBaseline />
|
||||
<SnackbarProvider maxSnack={3}>
|
||||
<Header />
|
||||
<Container className="main-container">
|
||||
<AppRouter />
|
||||
</Container>
|
||||
<Footer />
|
||||
</SnackbarProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
<app-header></app-header>
|
||||
<div class="grow-full container">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
<app-footer></app-footer>
|
|
@ -0,0 +1,4 @@
|
|||
.grow-full {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
})
|
||||
export class AppComponent {}
|
|
@ -0,0 +1,23 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
|
||||
import { AppRouterModule } from './router/router.module';
|
||||
import { AppComponent } from './app.component';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { HeaderModule } from './components/header/header.module';
|
||||
import { FooterModule } from './components/footer/footer.module';
|
||||
import { ProcessingComponent } from './routes/processing/processing.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
AppRouterModule,
|
||||
BrowserAnimationsModule,
|
||||
HeaderModule,
|
||||
FooterModule,
|
||||
],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppModule {}
|
|
@ -0,0 +1,2 @@
|
|||
<footer class="container">
|
||||
</footer>
|
|
@ -0,0 +1,7 @@
|
|||
footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
margin-top: 32px;
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-footer',
|
||||
templateUrl: './footer.component.html',
|
||||
styleUrls: ['./footer.component.scss']
|
||||
})
|
||||
export class FooterComponent {}
|
|
@ -0,0 +1,11 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { FooterComponent } from './footer.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [FooterComponent],
|
||||
imports: [CommonModule],
|
||||
exports: [FooterComponent],
|
||||
})
|
||||
export class FooterModule {}
|
|
@ -0,0 +1,8 @@
|
|||
<mat-toolbar color="primary" class="header">
|
||||
<div class="svg-logo">
|
||||
<img src="/assets/image/logo/picsur.svg" alt="Picsur" />
|
||||
</div>
|
||||
<span>Picsur</span>
|
||||
<span class="spacer"></span>
|
||||
<button mat-stroked-button>Login</button>
|
||||
</mat-toolbar>
|
|
@ -0,0 +1,27 @@
|
|||
.svg-logo {
|
||||
margin-right: 1rem;
|
||||
height: 100%;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
border-radius: 20%;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
mat-toolbar {
|
||||
height: 64px;
|
||||
box-shadow: 0px 2px 5px -3px rgba(0, 0, 0, 0.2),
|
||||
0px 5px 8px 0px rgba(0, 0, 0, 0.14), 0px 1px 14px 0px rgba(0, 0, 0, 0.12);
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-header',
|
||||
templateUrl: './header.component.html',
|
||||
styleUrls: ['./header.component.scss'],
|
||||
})
|
||||
export class HeaderComponent {}
|
|
@ -0,0 +1,12 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { HeaderComponent } from './header.component';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, MatToolbarModule, MatButtonModule],
|
||||
declarations: [HeaderComponent],
|
||||
exports: [HeaderComponent],
|
||||
})
|
||||
export class HeaderModule {}
|
|
@ -0,0 +1,3 @@
|
|||
export interface ProcessingViewMetadata {
|
||||
imageFile: File;
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { UploadComponent } from '../routes/upload/upload.component';
|
||||
import { NgxDropzoneModule } from 'ngx-dropzone';
|
||||
import {
|
||||
MatSnackBarModule,
|
||||
MAT_SNACK_BAR_DEFAULT_OPTIONS,
|
||||
} from '@angular/material/snack-bar';
|
||||
import { ProcessingComponent } from '../routes/processing/processing.component';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: UploadComponent },
|
||||
{
|
||||
path: 'processing',
|
||||
component: ProcessingComponent,
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
NgxDropzoneModule,
|
||||
MatSnackBarModule,
|
||||
MatProgressSpinnerModule,
|
||||
RouterModule.forRoot(routes),
|
||||
],
|
||||
declarations: [UploadComponent, ProcessingComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: MAT_SNACK_BAR_DEFAULT_OPTIONS,
|
||||
useValue: {
|
||||
duration: 2500,
|
||||
horizontalPosition: 'left',
|
||||
panelClass: ['mat-toolbar', 'mat-primary'],
|
||||
},
|
||||
},
|
||||
],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class AppRouterModule {}
|
|
@ -0,0 +1,6 @@
|
|||
<div class="content-border">
|
||||
<div class="centered">
|
||||
<h1>Processing</h1>
|
||||
<mat-spinner color="accent"></mat-spinner>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,23 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { ProcessingViewMetadata } from 'src/app/models/processing-view-metadata';
|
||||
import { validate } from 'class-validator';
|
||||
import { plainToClass } from 'class-transformer';
|
||||
|
||||
@Component({
|
||||
templateUrl: './processing.component.html',
|
||||
styleUrls: ['./processing.component.scss'],
|
||||
})
|
||||
export class ProcessingComponent implements OnInit {
|
||||
constructor(private router: Router) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
const state = history.state as ProcessingViewMetadata;
|
||||
if (!state) {
|
||||
this.router.navigate(['/'], { replaceUrl: true });
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(state);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
<div class="content-border">
|
||||
<ngx-dropzone (change)="onSelect($event)">
|
||||
<div class="centered">
|
||||
<h1>Upload Image</h1>
|
||||
<p>Drag and drop an image here, or click to select an image</p>
|
||||
</div>
|
||||
</ngx-dropzone>
|
||||
</div>
|
|
@ -0,0 +1,18 @@
|
|||
ngx-dropzone {
|
||||
display: inherit;
|
||||
align-items: inherit;
|
||||
height: initial;
|
||||
background: inherit;
|
||||
cursor: inherit;
|
||||
color: inherit;
|
||||
border: initial;
|
||||
border-radius: initial;
|
||||
font-size: inherit;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
&.ngx-dz-hovered {
|
||||
border-style: initial;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Router } from '@angular/router';
|
||||
import { NgxDropzoneChangeEvent } from 'ngx-dropzone';
|
||||
import { ProcessingViewMetadata } from '../../models/processing-view-metadata';
|
||||
|
||||
@Component({
|
||||
templateUrl: './upload.component.html',
|
||||
styleUrls: ['./upload.component.scss'],
|
||||
})
|
||||
export class UploadComponent {
|
||||
constructor(private snackBar: MatSnackBar, private router: Router) {}
|
||||
onSelect(event: NgxDropzoneChangeEvent) {
|
||||
if (event.addedFiles.length > 1) {
|
||||
this.snackBar.open(
|
||||
'You uploaded multiple images, only one has been uploaded'
|
||||
);
|
||||
}
|
||||
|
||||
const metadata: ProcessingViewMetadata = {
|
||||
imageFile: event.addedFiles[0],
|
||||
};
|
||||
this.router.navigate(['/processing'], { state: metadata });
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
pog
|
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
After Width: | Height: | Size: 6.9 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
|
@ -0,0 +1,61 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="1024"
|
||||
height="1024"
|
||||
viewBox="0 0 270.93333 270.93333"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
|
||||
sodipodi:docname="picsur.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:document-units="px"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:zoom="0.38262135"
|
||||
inkscape:cx="799.74627"
|
||||
inkscape:cy="656.00103"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1011"
|
||||
inkscape:window-x="1280"
|
||||
inkscape:window-y="32"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs2" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<rect
|
||||
style="fill:#121212;fill-opacity:1;stroke:none;stroke-width:30.588;stroke-linecap:round;stroke-linejoin:round"
|
||||
id="rect1068"
|
||||
width="270.93332"
|
||||
height="270.93332"
|
||||
x="0"
|
||||
y="-3.5527137e-15" />
|
||||
<circle
|
||||
style="fill:#43a047;fill-opacity:1;stroke:none;stroke-width:19.7396;stroke-linecap:round;stroke-linejoin:round"
|
||||
id="path1293-6"
|
||||
cx="135.46666"
|
||||
cy="212.93398"
|
||||
r="27.119791" />
|
||||
<path
|
||||
style="fill:none;stroke:#43a047;stroke-width:54.1131;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 135.40342,57.936334 V 136.24119"
|
||||
id="path5020"
|
||||
sodipodi:nodetypes="cc" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
|
@ -1,18 +0,0 @@
|
|||
import React, { ForwardedRef } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import './centered.scoped.scss';
|
||||
|
||||
type PropsType = React.DetailedHTMLProps<
|
||||
React.HTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
>;
|
||||
|
||||
const Centered = forwardRef(
|
||||
(props: PropsType, ref: ForwardedRef<HTMLDivElement>) => {
|
||||
let filteredProps = { ...props };
|
||||
|
||||
return <div className="centered" ref={ref} {...filteredProps} />;
|
||||
},
|
||||
);
|
||||
|
||||
export default Centered;
|
|
@ -1,3 +0,0 @@
|
|||
.footer-container {
|
||||
margin-top: 32px;
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
import { Box, Container } from '@mui/material';
|
||||
import './footer.scoped.scss';
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<Box sx={{ flexGrow: 0 }} className="footer">
|
||||
<Container className="footer-container"></Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
.svg-logo {
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
border-radius: 20%;
|
||||
}
|
||||
|
||||
.text-link {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 32px;
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
import { AppBar, Toolbar, Typography, Button } from '@mui/material';
|
||||
import { Box } from '@mui/system';
|
||||
import { Link } from 'react-router-dom';
|
||||
import './header.scoped.scss';
|
||||
|
||||
export default function Header() {
|
||||
return (
|
||||
<Box sx={{ flexGrow: 0 }} className="header">
|
||||
<AppBar position="static">
|
||||
<Toolbar>
|
||||
<Link to="/">
|
||||
<Box
|
||||
component="div"
|
||||
sx={{ mr: 2, display: { xs: 'none', md: 'flex' } }}
|
||||
>
|
||||
<img
|
||||
src="/image/logo/picsur.svg"
|
||||
alt="Picsur"
|
||||
className="svg-logo"
|
||||
/>
|
||||
</Box>
|
||||
</Link>
|
||||
|
||||
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
|
||||
<Link to="/" className="text-link">
|
||||
Picsur
|
||||
</Link>
|
||||
</Typography>
|
||||
<Button color="inherit">Login</Button>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
</Box>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
const Environment = {
|
||||
production: true,
|
||||
};
|
||||
|
||||
export default Environment;
|
|
@ -0,0 +1,5 @@
|
|||
const Environment = {
|
||||
production: false,
|
||||
};
|
||||
|
||||
export default Environment;
|
|
@ -1,11 +0,0 @@
|
|||
import { MultiPartRequest } from '../api/api';
|
||||
|
||||
export class ImageUploadRequest implements MultiPartRequest {
|
||||
constructor(private image: File) {}
|
||||
|
||||
public createFormData(): FormData {
|
||||
const data = new FormData();
|
||||
data.append('image', this.image);
|
||||
return data;
|
||||
}
|
||||
}
|
|
@ -2,16 +2,16 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="/image/logo/picsur.ico" />
|
||||
<link rel="icon" href="/assets/image/logo/picsur.ico" />
|
||||
<base href="/" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<link rel="stylesheet" href="/css/normalize.css" />
|
||||
<link rel="stylesheet" href="/css/opensans.css" />
|
||||
<link rel="stylesheet" href="/assets/css/normalize.css" />
|
||||
|
||||
<title>Picsur</title>
|
||||
</head>
|
||||
<body>
|
||||
<body class="mat-typography mat-app-background">
|
||||
<noscript>Sorry, but you need javascript</noscript>
|
||||
<div id="root"></div>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
|
@ -1,13 +0,0 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import App from './app';
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root'),
|
||||
);
|
|
@ -1,13 +0,0 @@
|
|||
export default function Debounce(fn: Function, ms: number) {
|
||||
let timer: number | undefined;
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
|
||||
timer = setTimeout((_) => {
|
||||
timer = undefined;
|
||||
|
||||
fn();
|
||||
}, ms);
|
||||
};
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
import { useSnackbar } from 'notistack';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export default function useError() {
|
||||
const notify = useSnackbar();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (error: string) => {
|
||||
notify.enqueueSnackbar(error, {
|
||||
variant: 'error',
|
||||
});
|
||||
navigate('/');
|
||||
};
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import { enableProdMode } from '@angular/core';
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
|
||||
import { AppModule } from './app/app.module';
|
||||
import Environment from './environments/environment';
|
||||
|
||||
if (Environment.production) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic()
|
||||
.bootstrapModule(AppModule)
|
||||
.catch((err) => console.error(err));
|
|
@ -0,0 +1,53 @@
|
|||
/**
|
||||
* This file includes polyfills needed by Angular and is loaded before the app.
|
||||
* You can add your own extra polyfills to this file.
|
||||
*
|
||||
* This file is divided into 2 sections:
|
||||
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
|
||||
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
|
||||
* file.
|
||||
*
|
||||
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
|
||||
* automatically update themselves. This includes recent versions of Safari, Chrome (including
|
||||
* Opera), Edge on the desktop, and iOS and Chrome on mobile.
|
||||
*
|
||||
* Learn more in https://angular.io/guide/browser-support
|
||||
*/
|
||||
|
||||
/***************************************************************************************************
|
||||
* BROWSER POLYFILLS
|
||||
*/
|
||||
|
||||
/**
|
||||
* By default, zone.js will patch all possible macroTask and DomEvents
|
||||
* user can disable parts of macroTask/DomEvents patch by setting following flags
|
||||
* because those flags need to be set before `zone.js` being loaded, and webpack
|
||||
* will put import in the top of bundle, so user need to create a separate file
|
||||
* in this directory (for example: zone-flags.ts), and put the following flags
|
||||
* into that file, and then add the following code before importing zone.js.
|
||||
* import './zone-flags';
|
||||
*
|
||||
* The flags allowed in zone-flags.ts are listed here.
|
||||
*
|
||||
* The following flags will work for all browsers.
|
||||
*
|
||||
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
|
||||
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
|
||||
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
|
||||
*
|
||||
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
|
||||
* with the following flag, it will bypass `zone.js` patch for IE/Edge
|
||||
*
|
||||
* (window as any).__Zone_enable_cross_context_check = true;
|
||||
*
|
||||
*/
|
||||
|
||||
/***************************************************************************************************
|
||||
* Zone JS is required by default for Angular itself.
|
||||
*/
|
||||
import 'zone.js'; // Included with Angular CLI.
|
||||
|
||||
|
||||
/***************************************************************************************************
|
||||
* APPLICATION IMPORTS
|
||||
*/
|
|
@ -1 +0,0 @@
|
|||
/// <reference types="react-scripts" />
|
|
@ -1,3 +0,0 @@
|
|||
export default function LoginView() {
|
||||
return <p>login</p>;
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
import Centered from '../../components/centered/centered';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useEffect } from 'react';
|
||||
import { HasFailed } from 'picsur-shared/dist/types';
|
||||
import ImagesApi from '../../api/images';
|
||||
import useError from '../../lib/useerror';
|
||||
|
||||
export interface ProcessingViewMetadata {
|
||||
imageFile: File;
|
||||
}
|
||||
|
||||
function ProcessingView(props: any) {
|
||||
const state: ProcessingViewMetadata = useLocation().state as any;
|
||||
const navigate = useNavigate();
|
||||
const exitError = useError();
|
||||
|
||||
async function onRendered() {
|
||||
if (!state) return exitError('Error');
|
||||
|
||||
const hash = await ImagesApi.I.UploadImage(state.imageFile);
|
||||
if (HasFailed(hash)) return exitError(hash.getReason());
|
||||
|
||||
navigate('/view/' + hash, { replace: true });
|
||||
}
|
||||
|
||||
// Run when rendered
|
||||
useEffect(() => {
|
||||
onRendered().catch(console.error);
|
||||
});
|
||||
|
||||
return (
|
||||
<section className="contentwindow">
|
||||
<Centered>
|
||||
<h1>Processing</h1>
|
||||
<CircularProgress />
|
||||
</Centered>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProcessingView;
|
|
@ -1,16 +0,0 @@
|
|||
import { Routes, Route } from 'react-router-dom';
|
||||
import LoginView from './login/login';
|
||||
import ProcessingView from './processing/processing';
|
||||
import UploadView from './upload/upload';
|
||||
import ViewView from './view/view';
|
||||
|
||||
export default function AppRouter() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/" element={<UploadView />} />
|
||||
<Route path="login" element={<LoginView />} />
|
||||
<Route path="processing" element={<ProcessingView />} />
|
||||
<Route path="view/:hash" element={<ViewView />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
import { useSnackbar } from 'notistack';
|
||||
import Dropzone from 'react-dropzone';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import Centered from '../../components/centered/centered';
|
||||
import { ProcessingViewMetadata } from '../processing/processing';
|
||||
|
||||
export default function UploadView() {
|
||||
const navigate = useNavigate();
|
||||
const snackbar = useSnackbar();
|
||||
|
||||
function onAcceptedFiles(files: File[]) {
|
||||
if (files.length > 1) {
|
||||
snackbar.enqueueSnackbar(
|
||||
'You uploaded multiple images, only one has been uploaded',
|
||||
{
|
||||
variant: 'info',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const metadata: ProcessingViewMetadata = {
|
||||
imageFile: files[0],
|
||||
};
|
||||
navigate('/processing', {
|
||||
state: metadata,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dropzone onDrop={onAcceptedFiles}>
|
||||
{({ getRootProps, getInputProps }) => (
|
||||
<section className="contentwindow">
|
||||
<Centered {...getRootProps()}>
|
||||
<input {...getInputProps()} />
|
||||
|
||||
<h1>Upload Image</h1>
|
||||
<p>Drag and drop an image here, or click to select an image</p>
|
||||
</Centered>
|
||||
</section>
|
||||
)}
|
||||
</Dropzone>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
.uploadedimage {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.contentwindow {
|
||||
padding: 2rem;
|
||||
}
|
|
@ -1,103 +0,0 @@
|
|||
import {
|
||||
Button,
|
||||
Container,
|
||||
Grid,
|
||||
IconButton,
|
||||
TextField,
|
||||
} from '@mui/material';
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
import { useSnackbar } from 'notistack';
|
||||
import { isHash } from 'class-validator';
|
||||
import ImagesApi from '../../api/images';
|
||||
import Centered from '../../components/centered/centered';
|
||||
|
||||
import './view.scoped.scss';
|
||||
import { HasFailed } from 'picsur-shared/dist/types';
|
||||
import useError from '../../lib/useerror';
|
||||
|
||||
// Stupid names go brrr
|
||||
export default function ViewView() {
|
||||
const exitError = useError();
|
||||
const navigate = useNavigate();
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
const hash = useParams().hash ?? '';
|
||||
|
||||
const effectHandler = async () => {
|
||||
if (!isHash(hash, 'sha256')) return exitError('Invalid image link');
|
||||
|
||||
const imageMeta = await ImagesApi.I.GetImageMeta(hash);
|
||||
if (HasFailed(imageMeta)) return exitError(imageMeta.getReason());
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
effectHandler();
|
||||
});
|
||||
|
||||
const imageURL = ImagesApi.GetImageURL(hash);
|
||||
const imageLinks = ImagesApi.CreateImageLinks(imageURL);
|
||||
|
||||
function createCopyField(label: string, value: string) {
|
||||
const copy = () => {
|
||||
navigator.clipboard.writeText(value);
|
||||
enqueueSnackbar(`Copied ${label}!`, { variant: 'success' });
|
||||
};
|
||||
return (
|
||||
<TextField
|
||||
label={label}
|
||||
defaultValue={value}
|
||||
fullWidth={true}
|
||||
InputProps={{
|
||||
readOnly: true,
|
||||
endAdornment: (
|
||||
<IconButton onClick={copy}>
|
||||
<ContentCopyIcon />
|
||||
</IconButton>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="contentwindow">
|
||||
<Centered>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12}>
|
||||
<h1>Uploaded Image</h1>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Container maxWidth="sm">
|
||||
<img className="uploadedimage" alt="Uploaded" src={imageURL} />
|
||||
</Container>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
{createCopyField('Image URL', imageURL)}
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
{createCopyField('Markdown', imageLinks.markdown)}
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
{createCopyField('HTML', imageLinks.html)}
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
{createCopyField('BBCode', imageLinks.bbcode)}
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
{createCopyField('Rst', imageLinks.rst)}
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="large"
|
||||
onClick={() => navigate('/')}
|
||||
>
|
||||
Upload Another
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Centered>
|
||||
</section>
|
||||
);
|
||||
}
|
|
@ -1,3 +1,11 @@
|
|||
.content-border {
|
||||
border-radius: 20px;
|
||||
height: 100%;
|
||||
|
||||
border-style: solid;
|
||||
border-width: 5px;
|
||||
}
|
||||
|
||||
.centered {
|
||||
display: flex;
|
||||
flex-direction: column;
|
|
@ -0,0 +1,273 @@
|
|||
/**
|
||||
* Generated theme by Material Theme Generator
|
||||
* https://materialtheme.arcsine.dev
|
||||
* Fork at: https://materialtheme.arcsine.dev/?c=YHBhbGV0dGU$YHByaW1hcnk$YF48IzI3MjcyNyIsIj9lcjwjYmViZWJlIiwiO2VyPCMxNzE3MTd$LCIlPmBePCM0M2EwNDciLCI~ZXI8I2M3ZTNjOCIsIjtlcjwjMmM4NDJmfiwid2Fybj5gXjwjZjQ1MTFlIiwiP2VyPCNmY2NiYmMiLCI7ZXI8I2VmMzcxMn4sIj9UZXh0PCMwMDAwMDAiLCI~PTwjZmFmYWZhIiwiO1RleHQ8I2ZmZmZmZiIsIjs9PCMxMjEyMTJ$LCJmb250cz5bYEA8KC00fixgQDwoLTN$LGBAPCgtMn4sYEA8KC0xfixgQDxoZWFkbGluZX4sYEA8dGl0bGV$LGBAPHN1YiktMn4sYEA8c3ViKS0xfixgQDxib2R5LTJ$LGBAPGJvZHktMX4sYEA8YnV0dG9ufixgQDxjYXB0aW9ufixgQDxpbnB1dCIsInNpemU$bnVsbH1dLCJpY29uczxSb3VuZGVkIiwiP25lc3M$ZmFsc2UsInZlcnNpb24$MTF9
|
||||
*/
|
||||
|
||||
@import "~@angular/material/theming";
|
||||
|
||||
// Include the common styles for Angular Material. We include this here so that you only
|
||||
// have to load a single css file for Angular Material in your app.
|
||||
|
||||
// Fonts
|
||||
@import "https://fonts.googleapis.com/icon?family=Material+Icons+Round";
|
||||
@import url("https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap");
|
||||
|
||||
$fontConfig: (
|
||||
display-4: mat-typography-level(112px, 112px, 300, "Roboto", -0.0134em),
|
||||
display-3: mat-typography-level(56px, 56px, 400, "Roboto", -0.0089em),
|
||||
display-2: mat-typography-level(45px, 48px, 400, "Roboto", 0em),
|
||||
display-1: mat-typography-level(34px, 40px, 400, "Roboto", 0.0074em),
|
||||
headline: mat-typography-level(24px, 32px, 400, "Roboto", 0em),
|
||||
title: mat-typography-level(20px, 32px, 500, "Roboto", 0.0075em),
|
||||
subheading-2: mat-typography-level(16px, 28px, 400, "Roboto", 0.0094em),
|
||||
subheading-1: mat-typography-level(15px, 24px, 500, "Roboto", 0.0067em),
|
||||
body-2: mat-typography-level(14px, 24px, 500, "Roboto", 0.0179em),
|
||||
body-1: mat-typography-level(14px, 20px, 400, "Roboto", 0.0179em),
|
||||
button: mat-typography-level(14px, 14px, 500, "Roboto", 0.0893em),
|
||||
caption: mat-typography-level(12px, 20px, 400, "Roboto", 0.0333em),
|
||||
input: mat-typography-level(inherit, 1.125, 400, "Roboto", 1.5px),
|
||||
);
|
||||
|
||||
// Foreground Elements
|
||||
|
||||
// Light Theme Text
|
||||
$dark-text: #000000;
|
||||
$dark-primary-text: rgba($dark-text, 0.87);
|
||||
$dark-accent-text: rgba($dark-primary-text, 0.54);
|
||||
$dark-disabled-text: rgba($dark-primary-text, 0.38);
|
||||
$dark-dividers: rgba($dark-primary-text, 0.12);
|
||||
$dark-focused: rgba($dark-primary-text, 0.12);
|
||||
|
||||
$mat-light-theme-foreground-palette: (
|
||||
base: black,
|
||||
divider: $dark-dividers,
|
||||
dividers: $dark-dividers,
|
||||
disabled: $dark-disabled-text,
|
||||
disabled-button: rgba($dark-text, 0.26),
|
||||
disabled-text: $dark-disabled-text,
|
||||
elevation: black,
|
||||
secondary-text: $dark-accent-text,
|
||||
hint-text: $dark-disabled-text,
|
||||
accent-text: $dark-accent-text,
|
||||
icon: $dark-accent-text,
|
||||
icons: $dark-accent-text,
|
||||
text: $dark-primary-text,
|
||||
slider-min: $dark-primary-text,
|
||||
slider-off: rgba($dark-text, 0.26),
|
||||
slider-off-active: $dark-disabled-text,
|
||||
);
|
||||
|
||||
// Dark Theme text
|
||||
$light-text: #ffffff;
|
||||
$light-primary-text: $light-text;
|
||||
$light-accent-text: rgba($light-primary-text, 0.7);
|
||||
$light-disabled-text: rgba($light-primary-text, 0.5);
|
||||
$light-dividers: rgba($light-primary-text, 0.12);
|
||||
$light-focused: rgba($light-primary-text, 0.12);
|
||||
|
||||
$mat-dark-theme-foreground-palette: (
|
||||
base: $light-text,
|
||||
divider: $light-dividers,
|
||||
dividers: $light-dividers,
|
||||
disabled: $light-disabled-text,
|
||||
disabled-button: rgba($light-text, 0.3),
|
||||
disabled-text: $light-disabled-text,
|
||||
elevation: black,
|
||||
hint-text: $light-disabled-text,
|
||||
secondary-text: $light-accent-text,
|
||||
accent-text: $light-accent-text,
|
||||
icon: $light-text,
|
||||
icons: $light-text,
|
||||
text: $light-text,
|
||||
slider-min: $light-text,
|
||||
slider-off: rgba($light-text, 0.3),
|
||||
slider-off-active: rgba($light-text, 0.3),
|
||||
);
|
||||
|
||||
// Background config
|
||||
// Light bg
|
||||
$light-background: #fafafa;
|
||||
$light-bg-darker-5: darken($light-background, 5%);
|
||||
$light-bg-darker-10: darken($light-background, 10%);
|
||||
$light-bg-darker-20: darken($light-background, 20%);
|
||||
$light-bg-darker-30: darken($light-background, 30%);
|
||||
$light-bg-lighter-5: lighten($light-background, 5%);
|
||||
$dark-bg-tooltip: lighten(#121212, 20%);
|
||||
$dark-bg-alpha-4: rgba(#121212, 0.04);
|
||||
$dark-bg-alpha-12: rgba(#121212, 0.12);
|
||||
|
||||
$mat-light-theme-background-palette: (
|
||||
background: $light-background,
|
||||
status-bar: $light-bg-darker-20,
|
||||
app-bar: $light-bg-darker-5,
|
||||
hover: $dark-bg-alpha-4,
|
||||
card: $light-bg-lighter-5,
|
||||
dialog: $light-bg-lighter-5,
|
||||
tooltip: $dark-bg-tooltip,
|
||||
disabled-button: $dark-bg-alpha-12,
|
||||
raised-button: $light-bg-lighter-5,
|
||||
focused-button: $dark-focused,
|
||||
selected-button: $light-bg-darker-20,
|
||||
selected-disabled-button: $light-bg-darker-30,
|
||||
disabled-button-toggle: $light-bg-darker-10,
|
||||
unselected-chip: $light-bg-darker-10,
|
||||
disabled-list-option: $light-bg-darker-10,
|
||||
);
|
||||
|
||||
// Dark bg
|
||||
$dark-background: #121212;
|
||||
$dark-bg-lighter-5: lighten($dark-background, 5%);
|
||||
$dark-bg-lighter-10: lighten($dark-background, 10%);
|
||||
$dark-bg-lighter-20: lighten($dark-background, 20%);
|
||||
$dark-bg-lighter-30: lighten($dark-background, 30%);
|
||||
$light-bg-alpha-4: rgba(#fafafa, 0.04);
|
||||
$light-bg-alpha-12: rgba(#fafafa, 0.12);
|
||||
|
||||
// Background palette for dark themes.
|
||||
$mat-dark-theme-background-palette: (
|
||||
background: $dark-background,
|
||||
status-bar: $dark-bg-lighter-20,
|
||||
app-bar: $dark-bg-lighter-5,
|
||||
hover: $light-bg-alpha-4,
|
||||
card: $dark-bg-lighter-5,
|
||||
dialog: $dark-bg-lighter-5,
|
||||
tooltip: $dark-bg-lighter-20,
|
||||
disabled-button: $light-bg-alpha-12,
|
||||
raised-button: $dark-bg-lighter-5,
|
||||
focused-button: $light-focused,
|
||||
selected-button: $dark-bg-lighter-20,
|
||||
selected-disabled-button: $dark-bg-lighter-30,
|
||||
disabled-button-toggle: $dark-bg-lighter-10,
|
||||
unselected-chip: $dark-bg-lighter-20,
|
||||
disabled-list-option: $dark-bg-lighter-10,
|
||||
);
|
||||
|
||||
// Compute font config
|
||||
@include mat-core($fontConfig);
|
||||
|
||||
// Theme Config
|
||||
|
||||
body {
|
||||
--primary-color: #272727;
|
||||
--primary-lighter-color: #bebebe;
|
||||
--primary-darker-color: #171717;
|
||||
--text-primary-color: #{$light-primary-text};
|
||||
--text-primary-lighter-color: #{$dark-primary-text};
|
||||
--text-primary-darker-color: #{$light-primary-text};
|
||||
}
|
||||
$mat-primary: (
|
||||
main: #272727,
|
||||
lighter: #bebebe,
|
||||
darker: #171717,
|
||||
200: #272727,
|
||||
// For slide toggle,
|
||||
contrast:
|
||||
(
|
||||
main: $light-primary-text,
|
||||
lighter: $dark-primary-text,
|
||||
darker: $light-primary-text,
|
||||
),
|
||||
);
|
||||
$theme-primary: mat-palette($mat-primary, main, lighter, darker);
|
||||
|
||||
body {
|
||||
--accent-color: #43a047;
|
||||
--accent-lighter-color: #c7e3c8;
|
||||
--accent-darker-color: #2c842f;
|
||||
--text-accent-color: #{$light-primary-text};
|
||||
--text-accent-lighter-color: #{$dark-primary-text};
|
||||
--text-accent-darker-color: #{$light-primary-text};
|
||||
}
|
||||
$mat-accent: (
|
||||
main: #43a047,
|
||||
lighter: #c7e3c8,
|
||||
darker: #2c842f,
|
||||
200: #43a047,
|
||||
// For slide toggle,
|
||||
contrast:
|
||||
(
|
||||
main: $light-primary-text,
|
||||
lighter: $dark-primary-text,
|
||||
darker: $light-primary-text,
|
||||
),
|
||||
);
|
||||
$theme-accent: mat-palette($mat-accent, main, lighter, darker);
|
||||
|
||||
body {
|
||||
--warn-color: #f4511e;
|
||||
--warn-lighter-color: #fccbbc;
|
||||
--warn-darker-color: #ef3712;
|
||||
--text-warn-color: #{$light-primary-text};
|
||||
--text-warn-lighter-color: #{$dark-primary-text};
|
||||
--text-warn-darker-color: #{$light-primary-text};
|
||||
}
|
||||
$mat-warn: (
|
||||
main: #f4511e,
|
||||
lighter: #fccbbc,
|
||||
darker: #ef3712,
|
||||
200: #f4511e,
|
||||
// For slide toggle,
|
||||
contrast:
|
||||
(
|
||||
main: $light-primary-text,
|
||||
lighter: $dark-primary-text,
|
||||
darker: $light-primary-text,
|
||||
),
|
||||
);
|
||||
$theme-warn: mat-palette($mat-warn, main, lighter, darker);
|
||||
$theme: mat-dark-theme($theme-primary, $theme-accent, $theme-warn);
|
||||
$altTheme: mat-light-theme($theme-primary, $theme-accent, $theme-warn);
|
||||
|
||||
// Theme Init
|
||||
@include angular-material-theme($theme);
|
||||
|
||||
.theme-alternate {
|
||||
@include angular-material-theme($altTheme);
|
||||
}
|
||||
|
||||
// Specific component overrides, pieces that are not in line with the general theming
|
||||
|
||||
// Handle buttons appropriately, with respect to line-height
|
||||
.mat-raised-button,
|
||||
.mat-stroked-button,
|
||||
.mat-flat-button {
|
||||
padding: 0 1.15em;
|
||||
margin: 0 0.65em;
|
||||
min-width: 3em;
|
||||
line-height: 36.4px;
|
||||
}
|
||||
|
||||
.mat-standard-chip {
|
||||
padding: 0.5em 0.85em;
|
||||
min-height: 2.5em;
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
font-size: 24px;
|
||||
font-family: "Material Icons Round", "Material Icons";
|
||||
.mat-badge-content {
|
||||
font-family: "Roboto";
|
||||
}
|
||||
}
|
||||
|
||||
// Include bootstrap-grid.scss from node_modules
|
||||
@import "../node_modules/bootstrap/scss/bootstrap-grid.scss";
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
app-root {
|
||||
height: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.container {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@import "./styles.personal";
|
|
@ -1,17 +1,27 @@
|
|||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"target": "ES6",
|
||||
"module": "CommonJS",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable"
|
||||
],
|
||||
"jsx": "react-jsx",
|
||||
"noEmit": true,
|
||||
"skipLibCheck": true
|
||||
"baseUrl": "./",
|
||||
"outDir": "./out-tsc/",
|
||||
|
||||
"target": "es2017",
|
||||
"module": "es2020",
|
||||
|
||||
"lib": ["es2020", "dom"],
|
||||
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"downlevelIteration": true,
|
||||
"importHelpers": true,
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
"files": ["src/main.ts", "src/polyfills.ts"],
|
||||
"include": ["src/**/*.d.ts"],
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,8 @@
|
|||
"forceConsistentCasingInFileNames": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"noImplicitThis": true,
|
||||
|
|
Loading…
Reference in New Issue