Archived
Private
Public Access
1
0
This commit is contained in:
2022-12-18 13:30:02 +01:00
commit 0e94ffa3c6
85 changed files with 26673 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
# Ignore the node_modules directory
node_modules
# Ignore the dist directory
dist
# Ignore the .git directory
.git
# Ignore the .gitignore file
.gitignore
# Ignore the .dockerignore file
.dockerignore

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

42
ProjectManager.Frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,42 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# 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
Thumbs.db

View File

@@ -0,0 +1,4 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
"recommendations": ["angular.ng-template"]
}

View File

@@ -0,0 +1,20 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "ng serve",
"type": "pwa-chrome",
"request": "launch",
"preLaunchTask": "npm: start",
"url": "http://localhost:4200/"
},
{
"name": "ng test",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: test",
"url": "http://localhost:9876/debug.html"
}
]
}

View File

@@ -0,0 +1,42 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "start",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
},
{
"type": "npm",
"script": "test",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
}
]
}

View File

@@ -0,0 +1,11 @@
#stage 1
FROM node:18 as node
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build:ssr --prod
#stage 2
FROM node:18
COPY --from=node /app/dist /app/dist
WORKDIR /app
CMD ["node", "dist/ProjectManager.Frontend/server/main.js"]

View File

@@ -0,0 +1,160 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"ProjectManager.Frontend": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/ProjectManager.Frontend/browser",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"@angular/material/prebuilt-themes/indigo-pink.css",
"src/styles.scss"
],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "10mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "10mb"
}
],
"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": "ProjectManager.Frontend:build:production"
},
"development": {
"browserTarget": "ProjectManager.Frontend:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "ProjectManager.Frontend:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"@angular/material/prebuilt-themes/indigo-pink.css",
"src/styles.scss"
],
"scripts": []
}
},
"server": {
"builder": "@angular-devkit/build-angular:server",
"options": {
"outputPath": "dist/ProjectManager.Frontend/server",
"main": "server.ts",
"tsConfig": "tsconfig.server.json",
"inlineStyleLanguage": "scss"
},
"configurations": {
"production": {
"outputHashing": "media"
},
"development": {
"optimization": false,
"sourceMap": true,
"extractLicenses": false,
"vendorChunk": true
}
},
"defaultConfiguration": "production"
},
"serve-ssr": {
"builder": "@nguniversal/builders:ssr-dev-server",
"configurations": {
"development": {
"browserTarget": "ProjectManager.Frontend:build:development",
"serverTarget": "ProjectManager.Frontend:server:development"
},
"production": {
"browserTarget": "ProjectManager.Frontend:build:production",
"serverTarget": "ProjectManager.Frontend:server:production"
}
},
"defaultConfiguration": "development"
},
"prerender": {
"builder": "@nguniversal/builders:prerender",
"options": {
"routes": [
"/"
]
},
"configurations": {
"production": {
"browserTarget": "ProjectManager.Frontend:build:production",
"serverTarget": "ProjectManager.Frontend:server:production"
},
"development": {
"browserTarget": "ProjectManager.Frontend:build:development",
"serverTarget": "ProjectManager.Frontend:server:development"
}
},
"defaultConfiguration": "production"
}
}
}
}
}

23715
ProjectManager.Frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,50 @@
{
"name": "project-manager.frontend",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test",
"dev:ssr": "ng run ProjectManager.Frontend:serve-ssr",
"serve:ssr": "node dist/ProjectManager.Frontend/server/main.js",
"build:ssr": "ng build && ng run ProjectManager.Frontend:server",
"prerender": "ng run ProjectManager.Frontend:prerender"
},
"private": true,
"dependencies": {
"@angular/animations": "^15.0.4",
"@angular/cdk": "^15.0.1",
"@angular/common": "^15.0.4",
"@angular/compiler": "^15.0.4",
"@angular/core": "^15.0.4",
"@angular/forms": "^15.0.4",
"@angular/material": "^15.0.0",
"@angular/platform-browser": "^15.0.4",
"@angular/platform-browser-dynamic": "^15.0.4",
"@angular/platform-server": "^15.0.4",
"@angular/router": "^15.0.4",
"@nguniversal/express-engine": "^15.0.0",
"express": "^4.15.2",
"rxjs": "~7.5.0",
"tslib": "^2.3.0",
"zone.js": "~0.12.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^15.0.4",
"@angular/cli": "~15.0.4",
"@angular/compiler-cli": "^15.0.4",
"@nguniversal/builders": "^15.0.0",
"@types/express": "^4.17.0",
"@types/jasmine": "~4.3.0",
"@types/node": "^14.15.0",
"jasmine-core": "~4.5.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.0.0",
"typescript": "~4.8.2"
}
}

View File

@@ -0,0 +1,66 @@
import 'zone.js/node';
import { APP_BASE_HREF } from '@angular/common';
import { ngExpressEngine } from '@nguniversal/express-engine';
import * as express from 'express';
import { existsSync } from 'fs';
import { join } from 'path';
import { AppServerModule } from './src/main.server';
// The Express app is exported so that it can be used by serverless Functions.
export function app(): express.Express {
const server = express();
const distFolder = join(process.cwd(), 'dist/ProjectManager.Frontend/browser');
const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/main/modules/express-engine)
server.engine('html', ngExpressEngine({
bootstrap: AppServerModule,
}));
server.set('view engine', 'html');
server.set('views', distFolder);
// Example Express Rest API endpoints
// server.get('/api/**', (req, res) => { });
server.get('/backend', (req, res) => {
let backend = process.env['BACKEND']
if (!backend?.endsWith("/")) backend += "/";
res.json({url: backend});
});
// Serve static files from /browser
server.get('*.*', express.static(distFolder, {
maxAge: '1y'
}));
// All regular routes use the Universal engine
server.get('*', (req, res) => {
res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
});
return server;
}
function run(): void {
const port = process.env['PORT'] || 4000;
// Start up the Node server
const server = app();
server.listen(port, () => {
console.log(`Node Express server listening on http://localhost:${port}`);
});
}
// Webpack will replace 'require' with '__webpack_require__'
// '__non_webpack_require__' is a proxy to Node 'require'
// The below code is to ensure that the server is run only when not requiring the bundle.
declare const __non_webpack_require__: NodeRequire;
const mainModule = __non_webpack_require__.main;
const moduleFilename = mainModule && mainModule.filename || '';
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
run();
}
export * from './src/main.server';

