first changes
This commit is contained in:
parent
719a42ea1c
commit
25f0188dec
16
.editorconfig
Normal file
16
.editorconfig
Normal 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
|
||||
18
angular.json
18
angular.json
@ -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
19
bin/script/firstRun
Executable 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"
|
||||
@ -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
29
bin/script/update
Executable 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"
|
||||
@ -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
|
||||
36
package.json
36
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1 +1,5 @@
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
<div class="absolute bottom-16 md:bottom-0 w-full z-50">
|
||||
<app-notification-bar />
|
||||
</div>
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
57
src/app/core/auth/auth.module.ts
Normal file
57
src/app/core/auth/auth.module.ts
Normal 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 {}
|
||||
23
src/app/core/auth/components/auth/auth.component.html
Normal file
23
src/app/core/auth/components/auth/auth.component.html
Normal 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>
|
||||
44
src/app/core/auth/components/auth/auth.component.ts
Normal file
44
src/app/core/auth/components/auth/auth.component.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -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");
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -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");
|
||||
});
|
||||
}
|
||||
}
|
||||
23
src/app/core/auth/components/login/login.component.html
Normal file
23
src/app/core/auth/components/login/login.component.html
Normal 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>
|
||||
29
src/app/core/auth/components/login/login.component.ts
Normal file
29
src/app/core/auth/components/login/login.component.ts
Normal 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");
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -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");
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -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");
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
<app-navigation></app-navigation>
|
||||
|
||||
<div>
|
||||
Home
|
||||
</div>
|
||||
@ -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) {
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -1,9 +0,0 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-navigation',
|
||||
templateUrl: './navigation.component.html',
|
||||
styleUrls: ['./navigation.component.scss'],
|
||||
})
|
||||
export class NavigationComponent {
|
||||
}
|
||||
@ -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>
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 {}
|
||||
|
||||
24
src/app/core/guards/auth.guard.ts
Normal file
24
src/app/core/guards/auth.guard.ts
Normal 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;
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
9
src/app/core/home/components/home/home.component.html
Normal file
9
src/app/core/home/components/home/home.component.html
Normal 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>
|
||||
9
src/app/core/home/components/home/home.component.ts
Normal file
9
src/app/core/home/components/home/home.component.ts
Normal 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 {}
|
||||
@ -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>
|
||||
121
src/app/core/home/components/navigation/navigation.component.ts
Normal file
121
src/app/core/home/components/navigation/navigation.component.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
22
src/app/core/home/components/settings/settings.component.ts
Normal file
22
src/app/core/home/components/settings/settings.component.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
<!--
|
||||
<shared-table [items]="items" [columns]="columns" [rowActions]="rowActions">
|
||||
</shared-table>
|
||||
-->
|
||||
@ -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() {}
|
||||
}
|
||||
@ -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>
|
||||
@ -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 => {});
|
||||
}
|
||||
}
|
||||
82
src/app/core/home/home.module.ts
Normal file
82
src/app/core/home/home.module.ts
Normal 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 {}
|
||||
7
src/app/core/models/change-password-request.model.ts
Normal file
7
src/app/core/models/change-password-request.model.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export interface ChangePasswordRequest {
|
||||
password: string;
|
||||
newPassword: string;
|
||||
}
|
||||
|
||||
export interface ChangePasswordResponse {
|
||||
}
|
||||
7
src/app/core/models/change-username-request.model.ts
Normal file
7
src/app/core/models/change-username-request.model.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export interface ChangeUsernameRequest {
|
||||
password: string;
|
||||
newUsername: string;
|
||||
}
|
||||
|
||||
export interface ChangeUsernameResponse {
|
||||
}
|
||||
12
src/app/core/models/confirm-registration-request.model.ts
Normal file
12
src/app/core/models/confirm-registration-request.model.ts
Normal 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[];
|
||||
}
|
||||
6
src/app/core/models/forgot-password-request.model.ts
Normal file
6
src/app/core/models/forgot-password-request.model.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export interface ForgotPasswordRequest {
|
||||
mail: string;
|
||||
}
|
||||
|
||||
export interface ForgotPasswordResponse {
|
||||
}
|
||||
8
src/app/core/models/login-request.model.ts
Normal file
8
src/app/core/models/login-request.model.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export interface LoginUserRequest {
|
||||
identifier: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface LoginUserResponse {
|
||||
sessionId: string;
|
||||
}
|
||||
6
src/app/core/models/logout-request.model.ts
Normal file
6
src/app/core/models/logout-request.model.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export interface LogoutUserRequest {
|
||||
}
|
||||
|
||||
export interface LogoutUserResponse {
|
||||
response?: string|undefined;
|
||||
}
|
||||
8
src/app/core/models/register-user-request.model.ts
Normal file
8
src/app/core/models/register-user-request.model.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export interface RegisterUserRequest {
|
||||
username: string;
|
||||
mail: string;
|
||||
}
|
||||
|
||||
export interface RegisterUserResponse {
|
||||
response?: string|undefined;
|
||||
}
|
||||
8
src/app/core/models/reset-password-request.model.ts
Normal file
8
src/app/core/models/reset-password-request.model.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export interface ResetPasswordRequest {
|
||||
passwordToken: string;
|
||||
newPassword: string;
|
||||
passwordConfirmation: string;
|
||||
}
|
||||
|
||||
export interface ResetPasswordResponse {
|
||||
}
|
||||
13
src/app/core/models/user-state-request.model.ts
Normal file
13
src/app/core/models/user-state-request.model.ts
Normal 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;
|
||||
}
|
||||
30
src/app/core/services/app.service.ts
Normal file
30
src/app/core/services/app.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
122
src/app/core/services/auth.service.ts
Normal file
122
src/app/core/services/auth.service.ts
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
32
src/app/core/services/notification.service.ts
Normal file
32
src/app/core/services/notification.service.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
5
src/app/shared/components/button/button.component.html
Normal file
5
src/app/shared/components/button/button.component.html
Normal 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>
|
||||
13
src/app/shared/components/button/button.component.ts
Normal file
13
src/app/shared/components/button/button.component.ts
Normal 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;
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
21
src/app/shared/components/checkbox/checkbox.component.html
Normal file
21
src/app/shared/components/checkbox/checkbox.component.html
Normal 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>
|
||||
34
src/app/shared/components/checkbox/checkbox.component.ts
Normal file
34
src/app/shared/components/checkbox/checkbox.component.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
3
src/app/shared/components/display/display.component.html
Normal file
3
src/app/shared/components/display/display.component.html
Normal 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>
|
||||
11
src/app/shared/components/display/display.component.ts
Normal file
11
src/app/shared/components/display/display.component.ts
Normal 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;
|
||||
}
|
||||
@ -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>
|
||||
@ -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;
|
||||
|
||||
}
|
||||
21
src/app/shared/components/group/group.component.html
Normal file
21
src/app/shared/components/group/group.component.html
Normal 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>
|
||||
16
src/app/shared/components/group/group.component.ts
Normal file
16
src/app/shared/components/group/group.component.ts
Normal 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";
|
||||
}
|
||||
}
|
||||
17
src/app/shared/components/input/input.component.html
Normal file
17
src/app/shared/components/input/input.component.html
Normal 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"/>
|
||||
21
src/app/shared/components/input/input.component.ts
Normal file
21
src/app/shared/components/input/input.component.ts
Normal 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;
|
||||
}
|
||||
5
src/app/shared/components/label/label.component.html
Normal file
5
src/app/shared/components/label/label.component.html
Normal file
@ -0,0 +1,5 @@
|
||||
<label
|
||||
[for]="for"
|
||||
class="block mb-2 text-sm font-medium text-skin-primary-muted">
|
||||
{{ label | translate }}
|
||||
</label>
|
||||
19
src/app/shared/components/label/label.component.ts
Normal file
19
src/app/shared/components/label/label.component.ts
Normal 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;
|
||||
}
|
||||
10
src/app/shared/components/modal/modal.component.html
Normal file
10
src/app/shared/components/modal/modal.component.html
Normal 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>
|
||||
11
src/app/shared/components/modal/modal.component.ts
Normal file
11
src/app/shared/components/modal/modal.component.ts
Normal 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;
|
||||
}
|
||||
@ -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
Loading…
Reference in New Issue
Block a user