first changes

This commit is contained in:
flo 2025-01-02 00:26:23 +01:00
parent 719a42ea1c
commit 25f0188dec
149 changed files with 2904 additions and 541 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

View File

@ -47,18 +47,19 @@
"src/favicon.ico"
],
"styles": [
"@angular/material/prebuilt-themes/deeppurple-amber.css",
"src/styles.scss"
],
"scripts": []
"scripts": [
"node_modules/flowbite/dist/flowbite.min.js"
]
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "800kb",
"maximumError": "2mb"
"maximumWarning": "5mb",
"maximumError": "10mb"
},
{
"type": "anyComponentStyle",
@ -69,9 +70,7 @@
"outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
@ -83,10 +82,10 @@
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"browserTarget": "template:build:production"
"buildTarget": "template:build:production"
},
"development": {
"browserTarget": "template:build:development"
"buildTarget": "template:build:development"
}
},
"defaultConfiguration": "development"
@ -94,7 +93,7 @@
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "template:build"
"buildTarget": "template:build"
}
},
"test": {
@ -110,7 +109,6 @@
"src/favicon.ico"
],
"styles": [
"@angular/material/prebuilt-themes/deeppurple-amber.scss",
"src/styles.scss"
],
"scripts": []

19
bin/script/firstRun 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/../../../)
source $ENV_DIR/bin/denv_msg
source $ENV_DIR/bin/drun
# Install node Packages
denv_echo_msg "[frontend]: NPM install"
drun frontend npm install
drun frontend npm install -g @angular/cli
denv_success_msg "[frontend]: NPM install done"
# Initial build of website
denv_echo_msg "[frontend]: NPM run build"
drun frontend npm run build
denv_success_msg "[frontend]: NPM run build done"

View File

@ -4,40 +4,10 @@ SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
PROJECT_DIR=$(realpath $SCRIPT_DIR/../../)
ENV_DIR=$(realpath $PROJECT_DIR/../../../)
EXIT=0
source $ENV_DIR/bin/denv_msg
# Check docker-compose.yml file
if [ ! -f "$PROJECT_DIR/docker/docker-compose.yml" ]
then
if [ ! -f "$PROJECT_DIR/docker/docker-compose.yml" ] ; then
cp "$PROJECT_DIR/docker/docker-compose.yml.dist" "$PROJECT_DIR/docker/docker-compose.yml"
EXIT=1
denv_info_msg "[frontend] Created docker-compose.yml"
fi
# Check docker-compose-mac.yml file
if [ ! -f "$PROJECT_DIR/docker/docker-compose-mac.yml" ]
then
cp "$PROJECT_DIR/docker/docker-compose-mac.yml.dist" "$PROJECT_DIR/docker/docker-compose-mac.yml"
EXIT=1
fi
if [ $EXIT -eq 1 ]
then
echo "docker-compose or env files created, please change variables and call init again"
exit 1
fi
# Source key-scripts
source $ENV_DIR/bin/drun
source $ENV_DIR/bin/dexec
# Build and start docker containers
dexec template-frontend build
dexec template-frontend up -d
# Install node Packages
drun template-frontend npm install
drun template-frontend npm install -g @angular/cli
# Initial build of website
drun template-frontend npm run build

29
bin/script/update Executable file
View File

@ -0,0 +1,29 @@
#!/bin/bash
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
PROJECT_DIR=$(realpath $SCRIPT_DIR/../../)
ENV_DIR=$(realpath $PROJECT_DIR/../../../)
CWD=$(pwd)
source $ENV_DIR/bin/denv_msg
source $ENV_DIR/bin/drun
# Pull branch in project directory
denv_echo_msg "[frontend]: Git pull"
cd "$PROJECT_DIR"
git pull
denv_success_msg "[frontend]: Git pull done"
# Install node Packages
denv_echo_msg "[frontend]: NPM install"
drun frontend npm install
denv_success_msg "[frontend]: NPM install done"
# Initial build of website
denv_echo_msg "[frontend]: NPM run build"
drun frontend npm run build
denv_success_msg "[frontend]: NPM run build done"
# Switch back to current working directory
cd "$CWD"

View File

@ -1,31 +0,0 @@
networks:
template:
external: true
services:
template-frontend-app:
image: template-frontend-app
networks:
- template
volumes:
- /Users/flo/dev/frontend/template/:/var/www/
build:
context: ./../
dockerfile: ./docker/npm/dockerfile
tty: true
template-frontend-nginx:
image: template-frontend-nginx
networks:
- template
volumes:
- /Users/flo/dev/frontend/template/:/var/www/html:z
build:
context: ./../
dockerfile: ./docker/nginx/dockerfile
labels:
- "traefik.http.routers.frontend.rule=Host(`template.local`)"
- "traefik.http.routers.frontend.entrypoints=websecure"
- "traefik.http.routers.frontend.tls.certresolver=le"
depends_on:
- template-frontend-app

View File

@ -9,24 +9,28 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^15.2.10",
"@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",
"@angular/animations": "^19.0.4",
"@angular/common": "^19.0.4",
"@angular/compiler": "^19.0.4",
"@angular/core": "^19.0.4",
"@angular/forms": "^19.0.4",
"@angular/material": "^18.2.4",
"@angular/platform-browser": "^19.0.4",
"@angular/platform-browser-dynamic": "^19.0.4",
"@angular/router": "^19.0.4",
"@ngx-translate/core": "^15.0.0",
"@ngx-translate/http-loader": "^8.0.0",
"angular-svg-icon": "^18.0.2",
"apexcharts": "^4.0.0",
"flowbite": "^2.5.1",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.12.0"
"zone.js": "~0.15.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^15.2.6",
"@angular/cli": "~15.2.6",
"@angular/compiler-cli": "^15.2.0",
"@angular-devkit/build-angular": "^19.0.5",
"@angular/cli": "~19.0.5",
"@angular/compiler-cli": "^19.0.4",
"@types/jasmine": "~4.3.0",
"jasmine-core": "~4.5.0",
"karma": "~6.4.0",
@ -34,7 +38,7 @@
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.0.0",
"tailwindcss": "^3.4.3",
"typescript": "~4.9.4"
"tailwindcss": "^3.4.10",
"typescript": "~5.6.3"
}
}

View File

@ -1 +1,5 @@
<router-outlet></router-outlet>
<div class="absolute bottom-16 md:bottom-0 w-full z-50">
<app-notification-bar />
</div>

View File

@ -1,9 +1,59 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnInit } from "@angular/core";
import { AuthService } from "./core/services/auth.service";
import { TranslateService } from "@ngx-translate/core";
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.scss"],
standalone: false
})
export class AppComponent {
export class AppComponent implements OnInit {
constructor(
private authService: AuthService,
private translate: TranslateService
) {
var language = navigator.language;
switch(language) {
case "en":
case "en_EN":
case "en-EN":
language = 'en';
break;
case "de":
case "de_DE":
case "de-DE":
language = 'de';
break;
case "es":
case "es_ES":
case "es-ES":
language = 'es';
break;
case "fr":
case "fr_FR":
case "fr-FR":
language = 'fr';
break;
case "tr":
case "tr_TR":
case "tr-TR":
language = 'tr';
break;
default:
language = 'en';
}
this.translate.setDefaultLang(language);
this.translate.use(language);
this.translate.onLangChange.subscribe(() => {
document.title = this.translate.instant("title");
});
}
ngOnInit(): void {
this.authService.readUserState();
}
}

View File

@ -1,30 +1,56 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { AppComponent } from './app.component';
import { RouterModule, Routes } from '@angular/router';
import { SharedModule } from './shared/shared.module';
import { HomeComponent } from './core/components/home/home.component';
import { CoreModule } from './core/core.module';
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { HttpClient, provideHttpClient, withInterceptorsFromDi } from "@angular/common/http";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { TranslateModule, TranslateLoader, TranslatePipe } from "@ngx-translate/core";
import { TranslateHttpLoader } from "@ngx-translate/http-loader";
import { AppComponent } from "./app.component";
import { RouterModule, Routes } from "@angular/router";
import { SharedModule } from "./shared/shared.module";
import { CoreModule } from "./core/core.module";
import { AngularSvgIconModule } from 'angular-svg-icon';
import { DatePipe } from "@angular/common";
const routes: Routes = [
{ path: 'home', component: HomeComponent },
{ path: '', redirectTo: 'home', pathMatch: 'full' },
{
path: "auth",
loadChildren: () =>
import("./core/auth/auth.module").then((m) => m.AuthModule),
},
{
path: "",
loadChildren: () =>
import("./core/home/home.module").then((m) => m.HomeModule),
}
];
export function createTranslateLoader(http: HttpClient) {
return new TranslateHttpLoader(http, "./assets/i18n/", ".json");
}
@NgModule({
declarations: [AppComponent],
bootstrap: [AppComponent],
imports: [
BrowserModule,
BrowserAnimationsModule,
HttpClientModule,
RouterModule,
RouterModule.forRoot(routes),
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: createTranslateLoader,
deps: [HttpClient],
},
}),
AngularSvgIconModule.forRoot(),
SharedModule,
CoreModule,
CoreModule
],
providers: [
provideHttpClient(withInterceptorsFromDi()),
TranslatePipe,
DatePipe
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}

