Compare commits

...

2 Commits

Author SHA1 Message Date
Flo
d8d09d7bac internationalization 2024-08-31 22:18:08 +00:00
Flo
5facddcb5a intermediate commit 2024-08-30 21:35:28 +00:00
31 changed files with 581 additions and 202 deletions

View File

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

View File

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

View File

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

View File

@ -1,8 +1,13 @@
import { NgModule } from "@angular/core"; import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser"; 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 { 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 { AppComponent } from "./app.component";
import { RouterModule, Routes } from "@angular/router"; import { RouterModule, Routes } from "@angular/router";
import { SharedModule } from "./shared/shared.module"; 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({ @NgModule({
declarations: [AppComponent, HomeComponent], declarations: [AppComponent, HomeComponent],
bootstrap: [AppComponent],
imports: [ imports: [
BrowserModule, BrowserModule,
BrowserAnimationsModule, BrowserAnimationsModule,
HttpClientModule,
RouterModule, RouterModule,
RouterModule.forRoot(routes), RouterModule.forRoot(routes),
SharedModule, SharedModule,
CoreModule, CoreModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: createTranslateLoader,
deps: [HttpClient],
},
}),
], ],
providers: [], providers: [provideHttpClient(withInterceptorsFromDi())],
bootstrap: [AppComponent],
}) })
export class AppModule {} export class AppModule {}

View File

@ -7,6 +7,7 @@ import { RouterModule, Routes } from "@angular/router";
import { ReactiveFormsModule } from "@angular/forms"; import { ReactiveFormsModule } from "@angular/forms";
import { ForgotPasswordComponent } from "./components/forgot-password/forgot-password.component"; import { ForgotPasswordComponent } from "./components/forgot-password/forgot-password.component";
import { ResetPasswordComponent } from "./components/reset-password/reset-password.component"; import { ResetPasswordComponent } from "./components/reset-password/reset-password.component";
import { TranslateModule } from "@ngx-translate/core";
const routes: Routes = [ const routes: Routes = [
{ path: "login", component: LoginComponent }, { path: "login", component: LoginComponent },
@ -38,6 +39,11 @@ const routes: Routes = [
ForgotPasswordComponent, ForgotPasswordComponent,
ResetPasswordComponent, ResetPasswordComponent,
], ],
imports: [RouterModule.forChild(routes), CommonModule, ReactiveFormsModule], imports: [
RouterModule.forChild(routes),
CommonModule,
ReactiveFormsModule,
TranslateModule,
],
}) })
export class AuthModule {} export class AuthModule {}

View File

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

View File

@ -2,10 +2,10 @@
<div class="max-w-sm mx-auto mb-10"> <div class="max-w-sm mx-auto mb-10">
<h1 class="font-bold text-center text-skin-primary text-5xl mb-5"> <h1 class="font-bold text-center text-skin-primary text-5xl mb-5">
Beekeeper {{ "title" | translate }}
</h1> </h1>
<h1 class="font-bold text-center text-skin-accent text-xl"> <h1 class="font-bold text-center text-skin-accent text-xl">
Passwort vergessen? {{ "auth.forgot-password" | translate }}
</h1> </h1>
</div> </div>
@ -14,7 +14,8 @@
<label <label
for="mail" for="mail"
class="block mb-2 text-sm font-medium text-skin-primary-muted" class="block mb-2 text-sm font-medium text-skin-primary-muted"
>E-Mail</label >
{{ "auth.email" | translate }}</label
> >
<input <input
formControlName="mail" formControlName="mail"
@ -30,6 +31,6 @@
type="submit" 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" 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> </button>
</form> </form>

View File

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

View File

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

View File

@ -2,10 +2,10 @@
<div class="max-w-sm mx-auto mb-10"> <div class="max-w-sm mx-auto mb-10">
<h1 class="font-bold text-center text-skin-primary text-5xl mb-5"> <h1 class="font-bold text-center text-skin-primary text-5xl mb-5">
Beekeeper {{ "title" | translate }}
</h1> </h1>
<h1 class="font-bold text-center text-skin-accent text-xl"> <h1 class="font-bold text-center text-skin-accent text-xl">
Passwort zurücksetzen {{ "auth.reset-password" | translate }}
</h1> </h1>
</div> </div>
@ -14,7 +14,7 @@
<label <label
for="newPassword" for="newPassword"
class="block mb-2 text-sm font-medium text-skin-primary-muted" class="block mb-2 text-sm font-medium text-skin-primary-muted"
>Neues Passwort</label >{{ "auth.new-password" | translate }}</label
> >
<input <input
formControlName="newPassword" formControlName="newPassword"
@ -28,7 +28,7 @@
<label <label
for="passwordConfirmation" for="passwordConfirmation"
class="block mb-2 text-sm font-medium text-skin-primary-muted" class="block mb-2 text-sm font-medium text-skin-primary-muted"
>Passwort wiederholen</label >{{ "auth.password-confirmation" | translate }}</label
> >
<input <input
formControlName="passwordConfirmation" formControlName="passwordConfirmation"
@ -44,6 +44,6 @@
type="submit" 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" 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> </button>
</form> </form>

