internationalization

This commit is contained in:
Flo 2024-08-31 22:18:08 +00:00
parent 5facddcb5a
commit d8d09d7bac
30 changed files with 488 additions and 254 deletions

View File

@ -84,10 +84,10 @@
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"browserTarget": "bee:build:production"
"buildTarget": "bee:build:production"
},
"development": {
"browserTarget": "bee:build:development"
"buildTarget": "bee:build:development"
}
},
"defaultConfiguration": "development"
@ -95,7 +95,7 @@
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "bee:build"
"buildTarget": "bee:build"
}
},
"test": {

View File

@ -9,24 +9,25 @@
},
"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/platform-browser": "^15.2.0",
"@angular/platform-browser-dynamic": "^15.2.0",
"@angular/router": "^15.2.0",
"@angular/animations": "^18.2.2",
"@angular/common": "^18.2.2",
"@angular/compiler": "^18.2.2",
"@angular/core": "^18.2.2",
"@angular/forms": "^18.2.2",
"@angular/platform-browser": "^18.2.2",
"@angular/platform-browser-dynamic": "^18.2.2",
"@angular/router": "^18.2.2",
"@ngx-translate/core": "^15.0.0",
"@ngx-translate/http-loader": "^8.0.0",
"flowbite": "^2.5.1",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.12.0"
"zone.js": "~0.14.10"
},
"devDependencies": {
"@angular-devkit/build-angular": "^15.2.6",
"@angular/cli": "~15.2.6",
"@angular/compiler-cli": "^15.2.0",
"@angular-devkit/build-angular": "^18.2.2",
"@angular/cli": "~18.2.2",
"@angular/compiler-cli": "^18.2.2",
"@types/jasmine": "~4.3.0",
"jasmine-core": "~4.5.0",
"karma": "~6.4.0",
@ -35,6 +36,6 @@
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.0.0",
"tailwindcss": "^3.4.10",
"typescript": "~4.9.4"
"typescript": "~5.4.5"
}
}

View File

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

View File

@ -1,6 +1,7 @@
import { Component, OnInit } from "@angular/core";
import { AuthService } from "./core/services/auth.service";
import { AppService } from "./core/services/app.service";
import { TranslateService } from "@ngx-translate/core";
@Component({
selector: "app-root",
@ -10,8 +11,13 @@ import { AppService } from "./core/services/app.service";
export class AppComponent implements OnInit {
constructor(
private authService: AuthService,
private appService: AppService
) {}
private translate: TranslateService
) {
var language = navigator.language;
translate.setDefaultLang(language);
translate.use(language);
}
ngOnInit(): void {
this.authService.readUserState();

View File

@ -1,8 +1,13 @@
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { HttpClientModule } from "@angular/common/http";
import {
HttpClient,
provideHttpClient,
withInterceptorsFromDi,
} from "@angular/common/http";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { TranslateModule, TranslateLoader } 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";
@ -25,18 +30,28 @@ const routes: Routes = [
},
];
export function createTranslateLoader(http: HttpClient) {
return new TranslateHttpLoader(http, "./assets/i18n/", ".json");
}
@NgModule({
declarations: [AppComponent, HomeComponent],
bootstrap: [AppComponent],
imports: [
BrowserModule,
BrowserAnimationsModule,
HttpClientModule,
RouterModule,
RouterModule.forRoot(routes),
SharedModule,
CoreModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: createTranslateLoader,
deps: [HttpClient],
},
}),
],
providers: [],
bootstrap: [AppComponent],
providers: [provideHttpClient(withInterceptorsFromDi())],
})
export class AppModule {}

View File

@ -7,6 +7,7 @@ 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";
const routes: Routes = [
{ path: "login", component: LoginComponent },
@ -38,6 +39,11 @@ const routes: Routes = [
ForgotPasswordComponent,
ResetPasswordComponent,
],
imports: [RouterModule.forChild(routes), CommonModule, ReactiveFormsModule],
imports: [
RouterModule.forChild(routes),
CommonModule,
ReactiveFormsModule,
TranslateModule,
],
})
export class AuthModule {}

View File