View File

@ -1,21 +0,0 @@
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,57 @@
import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import { LoginComponent } from "./components/login/login.component";
import { RegistrationComponent } from "./components/registration/registration.component";
import { ConfirmRegistrationComponent } from "./components/confirm-registration/confirm-registration.component";
import { RouterModule, Routes } from "@angular/router";
import { ReactiveFormsModule } from "@angular/forms";
import { ForgotPasswordComponent } from "./components/forgot-password/forgot-password.component";
import { ResetPasswordComponent } from "./components/reset-password/reset-password.component";
import { TranslateModule } from "@ngx-translate/core";
import { SharedModule } from "src/app/shared/shared.module";
import { CoreModule } from "../core.module";
import { AuthComponent } from "./components/auth/auth.component";
import { AngularSvgIconModule } from "angular-svg-icon";
const routes: Routes = [
{
path: "",
component: AuthComponent,
children: [
{ path: "login", component: LoginComponent },
{ path: "registration", component: RegistrationComponent },
{ path: "forgot-password", component: ForgotPasswordComponent },
{ path: "registration/:registrationId", component: ConfirmRegistrationComponent },
{ path: "reset-password/:passwordToken", component: ResetPasswordComponent },
{ path: "**", redirectTo: "login" },
]
}
];
@NgModule({
declarations: [
LoginComponent,
RegistrationComponent,
ConfirmRegistrationComponent,
ForgotPasswordComponent,
ResetPasswordComponent,
AuthComponent,
],
exports: [
LoginComponent,
RegistrationComponent,
ConfirmRegistrationComponent,
ForgotPasswordComponent,
ResetPasswordComponent,
],
imports: [
RouterModule.forChild(routes),
AngularSvgIconModule,
CommonModule,
ReactiveFormsModule,
TranslateModule,
SharedModule,
CoreModule
],
})
export class AuthModule {}

View File

@ -0,0 +1,23 @@
<div class="absolute top-1 right-1 flex flex-col gap-1 text-skin-primary-muted">
<app-language-picker />
<app-theme-picker />
</div>
<div class="h-screen max-w-sm mx-auto flex flex-col">
<div class="grow"></div>
<div class="w-full">
<div class="mb-10 font-bold text-center">
<svg-icon src="assets/weed.svg" svgClass="w-24 h-24 md:w-32 md:h-32 mx-auto text-skin-accent mb-5" />
<h1 class="text-skin-primary text-5xl sweetleaf mb-5"> {{ "title" | translate }} </h1>
<h2 class="text-skin-accent text-xl"> {{ label | translate }} </h2>
</div>
<div class="max-w-sm mx-auto">
<router-outlet />
</div>
</div>
<div class="grow"></div>
</div>

View File

@ -0,0 +1,44 @@
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, NavigationEnd, Router } from "@angular/router";
import { initFlowbite } from "flowbite";
import { filter, map } from "rxjs";
import { AuthService } from "src/app/core/services/auth.service";
@Component({
selector: "app-auth",
templateUrl: "./auth.component.html",
styleUrls: ["./auth.component.scss"],
standalone: false
})
export class AuthComponent implements OnInit {
label: string = '';
constructor(
private authService: AuthService,
private route: ActivatedRoute,
private router: Router
) {
this.router.events
.pipe(
filter(event => event instanceof NavigationEnd),
map(() => this.route)
).subscribe(newRoute => {
if ((newRoute.firstChild?.routeConfig?.path ?? undefined) === undefined) {
this.label = '';
} else {
let routePart = newRoute.firstChild!.routeConfig!.path!;
let routeParts = routePart.split('/');
this.label = 'auth.' + routeParts[0];
}
});
this.authService.readUserState().subscribe(response => {
this.authService.currentState$.next(response);
this.router.navigateByUrl("/dashboard");
});
}
ngOnInit(): void {
initFlowbite();
}
}

View File

@ -0,0 +1,11 @@
<form class="max-w-sm mx-auto" [formGroup]="confirmRegistrationForm">
<div class="mb-5">
<shared-label for="password" label="auth.password" />
<shared-input key="password" inputType="password" required="true" />
</div>
<div class="mb-5">
<shared-label for="passwordConfirmation" label="auth.password-confirmation" />
<shared-input key="passwordConfirmation" inputType="password" required="true" />
</div>
<shared-submit label="auth.confirm-registration" (onSubmit)="confirm()" />
</form>

View File

@ -0,0 +1,42 @@
import { Component } from "@angular/core";
import { FormControl, FormGroup, Validators } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { AuthService } from "src/app/core/services/auth.service";
@Component({
selector: "app-confirm-registration",
templateUrl: "./confirm-registration.component.html",
styleUrls: ["./confirm-registration.component.scss"],
standalone: false
})
export class ConfirmRegistrationComponent {
confirmRegistrationForm = new FormGroup({
password: new FormControl("", [Validators.required]),
passwordConfirmation: new FormControl("", [Validators.required]),
});
registrationId: string | undefined;
constructor(
private authService: AuthService,
private activatedRoute: ActivatedRoute,
private router: Router
) {
this.activatedRoute.params.subscribe((params) => {
this.registrationId = params["registrationId"];
});
}
confirm(): void {
if (this.registrationId === undefined) return;
this.authService.confirmRegistration({
id: this.registrationId!,
password: this.confirmRegistrationForm.value.password!,
passwordConfirmation:
this.confirmRegistrationForm.value.passwordConfirmation!,
}).subscribe(response => {
this.router.navigateByUrl("/auth");
});
}
}

View File

@ -0,0 +1,7 @@
<form class="max-w-sm mx-auto" [formGroup]="forgotPasswordForm">
<div class="mb-5">
<shared-label for="email" label="auth.email" />
<shared-input key="mail" inputType="email" required="true" />
</div>
<shared-submit label="auth.reset-password" (onSubmit)="confirm()" />
</form>

View File

@ -0,0 +1,27 @@
import { Component } from "@angular/core";
import { FormControl, FormGroup, Validators } from "@angular/forms";
import { Router } from "@angular/router";
import { AuthService } from "src/app/core/services/auth.service";
@Component({
selector: "app-forgot-password",
templateUrl: "./forgot-password.component.html",
styleUrls: ["./forgot-password.component.scss"],
standalone: false
})
export class ForgotPasswordComponent {
forgotPasswordForm = new FormGroup({
mail: new FormControl("", [Validators.required, Validators.email]),
});
constructor(private authService: AuthService, private router: Router) {
}
confirm(): void {
this.authService.forgotPassword({
mail: this.forgotPasswordForm.value.mail!,
}).subscribe(response => {
this.router.navigateByUrl("/auth");
});
}
}

View File

@ -0,0 +1,23 @@
<form class="max-w-sm mx-auto" [formGroup]="loginForm">
<div class="mb-5">
<shared-label for="identifier" label="auth.username-or-email" />
<shared-input key="identifier" inputType="text" required="true" />
<p class="mt-2 text-sm text-skin-primary-muted">
{{ "auth.not-yet-registered" | translate }}
<a routerLink="/auth/registration" class="font-medium text-skin-accent hover:underline hover:font-bold">
{{ "auth.register-now" | translate }}
</a>
</p>
</div>
<div class="mb-5">
<shared-label for="password" label="auth.password" />
<shared-input key="password" inputType="password" required="true" />
<p class="mt-2 text-sm text-skin-primary-muted">
<a routerLink="/auth/forgot-password" class="font-medium text-skin-accent hover:underline hover:font-bold">
{{ "auth.forgot-password" | translate }}?
</a>
</p>
</div>
<shared-submit label="auth.login" (onSubmit)="login()" />
</form>