View File

@ -1,5 +1,10 @@
<app-navigation></app-navigation> <app-navigation></app-navigation>
<div class="max-w-screen-xl mx-auto p-4"> <div class="mx-auto p-5 flex flex-row">
<router-outlet></router-outlet> <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> </div>

View File

@ -1,108 +1,97 @@
<nav class="bg-skin-accent"> <nav class="bg-skin-accent">
<div <div class="h-20 flex flex-wrap items-center justify-between mx-auto p-4">
class="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4" <!--Burger Menu-->
> <button
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 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" fill="none" viewBox="0 0 17 14">
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M1 1h15M1 7h15M1 13h15"
/>
</svg>
</button>
<!-- Title --> <!-- Title -->
<a routerLink="/" class="flex items-center space-x-3"> <a routerLink="/" class="flex items-center space-x-3">
<img src="assets/icon.png" class="h-10" alt="Beekeeper Logo" /> <img src="assets/icon.png" class="h-10 w-10" alt="Beekeeper Logo" />
<span <span
class="text-skin-primary self-center text-4xl font-semibold whitespace-nowrap" class="text-skin-primary self-center text-4xl font-semibold whitespace-nowrap"
>Bienenkeeper</span >{{ "title" | translate }}</span
> >
</a> </a>
<div class="flex items-center md:order-2 space-x-3 md:space-x-0"> <!--User Bubble-->
<!--User Bubble--> <button
<button type="button"
type="button" class="w-12 h-12 text-sm bg-skin-primary rounded-full focus:ring-4 focus:ring-skin-primary"
class="block w-10 h-10 text-sm bg-skin-primary rounded-full md:me-0 focus:ring-4 focus:ring-skin-primary" id="user-menu-button"
id="user-menu-button" aria-expanded="false"
aria-expanded="false" data-dropdown-toggle="user-dropdown"
data-dropdown-toggle="user-dropdown" data-dropdown-placement="bottom"
data-dropdown-placement="bottom"
>
<span class="text-skin-accent rounded-full font-bold p-2 uppercase">{{
state?.username[0] + state?.roleIdentifier[0]
}}</span>
</button>
<!--Burger Menu-->
<button
data-collapse-toggle="navbar-user"
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"
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"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M1 1h15M1 7h15M1 13h15"
/>
</svg>
</button>
</div>
<!-- User dropdown -->
<div id="user-dropdown" class="hidden min-w-full md:min-w-72 px-4">
<div
class="text-base list-none divide-y divide-skin-primary rounded-lg shadow-sm shadow-skin-primary bg-skin-primary"
>
<div class="px-4 py-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="p-4" aria-labelledby="user-menu-button">
<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>
</div>
<!-- Navigatoin -->
<div
class="items-center justify-between hidden w-full md:flex md:w-auto md:order-1"
id="navbar-user"
> >
<ul <span class="text-skin-accent rounded-full font-bold p-2 uppercase">{{
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" state?.username[0] + state?.roleIdentifier[0]
> }}</span>
<li *ngFor="let navigationLink of navigationLinks"> </button>
<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> </div>
</nav> </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"
>
<div class="px-4 py-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="p-4" aria-labelledby="user-menu-button">
<li *ngFor="let usermenuButton of usermenuButtons">
<button
class="w-full text-center block py-2 px-3 rounded text-skin-primary hover:bg-skin-accent"
[routerLink]="
usermenuButton.routerLink === undefined
? null
: [usermenuButton.routerLink]
"
(click)="clickUserMenuButtonLabel(usermenuButton)"
>
{{ getUserMenuButtonLabel(usermenuButton) | translate }}
</button>
</li>
</ul>
</div>
</div>

View File

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

View File

