Initial commit

This commit is contained in:
Flo 2024-02-14 20:59:56 +01:00 committed by GitHub
commit 76232e4856
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
58 changed files with 14144 additions and 0 deletions

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

31
.github/workflows/node.js.yml vendored Normal file
View File

@ -0,0 +1,31 @@
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
name: Node.js CI
on:
push:
branches: [ "prod" ]
pull_request:
branches: [ "prod" ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.x]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm install
- run: npm install -g @angular/cli
- run: npm run build
- run: npm test

45
.gitignore vendored Normal file
View File

@ -0,0 +1,45 @@
# 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/*
!.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

27
README.md Normal file
View File

@ -0,0 +1,27 @@
# Homepage
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 15.2.6.
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application 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.

126
angular.json Normal file
View File

@ -0,0 +1,126 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"homepage": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss",
"skipTests": true
},
"@schematics/angular:class": {
"skipTests": true
},
"@schematics/angular:directive": {
"skipTests": true
},
"@schematics/angular:guard": {
"skipTests": true
},
"@schematics/angular:module": {
"skipTests": true
},
"@schematics/angular:pipe": {
"skipTests": true
},
"@schematics/angular:service": {
"skipTests": true
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/homepage",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"assets": [
"src/assets",
"src/favicon.ico"
],
"styles": [
"@angular/material/prebuilt-themes/deeppurple-amber.css",
"src/styles.scss"
],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "800kb",
"maximumError": "2mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"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": "homepage:build:production"
},
"development": {
"browserTarget": "homepage:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "homepage:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"assets": [
"src/assets",
"src/favicon.ico"
],
"styles": [
"@angular/material/prebuilt-themes/deeppurple-amber.scss",
"src/styles.scss"
],
"scripts": []
}
}
}
}
},
"cli": {
"analytics": false
}
}

3
bin/script/build Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
$SCRIPT_DIR/exec build

3
bin/script/down Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
$SCRIPT_DIR/exec down

23
bin/script/exec Executable file
View File

@ -0,0 +1,23 @@
#!/bin/bash
COMMAND="$@"
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
#LOAD ENV VARIABLES FROM .ENV
#export $(grep -v '^#' "${SCRIPT_DIR}/../../.env" | xargs)
#MAC
if [[ "$OSTYPE" == "darwin"* ]]; then
docker-compose -f "${SCRIPT_DIR}/../../docker/docker-compose-mac.yml" $COMMAND
#LINUX
elif [[ "$OSTYPE" == "linux-gnu" ]]; then
docker-compose -f "${SCRIPT_DIR}/../../docker/docker-compose.yml" $COMMAND
else
echo "Dieses Skript wird auf deinem Gerät nicht unterstützt"
exit 1
fi
#UNSET ENV VARIABLES FROM .ENV
#unset $(grep -v '^#' "${SCRIPT_DIR}/../../.env" | sed -E 's/(.*)=.*/\1/' | xargs)

19
bin/script/init Executable file
View File

@ -0,0 +1,19 @@
#!/bin/bash
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
PROJECT_DIR=$(realpath $SCRIPT_DIR/../../)
ENV_DIR=$(realpath $PROJECT_DIR/../../)
# Build and start docker containers
$SCRIPT_DIR/exec build
$SCRIPT_DIR/exec up -d
# Source drun
source $ENV_DIR/bin/script/drun
# Install node Packages
drun homepage-frontend npm install
drun homepage-frontend npm install -g @angular/cli
# Initial build of website
drun homepage-frontend npm run build

3
bin/script/stop Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
$SCRIPT_DIR/exec stop

3
bin/script/up Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
$SCRIPT_DIR/exec up -d

View File