View File

@ -0,0 +1,29 @@
import { Component } from "@angular/core";
import { FormControl, FormGroup, Validators } from "@angular/forms";
import { Router } from "@angular/router";
import { AuthService } from "src/app/core/services/auth.service";
@Component({
selector: "app-login",
templateUrl: "./login.component.html",
styleUrls: ["./login.component.scss"],
standalone: false
})
export class LoginComponent {
loginForm = new FormGroup({
identifier: new FormControl("", [Validators.required]),
password: new FormControl("", [Validators.required]),
});
constructor(private authService: AuthService, private router: Router) {
}
login(): void {
this.authService.login({
identifier: this.loginForm.value.identifier!,
password: this.loginForm.value.password!,
}).subscribe(response => {
this.router.navigateByUrl("/dashboard");
});
}
}

View File

@ -0,0 +1,17 @@
<form class="max-w-sm mx-auto" [formGroup]="registrationForm">
<div class="mb-5">
<shared-label for="mail" label="auth.email" />
<shared-input key="mail" inputType="email" required="true" placeholder="your@email.com" />
<p class="mt-2 text-sm text-skin-primary-muted">
{{ "auth.already-registered" | translate }}
<a routerLink="/auth/login" class="font-medium text-skin-accent hover:underline hover:font-bold">
{{ "auth.login-now" | translate }}
</a>
</p>
</div>
<div class="mb-5">
<shared-label for="username" label="auth.username" />
<shared-input key="username" inputType="string" required="true" />
</div>
<shared-submit label="auth.register" (onSubmit)="register()" />
</form>

View File

@ -0,0 +1,29 @@
import { Component } from "@angular/core";
import { FormControl, FormGroup, Validators } from "@angular/forms";
import { Router } from "@angular/router";
import { AuthService } from "src/app/core/services/auth.service";
@Component({
selector: "app-registration",
templateUrl: "./registration.component.html",
styleUrls: ["./registration.component.scss"],
standalone: false
})
export class RegistrationComponent {
registrationForm = new FormGroup({
mail: new FormControl("", [Validators.required]),
username: new FormControl("", [Validators.required]),
});
constructor(private authService: AuthService, private router: Router) {
}
register(): void {
this.authService.register({
mail: this.registrationForm.value.mail!,
username: this.registrationForm.value.username!,
}).subscribe(response => {
this.router.navigateByUrl("/auth");
});
}
}

View File

@ -0,0 +1,11 @@
<form class="max-w-sm mx-auto" [formGroup]="resetPasswordForm">
<div class="mb-5">
<shared-label for="newPassword" label="auth.new-password" />
<shared-input key="newPassword" inputType="password" required="true" />
</div>
<div class="mb-5">
<shared-label for="passwordConfirmation" label="auth.password-confirmation" />
<shared-input key="passwordConfirmation" inputType="password" required="true" />
</div>
<shared-submit label="auth.reset-password" (onSubmit)="confirm()" />
</form>

View File

@ -0,0 +1,43 @@
import { Component } from "@angular/core";
import { FormControl, FormGroup, Validators } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { initFlowbite } from "flowbite";
import { filter } from "rxjs";
import { AuthService } from "src/app/core/services/auth.service";
@Component({
selector: "app-reset-password",
templateUrl: "./reset-password.component.html",
styleUrls: ["./reset-password.component.scss"],
standalone: false
})
export class ResetPasswordComponent {
resetPasswordForm = new FormGroup({
newPassword: new FormControl("", [Validators.required]),
passwordConfirmation: new FormControl("", [Validators.required]),
});
passwordToken: string | undefined;
constructor(
private authService: AuthService,
private activatedRoute: ActivatedRoute,
private router: Router
) {
this.activatedRoute.params.subscribe((params) => {
this.passwordToken = params["passwordToken"];
});
}
confirm(): void {
if (this.passwordToken === undefined) return;
this.authService.resetPassword({
passwordToken: this.passwordToken!,
newPassword: this.resetPasswordForm.value.newPassword!,
passwordConfirmation: this.resetPasswordForm.value.passwordConfirmation!,
}).subscribe(response => {
this.router.navigateByUrl("/auth");
});
}
}

View File

@ -1,5 +0,0 @@
<app-navigation></app-navigation>
<div>
Home
</div>

View File

@ -1,12 +0,0 @@
import { Component } from '@angular/core';
import { RequestService } from 'src/app/core/services/request.service';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss'],
})
export class HomeComponent {
constructor(public requestService: RequestService) {
}
}

View File

@ -0,0 +1,23 @@
<form class="block">
<div class="flex bg-skin-primary-muted hover:bg-skin-secondary rounded py-2 px-3">
<button id="states-button" data-dropdown-toggle="dropdown-states" class="w-full flex gap-2 inline-flex items-center font-medium text-center rounded-lg font-semibold hover:font-bold" type="button">
<div class="flex-grow text-md text-left">{{ "languages." + selectedLanguage.label | translate}}</div>
<svg-icon svgClass="w-4 h-4" [src]="'/assets/' + selectedLanguage.label + '.svg'" />
</button>
<div id="dropdown-states" class="hidden text-skin-primary-muted bg-skin-primary-muted divide-y divide-skin-primary rounded-lg shadow min-w-44 border border-skin-primary">
<ul class="p-2 text-md" aria-labelledby="states-button">
<li *ngFor="let language of languages">
<button type="button" class="p-2 w-full hover:text-skin-primary hover:bg-skin-accent rounded-lg font-semibold hover:font-bold" (click)="selectLanguage(language)">
<div class="flex flex-row items-center gap-2">
<div class="flex-grow text-left">
{{ "languages." + language.label | translate }}
</div>
<svg-icon svgClass="w-4 h-4" [src]="'/assets/' + language.label + '.svg'" />
</div>
</button>
</li>
</ul>
</div>
</div>
</form>

View File

@ -0,0 +1,67 @@
import { Component, OnInit } from "@angular/core";
import { initFlowbite } from "flowbite";
import { AppService } from "../../services/app.service";
import { TranslateService } from "@ngx-translate/core";
interface Language {
imageSrc: string;
label: string;
}
@Component({
selector: "app-language-picker",
templateUrl: "./language-picker.component.html",
styleUrls: ["./language-picker.component.scss"],
standalone: false
})
export class LanguagePickerComponent implements OnInit {
languages: Language[] = [
{
label: "de",
imageSrc: "assets/de.svg"
},
{
label: "en",
imageSrc: "assets/en.svg"
},
{
label: "tr",
imageSrc: "assets/tr.svg"
},
{
label: "es",
imageSrc: "assets/es.svg"
},
{
label: "fr",
imageSrc: "assets/fr.svg"
},
{
label: "it",
imageSrc: "assets/it.svg"
},
{
label: "jp",
imageSrc: "assets/jp.svg"
},
];
selectedLanguage: Language;
constructor(
private appService: AppService,
private translate: TranslateService,
) {
this.selectedLanguage = this.languages.find((f) => {
f.label === translate.currentLang;
}) ?? this.languages[0];
}
ngOnInit(): void {
initFlowbite();
}
selectLanguage(language: Language): void {
this.translate.use(language.label);
this.selectedLanguage = language;
}
}

View File

@ -1,5 +0,0 @@
<a routerLink="/home">
<h1 class="w-full bg-zinc-700 p-5 text-white text-6xl font-bold text-center">
Navigation
</h1>
</a>

View File

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

View File

@ -0,0 +1,42 @@
<div
*ngFor="let item of items"
class="flex items-center gap-3 opacity-90 p-2 mb-3 rounded-lg text-skin-primary bg-skin-primary-muted md:max-w-3xl mx-5 md:mx-auto shadow-sm shadow-skin-primary"
>
<svg
class="flex-shrink-0 w-4 h-4 text-skin-accent"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z"
/>
</svg>
<span class="sr-only">{{ item.element.type }}</span>
<div class="grow text-center font-bold text-skin-primary">
{{ item.element.message }}
</div>
<button
type="button"
class="bg-skin-accent-muted rounded-lg p-2 hover:bg-skin-accent inline-flex items-center justify-center h-8 w-8"
(click)="dismiss(item)"
>
<span class="sr-only">Close</span>
<svg
class="w-3 h-3"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 14 14"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"
/>
</svg>
</button>
</div>