@ -0,0 +1,42 @@
<div
*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
class="flex-shrink-0 w-4 h-4"
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="ms-3 text-sm font-medium">
{{ 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(item)"
>
<span class="sr-only">Close</span>
<svg
class="w-3 h-3"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 14 14"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"
/>
</svg>
</button>
</div>

View File

@ -0,0 +1,69 @@
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"],
})
export class NotificationBarComponent {
items: NotificationItem[] = [];
isRunning: boolean;
constructor(private notificationService: NotificationService) {
this.isRunning = false;
this.notificationService.notification$.subscribe((notification) => {
if (notification !== undefined) {
this.items.push({
element: notification,
shownAt: new Date(),
});
this.startTimeoutIfNecessary();
}
});
}
startTimeoutIfNecessary(): void {
if (this.isRunning === true) {
return;
}
let intervalId = setInterval(() => {
this.isRunning = true;
let now = new Date();
this.items.forEach((item) => {
if (item.element.timeout !== undefined) {
let timeout = new Date(
item.shownAt.getTime() + item.element.timeout * 1000
);
if (timeout < now) {
this.dismiss(item);
}
}
});
if (this.items.length === 0) {
clearInterval(intervalId);
this.isRunning = false;
}
}, 1000);
}
dismiss(item: NotificationItem): void {
this.items = this.items.filter((i) => i !== item);
}
}

View File