@ -0,0 +1,31 @@
version: '3'
networks:
homepage:
external: true
services:
homepage-frontend-app:
image: homepage-frontend-app
networks:
- homepage
volumes:
- /Users/flo/dev/frontend/homepage/:/var/www/
build:
context: ./../
dockerfile: ./docker/npm/dockerfile
tty: true
homepage-frontend-nginx:
image: homepage-frontend-nginx
networks:
- homepage
volumes:
- /Users/flo/dev/frontend/homepage/:/var/www/html:z
build:
context: ./../
dockerfile: ./docker/nginx/dockerfile
ports:
- 80:80
depends_on:
- homepage-frontend-app

31
docker/docker-compose.yml Normal file
View File

@ -0,0 +1,31 @@
version: '3'
networks:
homepage:
external: true
services:
homepage-frontend-app:
image: homepage-frontend-app
networks:
- homepage
volumes:
- ./../:/var/www/
build:
context: ./../
dockerfile: ./docker/npm/dockerfile
tty: true
homepage-frontend-nginx:
image: homepage-frontend-nginx
networks:
- homepage
volumes:
- ./../:/var/www/html:z
build:
context: ./../
dockerfile: ./docker/nginx/dockerfile
ports:
- 80:80
depends_on:
- homepage-frontend-app

View File

@ -0,0 +1,25 @@
upstream host-backend-nginx {
server homepage-backend-nginx:80;
}
server{
listen 80 default_server;
root /var/www/html/dist/homepage;
index index.html;
location / {
try_files $uri$args $uri$args/ /index.html;
}
location /api/ {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_pass http://host-backend-nginx;
}
}

5
docker/nginx/dockerfile Normal file
View File

@ -0,0 +1,5 @@
FROM nginx:alpine
COPY docker/nginx/config/nginx.conf /etc/nginx/conf.d/default.conf
CMD ["nginx", "-g", "daemon off;"]

12
docker/npm/dockerfile Normal file
View File

@ -0,0 +1,12 @@
FROM node:latest
WORKDIR /var/www/html
# confirm installation
RUN node -v
RUN npm -v
# use newest npm version
RUN npm install npm
CMD ["/bin/bash"]

12673
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

40
package.json Normal file
View File

@ -0,0 +1,40 @@
{
"name": "homepage",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
"private": true,
"dependencies": {
"@angular/animations": "^15.2.0",
"@angular/cdk": "^15.2.9",
"@angular/common": "^15.2.0",
"@angular/compiler": "^15.2.0",
"@angular/core": "^15.2.0",
"@angular/forms": "^15.2.0",
"@angular/material": "^15.2.9",
"@angular/platform-browser": "^15.2.0",
"@angular/platform-browser-dynamic": "^15.2.0",
"@angular/router": "^15.2.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.12.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^15.2.6",
"@angular/cli": "~15.2.6",
"@angular/compiler-cli": "^15.2.0",
"@types/jasmine": "~4.3.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.9.4"
}
}

View File

@ -0,0 +1,20 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { LoginComponent } from './auth/login/login.component';
import { HomeComponent } from './home/home.component';
import { RegistrationComponent } from './auth/registration/registration.component';
import { RegistrationConfirmationComponent } from './auth/registration-confirmation/registration-confirmation.component';
const routes: Routes = [
{ path: 'auth', component: LoginComponent },
{ path: 'auth/registration', component: RegistrationComponent },
{ path: 'auth/registration/:id', component: RegistrationConfirmationComponent },
{ path: 'home', component: HomeComponent },
{ path: '', redirectTo: '/auth', pathMatch: 'full' },
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }

View File

@ -0,0 +1 @@
<router-outlet></router-outlet>

View File

@ -0,0 +1,4 @@
app-login {
position: absolute;
top: 50%;
}

9
src/app/app.component.ts Normal file
View File

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

74
src/app/app.module.ts Normal file
View File