View File

@ -0,0 +1,70 @@
import { Component } from "@angular/core";
import { NotificationService } from "../../services/notification.service";
export interface NotificationElement {
message: string;
type: "info" | "danger" | "warn" | "success";
timeout: number | undefined;
}
interface NotificationItem {
element: NotificationElement;
shownAt: Date;
}
@Component({
selector: "app-notification-bar",
templateUrl: "./notification-bar.component.html",
styleUrls: ["./notification-bar.component.scss"],
standalone: false
})
export class NotificationBarComponent {
items: NotificationItem[] = [];
isRunning: boolean;
constructor(private notificationService: NotificationService) {
this.isRunning = false;
this.notificationService.notification$.subscribe((notification) => {
if (notification !== undefined) {
this.items.push({
element: notification,
shownAt: new Date(),
});
this.startTimeoutIfNecessary();
}
});
}
startTimeoutIfNecessary(): void {
if (this.isRunning === true) {
return;
}
let intervalId = setInterval(() => {
this.isRunning = true;
let now = new Date();
this.items.forEach((item) => {
if (item.element.timeout !== undefined) {
let timeout = new Date(
item.shownAt.getTime() + item.element.timeout * 1000
);
if (timeout < now) {
this.dismiss(item);
}
}
});
if (this.items.length === 0) {
clearInterval(intervalId);
this.isRunning = false;
}
}, 1000);
}
dismiss(item: NotificationItem): void {
this.items = this.items.filter((i) => i !== item);
}
}

View File

@ -0,0 +1,23 @@
<form class="block">
<div class="flex bg-skin-primary-muted hover:bg-skin-secondary rounded py-2 px-3">
<button id="themes-button" data-dropdown-toggle="dropdown-themes" class="w-full flex gap-2 inline-flex items-center font-medium text-center rounded-lg font-semibold hover:font-bold" type="button">
<div class="flex-grow text-md text-left">{{ "menu.theme." + selectedTheme.label | translate}}</div>
<svg-icon svgClass="w-4 h-4" [src]="'/assets/' + selectedTheme.label + '.svg'" />
</button>
<div id="dropdown-themes" class="hidden text-skin-primary-muted bg-skin-primary-muted divide-y divide-skin-primary rounded-lg shadow min-w-44 border border-skin-primary">
<ul class="p-2 text-md" aria-labelledby="themes-button">
<li *ngFor="let theme of themes">
<button type="button" class="p-2 w-full hover:text-skin-primary hover:bg-skin-accent rounded-lg font-semibold hover:font-bold" (click)="selectTheme(theme)">
<div class="flex flex-row items-center gap-2">
<div class="flex-grow text-left">
{{ "menu.theme." + theme.label | translate }}
</div>
<svg-icon svgClass="w-4 h-4" [src]="'/assets/' + theme.label + '.svg'" />
</div>
</button>
</li>
</ul>
</div>
</div>
</form>

View File

@ -0,0 +1,46 @@
import { Component, OnInit } from "@angular/core";
import { initFlowbite } from "flowbite";
import { AppService } from "../../services/app.service";
import { TranslateService } from "@ngx-translate/core";
interface Theme {
imageSrc: string;
label: string;
}
@Component({
selector: "app-theme-picker",
templateUrl: "./theme-picker.component.html",
styleUrls: ["./theme-picker.component.scss"],
standalone: false
})
export class ThemePickerComponent implements OnInit {
themes: Theme[] = [
{
label: "dark",
imageSrc: "assets/dark.svg"
},
{
label: "light",
imageSrc: "assets/light.svg"
}
];
selectedTheme: Theme;
constructor(
private appService: AppService,
) {
this.selectedTheme = appService.darkMode ? this.themes[0] : this.themes[1];
}
ngOnInit(): void {
initFlowbite();
}
selectTheme(theme: Theme): void {
if (this.selectedTheme != theme) {
this.appService.toggleDarkMode();
this.selectedTheme = theme;
}
}
}

View File

@ -1,15 +1,44 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HomeComponent } from './components/home/home.component';
import { NavigationComponent } from './components/navigation/navigation.component';
import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import { AuthGuard } from "./guards/auth.guard";
import { AuthService } from "./services/auth.service";
import { RequestService } from "./services/request.service";
import { AppService } from "./services/app.service";
import { SharedModule } from "../shared/shared.module";
import { RouterModule } from "@angular/router";
import { ReactiveFormsModule } from "@angular/forms";
import { NotificationBarComponent } from "./components/notification-bar/notification-bar.component";
import { NotificationService } from "./services/notification.service";
import { TranslateModule } from "@ngx-translate/core";
import { AngularSvgIconModule } from 'angular-svg-icon';
import { LanguagePickerComponent } from "./components/language-picker/language-picker.component";
import { ThemePickerComponent } from "./components/theme-picker/theme-picker.component";
@NgModule({
declarations: [HomeComponent, NavigationComponent],
exports: [HomeComponent, NavigationComponent],
declarations: [
NotificationBarComponent,
LanguagePickerComponent,
ThemePickerComponent,
],
exports: [
NotificationBarComponent,
LanguagePickerComponent,
ThemePickerComponent,
],
imports: [
CommonModule
]
AngularSvgIconModule,
CommonModule,
SharedModule,
RouterModule,
ReactiveFormsModule,
TranslateModule,
],
providers: [
AuthGuard,
AuthService,
RequestService,
AppService,
NotificationService,
],
})
export class CoreModule { }
export class CoreModule {}

View File

@ -0,0 +1,24 @@
import { Injectable } from "@angular/core";
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot, UrlTree } from "@angular/router";
import { filter, map, Observable } from "rxjs";
import { AuthService } from "../services/auth.service";
@Injectable()
export class AuthGuard {
constructor(private authService: AuthService, private router: Router) {}
canActivate(): Observable<boolean> | boolean {
return this.authService.currentState$
.pipe(
map(
(currentState) => {
if (currentState === null) {
this.router.navigateByUrl("/auth");
return false;
}
return true;
}
)
);
}
}

View File

@ -0,0 +1,9 @@
<app-navigation style="z-index: 10000; position:relative;"></app-navigation>
<div class="mx-auto p-3 lg:p-5 flex flex-row">
<div class="lg:pl-64 pt-16 lg:pt-24 pb-16 lg:pb-0 w-full">
<div id="content" class="basis-full">
<router-outlet></router-outlet>
</div>
</div>
</div>

View File

@ -0,0 +1,9 @@
import { Component } from "@angular/core";
@Component({
selector: "app-home",
templateUrl: "./home.component.html",
styleUrls: ["./home.component.scss"],
standalone: false
})
export class HomeComponent {}

View File