@ -2,10 +2,10 @@
<div class="max-w-sm mx-auto mb-10">
<h1 class="font-bold text-center text-skin-primary text-5xl mb-5">
Beekeeper
{{ "title" | translate }}
</h1>
<h1 class="font-bold text-center text-skin-accent text-xl">
Registrierung Abschließen
{{ "auth.confirm-registration" | translate }}
</h1>
</div>
@ -14,7 +14,7 @@
<label
for="password"
class="block mb-2 text-sm font-medium text-skin-primary-muted"
>Passwort</label
>{{ "auth.password" | translate }}</label
>
<input
formControlName="password"
@ -28,7 +28,7 @@
<label
for="passwordConfirmation"
class="block mb-2 text-sm font-medium text-skin-primary-muted"
>Passwort wiederholen</label
>{{ "auth.password-confirmation" | translate }}</label
>
<input
formControlName="passwordConfirmation"
@ -44,6 +44,6 @@
type="submit"
class="w-full 9xl:w-auto font-bold text-skin-secondary bg-skin-accent hover:text-skin-primary rounded-lg text-sm px-5 py-2.5 text-center"
>
Registrieren
{{ "auth.confirm-registration" | translate }}
</button>
</form>

View File

@ -2,10 +2,10 @@
<div class="max-w-sm mx-auto mb-10">
<h1 class="font-bold text-center text-skin-primary text-5xl mb-5">
Beekeeper
{{ "title" | translate }}
</h1>
<h1 class="font-bold text-center text-skin-accent text-xl">
Passwort vergessen?
{{ "auth.forgot-password" | translate }}
</h1>
</div>
@ -14,7 +14,8 @@
<label
for="mail"
class="block mb-2 text-sm font-medium text-skin-primary-muted"
>E-Mail</label
>
{{ "auth.email" | translate }}</label
>
<input
formControlName="mail"
@ -30,6 +31,6 @@
type="submit"
class="w-full 9xl:w-auto font-bold text-skin-secondary bg-skin-accent hover:text-skin-primary rounded-lg text-sm px-5 py-2.5 text-center"
>
Passwort zurücksetzen
{{ "auth.reset-password" | translate }}
</button>
</form>

View File

@ -2,9 +2,11 @@
<div class="max-w-sm mx-auto mb-10">
<h1 class="font-bold text-center text-skin-primary text-5xl mb-5">
Beekeeper
{{ "title" | translate }}
</h1>
<h1 class="font-bold text-center text-skin-accent text-xl">
{{ "auth.login" | translate }}
</h1>
<h1 class="font-bold text-center text-skin-accent text-xl">Anmeldung</h1>
</div>
<form class="max-w-sm mx-auto" [formGroup]="loginForm">
@ -12,7 +14,7 @@
<label
for="identifier"
class="block mb-2 text-sm font-medium text-skin-primary-muted"
>Benutzername oder E-Mail</label
>{{ "auth.username-or-email" | translate }}</label
>
<input
formControlName="identifier"
@ -26,11 +28,11 @@
id="helper-text-explanation"
class="mt-2 text-sm text-skin-primary-muted"
>
Neu hier?
{{ "auth.not-yet-registered" | translate }}
<a
routerLink="/auth/registration"
class="font-medium text-skin-accent hover:underline hover:font-bold"
>Jetzt registrieren!</a
>{{ "auth.register-now" | translate }}</a
>
</p>
</div>
@ -38,7 +40,7 @@
<label
for="password"
class="block mb-2 text-sm font-medium text-skin-primary-muted"
>Passwort</label
>{{ "auth.password" | translate }}</label
>
<input
formControlName="password"
@ -54,7 +56,7 @@
<a
routerLink="/auth/forgot-password"
class="font-medium text-skin-accent hover:underline hover:font-bold"
>Passwort vergessen?</a
>{{ "auth.forgot-password" | translate }}?</a
>
</p>
</div>
@ -64,6 +66,6 @@
type="submit"
class="w-full 9xl:w-auto font-bold text-skin-secondary bg-skin-accent hover:text-skin-primary rounded-lg text-sm px-5 py-2.5 text-center"
>
Anmelden
{{ "auth.login" | translate }}
</button>
</form>

View File