View File

@@ -0,0 +1,24 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import {LoginComponent} from "./sites/login/login.component";
import {RegisterComponent} from "./sites/register/register.component";
import {ProfileComponent} from "./sites/profile/profile.component";
import {DashboardComponent} from "./sites/dashboard/dashboard.component";
import {ProjectComponent} from "./sites/project/project.component";
const routes: Routes = [
{path: "login", component: LoginComponent},
{path: "register", component: RegisterComponent},
{path: "dashboard", component: DashboardComponent},
{path: "profile", component: ProfileComponent},
{path: "project/:id", component: ProjectComponent},
{path: "**", pathMatch: "full", redirectTo: "/dashboard"}
];
@NgModule({
imports: [RouterModule.forRoot(routes, {
initialNavigation: 'enabledBlocking'
})],
exports: [RouterModule]
})
export class AppRoutingModule { }

View File

@@ -0,0 +1,5 @@
<app-navigation *ngIf="isLoginRoute() || crud.user != undefined; else loading"></app-navigation>
<ng-template #loading>
<mat-spinner></mat-spinner>
</ng-template>

View File

@@ -0,0 +1,41 @@
import { Component } from '@angular/core';
import {CrudService} from "./services/crud.service";
import {Router} from "@angular/router";
import {ProjectService} from "./services/project.service";
import {StorageService} from "./services/storage.service";
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent{
public constructor(public crud: CrudService, private router: Router, private projects: ProjectService, private storage: StorageService/* ProjectService gets dependency injected because the onUserUpdate listener needs to be created */) {
if (typeof document === 'undefined') return;
document.body.classList.toggle("darkMode", storage.getItem("darkMode") == "true");
setTimeout(this.validateToken.bind(this), 0);
}
public isLoginRoute(): boolean {
return (this.router.url == '/login' || this.router.url == '/register');
}
private async validateToken() {
while (this.crud.backendUrl == undefined) {
await this.timeout(200);
}
if (this.isLoginRoute()) return;
if (!await this.crud.isAuthenticated())
await this.router.navigate(["/login"]);
else await this.crud.loadUser();
}
private timeout(ms: number): Promise<void> {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms);
});
}
}

View File