@ -0,0 +1,79 @@
<!-- Title Bar -->
<div class="bg-skin-accent fixed top-0 w-full border-b border-skin-primary z-50">
<div class="h-16 lg:h-24 flex flex-wrap items-center justify-between mx-auto px-3 lg:px-5">
<a routerLink="/" class="text-skin-primary hover:text-skin-primary-muted flex items-center space-x-3 p-2">
<svg-icon src="assets/template.svg" svgClass="w-12 h-12 lg:w-16 lg:h-16 mr-2 lg:mr-3" />
<span class="self-center text-4xl lg:text-5xl font-bold whitespace-nowrap sweetleaf pt-2">
{{ "title" | translate }}
</span>
</a>
<!--User Bubble-->
<button type="button" id="user-menu-button" class="inline-flex items-center p-2 w-10 h-10 lg:w-12 lg:h-12 justify-center text-sm lg:text-lg text-skin-primary-muted hover:text-skin-primary rounded-lg bg-skin-primary-muted uppercase font-semibold"
aria-expanded="false"
data-dropdown-toggle="user-dropdown"
>
<span *ngIf="state !== undefined && state !== null">
{{ state?.username[0] + state?.roleIdentifier[0] }}
</span>
<span *ngIf="state === undefined || state === null">
G
</span>
</button>
<!-- User dropdown -->
<div id="user-dropdown" class="hidden min-w-full lg:min-w-64 px-4 z-50">
<div class="rounded-lg border border-skin-primary shadow-sm shadow-skin-primary bg-skin-primary-muted p-3">
<div class="rounded-lg bg-skin-secondary flex flex-col text-skin-primary p-2 mb-2">
<div *ngIf="state !== undefined && state !== null">
<span class="block text-lg font-bold">{{state.username}}</span>
<span class="block text-sm text-skin-primary-muted truncate">{{state.roleIdentifier}}</span>
</div>
<div *ngIf="state === undefined || state === null">
<span class="block text-lg font-bold">{{"menu.guest" | translate}}</span>
</div>
</div>
<div class="flex flex-col text-skin-primary-muted">
<div>
<ul aria-labelledby="user-menu-button">
<li *ngFor="let usermenuButton of usermenuButtons">
<button
*ngIf="usermenuButton.isVisible === undefined || usermenuButton.isVisible()"
class="block py-2 px-3 rounded text-md text-left hover:text-skin-primary hover:bg-skin-accent w-full font-semibold hover:font-bold"
[routerLink]="usermenuButton.routerLink === undefined ? null : [usermenuButton.routerLink]"
(click)="clickUserMenuButtonLabel(usermenuButton)">
{{ getUserMenuButtonLabel(usermenuButton) | translate }}
</button>
</li>
</ul>
</div>
<div>
<app-theme-picker />
<app-language-picker />
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Navbar -->
<nav class="fixed inline bottom-0 lg:top-24 z-2 w-full lg:w-64 h-16 lg:h-screen bg-skin-primary-muted" aria-label="Sidebar">
<ul class="text-xl p-1 lg:p-4 rounded-lg flex flex-row lg:flex-col">
<li *ngFor="let navigationLink of navigationLinks" class="flex-1">
<div *ngIf="navigationLink.isVisible === undefined || navigationLink.isVisible()" class="text-skin-primary-muted font-semibold mb-2">
<a [routerLink]="navigationLink.routerLink"
class="block p-1 lg:p-3 rounded hover:text-skin-secondary hover:bg-skin-accent text-md lg:text-xl"
routerLinkActive="text-skin-primary font-bold underline">
<div class="flex flex-col lg:flex-row gap-1 lg:gap-5">
<svg-icon *ngIf="navigationLink.imageSrc !== undefinded" [src]="navigationLink.imageSrc" svgClass="w-6 h-6 lg:w-8 lg:h-8 m-auto basis-auto" />
<div class="m-auto basis-full text-sm lg:text-xl">
{{ navigationLink.label | translate }}
</div>
</div>
</a>
</div>
</li>
</ul>
</nav>

View File

@ -0,0 +1,121 @@
import { Component, OnInit } from "@angular/core";
import { initFlowbite } from "flowbite";
import { Router } from "@angular/router";
import { StateResponse } from "src/app/core/models/user-state-request.model";
import { AuthService } from "src/app/core/services/auth.service";
interface NavigationLink {
imageSrc?: string;
label: string;
routerLink: string;
isVisible?: Function | undefined;
}
interface UserMenuButton {
label: Function | string;
routerLink?: string | undefined;
clickCallback?: Function | undefined;
isVisible?: Function | undefined;
}
@Component({
selector: "app-navigation",
templateUrl: "./navigation.component.html",
styleUrls: ["./navigation.component.scss"],
standalone: false
})
export class NavigationComponent implements OnInit {
navigationLinks: NavigationLink[] = [
{
routerLink: "/dashboard",
imageSrc: "assets/template.svg",
label: "navigation.home",
},
{
routerLink: "/location",
imageSrc: "assets/lightbulb.svg",
label: "navigation.locations",
isVisible: () => this.state?.roleIdentifier === 'admin'
},
{
routerLink: "/plant",
imageSrc: "assets/template.svg",
label: "navigation.plants",
isVisible: () => this.state?.roleIdentifier === 'admin'
},
{
routerLink: "/device",
imageSrc: "assets/plug.svg",
label: "navigation.devices",
isVisible: () => this.state?.roleIdentifier === 'admin'
},
{
routerLink: "/admin",
imageSrc: "assets/admin.svg",
label: "navigation.admin",
isVisible: () => this.state?.roleIdentifier === 'admin'
},
];
usermenuButtons: UserMenuButton[] = [
{
label: () => "menu.settings",
routerLink: "/settings",
},
{
label: () => "menu.login",
clickCallback: () => {
this.router.navigateByUrl('/auth');
},
isVisible: () => this.state === undefined
},
{
label: () => "menu.logout",
clickCallback: () => {
this.logout();
},
isVisible: () => this.state !== undefined
},
];
state: StateResponse | undefined;
constructor(
private authService: AuthService,
private router: Router,
) {
this.state = undefined;
this.authService.readUserState().subscribe(response => {
this.authService.currentState$.next(response);
this.state = response;
});
}
ngOnInit(): void {
initFlowbite();
}
logout(): void {
this.authService
.logout()
.subscribe( response => {
this.state = undefined;
this.router.navigateByUrl('/dashboard')
});
}
clickUserMenuButtonLabel(button: UserMenuButton) {
if (button.clickCallback !== undefined) {
button.clickCallback();
}
}
getUserMenuButtonLabel(button: UserMenuButton) {
if (typeof button.label === "function") {
return button.label();
} else {
return button.label;
}
}
}

View File

@ -0,0 +1,22 @@
<div class="rounded-lg bg-skin-primary-muted p-5 flex flex-col gap-2">
<div *ngIf="state !== undefined" class="inline-block text-skin-primary bg-skin-accent rounded-lg p-2">
<div class="font-bold">
{{ state.username }}
</div>
<div>
{{ state.roleIdentifier }}
</div>
</div>
<shared-tabs [tabs]="[
{ label: 'settings.tab-profile', selector: 'tabProfile' },
{ label: 'settings.tab-security', selector: 'tabSecurity' }
]">
<ng-template tabSelector="tabProfile">
<app-tab-profile />
</ng-template>
<ng-template tabSelector="tabSecurity">
<app-tab-security />
</ng-template>
</shared-tabs>
</div>

View File

@ -0,0 +1,22 @@
import { Component, OnInit } from "@angular/core";
import { initFlowbite } from "flowbite";
import { StateResponse } from "src/app/core/models/user-state-request.model";
import { AuthService } from "src/app/core/services/auth.service";
@Component({
selector: "app-settings",
templateUrl: "./settings.component.html",
styleUrls: ["./settings.component.scss"],
standalone: false
})
export class SettingsComponent implements OnInit {
state?: StateResponse;
constructor(private authService: AuthService) {
this.authService.currentState$.subscribe(state => {this.state = state});
}
ngOnInit(): void {
initFlowbite();
}
}

View File

@ -0,0 +1,4 @@
<!--
<shared-table [items]="items" [columns]="columns" [rowActions]="rowActions">
</shared-table>
-->

View File

@ -0,0 +1,18 @@
import { Component } from "@angular/core";
interface Product {
description: string;
id: string;
price: number;
stock: number;
}
@Component({
selector: "app-tab-profile",
templateUrl: "./tab-profile.component.html",
styleUrls: ["./tab-profile.component.scss"],
standalone: false
})
export class TabProfileComponent {
constructor() {}
}

View File

@ -0,0 +1,33 @@
<div class="flex flex-col gap-2 text-skin-primary">
<shared-group label="settings.security.change-password" size="medium">
<form [formGroup]="changePasswordForm">
<div class="mb-5">
<shared-label for="password" label="settings.security.current-password" />
<shared-input key="password" inputType="password" required="true" />
</div>
<div class="mb-5">
<shared-label for="newPassword" label="settings.security.new-password" />
<shared-input key="newPassword" inputType="password" required="true" />
</div>
<div class="mb-5">
<shared-label for="newPasswordConfirmation" label="settings.security.password-confirmation" />
<shared-input key="newPasswordConfirmation" inputType="password" required="true" />
</div>
<shared-submit label="settings.security.change-password" (onSubmit)="changePassword()" />
</form>
</shared-group>
<shared-group label="settings.security.change-username" size="medium">
<form [formGroup]="changeUsernameForm">
<div class="mb-5">
<shared-label for="password" label="settings.security.current-password" />
<shared-input key="password" inputType="password" required="true" />
</div>
<div class="mb-5">
<shared-label for="newUsername" label="settings.security.new-username" />
<shared-input key="newUsername" inputType="string" required="true" />
</div>
<shared-submit label="settings.security.change-username" (onSubmit)="changeUsername()"/>
</form>
</shared-group>
</div>