@ -2,9 +2,11 @@
<div class="max-w-sm mx-auto mb-10">
<h1 class="font-bold text-center text-skin-primary text-5xl mb-5">
Beekeeper
{{ "title" | translate }}
</h1>
<h1 class="font-bold text-center text-skin-accent text-xl">
{{ "auth.registration" | translate }}
</h1>
<h1 class="font-bold text-center text-skin-accent text-xl">Registrierung</h1>
</div>
<form class="max-w-sm mx-auto" [formGroup]="registrationForm">
@ -12,7 +14,8 @@
<label
for="mail"
class="block mb-2 text-sm font-medium text-skin-primary-muted"
>E-Mail</label
>
{{ "auth.email" | translate }}</label
>
<input
formControlName="mail"
@ -27,7 +30,8 @@
<label
for="username"
class="block mb-2 text-sm font-medium text-skin-primary-muted"
>Benutzername</label
>
{{ "auth.reset-password" | translate }}</label
>
<input
formControlName="username"
@ -40,11 +44,11 @@
id="helper-text-explanation"
class="mt-2 text-sm text-skin-primary-muted"
>
Bereits registiert?
{{ "auth.already-registered" | translate }}
<a
routerLink="/auth/login"
class="font-medium text-skin-accent hover:underline hover:font-bold"
>Jetzt anmelden!</a
>{{ "auth.login-now" | translate }}</a
>
</p>
</div>
@ -54,6 +58,6 @@
type="submit"
class="w-full 9xl:w-auto font-bold text-skin-secondary bg-skin-accent hover:text-skin-primary rounded-lg text-sm px-5 py-2.5 text-center"
>
Registrieren
{{ "auth.register" | translate }}
</button>
</form>

View File

@ -2,10 +2,10 @@
<div class="max-w-sm mx-auto mb-10">
<h1 class="font-bold text-center text-skin-primary text-5xl mb-5">
Beekeeper
{{ "title" | translate }}
</h1>
<h1 class="font-bold text-center text-skin-accent text-xl">
Passwort zurücksetzen
{{ "auth.reset-password" | translate }}
</h1>
</div>
@ -14,7 +14,7 @@
<label
for="newPassword"
class="block mb-2 text-sm font-medium text-skin-primary-muted"
>Neues Passwort</label
>{{ "auth.new-password" | translate }}</label
>
<input
formControlName="newPassword"
@ -28,7 +28,7 @@
<label
for="passwordConfirmation"
class="block mb-2 text-sm font-medium text-skin-primary-muted"
>Passwort wiederholen</label
>{{ "auth.password-confirmation" | translate }}</label
>
<input
formControlName="passwordConfirmation"
@ -44,6 +44,6 @@
type="submit"
class="w-full 9xl:w-auto font-bold text-skin-secondary bg-skin-accent hover:text-skin-primary rounded-lg text-sm px-5 py-2.5 text-center"
>
Neues Passwort bestätigen
{{ "auth.new-password" | translate }}
</button>
</form>

View File

@ -1,7 +1,10 @@
<app-navigation></app-navigation>
<button (click)="fehler()">Fehler</button>
<div class="max-w-screen-xl mx-auto p-4">
<div class="mx-auto p-5 flex flex-row">
<div id="sidebar-placehodler" class="basis-auto">
<div class="w-0 xl:w-64"></div>
</div>
<div id="content" class="basis-full">
<router-outlet></router-outlet>
</div>
</div>

View File

@ -1,18 +1,8 @@
import { Component } from "@angular/core";
import { NotificationService } from "../../services/notification.service";
@Component({
selector: "app-home",
templateUrl: "./home.component.html",
styleUrls: ["./home.component.scss"],
})
export class HomeComponent {
constructor(private notificationService: NotificationService) {}
fehler() {
this.notificationService.push({
message: "Ein Fehler ist aufgetreten :(",
type: "danger",
});
}
}
export class HomeComponent {}

View File