@ -0,0 +1,74 @@
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatMenuModule } from '@angular/material/menu';
import { MatListModule } from '@angular/material/list';
import { MatIconModule } from '@angular/material/icon';
import { MatGridListModule } from '@angular/material/grid-list'
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatDividerModule } from '@angular/material/divider';
import {MatDialogModule} from '@angular/material/dialog';
import {MatSnackBarModule} from '@angular/material/snack-bar';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { ProductListComponent } from './product/product-list/product-list.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { HomeComponent } from './home/home.component';
import { ProductDetailsComponent } from './product/product-details/product-details.component';
import { LoginComponent } from './auth/login/login.component';
import { CreateProductComponent } from './product/create-product/create-product.component';
import { RegistrationComponent } from './auth/registration/registration.component';
import { RegistrationConfirmationComponent } from './auth/registration-confirmation/registration-confirmation.component';
import { ProductComponent } from './product/product/product.component';
@NgModule({
declarations: [
AppComponent,
ProductListComponent,
ProductDetailsComponent,
LoginComponent,
HomeComponent,
CreateProductComponent,
RegistrationComponent,
RegistrationConfirmationComponent,
ProductComponent,
],
imports: [
BrowserModule,
HttpClientModule,
AppRoutingModule,
BrowserAnimationsModule,
MatSlideToggleModule,
MatCardModule,
MatButtonModule,
MatIconModule,
MatGridListModule,
MatFormFieldModule,
FormsModule,
MatExpansionModule,
MatMenuModule,
MatListModule,
MatToolbarModule,
MatSidenavModule,
MatInputModule,
MatSelectModule,
MatDividerModule,
MatDialogModule,
MatSnackBarModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

21
src/app/app.service.ts Normal file
View File

@ -0,0 +1,21 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class AppService {
private username: string|null;
constructor() {
this.username = null;
}
public getUsername(): string|null {
return this.username;
}
public setUsername(name: string|null): void {
this.username = name;
}
}

View File

@ -0,0 +1,41 @@
<mat-card>
<mat-card-header>
<mat-card-title>Login</mat-card-title>
<mat-card-subtitle>Bitte melde Dich an</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<div class="container">
<div class="row">
<mat-form-field>
<mat-label>Identifier</mat-label>
<input matInput [(ngModel)]="identifier">
</mat-form-field>
</div>
<div class="row">
<mat-form-field>
<mat-label>Password</mat-label>
<input matInput type="password" [(ngModel)]="password">
</mat-form-field>
</div>
<div class="row">
<div class="col-md">
<button mat-fab
extended
color="disabled"
(click)="register()">
Registrieren
</button>
</div>
<div class="col-md">
<button mat-fab
extended
color="primary"
(click)="login()">
Login
</button>
</div>
</div>
</div>
</mat-card-content>
</mat-card>

View File

@ -0,0 +1,23 @@
mat-card {
margin-left: auto;
margin-right: auto;
}
button {
width: 100%;
}
/* For tablet: */
@media only screen and (min-width: 600px) {
mat-card {
width: 70%;
}
}
/* For desktop: */
@media only screen and (min-width: 768px) {
mat-card {
width: 40%;
}
}

View File

@ -0,0 +1,60 @@
import { Component, OnInit } from '@angular/core';
import { RequestService } from '../../request.service';
import { Router } from "@angular/router"
import { AppService } from '../../app.service';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss']
})
export class LoginComponent implements OnInit {
identifier: string|null = null;
password: string|null = null;
constructor(
public appService : AppService,
public requestService : RequestService,
private router: Router
) {
}
ngOnInit(): void {
this.stateRequest();
}
login() {
this.requestService.post(
'auth/login-user',
{
identifier: this.identifier,
password: this.password
},
(response:any) => {
if(response.hasOwnProperty('sessionId')) {
this.router.navigate(['/home']);
}
}
);
}
register() {
console.log('goto registration');
this.router.navigate(['/auth/registration']);
}
private stateRequest() {
this.requestService.get(
'user/state',
{},
(response:any) => {
if (response.hasOwnProperty('sessionId')) {
this.appService.setUsername(response.username);
this.router.navigate(['/home']);
}
}
);
}
}

View File

@ -0,0 +1,33 @@
<mat-card>
<mat-card-header>
<mat-card-title>Registrierung bestätigen</mat-card-title>
<mat-card-subtitle>Bitte gib dein Passwort ein</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<div class="container">
<div class="row">
<mat-form-field>
<mat-label>Passwort</mat-label>
<input matInput type="password" [(ngModel)]="password">
</mat-form-field>
</div>
<div class="row">
<mat-form-field>
<mat-label>Passwort wiederholen</mat-label>
<input matInput type="password" [(ngModel)]="passwordConfirmation">
</mat-form-field>
</div>
<div class="row">
<div class="col-md">
<button mat-fab
extended
color="primary"
(click)="confirm()">
Bestätigen
</button>
</div>
</div>
</div>
</mat-card-content>
</mat-card>

View File

@ -0,0 +1,23 @@
mat-card {
margin-left: auto;
margin-right: auto;
}
button {
width: 100%;
}
/* For tablet: */
@media only screen and (min-width: 600px) {
mat-card {
width: 70%;
}
}
/* For desktop: */
@media only screen and (min-width: 768px) {
mat-card {
width: 40%;
}
}

View File

@ -0,0 +1,40 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { RequestService } from '../../request.service';
@Component({
selector: 'app-registration-confirmation',
templateUrl: './registration-confirmation.component.html',
styleUrls: ['./registration-confirmation.component.scss']
})
export class RegistrationConfirmationComponent implements OnInit {
public id: string;
public password: string|null = null;
public passwordConfirmation: string|null = null;
constructor(
private route: ActivatedRoute,
public requestService : RequestService,
private router: Router
) {
this.id = '';
}
ngOnInit() {
this.id = this.route.snapshot.paramMap.get('id') ?? '';
}
confirm() {
this.requestService.post(
'auth/confirm-registration',
{
id: this.id,
password: this.password,
passwordConfirmation: this.passwordConfirmation,
},
(response:any) => {
this.router.navigate(['/auth']);
}
)
}
}

View File

@ -0,0 +1,33 @@
<mat-card>
<mat-card-header>
<mat-card-title>Registrieren</mat-card-title>
<mat-card-subtitle>Bitte gib deine Benutzerdaten an:</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<div class="container">
<div class="row">
<mat-form-field>
<mat-label>Benutzername</mat-label>
<input matInput [(ngModel)]="username">
</mat-form-field>
</div>
<div class="row">
<mat-form-field>
<mat-label>E-Mail</mat-label>
<input matInput [(ngModel)]="mail">
</mat-form-field>
</div>
<div class="row">
<div class="col-md">
<button mat-fab
extended
color="primary"
(click)="register()">
Registrieren
</button>
</div>
</div>
</div>
</mat-card-content>
</mat-card>

View File

@ -0,0 +1,23 @@
mat-card {
margin-left: auto;
margin-right: auto;
}
button {
width: 100%;
}
/* For tablet: */
@media only screen and (min-width: 600px) {
mat-card {
width: 70%;
}
}
/* For desktop: */
@media only screen and (min-width: 768px) {
mat-card {
width: 40%;
}
}

View File

@ -0,0 +1,39 @@
import { Component } from '@angular/core';
import { AppService } from '../../app.service';
import { RequestService } from '../../request.service';
import { Router } from '@angular/router';
@Component({
selector: 'app-registration',
templateUrl: './registration.component.html',
styleUrls: ['./registration.component.scss']
})
export class RegistrationComponent {
username: string|null = null;
mail: string|null = null;
constructor(
public appService : AppService,
public requestService : RequestService,
private router: Router
) {
}
ngOnInit(): void {
this.username = "";
this.mail = "";
}
register(): void {
this.requestService.post(
'auth/register-user',
{
username: this.username,
mail: this.mail
},
(response:any) => {
this.router.navigate(['/auth']);
}
);
}
}

View File

@ -0,0 +1,26 @@
<mat-toolbar color="primary">
<button mat-icon-button (click)="drawer.toggle(); list.onGetProductList()">
<mat-icon>menu</mat-icon>
</button>
<span>
{{title}}
</span>
<button mat-button [matMenuTriggerFor]="menu">{{username}}</button>
<mat-menu #menu="matMenu">
<button mat-menu-item (click)="logout()">Logout</button>
</mat-menu>
</mat-toolbar>
<mat-drawer-container class="content">
<mat-drawer #drawer class="example-sidenav" mode="over">
<app-product-list #list (productSelected)="selectProduct($event); drawer.toggle(); list.onGetProductList()">
</app-product-list>
</mat-drawer>
<div class="example-sidenav-content">
<app-product-details [product]="selectedProduct"></app-product-details>
</div>
</mat-drawer-container>

View File

@ -0,0 +1,13 @@
span {
margin-left: auto;
margin-right: auto;
}
.content {
height: 100%;
}
.example-sidenav-content {
margin: 1em;
height: 100%;
}

View File

@ -0,0 +1,52 @@
import { Component } from '@angular/core';
import { Product } from '../product/product-list/product-list.component';
import { RequestService } from '../request.service';
import { Router } from "@angular/router"
import { AppService } from '../app.service';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss']
})
export class HomeComponent {
title = 'Homepage';
username: string = "(null)";
selectedProduct: Product|null = null;
constructor(
public appService : AppService,
public requestService : RequestService,
private router: Router
) {
}
ngOnInit(): void {
this.requestService.get(
'user/state',
{},
(response:any) => {
if (response.status == 401) {
this.router.navigate(['/auth']);
} else {
this.username = response.username;
}
}
);
}
logout() {
this.requestService.post(
'auth/logout-user',
{},
(response:any) => {
this.router.navigate(['/auth']);
}
);
}
selectProduct(product: any) {
this.selectedProduct = product;
}
}