View File

@ -0,0 +1,41 @@
import { Component } from "@angular/core";
import { FormControl, FormGroup, Validators } from "@angular/forms";
import { AuthService } from "src/app/core/services/auth.service";
@Component({
selector: "app-tab-security",
templateUrl: "./tab-security.component.html",
styleUrls: ["./tab-security.component.scss"],
standalone: false
})
export class TabSecurityComponent {
changePasswordForm = new FormGroup({
password: new FormControl("", [Validators.required]),
newPassword: new FormControl("", [Validators.required]),
newPasswordConfirmation: new FormControl("", [Validators.required]),
});
changeUsernameForm = new FormGroup({
password: new FormControl("", [Validators.required]),
newUsername: new FormControl("", [Validators.required]),
});
constructor(private authService: AuthService) {}
changePassword(): void {
if (this.changePasswordForm.value.newPassword !== this.changePasswordForm.value.newPasswordConfirmation)
return;
this.authService.changePassword({
password: this.changePasswordForm.value.password!,
newPassword: this.changePasswordForm.value.newPassword!,
}).subscribe(response => {});
}
changeUsername(): void {
this.authService.changeUsername({
password: this.changeUsernameForm.value.password!,
newUsername: this.changeUsernameForm.value.newUsername!,
}).subscribe(response => {});
}
}

View File

@ -0,0 +1,82 @@
import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import { RouterModule, Routes } from "@angular/router";
import { ReactiveFormsModule } from "@angular/forms";
import { TranslateModule } from "@ngx-translate/core";
import { SharedModule } from "src/app/shared/shared.module";
import { AngularSvgIconModule } from "angular-svg-icon";
import { HomeComponent } from "./components/home/home.component";
import { AuthGuard } from "../guards/auth.guard";
import { SettingsComponent } from "./components/settings/settings.component";
import { TabProfileComponent } from "./components/settings/tabs/tab-profile/tab-profile.component";
import { TabSecurityComponent } from "./components/settings/tabs/tab-security/tab-security.component";
import { NavigationComponent } from "./components/navigation/navigation.component";
import { CoreModule } from "../core.module";
const routes: Routes = [
{
path: "",
component: HomeComponent,
children: [
{
path: "settings",
canActivate: [AuthGuard],
component: SettingsComponent
},
{
path: "dashboard",
loadChildren: () =>
import('../../feature/dashboard/dashboard.module').then((m) => m.DashboardModule)
},
{
path: "plant",
canActivate: [AuthGuard],
loadChildren: () =>
import('../../feature/plant/plant.module').then((m) => m.PlantModule)
},
{
path: "device",
canActivate: [AuthGuard],
loadChildren: () =>
import('../../feature/device/device.module').then((m) => m.DeviceModule)
},
{
path: "location",
canActivate: [AuthGuard],
loadChildren: () =>
import('../../feature/location/location.module').then((m) => m.LocationModule)
},
{
path: "admin",
canActivate: [AuthGuard],
loadChildren: () =>
import('../../feature/admin/admin.module').then((m) => m.AdminModule)
},
{
path: '**',
redirectTo: 'dashboard',
pathMatch: 'full'
}
],
},
];
@NgModule({
declarations: [
SettingsComponent,
NavigationComponent,
TabProfileComponent,
TabSecurityComponent,
HomeComponent
],
imports: [
RouterModule.forChild(routes),
AngularSvgIconModule,
CommonModule,
ReactiveFormsModule,
TranslateModule,
SharedModule,
CoreModule
]
})
export class HomeModule {}

View File

@ -0,0 +1,7 @@
export interface ChangePasswordRequest {
password: string;
newPassword: string;
}
export interface ChangePasswordResponse {
}

View File

@ -0,0 +1,7 @@
export interface ChangeUsernameRequest {
password: string;
newUsername: string;
}
export interface ChangeUsernameResponse {
}

View File

@ -0,0 +1,12 @@
export interface ConfirmRegistrationRequest {
id: string;
password: string;
passwordConfirmation: string;
}
export interface ConfirmRegistrationResponse {
id: string;
username: string;
roleIdentifier: "admin"|"user";
permissions: string[];
}

View File

@ -0,0 +1,6 @@
export interface ForgotPasswordRequest {
mail: string;
}
export interface ForgotPasswordResponse {
}

View File

@ -0,0 +1,8 @@
export interface LoginUserRequest {
identifier: string;
password: string;
}
export interface LoginUserResponse {
sessionId: string;
}

View File

@ -0,0 +1,6 @@
export interface LogoutUserRequest {
}
export interface LogoutUserResponse {
response?: string|undefined;
}

View File

@ -0,0 +1,8 @@
export interface RegisterUserRequest {
username: string;
mail: string;
}
export interface RegisterUserResponse {
response?: string|undefined;
}

View File

@ -0,0 +1,8 @@
export interface ResetPasswordRequest {
passwordToken: string;
newPassword: string;
passwordConfirmation: string;
}
export interface ResetPasswordResponse {
}

View File

@ -0,0 +1,13 @@
export interface StateRequest {
}
export interface StateResponse {
id: string;
username: string;
roleIdentifier: any|null;
permissions: string[];
createdAt: string;
updatedAt: string;
sessionId: string;
role?: "admin"|"user"|undefined;
}

View File

@ -0,0 +1,30 @@
import { Injectable } from "@angular/core";
@Injectable()
export class AppService {
darkMode: boolean;
constructor() {
this.darkMode =
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches;
this.applyDarkMode();
}
private applyDarkMode(): void {
if (this.darkMode) {
document.documentElement.classList.add("theme-dark");
} else {
document.documentElement.classList.remove("theme-dark");
}
}
toggleDarkMode() {
this.darkMode = !this.darkMode;
this.applyDarkMode();
}
getDarkMode(): boolean {
return this.darkMode;
}
}

View File

@ -0,0 +1,122 @@
import { Injectable } from "@angular/core";
import { RequestService } from "./request.service";
import { StateResponse } from "../models/user-state-request.model";
import { BehaviorSubject, catchError, Observable } from "rxjs";
import { LoginUserRequest, LoginUserResponse } from "../models/login-request.model";
import { Router } from "@angular/router";
import {
RegisterUserRequest,
RegisterUserResponse,
} from "../models/register-user-request.model";
import {
ConfirmRegistrationRequest,
ConfirmRegistrationResponse,
} from "../models/confirm-registration-request.model";
import {
ResetPasswordRequest,
ResetPasswordResponse,
} from "../models/reset-password-request.model";
import {
ForgotPasswordRequest,
ForgotPasswordResponse,
} from "../models/forgot-password-request.model";
import {
ChangePasswordRequest,
ChangePasswordResponse,
} from "../models/change-password-request.model";
import {
ChangeUsernameRequest,
ChangeUsernameResponse,
} from "../models/change-username-request.model";
import { LogoutUserResponse } from "../models/logout-request.model";
@Injectable()
export class AuthService {
currentState$ = new BehaviorSubject<StateResponse | undefined>(
undefined
);
constructor(private requestService: RequestService, private router: Router) {}
readUserState()
{
return this.requestService.request<StateResponse>(
'get',
this.requestService.obtainUrl("user/state"),
{}
)
.pipe(
catchError(
error => {
this.currentState$.next(undefined);
throw 'User State not readable';
}
)
);
}
logout(): Observable<LogoutUserResponse> {
this.currentState$.next(undefined);
return this.requestService.call<LogoutUserResponse>(
"get",
"auth/logout-user",
{}
);
}
login(body: LoginUserRequest): Observable<LoginUserResponse> {
return this.requestService.call<LoginUserResponse>(
'post',
"auth/login-user",
body
);
}
register(body: RegisterUserRequest): Observable<RegisterUserResponse> {
return this.requestService.call<RegisterUserResponse>(
"post",
"auth/register-user",
body,
);
}
confirmRegistration(body: ConfirmRegistrationRequest): Observable<ConfirmRegistrationResponse> {
return this.requestService.call<ConfirmRegistrationResponse>(
'post',
'auth/confirm-registration',
body,
);
}
resetPassword(body: ResetPasswordRequest): Observable<ResetPasswordResponse> {
return this.requestService.call<ResetPasswordResponse>(
"post",
"auth/reset-password",
body,
);
}
forgotPassword(body: ForgotPasswordRequest): Observable<ForgotPasswordResponse> {
return this.requestService.call<ForgotPasswordResponse>(
'post',
"auth/forgot-password",
body,
);
}
changePassword(body: ChangePasswordRequest): Observable<ChangePasswordResponse> {
return this.requestService.call<ChangePasswordResponse>(
'post',
"user/change-password",
body,
);
}
changeUsername(body: ChangeUsernameRequest): Observable<ChangeUsernameResponse> {
return this.requestService.call<ChangeUsernameResponse>(
'post',
"user/change-username",
body,
);
}
}