@@ -0,0 +1,65 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NavigationComponent } from './components/navigation/navigation.component';
import {MatSidenavModule} from "@angular/material/sidenav";
import {MatToolbarModule} from "@angular/material/toolbar";
import {MatListModule} from "@angular/material/list";
import {MatIconModule} from "@angular/material/icon";
import {MatButtonModule} from "@angular/material/button";
import { LoginComponent } from './sites/login/login.component';
import {MatCardModule} from "@angular/material/card";
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
import {MatFormFieldModule} from "@angular/material/form-field";
import {MatInputModule} from "@angular/material/input";
import {HttpClientModule} from "@angular/common/http";
import { RegisterComponent } from './sites/register/register.component';
import {MatProgressSpinnerModule} from "@angular/material/progress-spinner";
import {ProfileComponent} from './sites/profile/profile.component';
import { DashboardComponent } from './sites/dashboard/dashboard.component';
import {MatDialogModule} from "@angular/material/dialog";
import { DialogComponent } from './components/dialog/dialog.component';
import {MatSnackBarModule} from "@angular/material/snack-bar";
import {MatTooltipModule} from "@angular/material/tooltip";
import { TextDialogComponent } from './components/text-dialog/text-dialog.component';
import { ProjectComponent } from './sites/project/project.component';
@NgModule({
declarations: [
AppComponent,
NavigationComponent,
LoginComponent,
RegisterComponent,
ProfileComponent,
DashboardComponent,
DialogComponent,
TextDialogComponent,
ProjectComponent
],
imports: [
BrowserModule.withServerTransition({ appId: 'serverApp' }),
AppRoutingModule,
BrowserAnimationsModule,
HttpClientModule,
MatSidenavModule,
MatToolbarModule,
MatListModule,
MatIconModule,
MatButtonModule,
MatCardModule,
ReactiveFormsModule,
MatFormFieldModule,
MatInputModule,
MatProgressSpinnerModule,
FormsModule,
MatDialogModule,
MatSnackBarModule,
MatTooltipModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

View File

@@ -0,0 +1,14 @@
import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';
import { AppModule } from './app.module';
import { AppComponent } from './app.component';
@NgModule({
imports: [
AppModule,
ServerModule,
],
bootstrap: [AppComponent],
})
export class AppServerModule {}

View File

@@ -0,0 +1,9 @@
<h1 mat-dialog-title *ngIf="data.title != undefined">{{data.title}}</h1>
<div mat-dialog-content *ngIf="data.subtitle != undefined">{{data.subtitle}}</div>
<div mat-dialog-actions *ngIf="data.buttons != undefined" id="buttons">
<button mat-button
(click)="dialogRef.close(button.value)"
*ngFor="let button of data.buttons"
[color]="button.color"
>{{button.text}}</button>
</div>

View File

@@ -0,0 +1,5 @@
#buttons {
width: 100%;
display: flex;
justify-content: space-evenly;
}

View File

@@ -0,0 +1,21 @@
import {Component, Inject} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {ThemePalette} from "@angular/material/core";
export interface DialogData {
title?: string;
subtitle?: string;
buttons?: {text: string, value: any, color: ThemePalette}[];
}
@Component({
selector: 'app-dialog',
templateUrl: './dialog.component.html',
styleUrls: ['./dialog.component.scss']
})
export class DialogComponent {
public constructor(
public dialogRef: MatDialogRef<DialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: DialogData,
) {}
}

View File

@@ -0,0 +1,49 @@
<mat-drawer-container>
<mat-drawer mode="side" [opened]="showActions()" id="sidebar" #drawer>
<mat-toolbar id="profile">
<mat-icon>person</mat-icon>
<span>{{crud.user?.username}}</span>
</mat-toolbar>
<mat-selection-list id="actions">
<mat-list-item routerLink="/dashboard">
<mat-icon matListItemIcon>dashboard</mat-icon>
<div matListItemTitle>Übersicht</div>
</mat-list-item>
<mat-list-item (click)="logout()">
<mat-icon matListItemIcon>logout</mat-icon>
<div matListItemTitle>Ausloggen</div>
</mat-list-item>
<mat-list-item (click)="createProject()">
<mat-icon matListItemIcon>add</mat-icon>
<div matListItemTitle>Neues Projekt</div>
</mat-list-item>
<mat-divider></mat-divider>
<div mat-subheader>Projekte</div>
<mat-list-item *ngFor="let project of projects.projects" (click)="openProject(project.projectId)">
<mat-icon matListItemIcon>open_in_new</mat-icon>
<div matListItemTitle>{{project.name}}</div>
<div matListItemLine [ngClass]="{startColor: project.running, stopColor: !project.running}">{{project.running ? 'Läuft' : 'Gestoppt'}}</div>
</mat-list-item>
</mat-selection-list>
</mat-drawer>
<mat-toolbar id="header">
<button mat-icon-button (click)="drawer.toggle()" *ngIf="showActions()"><mat-icon>menu</mat-icon></button>
<img src="favicon.ico" alt="logo" height="30px" draggable="false">
<span>Project Manager</span>
<section id="top-actions">
<button mat-icon-button (click)="onModeChange()" matTooltip="Farbmodus ändern"><mat-icon>{{darkMode ? 'light_mode' : 'dark_mode'}}</mat-icon></button>
<button mat-icon-button routerLink="/profile" *ngIf="showActions()" matTooltip="Profil Einstellungen"><mat-icon>account_circle</mat-icon></button>
</section>
</mat-toolbar>
<router-outlet></router-outlet>
<mat-spinner [ngStyle]="{'display': isSpinnerVisible() ? 'block' : 'none'}"></mat-spinner>
</mat-drawer-container>

View File

@@ -0,0 +1,36 @@
mat-drawer-container {
width: 100vw;
height: 100vh;
#sidebar {
width: 250px;
#profile {
gap: 20px;
overflow-x: hidden;
}
#actions {
height: calc(100vh - 80px);
overflow-y: auto;
}
}
}
#header {
gap: 10px;
padding-left: 5px;
#top-actions {
margin-left: auto;
}
}
mat-spinner {
position: absolute;
width: 100px;
height: 100px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}

View File

@@ -0,0 +1,68 @@
import {Component} from '@angular/core';
import {Router} from "@angular/router";
import {CrudService} from "../../services/crud.service";
import {ProjectService} from "../../services/project.service";
import {MatDialog} from "@angular/material/dialog";
import {TextDialogComponent} from "../text-dialog/text-dialog.component";
import {firstValueFrom} from "rxjs";
import {MatSnackBar} from "@angular/material/snack-bar";
import {StorageService} from "../../services/storage.service";
@Component({
selector: 'app-navigation',
templateUrl: './navigation.component.html',
styleUrls: ['./navigation.component.scss']
})
export class NavigationComponent {
public static spinnerVisible: boolean = false;
public darkMode: boolean;
public constructor(public router: Router, public crud: CrudService, public projects: ProjectService, public dialog: MatDialog, private snackBar: MatSnackBar, private storage: StorageService) {
this.darkMode = storage.getItem("darkMode") == "true";
}
public isSpinnerVisible(): boolean {
return NavigationComponent.spinnerVisible;
}
public onModeChange(): void {
this.darkMode = !document.body.classList.contains("darkMode");
document.body.classList.toggle("darkMode", this.darkMode);
this.storage.setItem("darkMode", JSON.stringify(this.darkMode));
}
public showActions(): boolean {
return this.router.url != '/' && this.router.url != '/login' && this.router.url != '/register';
}
public async logout() {
this.crud.setAuthKey(undefined);
this.crud.user = undefined;
await this.router.navigate(["login"]);
}
public async createProject() {
const dialogRef = this.dialog.open(TextDialogComponent, {
data: {title: "Neues Projekt", subtitle: "Name", buttons: [
{text: "Abbrechen", value: false},
{text: "Projekt erstellen", value: true, color: 'primary'}
]}
});
const result = await firstValueFrom(dialogRef.afterClosed()) as {success: boolean, data: string};
if (!result?.success) return;
NavigationComponent.spinnerVisible = true;
const projectId = await this.projects.addProject(result.data);
NavigationComponent.spinnerVisible = false;
if (projectId == undefined) {
this.snackBar.open("Projekt kann nicht erstellt werden!", undefined, {duration: 2000});
return;
}
await this.projects.loadProjects();
this.snackBar.open("Projekt erstellt!", undefined, {duration: 2000});
}
public openProject(projectId: string) {
window.open(`${this.crud.backendUrl}projects/${projectId}/url?token=${this.crud.authKey}`, '_blank').focus();
}
}