View File

@ -0,0 +1,28 @@
<mat-card>
<mat-card-header>
<mat-card-title>Neues Produkt</mat-card-title>
<mat-card-subtitle>Bitte gebe die Daten des neuen Produkts an</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<div class="container">
<div class="row">
<mat-form-field>
<mat-label>Identifier</mat-label>
<input matInput [(ngModel)]="identifier">
</mat-form-field>
</div>
<div class="row">
<mat-form-field>
<mat-label>Name</mat-label>
<input matInput [(ngModel)]="name">
</mat-form-field>
</div>
</div>
</mat-card-content>
<mat-card-actions>
<button mat-button (click)="cancel()">Abbrechen</button>
<button mat-button [mat-dialog-close]="{identifier: identifier, name: name}" cdkFocusInitial>Ok</button>
</mat-card-actions>
</mat-card>

View File

@ -0,0 +1,24 @@
import { Component } from '@angular/core';
import { MatDialogRef } from '@angular/material/dialog';
@Component({
selector: 'app-create-product',
templateUrl: './create-product.component.html',
styleUrls: ['./create-product.component.scss']
})
export class CreateProductComponent {
name: string
identifier: string
constructor(
private dialogRef: MatDialogRef<CreateProductComponent>
) {
this.name = '';
this.identifier = '';
}
cancel(){
this.dialogRef.close();
}
}