@ -27,7 +27,7 @@
aria-controls="profile" aria-controls="profile"
aria-selected="false" aria-selected="false"
> >
Profil {{ "settings.tab-profile" | translate }}
</button> </button>
</li> </li>
<li class="me-2" role="presentation"> <li class="me-2" role="presentation">
@ -40,7 +40,7 @@
aria-controls="security" aria-controls="security"
aria-selected="false" aria-selected="false"
> >
Sicherheit {{ "settings.tab-security" | translate }}
</button> </button>
</li> </li>
</ul> </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({ @Component({
selector: 'app-tab-profile', selector: "app-tab-profile",
templateUrl: './tab-profile.component.html', templateUrl: "./tab-profile.component.html",
styleUrls: ['./tab-profile.component.scss'] 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 class="grid grid-cols-1 md:grid-cols-2">
<div> <div>
<h2>Passwort ändern</h2> <h2>{{ "settings.security.change-password" | translate }}</h2>
<form class="mx-auto p-5" [formGroup]="changePasswordForm"> <form class="mx-auto p-5" [formGroup]="changePasswordForm">
<div class="mb-5"> <div class="mb-5">
<label <label
for="password" for="password"
class="block mb-2 text-sm font-medium text-skin-primary-muted" class="block mb-2 text-sm font-medium text-skin-primary-muted"
>Aktuelles Passwort</label >{{ "settings.security.current-password" | translate }}</label
> >
<input <input
formControlName="password" formControlName="password"
@ -21,7 +21,7 @@
<label <label
for="newPassword" for="newPassword"
class="block mb-2 text-sm font-medium text-skin-primary-muted" class="block mb-2 text-sm font-medium text-skin-primary-muted"
>Neues Passwort</label >{{ "settings.security.new-password" | translate }}</label
> >
<input <input
formControlName="newPassword" formControlName="newPassword"
@ -36,7 +36,7 @@
<label <label
for="newPasswordConfirmation" for="newPasswordConfirmation"
class="block mb-2 text-sm font-medium text-skin-primary-muted" class="block mb-2 text-sm font-medium text-skin-primary-muted"
>Neues Passwort bestätigen</label >{{ "settings.security.password-confirmation" | translate }}</label
> >
<input <input
formControlName="newPasswordConfirmation" formControlName="newPasswordConfirmation"
@ -53,19 +53,19 @@
type="submit" 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" 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> </button>
</form> </form>
</div> </div>
<div> <div>
<h2>Benutername ändern</h2> <h2>{{ "settings.security.change-username" | translate }}</h2>
<form class="mx-auto p-5" [formGroup]="changeUsernameForm"> <form class="mx-auto p-5" [formGroup]="changeUsernameForm">
<div class="mb-5"> <div class="mb-5">
<label <label
for="password" for="password"
class="block mb-2 text-sm font-medium text-skin-primary-muted" class="block mb-2 text-sm font-medium text-skin-primary-muted"
>Aktuelles Passwort</label >{{ "settings.security.current-password" | translate }}</label
> >
<input <input
formControlName="password" formControlName="password"
@ -80,7 +80,7 @@
<label <label
for="newUsername" for="newUsername"
class="block mb-2 text-sm font-medium text-skin-primary-muted" class="block mb-2 text-sm font-medium text-skin-primary-muted"
>Neuer Benutzername</label >{{ "settings.security.new-username" | translate }}</label
> >
<input <input
formControlName="newUsername" formControlName="newUsername"
@ -97,7 +97,7 @@
type="submit" 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" 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> </button>
</form> </form>
</div> </div>

View File

@ -11,6 +11,9 @@ import { TabProfileComponent } from "./components/settings/tabs/tab-profile/tab-
import { TabSecurityComponent } from "./components/settings/tabs/tab-security/tab-security.component"; import { TabSecurityComponent } from "./components/settings/tabs/tab-security/tab-security.component";
import { RouterModule } from "@angular/router"; import { RouterModule } from "@angular/router";
import { ReactiveFormsModule } from "@angular/forms"; 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({ @NgModule({
declarations: [ declarations: [
@ -18,14 +21,28 @@ import { ReactiveFormsModule } from "@angular/forms";
SettingsComponent, SettingsComponent,
TabProfileComponent, TabProfileComponent,
TabSecurityComponent, TabSecurityComponent,
NotificationBarComponent,
], ],
exports: [ exports: [
NavigationComponent, NavigationComponent,
SettingsComponent, SettingsComponent,
TabProfileComponent, TabProfileComponent,
TabSecurityComponent, TabSecurityComponent,
NotificationBarComponent,
],
imports: [
CommonModule,
SharedModule,
RouterModule,
ReactiveFormsModule,
TranslateModule,
],
providers: [
AuthGuard,
AuthService,
RequestService,
AppService,
NotificationService,
], ],
imports: [CommonModule, SharedModule, RouterModule, ReactiveFormsModule],
providers: [AuthGuard, AuthService, RequestService, AppService],
}) })
export class CoreModule {} export class CoreModule {}

View File

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

View File

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

View File

@ -0,0 +1,34 @@
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 {
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

@ -1,12 +1,19 @@
import { HttpClient } from "@angular/common/http"; import { HttpClient } from "@angular/common/http";
import { Router } from "@angular/router"; import { Router } from "@angular/router";
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { NotificationService } from "./notification.service";
import { TranslateService } from "@ngx-translate/core";
@Injectable({ @Injectable({
providedIn: "root", providedIn: "root",
}) })
export class RequestService { export class RequestService {
constructor(private http: HttpClient, private router: Router) {} constructor(
private http: HttpClient,
private router: Router,
private notificationService: NotificationService,
private translate: TranslateService
) {}
public post( public post(
apiPath: string, apiPath: string,
@ -80,13 +87,13 @@ export class RequestService {
private handleError(answer: any): void { private handleError(answer: any): void {
if (answer.status == 401) { if (answer.status == 401) {
console.log("Deine Sitzung konnte nicht gefunden werden"); this.showSnackBar("Deine Sitzung konnte nicht gefunden werden");
this.router.navigate(["/auth"]); this.router.navigate(["/auth"]);
return; return;
} }
if (answer.status == 403) { if (answer.status == 403) {
this.showSnackBar("Du bist nicht für diese Aktion autorisiert", "Ok"); this.showSnackBar("Du bist nicht für diese Aktion autorisiert");
return; return;
} }
@ -98,17 +105,17 @@ export class RequestService {
} }
if (errorObject.hasOwnProperty("code")) { if (errorObject.hasOwnProperty("code")) {
throw errorObject.code.toString(); throw (
this.translate.instant("error." + errorObject.code.toString()) ??
errorObject.code.toString()
);
} }
} catch (error: any) { } catch (error: any) {
this.showSnackBar(error.toString(), "Ok"); this.showSnackBar(error.toString());
} }
} }
private showSnackBar(message: string, action?: string) { private showSnackBar(message: string) {
/*this.snackBar.open(message.toString(), action, { this.notificationService.push(message, "info");
duration: 3000,
});*/
console.log(message);
} }
} }

View File

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

@ -8,8 +8,8 @@
--color-text-primary-muted: 100, 100, 100; --color-text-primary-muted: 100, 100, 100;
--color-text-secondary: 255, 255, 255; --color-text-secondary: 255, 255, 255;
--color-text-secondary-muted: 170, 170, 170; --color-text-secondary-muted: 170, 170, 170;
--color-text-accent: 255, 199, 44; --color-text-accent: 250, 183, 0;
--color-text-accent-muted: 255, 199, 44; --color-text-accent-muted: 250, 183, 0;
--color-primary: 244, 244, 245; --color-primary: 244, 244, 245;
--color-secondary: 238, 238, 240; --color-secondary: 238, 238, 240;

View File

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