View File

@@ -0,0 +1,14 @@
<h1 mat-dialog-title *ngIf="data.title != undefined">{{data.title}}</h1>
<form mat-dialog-content (submit)="$event.preventDefault(); dialogRef.close({success: true, data: text.value})">
<mat-form-field>
<mat-label>{{data.subtitle}}</mat-label>
<input type="text" matInput #text>
</mat-form-field>
</form>
<div mat-dialog-actions *ngIf="data.buttons != undefined" id="buttons">
<button mat-button
(click)="dialogRef.close({success: button.value, data: text.value})"
*ngFor="let button of data.buttons"
[color]="button.color"
>{{button.text}}</button>
</div>

View File

@@ -0,0 +1,15 @@
import {Component, Inject} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {DialogData} from "../dialog/dialog.component";
@Component({
selector: 'app-text-dialog',
templateUrl: './text-dialog.component.html',
styleUrls: ['./text-dialog.component.scss']
})
export class TextDialogComponent {
public constructor(
public dialogRef: MatDialogRef<TextDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: DialogData,
) {}
}

View File

@@ -0,0 +1,10 @@
export interface Project {
projectId?: string;
ownerId?: string;
name?: string;
port?: number;
containerName?: string;
proxyId?: number;
certificateId?: number;
running?: boolean;
}

View File

@@ -0,0 +1,6 @@
export interface User {
userId?: string;
email?: string;
username?: string;
password?: string;
}

View File

@@ -0,0 +1,120 @@
import { Injectable } from '@angular/core';
import {HttpClient, HttpErrorResponse, HttpHeaders} from "@angular/common/http";
import {firstValueFrom} from "rxjs";
import {User} from "../entities/user";
import {StorageService} from "./storage.service";
let backend: string;
export interface BackendResponse<T> {
content: T;
success: boolean;
code: number;
message?: string;
}
@Injectable({
providedIn: 'root'
})
export class CrudService {
public user: User;
public onUserUpdate: (() => void)[] = [];
public authKey: string;
private headers: HttpHeaders = new HttpHeaders({
'Content-Type': 'application/json',
'Authorization': ''
});
constructor(private client: HttpClient, private storage: StorageService) {
this.getBackendUrl().then(() => {
this.authKey = storage.getItem("api_key");
this.setAuthKey(this.authKey);
})
}
private async getBackendUrl() {
backend = (await firstValueFrom(this.client.get<{url: string}>(location?.origin + "/backend"))).url;
}
public setAuthKey(key: string): void {
this.authKey = key;
this.headers = this.headers.set("Authorization", key || '');
this.storage.setItem("api_key", key);
}
public async isAuthenticated(): Promise<boolean> {
if (this.authKey == undefined) return false;
const result = await this.sendGetRequest("users/token");
return result.success;
}
public async loadUser(forceLoad: boolean = false): Promise<User> {
if (this.authKey == undefined) return undefined;
if (this.user != undefined && !forceLoad) return this.user;
this.onUserUpdate.forEach(update => update.call(this));
const response = await this.sendGetRequest<User>("users/me");
this.user = response.content;
return this.user;
}
public get backendUrl(): string {
return backend;
}
public async sendGetRequest<T>(endpoint: string): Promise<BackendResponse<T>> {
try {
const result = await firstValueFrom(this.client.get<T>(backend + endpoint, {headers: this.headers}));
return {content: result, success: true, code: 200};
} catch (e) {
const error = e as HttpErrorResponse;
if (error.status == 0)
return {content: undefined, success: false, code: error.status, message: "Server nicht erreichbar!"};
return {content: undefined, success: false, code: error.status, message: error.error};
}
}
public async sendPutRequest<T>(endpoint: string, body?: any): Promise<BackendResponse<T>> {
try {
const result = await firstValueFrom(this.client.put<T>(backend + endpoint, body, {headers: this.headers}));
return {content: result, success: true, code: 200};
} catch (e) {
const error = e as HttpErrorResponse;
if (error.status == 0)
return {content: undefined, success: false, code: error.status, message: "Server nicht erreichbar!"};
return {content: undefined, success: false, code: error.status, message: error.error};
}
}
public async sendPostRequest<T>(endpoint: string, body?: any): Promise<BackendResponse<T>> {
try {
const result = await firstValueFrom(this.client.post<T>(backend + endpoint, body, {headers: this.headers}));
return {content: result, success: true, code: 200};
} catch (e) {
const error = e as HttpErrorResponse;
if (error.status == 0)
return {content: undefined, success: false, code: error.status, message: "Server nicht erreichbar!"};
return {content: undefined, success: false, code: error.status, message: error.error};
}
}
public async sendDeleteRequest<T>(endpoint: string): Promise<BackendResponse<T>> {
try {
const result = await firstValueFrom(this.client.delete<T>(backend + endpoint, {headers: this.headers}));
return {content: result, success: true, code: 200};
} catch (e) {
const error = e as HttpErrorResponse;
if (error.status == 0)
return {content: undefined, success: false, code: error.status, message: "Server nicht erreichbar!"};
return {content: undefined, success: false, code: error.status, message: error.error};
}
}
}

View File