@ -1,47 +1,15 @@
<nav class="bg-skin-accent">
<div
class="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4"
>
<!-- Title -->
<a routerLink="/" class="flex items-center space-x-3">
<img src="assets/icon.png" class="h-10" alt="Beekeeper Logo" />
<span
class="text-skin-primary self-center text-4xl font-semibold whitespace-nowrap"
>Bienenkeeper</span
>
</a>
<div class="flex items-center md:order-2 space-x-3 md:space-x-0">
<!--User Bubble-->
<button
type="button"
class="w-10 h-10 text-sm bg-skin-primary rounded-full hidden md:block md:me-0 focus:ring-4 focus:ring-skin-primary"
id="user-menu-button"
aria-expanded="false"
data-dropdown-toggle="user-dropdown"
data-dropdown-placement="bottom"
>
<span class="text-skin-accent rounded-full font-bold p-2 uppercase">{{
state?.username[0] + state?.roleIdentifier[0]
}}</span>
</button>
<div class="h-20 flex flex-wrap items-center justify-between mx-auto p-4">
<!--Burger Menu-->
<button
data-collapse-toggle="navbar-user"
data-collapse-toggle="default-sidebar"
type="button"
class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm text-skin-primary rounded-lg md:hidden hover:bg-skin-primary"
aria-controls="navbar-user"
class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm text-skin-primary rounded-lg hover:bg-skin-primary xl:hidden"
aria-controls="default-sidebar"
aria-expanded="false"
>
<span class="sr-only">Open main menu</span>
<svg
class="w-5 h-5"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 17 14"
>
<svg class="w-5 h-5" aria-hidden="true" fill="none" viewBox="0 0 17 14">
<path
stroke="currentColor"
stroke-linecap="round"
@ -51,10 +19,53 @@
/>
</svg>
</button>
</div>
<!-- User dropdown -->
<div id="user-dropdown" class="hidden min-w-full md:min-w-72 px-4">
<!-- Title -->
<a routerLink="/" class="flex items-center space-x-3">
<img src="assets/icon.png" class="h-10 w-10" alt="Beekeeper Logo" />
<span
class="text-skin-primary self-center text-4xl font-semibold whitespace-nowrap"
>{{ "title" | translate }}</span
>
</a>
<!--User Bubble-->
<button
type="button"
class="w-12 h-12 text-sm bg-skin-primary rounded-full focus:ring-4 focus:ring-skin-primary"
id="user-menu-button"
aria-expanded="false"
data-dropdown-toggle="user-dropdown"
data-dropdown-placement="bottom"
>
<span class="text-skin-accent rounded-full font-bold p-2 uppercase">{{
state?.username[0] + state?.roleIdentifier[0]
}}</span>
</button>
</div>
</nav>
<!-- Navbar -->
<div
id="default-sidebar"
class="fixed hidden xl:block -bottom-20 left-0 z-40 w-64 h-screen translate-x-0 bg-skin-secondary"
aria-label="Sidebar"
>
<ul class="text-xl p-4 rounded-lg">
<li *ngFor="let navigationLink of navigationLinks">
<a
class="block py-2 px-3 rounded text-skin-primary hover:text-skin-secondary hover:bg-skin-accent"
[routerLink]="navigationLink.routerLink"
[routerLinkActiveOptions]="{ exact: true }"
routerLinkActive="font-bold"
>{{ getNavigationLinkLabel(navigationLink) | translate }}</a
>
</li>
</ul>
</div>
<!-- User dropdown -->
<div id="user-dropdown" class="hidden min-w-full md:min-w-64 px-4 z-50">
<div
class="text-base list-none divide-y divide-skin-primary rounded-lg shadow-sm shadow-skin-primary bg-skin-primary"
>
@ -70,7 +81,7 @@
<ul class="p-4" aria-labelledby="user-menu-button">
<li *ngFor="let usermenuButton of usermenuButtons">
<button
class="w-full text-left block py-2 px-3 rounded text-skin-primary hover:bg-skin-accent"
class="w-full text-center block py-2 px-3 rounded text-skin-primary hover:bg-skin-accent"
[routerLink]="
usermenuButton.routerLink === undefined
? null
@ -78,62 +89,9 @@
"
(click)="clickUserMenuButtonLabel(usermenuButton)"
>
{{ getUserMenuButtonLabel(usermenuButton) }}
{{ getUserMenuButtonLabel(usermenuButton) | translate }}
</button>
</li>
</ul>
</div>
</div>
<!-- Navigation -->
<div
class="items-center justify-between hidden w-full md:flex md:w-auto md:order-1"
id="navbar-user"
>
<div
class="text-base mt-4 list-none md:divide-y md:divide-skin-primary rounded-lg shadow-sm shadow-skin-primary md:hidden bg-skin-primary"
>
<div class="px-4 pt-3">
<span class="block text-sm text-skin-accent font-bold">{{
state?.username
}}</span>
<span class="block text-sm text-skin-primary-muted truncate">{{
state?.roleIdentifier
}}</span>
</div>
<ul
class="flex flex-col text-xl p-4 rounded-lg bg-skin-primary md:bg-skin-accent md:space-x-8"
>
<li *ngFor="let usermenuButton of usermenuButtons">
<button
[routerLink]="
usermenuButton.routerLink === undefined
? null
: [usermenuButton.routerLink]
"
class="w-full text-left block py-2 px-3 rounded text-skin-primary hover:bg-skin-accent"
(click)="clickUserMenuButtonLabel(usermenuButton)"
>
{{ getUserMenuButtonLabel(usermenuButton) }}
</button>
</li>
</ul>
</div>
<ul
class="flex flex-col text-xl p-4 md:p-0 mt-4 rounded-lg bg-skin-primary md:bg-skin-accent md:space-x-8 md:flex-row md:mt-0 md:border-0"
>
<li *ngFor="let navigationLink of navigationLinks">
<a
class="block py-2 px-3 rounded text-skin-primary hover:text-skin-secondary hover:bg-skin-accent md:p-0"
[routerLink]="navigationLink.routerLink"
[routerLinkActiveOptions]="{ exact: true }"
routerLinkActive="font-bold"
>{{ navigationLink.label }}</a
>
</li>
</ul>
</div>
</div>
</nav>
</div>