View File

@ -0,0 +1,56 @@
<iframe *ngIf="url != null" [src]="url">
</iframe>
<mat-card *ngIf="product != null">
<mat-card-header>
<mat-card-title>{{product.name}}</mat-card-title>
<mat-card-subtitle>{{product.identifier}} - {{product.id}}</mat-card-subtitle>
</mat-card-header>
<mat-divider/>
<mat-card-content>
<div class="container">
<div class="row">
<mat-form-field>
<mat-label>Identifier</mat-label>
<input matInput [(ngModel)]="product.identifier">
</mat-form-field>
</div>
<div class="row">
<mat-form-field>
<mat-label>Name</mat-label>
<input matInput [(ngModel)]="product.name">
</mat-form-field>
</div>
<div class="row">
<mat-form-field>
<mat-label>Url</mat-label>
<input matInput [(ngModel)]="product.url">
</mat-form-field>
</div>
<div class="row">
<div class="col-md">
<mat-form-field appearance="outline">
<mat-label>Erstellt am</mat-label>
<input matInput [(ngModel)]="product.created" readonly>
</mat-form-field>
</div>
<div class="col-md">
<mat-form-field appearance="outline">
<mat-label>Zuletzt geändert am</mat-label>
<input matInput [(ngModel)]="product.updated" readonly>
</mat-form-field>
</div>
</div>
</div>
</mat-card-content>
<mat-card-actions align="end">
<button mat-fab color="primary"
(click)="updateProduct(product)">
<mat-icon>save</mat-icon>
</button>
</mat-card-actions>
</mat-card>