@@ -0,0 +1,58 @@
import { Injectable } from '@angular/core';
import {CrudService} from "./crud.service";
import {Project} from "../entities/project";
@Injectable({
providedIn: 'root'
})
export class ProjectService {
public projects: Project[] = []
constructor(private crud: CrudService) {
crud.onUserUpdate.push(this.loadProjects.bind(this));
}
public async loadProjects() {
this.projects = [];
const result = (await this.crud.sendGetRequest<{projects: Project[], running: boolean[]}>("projects")).content;
for (let i = 0; i < result.projects.length; i++) {
this.projects[i] = result.projects[i];
this.projects[i].running = result.running[i];
}
}
public async getProject(projectId: string): Promise<Project> {
const response = await this.crud.sendGetRequest<Project>("projects/" + projectId);
return response.content;
}
public async addProject(name: string): Promise<string> {
const response = await this.crud.sendPostRequest<{projectId: string}>("projects", {name});
return response.content?.projectId;
}
public async editProject(projectId: string, name: string): Promise<boolean> {
const response = await this.crud.sendPutRequest("projects/" + projectId, {name});
return response.success;
}
public async deleteProject(projectId: string): Promise<boolean> {
const response = await this.crud.sendDeleteRequest("projects/" + projectId);
return response.success;
}
public async startProject(projectId: string): Promise<boolean> {
const response = await this.crud.sendGetRequest("projects/" + projectId + "/start");
return response.success;
}
public async stopProject(projectId: string): Promise<boolean> {
const response = await this.crud.sendGetRequest("projects/" + projectId + "/stop");
return response.success;
}
public async isProjectRunning(projectId: string): Promise<boolean> {
const response = await this.crud.sendGetRequest<{started: boolean}>("projects/" + projectId + "/status");
return response.content?.started;
}
}

View File

@@ -0,0 +1,16 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class StorageService {
public setItem(setItem,data){
if (typeof localStorage === "undefined") return;
localStorage.setItem(setItem, data);
}
public getItem(getItem){
if (typeof localStorage === "undefined") return undefined;
return localStorage.getItem(getItem);
}
}

View File

@@ -0,0 +1,23 @@
<section id="main">
<h1 id="welcome">Willkommen {{crud.user.username}}</h1>
<h2 id="title">Projekte</h2>
<div id="projects">
<span *ngIf="projects.projects.length == 0 && crud.user != undefined" class="disabled">Du hast noch keine Projekte erstellt</span>
<mat-card *ngFor="let project of projects.projects" class="project">
<mat-card-header>
<mat-card-title>{{project.name}}</mat-card-title>
<mat-card-subtitle>{{project.projectId}}</mat-card-subtitle>
</mat-card-header>
<mat-card-actions>
<button mat-button color="primary" (click)="router.navigate(['/project', project.projectId])">Öffnen</button>
<button mat-button color="accent" (click)="editProject(project.projectId)">Bearbeiten</button>
<button mat-button color="warn" (click)="deleteProject(project.projectId)">Löschen</button>
<button mat-icon-button color="warn" *ngIf="project.running" (click)="updateProjectStatus(project.projectId, false)"><mat-icon>pause</mat-icon></button>
<button mat-icon-button color="accent" *ngIf="!project.running" (click)="updateProjectStatus(project.projectId, true)"><mat-icon>play_arrow</mat-icon></button>
</mat-card-actions>
</mat-card>
</div>
</section>

View File

@@ -0,0 +1,46 @@
#main {
height: calc(100vh - 125px);
margin: 30px;
display: flex;
flex-direction: column;
#welcome {
font-size: 30px;
}
#projects {
display: flex;
gap: 30px;
flex-wrap: wrap;
overflow-y: auto;
.project {
width: 350px;
height: 200px;
overflow-y: auto;
opacity: 0;
animation: 200ms project ease-out forwards;
mat-card-actions {
margin-top: auto;
justify-content: space-evenly;
}
}
@for $i from 1 through 100 {
.project:nth-child(#{$i}n) {
animation-delay: #{$i * 0.1}s;
}
}
}
}
@keyframes project {
from {
opacity: 0;
}
to {
opacity: 1;
}
}

View File

@@ -0,0 +1,59 @@
import { Component } from '@angular/core';
import {ProjectService} from "../../services/project.service";
import {CrudService} from "../../services/crud.service";
import {Router} from "@angular/router";
import {MatDialog} from "@angular/material/dialog";
import {DialogComponent} from "../../components/dialog/dialog.component";
import {firstValueFrom} from "rxjs";
import {MatSnackBar} from "@angular/material/snack-bar";
import {TextDialogComponent} from "../../components/text-dialog/text-dialog.component";
import {NavigationComponent} from "../../components/navigation/navigation.component";
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.scss']
})
export class DashboardComponent {
public constructor(public crud: CrudService, public projects: ProjectService, public router: Router, private dialog: MatDialog, private snackBar: MatSnackBar) {}
public async editProject(projectId: string) {
const dialogRef = this.dialog.open(TextDialogComponent, {
data: {title: "Projekt umbenennen", subtitle: "Name", buttons: [
{text: "Abbrechen", value: false},
{text: "Projekt bearbeiten", value: true, color: 'primary'}
]}
});
const result = await firstValueFrom(dialogRef.afterClosed()) as {success: boolean, data: string};
if (!result?.success) return;
await this.projects.editProject(projectId, result.data);
await this.projects.loadProjects();
this.snackBar.open("Projekt aktualisiert!", undefined, {duration: 2000});
}
public async deleteProject(projectId: string) {
const dialogRef = this.dialog.open(DialogComponent, {
data: {title: "Möchtest du das Projekt wirklich löschen?", subtitle: "Alle gespeicherten Daten gehen dann verloren!", buttons: [
{text: "Abbrechen", value: false},
{text: "Löschen", value: true, color: 'warn'}
]}
});
const result = await firstValueFrom(dialogRef.afterClosed());
if (!result) return;
NavigationComponent.spinnerVisible = true;
await this.projects.deleteProject(projectId);
NavigationComponent.spinnerVisible = false;
await this.projects.loadProjects();
this.snackBar.open("Projekt gelöscht!", undefined, {duration: 2000});
}
public async updateProjectStatus(projectId: string, start: boolean) {
if (start) await this.projects.startProject(projectId);
else await this.projects.stopProject(projectId);
await this.projects.loadProjects();
}
}