View File

@ -3,11 +3,13 @@ import { initFlowbite } from "flowbite";
import { AuthService } from "../../services/auth.service";
import { UserStateResponse } from "../../models/user-state-request.model";
import { Router } from "@angular/router";
import { filter, map } from "rxjs";
import { filter } from "rxjs";
import { AppService } from "../../services/app.service";
import { NotificationService } from "../../services/notification.service";
import { TranslateService } from "@ngx-translate/core";
interface NavigationLink {
label: string;
label: Function | string;
routerLink: string;
}
@ -24,25 +26,45 @@ interface UserMenuButton {
})
export class NavigationComponent implements OnInit {
navigationLinks: NavigationLink[] = [
{ routerLink: "/", label: "Startseite" },
{ routerLink: "/settings", label: "Völker" },
{ routerLink: "/", label: () => "navigation.home" },
{
routerLink: "/colonies",
label: () => "navigation.colonies",
},
];
usermenuButtons: UserMenuButton[] = [
{
label: "Einstellungen",
label: () => "menu.settings",
routerLink: "/settings",
clickCallback: undefined,
},
{
label: () => (this.darkMode ? "Hell" : "Dunkel"),
label: () => (this.darkMode ? "menu.theme.light" : "menu.theme.dark"),
routerLink: undefined,
clickCallback: () => {
this.toggleDarkmode();
},
},
{
label: "Ausloggen",
label: () => (this.language === "de" ? "languages.en" : "languages.de"),
routerLink: undefined,
clickCallback: () => {
this.toggleLanguage();
},
},
{
label: "Test",
routerLink: undefined,
clickCallback: () => {
this.notificationService.push(
this.translate.instant("error.Generic.SomethingWentWrong"),
"danger"
);
},
},
{
label: () => "menu.logout",
routerLink: undefined,
clickCallback: () => {
this.logout();
@ -52,13 +74,28 @@ export class NavigationComponent implements OnInit {
state: UserStateResponse | undefined | null;
darkMode: boolean;
language: "de" | "en";
constructor(
private authService: AuthService,
private appService: AppService,
private notificationService: NotificationService,
private translate: TranslateService,
private router: Router
) {
this.darkMode = this.appService.darkMode;
switch (translate.currentLang) {
case "en":
this.language = "en";
break;
case "de":
this.language = "de";
break;
default:
this.language = "de";
}
this.state = this.authService.currentState$.value;
this.authService.currentState$
.pipe(filter((state) => state === undefined))
@ -80,6 +117,19 @@ export class NavigationComponent implements OnInit {
this.darkMode = this.appService.darkMode;
}
toggleLanguage() {
switch (this.language) {
case "en":
this.language = "de";
break;
case "de":
this.language = "en";
break;
}
this.translate.use(this.language);
}
clickUserMenuButtonLabel(button: UserMenuButton) {
if (button.clickCallback !== undefined) {
button.clickCallback();
@ -93,4 +143,11 @@ export class NavigationComponent implements OnInit {
return button.label;
}
}
getNavigationLinkLabel(link: NavigationLink) {
if (typeof link.label === "function") {
return link.label();
} else {
return link.label;
}
}
}

View File

@ -1,5 +1,5 @@
<div
*ngFor="let element of elements"
*ngFor="let item of items"
class="flex items-center opacity-90 p-4 mb-4 text-skin-accent rounded-lg bg-skin-secondary md:max-w-3xl mx-5 md:mx-auto shadow-sm shadow-skin-primary"
>
<svg
@ -13,14 +13,14 @@
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">{{ element.type }}</span>
<span class="sr-only">{{ item.element.type }}</span>
<div class="ms-3 text-sm font-medium">
{{ element.message }}
{{ item.element.message }}
</div>
<button
type="button"
class="ms-auto -mx-1.5 -my-1.5 bg-skin-secondary text-skin-accent rounded-lg focus:ring-2 focus:ring-blue-400 p-1.5 hover:bg-skin-primary inline-flex items-center justify-center h-8 w-8"
(click)="dismiss(element)"
(click)="dismiss(item)"
>
<span class="sr-only">Close</span>
<svg

View File

@ -1,10 +1,15 @@
import { Component } from "@angular/core";
import { NotificationService } from "../../services/notification.service";
import { filter } from "rxjs";
export interface NotificationElement {
message: string;
type: "info" | "danger" | "warn";
type: "info" | "danger" | "warn" | "success";
timeout: number | undefined;
}
interface NotificationItem {
element: NotificationElement;
shownAt: Date;
}
@Component({
@ -13,17 +18,52 @@ export interface NotificationElement {
styleUrls: ["./notification-bar.component.scss"],
})
export class NotificationBarComponent {
elements: NotificationElement[] = [];
items: NotificationItem[] = [];
isRunning: boolean;
constructor(private notificationService: NotificationService) {
this.isRunning = false;
this.notificationService.notification$.subscribe((notification) => {
if (notification !== undefined) {
this.elements.push(notification);
this.items.push({
element: notification,
shownAt: new Date(),
});
this.startTimeoutIfNecessary();
}
});
}
dismiss(element: NotificationElement): void {
this.elements = this.elements.filter((e) => e !== element);
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

@ -27,7 +27,7 @@
aria-controls="profile"
aria-selected="false"
>
Profil
{{ "settings.tab-profile" | translate }}
</button>
</li>
<li class="me-2" role="presentation">
@ -40,7 +40,7 @@
aria-controls="security"
aria-selected="false"
>
Sicherheit
{{ "settings.tab-security" | translate }}
</button>
</li>
</ul>

View File

@ -1,10 +1,14 @@
import { Component } from '@angular/core';
import { Component } from "@angular/core";
interface User {
username: string;
mail: string;
age: number;
}
@Component({
selector: 'app-tab-profile',
templateUrl: './tab-profile.component.html',
styleUrls: ['./tab-profile.component.scss']
selector: "app-tab-profile",
templateUrl: "./tab-profile.component.html",
styleUrls: ["./tab-profile.component.scss"],
})
export class TabProfileComponent {
}
export class TabProfileComponent {}

View File

@ -1,12 +1,12 @@
<div class="grid grid-cols-1 md:grid-cols-2">
<div>
<h2>Passwort ändern</h2>
<h2>{{ "settings.security.change-password" | translate }}</h2>
<form class="mx-auto p-5" [formGroup]="changePasswordForm">
<div class="mb-5">
<label
for="password"
class="block mb-2 text-sm font-medium text-skin-primary-muted"
>Aktuelles Passwort</label
>{{ "settings.security.current-password" | translate }}</label
>
<input
formControlName="password"
@ -21,7 +21,7 @@
<label
for="newPassword"
class="block mb-2 text-sm font-medium text-skin-primary-muted"
>Neues Passwort</label
>{{ "settings.security.new-password" | translate }}</label
>
<input
formControlName="newPassword"
@ -36,7 +36,7 @@
<label
for="newPasswordConfirmation"
class="block mb-2 text-sm font-medium text-skin-primary-muted"
>Neues Passwort bestätigen</label
>{{ "settings.security.password-confirmation" | translate }}</label
>
<input
formControlName="newPasswordConfirmation"
@ -53,19 +53,19 @@
type="submit"
class="w-full 9xl:w-auto font-bold text-skin-secondary bg-skin-accent hover:text-skin-primary rounded-lg text-sm px-5 py-2.5 text-center"
>
Passwort ändern
{{ "settings.security.change-password" | translate }}
</button>
</form>
</div>
<div>
<h2>Benutername ändern</h2>
<h2>{{ "settings.security.change-username" | translate }}</h2>
<form class="mx-auto p-5" [formGroup]="changeUsernameForm">
<div class="mb-5">
<label
for="password"
class="block mb-2 text-sm font-medium text-skin-primary-muted"
>Aktuelles Passwort</label
>{{ "settings.security.current-password" | translate }}</label
>
<input
formControlName="password"
@ -80,7 +80,7 @@
<label
for="newUsername"
class="block mb-2 text-sm font-medium text-skin-primary-muted"
>Neuer Benutzername</label
>{{ "settings.security.new-username" | translate }}</label
>
<input
formControlName="newUsername"
@ -97,7 +97,7 @@
type="submit"
class="w-full 9xl:w-auto font-bold text-skin-secondary bg-skin-accent hover:text-skin-primary rounded-lg text-sm px-5 py-2.5 text-center"
>
Benutzername ändern
{{ "settings.security.change-username" | translate }}
</button>
</form>
</div>

View File

@ -13,6 +13,7 @@ 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";
@NgModule({
declarations: [
@ -29,7 +30,13 @@ import { NotificationService } from "./services/notification.service";
TabSecurityComponent,
NotificationBarComponent,
],
imports: [CommonModule, SharedModule, RouterModule, ReactiveFormsModule],
imports: [
CommonModule,
SharedModule,
RouterModule,
ReactiveFormsModule,
TranslateModule,
],
providers: [
AuthGuard,
AuthService,

View File

@ -1,16 +1,10 @@
import { Injectable } from "@angular/core";
import {
ActivatedRouteSnapshot,
CanActivate,
Router,
RouterStateSnapshot,
UrlTree,
} from "@angular/router";
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot, UrlTree } from "@angular/router";
import { filter, map, Observable } from "rxjs";
import { AuthService } from "../services/auth.service";
@Injectable()
export class AuthGuard implements CanActivate {
export class AuthGuard {
constructor(private authService: AuthService, private router: Router) {}
canActivate(): Observable<boolean> | boolean {

View File

@ -42,7 +42,6 @@ export class AuthService {
"user/state",
{},
(response: UserStateResponse) => {
console.log("set next state");
this.currentState$.next(response);
},
() => {

View File

@ -6,8 +6,29 @@ import { NotificationElement } from "../components/notification-bar/notification
export class NotificationService {
notification$ = new Subject<NotificationElement>();
push(notification: NotificationElement): void {
console.log(notification.type + ": " + notification.message);
this.notification$.next(notification);
push(message: string, type: "info" | "danger" | "warn" | "success"): void {
console.log(type + ": " + message);
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

@ -2,6 +2,7 @@ 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";
@Injectable({
providedIn: "root",
@ -10,7 +11,8 @@ export class RequestService {
constructor(
private http: HttpClient,
private router: Router,
private notificationService: NotificationService
private notificationService: NotificationService,
private translate: TranslateService
) {}
public post(
@ -85,7 +87,7 @@ export class RequestService {
private handleError(answer: any): void {
if (answer.status == 401) {
console.log("Deine Sitzung konnte nicht gefunden werden");
this.showSnackBar("Deine Sitzung konnte nicht gefunden werden");
this.router.navigate(["/auth"]);
return;
}
@ -103,7 +105,10 @@ export class RequestService {
}
if (errorObject.hasOwnProperty("code")) {
throw errorObject.code.toString();
throw (
this.translate.instant("error." + errorObject.code.toString()) ??
errorObject.code.toString()
);
}
} catch (error: any) {
this.showSnackBar(error.toString());
@ -111,9 +116,6 @@ export class RequestService {
}
private showSnackBar(message: string) {
this.notificationService.push({
type: "info",
message: message,
});
this.notificationService.push(message, "info");
}
}

View File

@ -17,7 +17,6 @@ export class TableComponent {
@Input() columns: ColumnDefinition[] = [];
isBoolean(obje: any): boolean {
console.log(obje);
return typeof obje === "boolean";
}
}

54
src/assets/i18n/de.json Normal file
View File

@ -0,0 +1,54 @@
{
"title": "Bienenkeeper",
"error": {
"Generic.SomethingWentWrong": "Ein unerwarteter Fehler ist aufgetreten. Bitte versuche es später erneut."
},
"languages": {
"de": "Deutsch",
"en": "Englisch"
},
"menu": {
"settings": "Einstellungen",
"logout": "Ausloggen",
"theme": {
"dark": "Dunkel",
"light": "Hell"
}
},
"navigation": {
"home": "Startseite",
"colonies": "Völker"
},
"auth": {
"password": "Password",
"email": "E-Mail",
"username-or-email": "Benutzername oder E-Mail",
"username": "Benutzername",
"login": "Anmelden",
"registration": "Registrierung",
"register": "Registrieren",
"reset-password": "Passwort zurücksetzen",
"new-password": "Neues Passwort",
"password-confirmation": "Passwort wiederholen",
"reset-password-confirmation": "Neues Passwort setzen",
"already-registered": "Bereits registriert?",
"not-yet-registered": "Noch nicht registriert?",
"login-now": "Jetzt anmelden!",
"register-now": "Jetzt registrieren!",
"forgot-password": "Passwort vergessen",
"confirm-registration": "Registrierung bestätigen"
},
"settings": {
"tab-security": "Sicherheit",
"tab-profile": "Profil",
"security": {
"change-username": "Benutzername ändern",
"change-password": "Passwort ändern",
"current-password": "Aktuelles Passwort",
"new-password": "Neues Passwort",
"new-username": "Neuer Benutzername",
"password-confirmation": "Passwort wiederholen"
}
}
}

54
src/assets/i18n/en.json Normal file
View File

@ -0,0 +1,54 @@
{
"title": "Beekeeper",
"error": {
"Generic.SomethingWentWrong": "An unexpected error has occurred. Please try again later."
},
"languages": {
"de": "German",
"en": "English"
},
"menu": {
"settings": "Settings",
"logout": "Logout",
"theme": {
"dark": "Dark",
"light": "Light"
}
},
"navigation": {
"home": "Home",
"colonies": "Colonies"
},
"auth": {
"password": "Password",
"email": "Email",
"username-or-email": "Username or Email",
"username": "Username",
"login": "Login",
"registration": "Registration",
"register": "Register",
"reset-password": "Reset Password",
"new-password": "New Password",
"password-confirmation": "Confirm Password",
"reset-password-confirmation": "Set New Password",
"already-registered": "Already registered?",
"not-yet-registered": "Not yet registered?",
"login-now": "Login now!",
"register-now": "Register now!",
"forgot-password": "Forgot Password",
"confirm-registration": "Confirm Registration"
},
"settings": {
"tab-security": "Security",
"tab-profile": "Profile",
"security": {
"change-username": "Change Username",
"change-password": "Change Password",
"current-password": "Current Password",
"new-password": "New Password",
"new-username": "New Username",
"password-confirmation": "Confirm Password"
}
}
}

1
src/assets/icon.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M17.4 9C17 7.8 16.2 7 15 6.5V5H14V6.4H13.6C12.5 6.4 11.6 6.8 10.8 7.6L10.4 8L9 7.5C8.7 7.4 8.4 7.3 8 7.3C7.4 7.3 6.8 7.5 6.3 7.9C5.7 8.3 5.4 8.8 5.2 9.3C5 10 5 10.6 5.2 11.3C5.5 12 5.8 12.5 6.3 12.8C5.9 14.3 6.2 15.6 7.3 16.7C8.1 17.5 9 17.9 10.1 17.9C10.6 17.9 10.9 17.9 11.2 17.8C11.8 18.6 12.6 19.1 13.6 19.1C13.9 19.1 14.3 19.1 14.6 19C15.2 18.8 15.6 18.4 16 17.9C16.4 17.3 16.6 16.8 16.6 16.2C16.6 15.8 16.6 15.5 16.5 15.2L16 13.6L16.6 13.2C17.4 12.4 17.8 11.3 17.7 10.1H19V9H17.4M7.7 11.3C7.1 11 6.9 10.6 7.1 10C7.3 9.4 7.7 9.2 8.3 9.4L11.5 10.6C9.9 11.4 8.7 11.6 7.7 11.3M14 16.9C13.4 17.1 13 16.9 12.7 16.3C12.4 15.3 12.6 14.1 13.4 12.5L14.6 15.6C14.8 16.3 14.6 16.7 14 16.9M15.2 11.6L14.6 10V9.9L14.3 9.6H14.2L12.6 9C13 8.7 13.4 8.5 13.9 8.5C14.4 8.5 14.9 8.7 15.3 9.1C15.7 9.5 15.9 9.9 15.9 10.4C15.7 10.7 15.5 11.2 15.2 11.6Z" /></svg>

After

Width:  |  Height:  |  Size: 935 B

View File

@ -12,6 +12,7 @@ module.exports = {
content: ["./src/**/*.{html,ts}", "./node_modules/flowbite/**/*.js"],
theme: {
extend: {
// "foregrounds"
textColor: {
skin: {
primary: withOpacity("--color-text-primary"),
@ -22,6 +23,18 @@ module.exports = {
"accent-muted": withOpacity("--color-text-accent-muted"),
},
},
fill: {
skin: {
primary: withOpacity("--color-text-primary"),
"primary-muted": withOpacity("--color-text-primary-muted"),
secondary: withOpacity("--color-text-secondary"),
"secondary-muted": withOpacity("--color-text-secondary-muted"),
accent: withOpacity("--color-text-accent"),
"accent-muted": withOpacity("--color-text-accent-muted"),
},
},
// "backgrounds"
backgroundColor: {
skin: {
primary: withOpacity("--color-primary"),
@ -29,6 +42,8 @@ module.exports = {
accent: withOpacity("--color-accent"),
},
},
// "delicate lines"
boxShadowColor: {
skin: {
primary: withOpacity("--color-text-secondary-muted"),