View File

@ -0,0 +1,36 @@
app-login {
margin: auto;
}
mat-form-field {
width: 100%;
}
mat-divider {
margin-top: 0.5em;
margin-bottom: 1em;
}
mat-card {
margin-right: auto;
margin-left: auto;
}
iframe {
width: 100%;
height: 100%;
}
/* For tablet: */
@media only screen and (min-width: 600px) {
mat-card {
max-width: 85%;
}
}
/* For desktop: */
@media only screen and (min-width: 768px) {
mat-card {
max-width: 85%;
}
}

View File

@ -0,0 +1,40 @@
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { RequestService } from '../../request.service';
import { Product } from '../product-list/product-list.component';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
@Component({
selector: 'app-product-details',
templateUrl: './product-details.component.html',
styleUrls: ['./product-details.component.scss']
})
export class ProductDetailsComponent implements OnChanges {
@Input() product: Product|null = null
url: SafeResourceUrl|null = null;
constructor(
public requestService : RequestService,
private domSanitizer: DomSanitizer
) {
}
ngOnChanges(changes: SimpleChanges) {
if (this.product != null && this.product.url != null) {
this.url = this.domSanitizer.bypassSecurityTrustResourceUrl(this.product.url);
}
else {
this.url = null;
}
}
updateProduct(product: Product) {
this.requestService.post(
'product/update-product',
product,
(response:any) => {
this.product = response
}
);
}
}

View File

@ -0,0 +1,17 @@
<mat-list>
<mat-list-item class="listItem" *ngFor="let product of products" (click)="selectProduct(product)">
<mat-icon matListItemIcon (click)="removeProduct(product)"> delete</mat-icon>
<span matListItemTitle>{{product.name}}</span>
<span matListItemLine>{{product.identifier}}</span>
</mat-list-item>
</mat-list>
<mat-divider></mat-divider>
<button mat-fab
extended
color="primary"
(click)="createProduct()">
<mat-icon>add</mat-icon>
Add
</button>

View File

@ -0,0 +1,18 @@
mat-divider {
margin-top: 0.5em;
margin-bottom: 1em;
}
.listItem {
margin-right: 1em;
}
.listItem:hover {
background-color: lightgray;
}
button {
margin-left: 1em;
margin-right: 1em;
width: 90%;
}

View File