View File

@@ -0,0 +1,29 @@
<mat-card>
<mat-card-title>Einloggen</mat-card-title>
<mat-divider></mat-divider>
<mat-card-content>
<form [formGroup]="form" (ngSubmit)="submit()">
<mat-form-field>
<mat-label>E-Mail</mat-label>
<input type="text" matInput formControlName="email" required>
<mat-error *ngIf="form.hasError('required', 'email')">E-Mail ist erforderlich</mat-error>
<mat-error *ngIf="form.hasError('email', 'email') && !form.hasError('required', 'email')">
Bitte geben Sie eine gültige E-Mail-Adresse ein
</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>Passwort</mat-label>
<input type="password" matInput formControlName="password" required>
<mat-error *ngIf="form.hasError('required', 'password')">Passwort ist erforderlich</mat-error>
</mat-form-field>
<mat-divider></mat-divider>
<span>Du besitzt keinen Account? <a routerLink="/register">Registrieren</a></span>
<mat-error *ngIf="error">{{error}}</mat-error>
<button type="submit" mat-button>Einloggen</button>
</form>
</mat-card-content>
</mat-card>

View File

@@ -0,0 +1,35 @@
:host {
display: flex;
justify-content: center;
padding-top: 300px;
}
mat-card {
user-select: none;
width: 500px;
mat-card-title {
display: flex;
justify-content: center;
align-items: center;
font-size: 30px;
height: 50px;
}
form {
display: flex;
flex-direction: column;
padding-block: 20px;
gap: 20px;
button {
width: min-content;
align-self: center;
}
}
a {
color: unset;
}
}

View File

@@ -0,0 +1,39 @@
import {Component} from '@angular/core';
import {FormControl, FormGroup, Validators} from "@angular/forms";
import {CrudService} from "../../services/crud.service";
import {User} from "../../entities/user";
import {Router} from "@angular/router";
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss']
})
export class LoginComponent {
public form: FormGroup = new FormGroup({
email: new FormControl('', [Validators.email]),
password: new FormControl(''),
});
public error: string;
public constructor(private crud: CrudService, private router: Router) {
this.form.reset();
this.error = "";
}
public async submit() {
this.error = "";
const email = this.form.get("email").value;
const password = this.form.get("password").value;
const user: User = {email: email, password: password};
const result = await this.crud.sendPostRequest<{token: string}>("users/login", user);
if (result.success) {
this.crud.setAuthKey(result.content.token);
await this.crud.loadUser(true);
await this.router.navigate(["/dashboard"]);
}else {
this.error = "E-Mail oder Passwort ist falsch";
}
}
}

View File

@@ -0,0 +1,57 @@
<mat-card id="content">
<mat-card-header>
<mat-card-title>Profil</mat-card-title>
<mat-card-subtitle>Einstellungen</mat-card-subtitle>
</mat-card-header>
<mat-card-content id="main">
<form [formGroup]="form" (ngSubmit)="update()" id="form">
<mat-form-field>
<mat-label>E-Mail</mat-label>
<input type="text" matInput formControlName="email" required>
<mat-error *ngIf="form.hasError('required', 'email')">E-Mail ist erforderlich</mat-error>
<mat-error *ngIf="form.hasError('email', 'email') && !form.hasError('required', 'email')">
Bitte geben Sie eine gültige E-Mail-Adresse ein
</mat-error>
<mat-error *ngIf="form.hasError('maxlength', 'email') && !form.hasError('required', 'email')">
Der eingegebene Wert ist zu lang
</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>Benutzername</mat-label>
<input type="text" matInput formControlName="username" required>
<mat-error *ngIf="form.hasError('required', 'username')">Benutzername ist erforderlich</mat-error>
<mat-error *ngIf="form.hasError('maxlength', 'username') && !form.hasError('required', 'username')">
Der eingegebene Wert ist zu lang
</mat-error>
</mat-form-field>
<div>
<mat-form-field>
<mat-label>Passwort</mat-label>
<input type="password" matInput formControlName="password">
<mat-error *ngIf="form.hasError('maxlength', 'password')">
Der eingegebene Wert ist zu lang
</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>Passwort wiederholen</mat-label>
<input type="password" matInput formControlName="passwordRepeat">
<mat-error *ngIf="form.hasError('maxlength', 'passwordRepeat')">
Der eingegebene Wert ist zu lang
</mat-error>
</mat-form-field>
</div>
<mat-error>{{error}}</mat-error>
<button type="submit" hidden></button>
</form>
</mat-card-content>
<mat-card-actions id="actions">
<button mat-button color="primary" (click)="update()">Account aktualisieren</button>
<button mat-button color="warn" (click)="delete()">Account löschen</button>
</mat-card-actions>
</mat-card>

View File

@@ -0,0 +1,30 @@
:host {
display: grid;
place-items: center;
height: calc(100% - 64px);
}
#content {
width: 600px;
#main {
margin-top: 30px;
margin-bottom: 30px;
#form {
display: flex;
flex-direction: column;
gap: 20px;
div {
display: grid;
gap: 10px;
grid-template-columns: repeat(2, 1fr);
}
}
}
#actions {
justify-content: space-evenly;
}
}

View File