View File

@ -0,0 +1,32 @@
import { Injectable } from "@angular/core";
import { BehaviorSubject, Observable, Subject } from "rxjs";
import { NotificationElement } from "../components/notification-bar/notification-bar.component";
@Injectable()
export class NotificationService {
notification$ = new Subject<NotificationElement>();
push(message: string, type: "info" | "danger" | "warn" | "success"): void {
let timeout = undefined;
switch (type) {
case "info":
timeout = 5;
break;
case "danger":
timeout = 10;
break;
case "warn":
timeout = 7;
break;
case "success":
timeout = 3;
break;
}
this.notification$.next({
message: message,
type: type,
timeout: timeout,
});
}
}

View File

@ -1,105 +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';
import { HttpClient } from "@angular/common/http";
import { Router } from "@angular/router";
import { Injectable } from "@angular/core";
import { NotificationService } from "./notification.service";
import { TranslateService } from "@ngx-translate/core";
import { Observable, catchError, first, map, take } from "rxjs";
@Injectable({
providedIn: 'root'
providedIn: "root",
})
export class RequestService {
constructor(
private http: HttpClient,
private router: Router,
private snackBar: MatSnackBar
) {
}
private notificationService: NotificationService,
private translate: TranslateService
) {}
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);
}
public call<Response>(method: 'post' | 'get', path: string, body: any|undefined): Observable<Response> {
return this.request<Response>(
method,
this.obtainUrl(path),
body
).pipe(
catchError(
error => {
this.handleError(error)
throw 'Error occurred, during API call'
}
)
);
}
postFiles(apiPath: string, files: File[], fct: Function) {
if (!files || files.length === 0) {
throw 'Need to select at least one file';
public request<Response>(method:'get'|'post', url: string, body: any|undefined): Observable<Response> {
if(method === 'get') {
return this.http.get<Response>(url);
} else {
return this.http.post<Response>(url, body);
}
const formData = new FormData();
for (let fileIndex = 0; fileIndex < files.length; fileIndex++) {
formData.append('file' + fileIndex, files[fileIndex]);
}
let url = this.obtainUrl(apiPath);
let observable = this.http.post<any>(url, formData).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 {
public obtainUrl(apiPath: string): string {
let hostString = window.location.host;
let protocol = window.location.protocol;
return protocol + '//' + hostString + '/api/' + apiPath;
return protocol + "//" + hostString + "/api/" + apiPath;
}
private handleError(answer: any): void {
public handleError(answer: any): void {
if (answer.status == 401) {
console.log('Deine Sitzung konnte nicht gefunden werden');
this.router.navigate(['/auth']);
this.notificationService.push(
this.translate.instant("error.unauthorized"),
'info'
);
this.router.navigate(["/auth"]);
return;
}
if (answer.status == 403) {
this.showSnackBar('Du bist nicht für diese Aktion autorisiert', 'Ok');
this.notificationService.push(
this.translate.instant("error.forbidden"),
'info'
);
return;
}
try {
let errorObject = answer.error.error;
if (errorObject.hasOwnProperty('message')) {
if (errorObject.hasOwnProperty("code")) {
throw (
this.translate.instant("error." + errorObject.code.toString()) ??
errorObject.code.toString()
);
}
if (errorObject.hasOwnProperty("message")) {
throw errorObject.message.toString();
}
if (errorObject.hasOwnProperty('code')) {
throw errorObject.code.toString();
}
} catch(error:any) {
this.showSnackBar(error.toString(), 'Ok');
} catch (error: any) {
this.notificationService.push(error.toString(), 'danger');
}
}
private showSnackBar(message: string, action?: string) {
this.snackBar.open(message.toString(), action, {
duration: 3000,
});
}
}

View File

@ -0,0 +1,5 @@
<button
(click)="this.onClick.emit()"
class="w-full flex-none cursor-pointer font-bold text-skin-primary bg-skin-accent hover:bg-skin-accent-muted focus:ring-2 focus:ring-skin-accent rounded-lg text-sm p-2.5 px-5 text-center">
{{ label | translate }}
</button>

View File

@ -0,0 +1,13 @@
import { Component, EventEmitter, inject, Input, Output } from "@angular/core";
import { ControlContainer, FormGroup } from "@angular/forms";
@Component({
selector: "shared-button",
templateUrl: "./button.component.html",
styleUrl: "./button.component.scss",
standalone: false
})
export class ButtonComponent {
@Output() onClick = new EventEmitter();
@Input({ required: true }) label!: string;
}

View File

@ -1,31 +1,11 @@
<div
id="card"
class="grow p-3 my-2 rounded-xl shadow-lg border border-gray-100 dark:bg-zinc-800 dark:border-zinc-900"
>
<div id="header">
<div class="flex flex-row">
<div *ngIf="icon !== null">
<img class="h-6 w-6 m-2" [src]="icon" />
</div>
<div
*ngIf="header !== null"
class="my-auto text-xl font-medium text-black dark:text-white truncate"
>
{{ header }}
</div>
</div>
<div
*ngIf="subHeader !== null"
class="text-slate-500 dark:text-slate-400 mx-2"
>
{{ subHeader }}
</div>
<div class="w-full bg-skin-secondary rounded-xl p-3 divide-y divide-skin-primary">
<div id="header" class="pb-3">
<ng-content select="[header]"></ng-content>
<div *ngIf="!!headerElement">{{ content }}</div>
</div>
<div id="content" class="mx-5 text-black dark:text-white">
<div *ngIf="content !== null" class="my-2 italic">"{{ content }}"</div>
<div *ngIf="content === null">
<ng-content></ng-content>
</div>
<div id="content" class="text-skin-primary p-3">
<ng-content select="[content]"></ng-content>
<div *ngIf="!!contentElement">{{ content }}</div>
</div>
</div>

View File

@ -1,13 +1,14 @@
import { Component, Input } from '@angular/core';
import { Component, ContentChild, ElementRef, Input } from "@angular/core";
@Component({
selector: 'shared-card',
templateUrl: './card.component.html',
styleUrls: ['./card.component.scss'],
selector: "shared-card",
templateUrl: "./card.component.html",
styleUrls: ["./card.component.scss"],
standalone: false
})
export class CardComponent {
@Input() header: string | null = null;
@Input() icon: string | null = null;
@Input() subHeader: string | null = null;
@Input() content: string | null = null;
@Input() header?:string|undefined;
@Input() content?:string|undefined;
@ContentChild('content', {static: false}) contentElement!: ElementRef;
@ContentChild('header', {static: false}) headerElement!: ElementRef;
}

View File

@ -0,0 +1,21 @@
<div class="w-full">
<input
*ngIf="required"
class="p-3.5 text-xl bg-skin-primary border border-skin-primary text-skin-accent rounded hover:border-skin-accent focus:border-skin-accent focus:ring-skin-accent"
[id]="key"
type="checkbox"
[checked]="this.formGroup.controls[this.key].value"
(change)="change()"
required>
<input
*ngIf="!required"
class="p-3.5 text-xl bg-skin-primary border border-skin-primary text-skin-accent rounded hover:border-skin-accent focus:border-skin-accent focus:ring-skin-accent"
[id]="key"
type="checkbox"
[checked]="this.formGroup.controls[this.key].value"
(change)="change()">
</div>

View File

@ -0,0 +1,34 @@
import { Component, inject, Input, OnInit } from "@angular/core";
import { AbstractControl, ControlContainer, FormGroup } from "@angular/forms";
@Component({
selector: "shared-checkbox",
templateUrl: "./checkbox.component.html",
styleUrl: "./checkbox.component.scss",
viewProviders: [
{
provide: ControlContainer,
useFactory: () => inject(ControlContainer, { skipSelf: true }),
},
],
standalone: false
})
export class CheckboxComponent implements OnInit {
@Input() required: boolean = false;
@Input({ required: true }) key!: string;
formGroup?: FormGroup;
constructor(private controlContainer: ControlContainer) {
}
ngOnInit(): void {
this.formGroup = this.controlContainer.control as FormGroup;
}
change() {
let formControl = this.formGroup!.controls[this.key];
formControl.setValue(!formControl.value);
console.log(formControl.value);
}
}

View File

@ -0,0 +1,3 @@
<div class="bg-skin-primary-muted border border-skin-primary text-skin-primary text-sm rounded-lg p-2.5 min-h-10">
{{ label | translate }}
</div>

View File

@ -0,0 +1,11 @@
import { Component, Input } from "@angular/core";
@Component({
selector: "shared-display",
templateUrl: "./display.component.html",
styleUrl: "./display.component.scss",
standalone: false
})
export class DisplayComponent {
@Input({ required: true }) label!: string;
}

View File

@ -1,21 +0,0 @@
<div
*ngIf="caption !== null"
class="text-lg font-bold mb-3 text-black dark:text-white"
>
{{ caption }}
</div>
<div class="flex flex-col md:space-x-4">
<div id="form-body" class="flex-1 basis-full">
<ng-content />
</div>
<div class="flex flex-row">
<div class="flex-1"></div>
<button
(click)="submit.emit()"
class="flex-0 p-2 min-w-24 bg-green-700 hover:bg-green-800 dark:bg-green-800 dark:hover:bg-green-900 rounded-lg"
>
<p class="text-lg text-white text-md font-bold">Submit</p>
</button>
</div>
</div>

View File

@ -1,12 +0,0 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'shared-form',
templateUrl: './form.component.html',
styleUrls: ['./form.component.scss']
})
export class FormComponent {
@Output() submit = new EventEmitter<any>();
@Input() caption: string|null = null;
}

View File

@ -0,0 +1,21 @@
<div class="rounded-lg p-3 md:p-5" [ngClass]="{
'bg-skin-primary-muted': size === 'large',
'bg-skin-secondary': size === 'medium'
}">
<div class="mb-2">
<div *ngIf="size === 'large'" class="rounded-lg flex flex-col text-skin-primary mb-3 md:mb-5">
<h2 class="font-bold bg-skin-accent rounded-lg text-2xl p-2">
{{ label | translate }}
</h2>
</div>
<div *ngIf="size === 'medium'">
<h3 class="font-bold text-lg">
{{ label | translate }}
</h3>
</div>
</div>
<div class="flex flex-col gap-2">
<ng-content />
</div>
</div>

View File

@ -0,0 +1,16 @@
import { Component, Input } from "@angular/core";
@Component({
selector: "shared-group",
templateUrl: "./group.component.html",
styleUrls: ["./group.component.scss"],
standalone: false
})
export class GroupComponent {
@Input({ required: true }) label!: string;
@Input() size: "large"|"medium";
constructor() {
this.size = "large";
}
}

View File

@ -0,0 +1,17 @@
<input
*ngIf="required"
class="bg-skin-primary border border-skin-primary text-skin-primary text-sm rounded-lg focus:ring-skin-accent focus:border-skin-accent block w-full p-2.5"
[id]="key"
[type]="inputType"
[formControlName]="key"
[placeholder]="placeholder"
required/>
<input
*ngIf="!required"
class="bg-skin-primary border border-skin-primary text-skin-primary text-sm rounded-lg focus:ring-skin-accent focus:border-skin-accent block w-full p-2.5"
[id]="key"
[type]="inputType"
[formControlName]="key"
[placeholder]="placeholder"/>

View File

@ -0,0 +1,21 @@
import { Component, inject, Input } from "@angular/core";
import { ControlContainer, FormGroup } from "@angular/forms";
@Component({
selector: "shared-input",
templateUrl: "./input.component.html",
styleUrl: "./input.component.scss",
viewProviders: [
{
provide: ControlContainer,
useFactory: () => inject(ControlContainer, { skipSelf: true }),
},
],
standalone: false
})
export class InputComponent {
@Input() placeholder: string = "";
@Input() required: boolean = false;
@Input({ required: true }) inputType!: string;
@Input({ required: true }) key!: string;
}

View File

@ -0,0 +1,5 @@
<label
[for]="for"
class="block mb-2 text-sm font-medium text-skin-primary-muted">
{{ label | translate }}
</label>

View File

@ -0,0 +1,19 @@
import { Component, inject, Input } from "@angular/core";
import { ControlContainer, FormGroup } from "@angular/forms";
@Component({
selector: "shared-label",
templateUrl: "./label.component.html",
styleUrl: "./label.component.scss",
viewProviders: [
{
provide: ControlContainer,
useFactory: () => inject(ControlContainer, { skipSelf: true }),
},
],
standalone: false
})
export class LabelComponent {
@Input({required:true}) label!: string;
@Input({ required: true }) for!: string;
}

View File

@ -0,0 +1,10 @@
<div class="bg-skin-primary-muted border border-skin-primary p-8">
<div class="max-w-sm mx-auto mb-10">
<h2 class="font-bold text-center text-skin-primary text-5xl">
{{label | translate}}
</h2>
</div>
<ng-content />
</div>

View File

@ -0,0 +1,11 @@
import { Component, ContentChild, ElementRef, Input } from "@angular/core";
@Component({
selector: "shared-modal",
templateUrl: "./modal.component.html",
styleUrls: ["./modal.component.scss"],
standalone: false
})
export class ModalComponent {
@Input({required: true}) label!: string;
}

View File

@ -1,9 +1,9 @@
<nav aria-label="Page navigation example">
<nav *ngIf="items.length > 1" aria-label="Paginator">
<ul class="flex items-center -space-x-px h-8 text-sm">
<li>
<button
(click)="setActivePage(-1)"
class="flex items-center justify-center px-3 h-8 ms-0 leading-tight text-gray-500 dark:text-white bg-white dark:bg-zinc-800 border border-e-0 border-gray-500 dark:border-zinc-800 rounded-s-lg hover:bg-gray-100 hover:text-gray-700"
class="flex items-center justify-center px-3 h-8 ms-0 leading-tight text-skin-primary bg-skin-secondary border border-skin-primary rounded-s-lg hover:bg-skin-secondary-muted"
>
<span class="sr-only">Previous</span>
<svg
@ -23,18 +23,18 @@
</svg>
</button>
</li>
<li *ngFor="let item of items" class="w-full">
<div
class="flex items-center justify-center h-8 leading-tight border border-gray-500 dark:border-zinc-800"
class="flex items-center justify-center h-8 leading-tight border border-skin-primary text-skin-primary"
[ngClass]="{
'bg-green-700 dark:bg-green-800 text-white': item.page === page,
'bg-white dark:bg-zinc-700 text-black dark:text-white':
item.page !== page
'bg-skin-accent font-bold text-skin-primary underline': item.page === page,
'bg-skin-secondary text-skin-primary-muted': item.page !== page
}"
>
<div *ngIf="item.page === null" class="px-3">...</div>
<button
class="px-3 w-full h-full hover:bg-gray-100 hover:text-gray-700"
class="px-3 w-full h-full hover:bg-skin-secondary-muted"
*ngIf="item.page !== null"
(click)="setActivePage(item.page)"
>
@ -42,10 +42,11 @@
</button>
</div>
</li>
<li>
<button
(click)="setActivePage(-2)"
class="flex items-center justify-center px-3 h-8 leading-tight text-gray-500 dark:text-white bg-white dark:bg-zinc-800 border border-gray-500 dark:border-zinc-800 rounded-e-lg hover:bg-gray-100 hover:text-gray-700"
class="flex items-center justify-center px-3 h-8 leading-tight text-skin-primary bg-skin-secondary border border-skin-primary rounded-e-lg hover:bg-skin-secondary-muted"
>
<span class="sr-only">Next</span>
<svg

Some files were not shown because too many files have changed in this diff Show More