@ -0,0 +1,82 @@
import { Component, EventEmitter, Output } from '@angular/core';
import { RequestService } from '../../request.service';
import {MatDialog, MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
import { CreateProductComponent } from '../create-product/create-product.component';
export interface Product {
id: string,
identifier: string,
name: string|null,
url: string|null,
created: string,
updated: string
}
@Component({
selector: 'app-product-list',
templateUrl: './product-list.component.html',
styleUrls: ['./product-list.component.scss']
})
export class ProductListComponent {
@Output() productSelected = new EventEmitter<Product|null>();
products: Product[] = []
constructor(
public requestService : RequestService,
public dialog: MatDialog
) {
}
onGetProductList(): void {
this.requestService.get(
'product/product-list',
{},
(answer:any) => this.products = answer
);
}
selectProduct(product: Product|null) {
this.productSelected.emit(product);
}
createProduct() {
const createProductDialogRef = this.dialog.open(
CreateProductComponent
)
createProductDialogRef.afterClosed().subscribe(
result => {
if(result != undefined) {
this.requestService.post(
'product/create-product',
{
identifier: result.identifier,
name: result.name
},
(answer:any) => {
this.onGetProductList();
this.selectProduct(answer);
}
);
}
}
)
return;
}
removeProduct(product: Product) {
this.selectProduct(null);
this.requestService.post(
'product/delete-product',
{
id: product.id
},
(answer:any) => {
this.selectProduct(null);
this.onGetProductList();
}
);
}
}

View File

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

View File

@ -0,0 +1,83 @@
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';
import { Injectable } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
@Injectable({
providedIn: 'root'
})
export class RequestService {
constructor(
private http: HttpClient,
private router: Router,
private snackBar: MatSnackBar
) {
}
post(apiPath: string, body: any, fct: Function)
{
let url = this.obtainUrl(apiPath);
let observable = this.http.post(url, body).subscribe(
(answer:any) => {
fct(answer);
},
(error:any) => {
this.handleError(error);
}
);
}
get(apiPath: string, body: any, fct: Function)
{
let url = this.obtainUrl(apiPath);
let observable = this.http.get(url, body).subscribe(
(answer:any) => {
fct(answer);
},
(error:any) => {
this.handleError(error);
}
);
}
private obtainUrl(apiPath: string): string {
let hostString = window.location.host;
let protocol = window.location.protocol;
return protocol + '//' + hostString + '/api/' + apiPath;
}
private handleError(answer: any): void {
if (answer.status == 401) {
console.log('Deine Sitzung konnte nicht gefunden werden');
this.router.navigate(['/auth']);
return;
}
if (answer.status == 403) {
this.showSnackBar('Du bist nicht für diese Aktion autorisiert', 'Ok');
return;
}
try {
let errorObject = answer.error.error;
if (errorObject.hasOwnProperty('message')) {
throw errorObject.message.toString();
}
if (errorObject.hasOwnProperty('code')) {
throw errorObject.code.toString();
}
} catch(error:any) {
this.showSnackBar(error.toString(), 'Ok');
}
}
private showSnackBar(message: string, action?: string) {
this.snackBar.open(message.toString(), action, {
duration: 3000,
});
}
}

0
src/assets/.gitkeep Normal file
View File

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 20C7.6 20 4 16.4 4 12S7.6 4 12 4 20 7.6 20 12 16.4 20 12 20M12 2C6.5 2 2 6.5 2 12S6.5 22 12 22 22 17.5 22 12 17.5 2 12 2M11 14H13V17H16V12H18L12 7L6 12H8V17H11V14" /></svg>

After

Width:  |  Height:  |  Size: 245 B

BIN
src/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

17
src/index.html Normal file
View File

@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Homepage</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">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
</head>
<body class="mat-typography mat-app-background">
<app-root></app-root>
</body>
</html>

7
src/main.ts Normal file
View File

@ -0,0 +1,7 @@
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));

13
src/styles.scss Normal file
View File

@ -0,0 +1,13 @@
body {
margin: 0;
height: 100vh;
}
.container {
padding: 0;
max-width: unset;
}
app-root {
height: 100%;
}

14
tsconfig.app.json Normal file
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"
]
}

33
tsconfig.json Normal file
View File

@ -0,0 +1,33 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true,
"strict": 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
}
}

14
tsconfig.spec.json Normal file
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"
]
}