@@ -0,0 +1,86 @@
import {Component} from '@angular/core';
import {CrudService} from "../../services/crud.service";
import {FormControl, FormGroup, Validators} from "@angular/forms";
import {MatDialog} from "@angular/material/dialog";
import {DialogComponent} from "../../components/dialog/dialog.component";
import {firstValueFrom} from "rxjs";
import {User} from "../../entities/user";
import {MatSnackBar} from "@angular/material/snack-bar";
import {Router} from "@angular/router";
@Component({
selector: 'app-profile',
templateUrl: './profile.component.html',
styleUrls: ['./profile.component.scss']
})
export class ProfileComponent {
public form: FormGroup = new FormGroup({
email: new FormControl('', [Validators.email, Validators.maxLength(255)]),
username: new FormControl('', [Validators.maxLength(255)]),
password: new FormControl('', [Validators.maxLength(255)]),
passwordRepeat: new FormControl('', [Validators.maxLength(255)])
});
public error: string;
public constructor(public crud: CrudService, private router: Router, public dialog: MatDialog, private snackBar: MatSnackBar) {
this.form.get("email").setValue(this.crud.user?.email);
this.form.get("username").setValue(this.crud.user?.username);
}
public async update() {
if (!this.form.valid) return;
const result = await this.openDialog("Änderungen speichern?");
if (!result) return;
this.error = "";
const email = this.form.get("email").value;
const username = this.form.get("username").value;
const password = this.form.get("password").value;
const passwordRepeat = this.form.get("passwordRepeat").value;
if (password != passwordRepeat) {
this.error = "Passwörter stimmen nicht überein";
return;
}
const user: User = {userId: this.crud.user.userId, email, username, password};
const response = await this.crud.sendPutRequest("users", user);
if (!response.success) {
this.error = "Aktualiserung fehlgeschlagen!";
return;
}
await this.crud.loadUser(true);
this.form.reset();
this.snackBar.open("Account aktualisiert!", undefined, {duration: 2000});
await this.router.navigate(["dashboard"]);
}
public async delete() {
const result = await this.openDialog("Möchtest du deinen Account wirklich löschen?", "All deine Projekte werden für immer gelöscht!", ['', 'warn']);
if (!result) return;
await this.crud.sendDeleteRequest("users");
this.crud.setAuthKey(undefined);
this.crud.user = undefined;
this.snackBar.open("Account gelöscht!", undefined, {duration: 2000});
await this.router.navigate(["login"]);
}
private openDialog(title: string, subtitle?: string, colors?: string[]): Promise<boolean> {
if (colors == undefined) colors = ['', 'accent'];
return new Promise<boolean>(async (resolve) => {
const dialogRef = this.dialog.open(DialogComponent, {
data: {title, subtitle, buttons: [
{text: "Abbrechen", value: false, color: colors[0]},
{text: "Bestätigen", value: true, color: colors[1]},
]}
});
resolve(await firstValueFrom(dialogRef.afterClosed()));
})
}
}

View File

@@ -0,0 +1 @@
<iframe #frame></iframe>

View File

@@ -0,0 +1,10 @@
:host {
display: flex;
height: calc(100% - 64px);
}
iframe {
border: none;
flex-grow: 1;
flex-shrink: 1;
}

View File

@@ -0,0 +1,23 @@
import {Component, ElementRef, ViewChild} from '@angular/core';
import {ActivatedRoute} from "@angular/router";
import {CrudService} from "../../services/crud.service";
@Component({
selector: 'app-project',
templateUrl: './project.component.html',
styleUrls: ['./project.component.scss']
})
export class ProjectComponent {
@ViewChild('frame') frame: ElementRef;
public constructor(public route: ActivatedRoute, public crud: CrudService) {
setTimeout(this.getRoute.bind(this), 0);
}
public getRoute() {
this.route.params.subscribe(params => {
this.frame.nativeElement.src = this.crud.backendUrl + 'projects/' + params['id'] + '/url?token=' + this.crud.authKey;
})
}
}

View File

@@ -0,0 +1,53 @@
<mat-card>
<mat-card-title>Registrieren</mat-card-title>
<mat-divider></mat-divider>
<mat-card-content>
<form [formGroup]="form" (ngSubmit)="submit()">
<mat-form-field>
<mat-label>E-Mail</mat-label>
<input type="text" matInput formControlName="email" required>
<mat-error *ngIf="form.hasError('required', 'email')">E-Mail ist erforderlich</mat-error>
<mat-error *ngIf="form.hasError('email', 'email') && !form.hasError('required', 'email')">
Bitte geben Sie eine gültige E-Mail-Adresse ein
</mat-error>
<mat-error *ngIf="form.hasError('maxlength', 'email') && !form.hasError('required', 'email')">
Der eingegebene Wert ist zu lang
</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>Benutzername</mat-label>
<input type="text" matInput formControlName="username" required>
<mat-error *ngIf="form.hasError('required', 'username')">Benutzername ist erforderlich</mat-error>
<mat-error *ngIf="form.hasError('maxlength', 'username') && !form.hasError('required', 'username')">
Der eingegebene Wert ist zu lang
</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>Passwort</mat-label>
<input type="password" matInput formControlName="password" required>
<mat-error *ngIf="form.hasError('required', 'password')">Passwort ist erforderlich</mat-error>
<mat-error *ngIf="form.hasError('maxlength', 'password') && !form.hasError('required', 'password')">
Der eingegebene Wert ist zu lang
</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>Passwort wiederholen</mat-label>
<input type="password" matInput formControlName="passwordRepeat" required>
<mat-error *ngIf="form.hasError('required', 'passwordRepeat')">Passwort ist erforderlich</mat-error>
<mat-error *ngIf="form.hasError('maxlength', 'passwordRepeat') && !form.hasError('required', 'passwordRepeat')">
Der eingegebene Wert ist zu lang
</mat-error>
</mat-form-field>
<mat-divider></mat-divider>
<span>Du hast bereits einen Account? <a routerLink="/login">Einloggen</a></span>
<mat-error *ngIf="error">{{error}}</mat-error>
<button type="submit" mat-button>Registrieren</button>
</form>
</mat-card-content>
</mat-card>

