Test angular

This commit is contained in:
rubikscraft 2022-02-28 21:09:27 +01:00
parent 39a59e7b9b
commit 7a3f62ac54
No known key found for this signature in database
GPG Key ID: 1463EBE9200A5CD4
84 changed files with 2740 additions and 5276 deletions

2
.vscode/tasks.json vendored
View File

@ -34,7 +34,7 @@
{
"type": "shell",
"label": "Start frontend",
"command": "yarn start",
"command": "yarn watch",
"options": {
"cwd": "./frontend"
},

View File

@ -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,
},
};

16
frontend/.browserslistrc Normal file
View File

@ -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

16
frontend/.editorconfig Normal file
View File

@ -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

View File

@ -1 +0,0 @@
BUILD_PATH='./dist'

58
frontend/.gitignore vendored
View File

@ -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

27
frontend/README.md Normal file
View File

@ -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.

88
frontend/angular.json Normal file
View File

@ -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"
}

View File

@ -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,
},
],
};

View File

@ -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"
}
}

View File

@ -1 +0,0 @@
../../../branding/logo/

View File

@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow: /

View File

@ -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() {}
}

View File

@ -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]`,
};
}
}

View File

@ -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;
}

View File

@ -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>
);
}

View File

@ -0,0 +1,5 @@
<app-header></app-header>
<div class="grow-full container">
<router-outlet></router-outlet>
</div>
<app-footer></app-footer>

View File

@ -0,0 +1,4 @@
.grow-full {
flex-grow: 1;
}

View File

@ -0,0 +1,8 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {}

View File

@ -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 {}

View File

@ -0,0 +1,2 @@
<footer class="container">
</footer>

View File

@ -0,0 +1,7 @@
footer {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 32px;
}

View File

@ -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 {}

View File

@ -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 {}

View File

@ -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>

View File

@ -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);
}

View File

@ -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 {}

View File

@ -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 {}

View File

@ -0,0 +1,3 @@
export interface ProcessingViewMetadata {
imageFile: File;
}

View 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 {}

View File

@ -0,0 +1,6 @@
<div class="content-border">
<div class="centered">
<h1>Processing</h1>
<mat-spinner color="accent"></mat-spinner>
</div>
</div>

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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 });
}
}

View File

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -1,3 +0,0 @@
.footer-container {
margin-top: 32px;
}

View File

@ -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>
);
}

View File

@ -1,14 +0,0 @@
.svg-logo {
height: 48px;
width: 48px;
border-radius: 20%;
}
.text-link {
color: inherit;
text-decoration: inherit;
}
.header {
margin-bottom: 32px;
}

View File

@ -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>
);
}

View File

@ -0,0 +1,5 @@
const Environment = {
production: true,
};
export default Environment;

View File

@ -0,0 +1,5 @@
const Environment = {
production: false,
};
export default Environment;

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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'),
);

View File

@ -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);
};
}

View File

@ -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('/');
};
}

13
frontend/src/main.ts Normal file
View File

@ -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));

53
frontend/src/polyfills.ts Normal file
View File

@ -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
*/

View File

@ -1 +0,0 @@
/// <reference types="react-scripts" />

View File

@ -1,3 +0,0 @@
export default function LoginView() {
return <p>login</p>;
}

View File

@ -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;

View File

@ -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>
);
}

View File

@ -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>
</>
);
}

View File

@ -1,10 +0,0 @@
.uploadedimage {
width: 100%;
height: auto;
border-radius: 20px;
}
.contentwindow {
padding: 2rem;
}

View File

@ -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>
);
}

View File

@ -1,3 +1,11 @@
.content-border {
border-radius: 20px;
height: 100%;
border-style: solid;
border-width: 5px;
}
.centered {
display: flex;
flex-direction: column;

273
frontend/src/styles.scss Normal file
View File

@ -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";

View File

@ -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
}
}

View File

@ -10,6 +10,8 @@
"forceConsistentCasingInFileNames": true,
"strictPropertyInitialization": false,
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noImplicitThis": true,

6359
yarn.lock

File diff suppressed because it is too large Load Diff