View File

@@ -0,0 +1,35 @@
:host {
display: flex;
justify-content: center;
padding-top: 300px;
}
mat-card {
user-select: none;
width: 500px;
mat-card-title {
display: flex;
justify-content: center;
align-items: center;
font-size: 30px;
height: 50px;
}
form {
display: flex;
flex-direction: column;
padding-block: 20px;
gap: 20px;
button {
width: min-content;
align-self: center;
}
}
a {
color: unset;
}
}

View File

@@ -0,0 +1,46 @@
import { Component } from '@angular/core';
import {FormControl, FormGroup, Validators} from "@angular/forms";
import {CrudService} from "../../services/crud.service";
import {Router} from "@angular/router";
import {User} from "../../entities/user";
@Component({
selector: 'app-register',
templateUrl: './register.component.html',
styleUrls: ['./register.component.scss']
})
export class RegisterComponent {
public form: FormGroup = new FormGroup({
email: new FormControl('', [Validators.email, Validators.maxLength(255)]),
username: new FormControl('', [Validators.maxLength(255)]),
password: new FormControl('', [Validators.maxLength(255)]),
passwordRepeat: new FormControl('', [Validators.maxLength(255)])
});
public error: string;
public constructor(private crud: CrudService, private router: Router) {}
public async submit() {
this.error = "";
const email = this.form.get("email").value;
const username = this.form.get("username").value;
const password = this.form.get("password").value;
const passwordRepeat = this.form.get("passwordRepeat").value;
if (password != passwordRepeat) {
this.error = "Passwörter stimmen nicht überein";
return;
}
const user: User = {email, username, password};
const result = await this.crud.sendPostRequest<{token: string}>("users/register", user);
if (!result.success) {
this.error = "Registrierung fehlgeschlagen";
return;
}
this.crud.setAuthKey(result.content.token);
await this.crud.loadUser(true);
await this.router.navigate(["/dashboard"]);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Project Manager</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body class="mat-typography darkMode">
<app-root></app-root>
</body>
</html>

View File

@@ -0,0 +1,2 @@
export { AppServerModule } from './app/app.server.module';

View File

@@ -0,0 +1,17 @@
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
function bootstrap() {
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));
};
if (document.readyState === 'complete') {
bootstrap();
} else {
document.addEventListener('DOMContentLoaded', bootstrap);
}

View File

@@ -0,0 +1,115 @@
@use 'sass:map';
@use '/node_modules/@angular/material' as mat;
@import "/node_modules/@angular/material/theming";
@include mat.core();
$angular-primary: mat.define-palette(mat.$blue-palette, 500, 100, 900);
$angular-accent: mat.define-palette(mat.$green-palette, A200, A100, A400);
$angular-warn: mat.define-palette(mat.$red-palette);
$angular-default-theme: mat.define-light-theme(
(
color: (
primary: $angular-primary,
accent: $angular-accent,
warn: $angular-warn,
),
)
);
$angular-dark-theme: mat.define-dark-theme(
(
color: (
primary: $angular-primary,
accent: $angular-accent,
warn: $angular-warn,
),
)
);
.darkMode {
@include mat.all-component-colors($angular-dark-theme);
$color-config: mat.get-color-config($angular-dark-theme);
$background: map.get($color-config, 'background');
::-webkit-scrollbar {
width: 10px;
}
::-webkit-scrollbar-track {
background: map.get($background, 'app-bar');
border-radius: 5px;
}
::-webkit-scrollbar-thumb {
background: map.get($background, 'hover');
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: map.get($background, 'focused-button');
}
body {
background-color: map.get($background, 'background');
}
.stopColor {
$stopColor: map.get($color-config, 'warn');
color: map.get($stopColor, 500);
}
.startColor {
$startColor: map.get($color-config, 'accent');
color: map.get($startColor, 500);
}
}
@include mat.all-component-themes($angular-default-theme);
$color-config: mat.get-color-config($angular-default-theme);
$background: map.get($color-config, 'background');
::-webkit-scrollbar {
width: 10px;
}
::-webkit-scrollbar-track {
background: map.get($background, 'app-bar');
border-radius: 5px;
}
::-webkit-scrollbar-thumb {
background: map.get($background, 'hover');
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: map.get($background, 'focused-button');
}
.disabled {
color: #999;
}
.stopColor {
$stopColor: map.get($color-config, 'warn');
color: map.get($stopColor, 500);
}
.startColor {
$startColor: map.get($color-config, 'accent');
color: map.get($startColor, 500);
}
html, body { height: 100%; }
body {
margin: 0;
font-family: Roboto, "Helvetica Neue", sans-serif;
background-color: map.get($background, 'background');
width: 100vw;
height: 100vh;
display: grid;
place-items: center;
}
* {
user-select: none;
}

View File

@@ -0,0 +1,14 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": [
"src/main.ts"
],
"include": [
"src/**/*.d.ts"
]
}

View File

@@ -0,0 +1,32 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"moduleResolution": "node",
"importHelpers": true,
"target": "ES2022",
"module": "ES2022",
"useDefineForClassFields": false,
"lib": [
"ES2022",
"dom"
]
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}

View File

@@ -0,0 +1,14 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.app.json",
"compilerOptions": {
"outDir": "./out-tsc/server",
"types": [
"node"
]
},
"files": [
"src/main.server.ts",
"server.ts"
]
}

View File

@@ -0,0 +1,14 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine"
]
},
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}