This commit is contained in:
Flo 2024-09-01 20:13:57 +00:00
parent d8d09d7bac
commit 1f99cfb53c
34 changed files with 551 additions and 345 deletions

View File

@ -15,8 +15,12 @@ export class AppComponent implements OnInit {
) { ) {
var language = navigator.language; var language = navigator.language;
translate.setDefaultLang(language); this.translate.setDefaultLang(language);
translate.use(language); this.translate.use(language);
this.translate.onLangChange.subscribe(() => {
document.title = this.translate.instant("title");
});
} }
ngOnInit(): void { ngOnInit(): void {

View File

@ -8,6 +8,7 @@ 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"; import { TranslateModule } from "@ngx-translate/core";
import { SharedModule } from "src/app/shared/shared.module";
const routes: Routes = [ const routes: Routes = [
{ path: "login", component: LoginComponent }, { path: "login", component: LoginComponent },
@ -44,6 +45,7 @@ const routes: Routes = [
CommonModule, CommonModule,
ReactiveFormsModule, ReactiveFormsModule,
TranslateModule, TranslateModule,
SharedModule,
], ],
}) })
export class AuthModule {} export class AuthModule {}

View File

@ -11,39 +11,20 @@
<form class="max-w-sm mx-auto" [formGroup]="confirmRegistrationForm"> <form class="max-w-sm mx-auto" [formGroup]="confirmRegistrationForm">
<div class="mb-5"> <div class="mb-5">
<label <shared-input
for="password" key="password"
class="block mb-2 text-sm font-medium text-skin-primary-muted" inputType="password"
>{{ "auth.password" | translate }}</label label="auth.password"
> required="true"
<input
formControlName="password"
type="password"
id="password"
class="bg-skin-primary border border-gray-300 text-skin-primary text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
required
/> />
</div> </div>
<div class="mb-5"> <div class="mb-5">
<label <shared-input
for="passwordConfirmation" key="passwordConfirmation"
class="block mb-2 text-sm font-medium text-skin-primary-muted" inputType="password"
>{{ "auth.password-confirmation" | translate }}</label label="auth.password-confirmation"
> required="true"
<input
formControlName="passwordConfirmation"
type="password"
id="passwordConfirmation"
class="bg-skin-primary border border-gray-300 text-skin-primary text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
required
/> />
</div> </div>
<button <shared-submit label="auth.confirm-registration" (onSubmit)="confirm()" />
(click)="confirm()"
[disabled]="!confirmRegistrationForm.valid"
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"
>
{{ "auth.confirm-registration" | translate }}
</button>
</form> </form>

View File

@ -40,5 +40,7 @@ export class ConfirmRegistrationComponent {
passwordConfirmation: passwordConfirmation:
this.confirmRegistrationForm.value.passwordConfirmation!, this.confirmRegistrationForm.value.passwordConfirmation!,
}); });
this.router.navigateByUrl("/auth/login");
} }
} }

View File

@ -11,26 +11,12 @@
<form class="max-w-sm mx-auto" [formGroup]="forgotPasswordForm"> <form class="max-w-sm mx-auto" [formGroup]="forgotPasswordForm">
<div class="mb-5"> <div class="mb-5">
<label <shared-input
for="mail" key="mail"
class="block mb-2 text-sm font-medium text-skin-primary-muted" inputType="email"
> label="auth.email"
{{ "auth.email" | translate }}</label required="true"
>
<input
formControlName="mail"
type="email"
id="mail"
class="bg-skin-primary border border-gray-300 text-skin-primary text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
required
/> />
</div> </div>
<button <shared-submit label="auth.reset-password" (onSubmit)="confirm()" />
(click)="confirm()"
[disabled]="!forgotPasswordForm.valid"
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"
>
{{ "auth.reset-password" | translate }}
</button>
</form> </form>

View File

@ -11,23 +11,13 @@
<form class="max-w-sm mx-auto" [formGroup]="loginForm"> <form class="max-w-sm mx-auto" [formGroup]="loginForm">
<div class="mb-5"> <div class="mb-5">
<label <shared-input
for="identifier" key="identifier"
class="block mb-2 text-sm font-medium text-skin-primary-muted" inputType="text"
>{{ "auth.username-or-email" | translate }}</label label="auth.username-or-email"
> required="true"
<input
formControlName="identifier"
type="text"
id="identifier"
class="bg-skin-primary border border-gray-300 text-skin-primary text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
placeholder="username72 / your@email.com"
required
/> />
<p <p class="mt-2 text-sm text-skin-primary-muted">
id="helper-text-explanation"
class="mt-2 text-sm text-skin-primary-muted"
>
{{ "auth.not-yet-registered" | translate }} {{ "auth.not-yet-registered" | translate }}
<a <a
routerLink="/auth/registration" routerLink="/auth/registration"
@ -37,22 +27,13 @@
</p> </p>
</div> </div>
<div class="mb-5"> <div class="mb-5">
<label <shared-input
for="password" key="password"
class="block mb-2 text-sm font-medium text-skin-primary-muted" inputType="password"
>{{ "auth.password" | translate }}</label label="auth.password"
> required="true"
<input
formControlName="password"
type="password"
id="password"
class="bg-skin-primary border border-gray-300 text-skin-primary text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
required
/> />
<p <p class="mt-2 text-sm text-skin-primary-muted">
id="helper-text-explanation"
class="mt-2 text-sm text-skin-primary-muted"
>
<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"
@ -60,12 +41,6 @@
> >
</p> </p>
</div> </div>
<button
(click)="login()" <shared-submit label="auth.login" (onSubmit)="login()" />
[disabled]="!loginForm.valid"
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"
>
{{ "auth.login" | translate }}
</button>
</form> </form>

View File

@ -11,34 +11,12 @@
<form class="max-w-sm mx-auto" [formGroup]="registrationForm"> <form class="max-w-sm mx-auto" [formGroup]="registrationForm">
<div class="mb-5"> <div class="mb-5">
<label <shared-input
for="mail" key="mail"
class="block mb-2 text-sm font-medium text-skin-primary-muted" inputType="email"
> label="auth.email"
{{ "auth.email" | translate }}</label required="true"
>
<input
formControlName="mail"
type="mail"
id="mail"
class="bg-skin-primary border border-gray-300 text-skin-primary text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
placeholder="your@email.com" placeholder="your@email.com"
required
/>
</div>
<div class="mb-5">
<label
for="username"
class="block mb-2 text-sm font-medium text-skin-primary-muted"
>
{{ "auth.reset-password" | translate }}</label
>
<input
formControlName="username"
type="string"
id="username"
class="bg-skin-primary border border-gray-300 text-skin-primary text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
required
/> />
<p <p
id="helper-text-explanation" id="helper-text-explanation"
@ -52,12 +30,13 @@
> >
</p> </p>
</div> </div>
<button <div class="mb-5">
(click)="register()" <shared-input
[disabled]="!registrationForm.valid" key="username"
type="submit" inputType="string"
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" label="auth.username"
> required="true"
{{ "auth.register" | translate }} />
</button> </div>
<shared-submit label="auth.register" (onSubmit)="register()" />
</form> </form>

View File

@ -11,39 +11,20 @@
<form class="max-w-sm mx-auto" [formGroup]="resetPasswordForm"> <form class="max-w-sm mx-auto" [formGroup]="resetPasswordForm">
<div class="mb-5"> <div class="mb-5">
<label <shared-input
for="newPassword" key="newPassword"
class="block mb-2 text-sm font-medium text-skin-primary-muted" inputType="password"
>{{ "auth.new-password" | translate }}</label label="auth.new-password"
> required="true"
<input
formControlName="newPassword"
type="password"
id="newPassword"
class="bg-skin-primary border border-gray-300 text-skin-primary text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
required
/> />
</div> </div>
<div class="mb-5"> <div class="mb-5">
<label <shared-input
for="passwordConfirmation" key="passwordConfirmation"
class="block mb-2 text-sm font-medium text-skin-primary-muted" inputType="password"
>{{ "auth.password-confirmation" | translate }}</label label="auth.password-confirmation"
> required="true"
<input
formControlName="passwordConfirmation"
type="password"
id="passwordConfirmation"
class="bg-skin-primary border border-gray-300 text-skin-primary text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
required
/> />
</div> </div>
<button <shared-submit label="auth.reset-password" (onSubmit)="confirm()" />
(click)="confirm()"
[disabled]="!resetPasswordForm.valid"
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"
>
{{ "auth.new-password" | translate }}
</button>
</form> </form>

View File

@ -21,18 +21,27 @@
</button> </button>
<!-- Title --> <!-- Title -->
<a routerLink="/" class="flex items-center space-x-3"> <a routerLink="/" class="text-skin-primary flex items-center space-x-3"
<img src="assets/icon.png" class="h-10 w-10" alt="Beekeeper Logo" /> ><svg
<span xmlns="http://www.w3.org/2000/svg"
class="text-skin-primary self-center text-4xl font-semibold whitespace-nowrap" viewBox="4 4 16 16"
>{{ "title" | translate }}</span class="w-12 h-12"
> >
<title>{{ "title" | translate }}</title>
<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>
<span class="self-center text-4xl font-semibold whitespace-nowrap">{{
"title" | translate
}}</span>
</a> </a>
<!--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="w-12 h-12 text-sm bg-skin-primary rounded-full focus:ring-4 focus:ring-skin-accent"
id="user-menu-button" id="user-menu-button"
aria-expanded="false" aria-expanded="false"
data-dropdown-toggle="user-dropdown" data-dropdown-toggle="user-dropdown"
@ -57,13 +66,26 @@
class="block py-2 px-3 rounded text-skin-primary hover:text-skin-secondary hover:bg-skin-accent" class="block py-2 px-3 rounded text-skin-primary hover:text-skin-secondary hover:bg-skin-accent"
[routerLink]="navigationLink.routerLink" [routerLink]="navigationLink.routerLink"
[routerLinkActiveOptions]="{ exact: true }" [routerLinkActiveOptions]="{ exact: true }"
routerLinkActive="font-bold" routerLinkActive="underline"
>{{ getNavigationLinkLabel(navigationLink) | translate }}</a
> >
<div class="flex flex-row gap-4">
<svg
*ngIf="navigationLink.path !== undefined"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="w-8 h-8 basis-auto"
>
<title>{{ navigationLink.label | translate }}</title>
<path fill="currentColor" [attr.d]="navigationLink.path" />
</svg>
<div class="basis-full">
{{ navigationLink.label | translate }}
</div>
</div>
</a>
</li> </li>
</ul> </ul>
</div> </div>
<!-- User dropdown --> <!-- User dropdown -->
<div id="user-dropdown" class="hidden min-w-full md:min-w-64 px-4 z-50"> <div id="user-dropdown" class="hidden min-w-full md:min-w-64 px-4 z-50">
<div <div
@ -81,7 +103,7 @@
<ul class="p-4" aria-labelledby="user-menu-button"> <ul class="p-4" aria-labelledby="user-menu-button">
<li *ngFor="let usermenuButton of usermenuButtons"> <li *ngFor="let usermenuButton of usermenuButtons">
<button <button
class="w-full text-center block py-2 px-3 rounded text-skin-primary hover:bg-skin-accent" class="block py-2 px-3 rounded text-skin-primary hover:text-skin-secondary hover:bg-skin-accent w-full text-right"
[routerLink]=" [routerLink]="
usermenuButton.routerLink === undefined usermenuButton.routerLink === undefined
? null ? null

View File

@ -9,7 +9,8 @@ import { NotificationService } from "../../services/notification.service";
import { TranslateService } from "@ngx-translate/core"; import { TranslateService } from "@ngx-translate/core";
interface NavigationLink { interface NavigationLink {
label: Function | string; path: string | undefined;
label: string;
routerLink: string; routerLink: string;
} }
@ -26,10 +27,20 @@ interface UserMenuButton {
}) })
export class NavigationComponent implements OnInit { export class NavigationComponent implements OnInit {
navigationLinks: NavigationLink[] = [ navigationLinks: NavigationLink[] = [
{ routerLink: "/", label: () => "navigation.home" }, {
routerLink: "/",
path: "M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z",
label: "navigation.home",
},
{
routerLink: "/settings",
path: "M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z",
label: "menu.settings",
},
{ {
routerLink: "/colonies", routerLink: "/colonies",
label: () => "navigation.colonies", path: "M23 14.5C23 12.94 21.97 11.63 20.55 11.18C20.83 10.68 21 10.11 21 9.5C21 7.94 19.97 6.63 18.55 6.18C18.83 5.68 19 5.11 19 4.5C19 2.57 17.43 1 15.5 1H8.5C6.57 1 5 2.57 5 4.5C5 5.11 5.17 5.68 5.45 6.18C4.04 6.63 3 7.94 3 9.5C3 10.11 3.17 10.68 3.45 11.18C2.04 11.63 1 12.94 1 14.5C1 15.76 1.67 16.84 2.67 17.46C2.25 18.03 2 18.74 2 19.5C2 21.43 3.57 23 5.5 23H18.5C20.43 23 22 21.43 22 19.5C22 18.74 21.75 18.03 21.33 17.46C22.33 16.84 23 15.76 23 14.5M8.5 3H15.5C16.33 3 17 3.67 17 4.5S16.33 6 15.5 6H8.5C7.67 6 7 5.33 7 4.5S7.67 3 8.5 3M6.5 8H17.5C18.33 8 19 8.67 19 9.5S18.33 11 17.5 11H6.5C5.67 11 5 10.33 5 9.5S5.67 8 6.5 8M4 19.5C4 18.67 4.67 18 5.5 18H9V21H5.5C4.67 21 4 20.33 4 19.5M18.5 21H15V18H18.5C19.33 18 20 18.67 20 19.5S19.33 21 18.5 21M19.5 16H14.82C14.4 14.84 13.3 14 12 14S9.6 14.84 9.18 16H4.5C3.67 16 3 15.33 3 14.5S3.67 13 4.5 13H19.5C20.33 13 21 13.67 21 14.5S20.33 16 19.5 16Z",
label: "navigation.colonies",
}, },
]; ];
@ -143,11 +154,4 @@ 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

@ -19,7 +19,7 @@
</div> </div>
<button <button
type="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" class="ms-auto -mx-1.5 -my-1.5 bg-skin-secondary text-skin-accent rounded-lg focus:ring-2 focus:ring-skin-accent p-1.5 hover:bg-skin-primary inline-flex items-center justify-center h-8 w-8"
(click)="dismiss(item)" (click)="dismiss(item)"
> >
<span class="sr-only">Close</span> <span class="sr-only">Close</span>

View File

@ -1,5 +1,5 @@
<div class="rounded-lg shadow-sm shadow-skin-primary bg-skin-secondary p-5"> <div class="rounded-lg shadow-sm shadow-skin-primary bg-skin-secondary p-5">
<div class="inline-block text-skin-secondary bg-skin-accent rounded-lg p-2"> <div class="inline-block text-skin-primary bg-skin-accent rounded-lg p-2">
<div class="font-bold"> <div class="font-bold">
{{ state.username }} {{ state.username }}
</div> </div>
@ -10,57 +10,49 @@
<div class="mb-4 border-b border-skin-primary"> <div class="mb-4 border-b border-skin-primary">
<ul <ul
class="flex flex-wrap -mb-px text-sm font-medium text-center" role="tablist"
id="default-styled-tab" class="flex flex-nowrap -mb-px text-sm font-medium text-center"
data-tabs-toggle="#default-styled-tab-content" data-tabs-toggle="#tab-outlet"
data-tabs-active-classes="text-skin-accent border-skin-accent" data-tabs-active-classes="text-skin-accent border-skin-accent"
data-tabs-inactive-classes="text-skin-primary-muted hover:text-skin-primary border-skin-primary hover:border-skin-accent" data-tabs-inactive-classes="text-skin-primary-muted hover:text-skin-primary border-skin-primary hover:border-skin-accent"
role="tablist"
> >
<li class="me-2" role="presentation"> <li class="me-2 w-full xl:w-auto" role="presentation">
<button <button
class="inline-block p-4 border-b-2 rounded-t-lg"
id="profile-styled-tab"
data-tabs-target="#styled-profile"
type="button"
role="tab" role="tab"
aria-controls="profile" class="block p-4 w-full xl:w-auto border-b-2 rounded-t-lg"
aria-selected="false" data-tabs-target="#profile"
> >
{{ "settings.tab-profile" | translate }} {{ "settings.tab-profile" | translate }}
</button> </button>
</li> </li>
<li class="me-2" role="presentation"> <li class="me-2 last:me-0 w-full xl:w-auto" role="presentation">
<button <button
class="inline-block p-4 border-b-2 rounded-t-lg" class="block p-4 w-full xl:w-auto border-b-2 rounded-t-lg"
id="security-styled-tab" data-tabs-target="#security"
data-tabs-target="#styled-security"
type="button"
role="tab" role="tab"
aria-controls="security"
aria-selected="false"
> >
{{ "settings.tab-security" | translate }} {{ "settings.tab-security" | translate }}
</button> </button>
</li> </li>
</ul> </ul>
</div> </div>
<div id="default-styled-tab-content" class="text-skin-primary"> <div id="tab-outlet" class="text-skin-primary">
<div <div role="tabpanel" class="hidden py-4 xl:p-4 rounded-lg" id="profile">
class="hidden p-4 rounded-lg"
id="styled-profile"
role="tabpanel"
aria-labelledby="profile-tab"
>
<app-tab-profile /> <app-tab-profile />
</div> </div>
<div <div role="tabpanel" class="hidden py-4 xl:p-4 rounded-lg" id="security">
class="hidden p-4 rounded-lg"
id="styled-security"
role="tabpanel"
aria-labelledby="security-tab"
>
<app-tab-security /> <app-tab-security />
</div> </div>
</div> </div>
<!--
<shared-tab-control [tabs]="tabs">
<div role="tabpanel" class="hidden py-4 xl:p-4 rounded-lg" id="profile">
<app-tab-profile />
</div>
<div role="tabpanel" class="hidden py-4 xl:p-4 rounded-lg" id="security">
<app-tab-security />
</div>
</shared-tab-control>
-->
</div> </div>

View File

@ -1,8 +1,8 @@
import { Component, OnInit } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { initFlowbite } from "flowbite"; import { initFlowbite } from "flowbite";
import { AuthService } from "../../services/auth.service"; import { AuthService } from "../../services/auth.service";
import { filter } from "rxjs";
import { UserStateResponse } from "../../models/user-state-request.model"; import { UserStateResponse } from "../../models/user-state-request.model";
import { TabItem } from "src/app/shared/models/tab-item.model";
@Component({ @Component({
selector: "app-settings", selector: "app-settings",
@ -11,6 +11,16 @@ import { UserStateResponse } from "../../models/user-state-request.model";
}) })
export class SettingsComponent implements OnInit { export class SettingsComponent implements OnInit {
state: UserStateResponse; state: UserStateResponse;
tabs: TabItem[] = [
{
label: "settings.tab-profile",
target: "profile",
},
{
label: "settings.tab-security",
target: "security",
},
];
constructor(private authService: AuthService) { constructor(private authService: AuthService) {
this.state = this.authService.currentState$.value!; this.state = this.authService.currentState$.value!;

View File

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

View File

@ -1,9 +1,15 @@
import { Component } from "@angular/core"; import { Component } from "@angular/core";
import { NotificationService } from "src/app/core/services/notification.service";
import {
ColumnDefinition,
RowAction,
} from "src/app/shared/components/table/table.component";
interface User { interface Product {
username: string; description: string;
mail: string; id: string;
age: number; price: number;
stock: number;
} }
@Component({ @Component({
@ -11,4 +17,75 @@ interface User {
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 {
constructor(private notificationService: NotificationService) {}
columns: ColumnDefinition[] = [
{
label: "test.product",
content: (item: Product) => item.description,
routerLink: (item: Product) => ["/", "product", item.id],
},
{
label: "test.price",
content: (item: Product) => item.price,
routerLink: undefined,
},
{
label: "test.delete",
content: "Löschen",
routerLink: undefined,
},
{
label: "test.stock",
content: (item: Product) => item.stock,
routerLink: undefined,
},
];
rowActions: RowAction[] = [
{
label: "test.edit",
path: "M20.71,7.04C21.1,6.65 21.1,6 20.71,5.63L18.37,3.29C18,2.9 17.35,2.9 16.96,3.29L15.12,5.12L18.87,8.87M3,17.25V21H6.75L17.81,9.93L14.06,6.18L3,17.25Z",
action: (item: Product) => {
this.edit(item);
},
},
{
label: "test.delete",
path: "M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z",
action: (item: Product) => {
this.delete(item);
},
},
];
items: Product[] = [
{
id: "1",
description: "Flüssiger Honig",
price: 6.5,
stock: 40,
},
{
id: "2",
description: "Cremiger Honig",
price: 5,
stock: 32,
},
{
id: "3",
description: "Labello",
price: 2.3,
stock: 7,
},
];
delete(item: Product) {
this.notificationService.push(item.description + " gelöscht", "info");
}
edit(item: Product) {
this.notificationService.push(item.description + " bearbeitet", "info");
}
}

View File

@ -1,104 +1,66 @@
<div class="grid grid-cols-1 md:grid-cols-2"> <div class="grid grid-cols-1 md:grid-cols-2 gap-10">
<div> <div>
<h2>{{ "settings.security.change-password" | translate }}</h2> <h2>{{ "settings.security.change-password" | translate }}</h2>
<form class="mx-auto p-5" [formGroup]="changePasswordForm"> <form class="mx-auto py-5" [formGroup]="changePasswordForm">
<div class="mb-5"> <div class="mb-5">
<label <shared-input
for="password" key="password"
class="block mb-2 text-sm font-medium text-skin-primary-muted" inputType="password"
>{{ "settings.security.current-password" | translate }}</label label="settings.security.current-password"
> required="true"
<input
formControlName="password"
type="password"
id="password"
class="bg-skin-primary border border-gray-300 text-skin-primary text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
required
/> />
</div> </div>
<div class="mb-5"> <div class="mb-5">
<label <shared-input
for="newPassword" key="newPassword"
class="block mb-2 text-sm font-medium text-skin-primary-muted" inputType="password"
>{{ "settings.security.new-password" | translate }}</label label="settings.security.new-password"
> required="true"
<input
formControlName="newPassword"
type="password"
id="newPassword"
class="bg-skin-primary border border-gray-300 text-skin-primary text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
required
/> />
</div> </div>
<div class="mb-5"> <div class="mb-5">
<label <shared-input
for="newPasswordConfirmation" key="newPasswordConfirmation"
class="block mb-2 text-sm font-medium text-skin-primary-muted" inputType="password"
>{{ "settings.security.password-confirmation" | translate }}</label label="settings.security.password-confirmation"
> required="true"
<input
formControlName="newPasswordConfirmation"
type="password"
id="newPasswordConfirmation"
class="bg-skin-primary border border-gray-300 text-skin-primary text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
required
/> />
</div> </div>
<button <shared-submit
(click)="changePassword()" label="settings.security.change-password"
[disabled]="!changePasswordForm.valid" (onSubmit)="changePassword()"
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"
>
{{ "settings.security.change-password" | translate }}
</button>
</form> </form>
</div> </div>
<div> <div>
<h2>{{ "settings.security.change-username" | translate }}</h2> <h2>{{ "settings.security.change-username" | translate }}</h2>
<form class="mx-auto p-5" [formGroup]="changeUsernameForm"> <form class="mx-auto py-5" [formGroup]="changeUsernameForm">
<div class="mb-5"> <div class="mb-5">
<label <shared-input
for="password" key="password"
class="block mb-2 text-sm font-medium text-skin-primary-muted" inputType="password"
>{{ "settings.security.current-password" | translate }}</label label="settings.security.current-password"
> required="true"
<input
formControlName="password"
type="password"
id="password"
class="bg-skin-primary border border-gray-300 text-skin-primary text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
required
/> />
</div> </div>
<div class="mb-5"> <div class="mb-5">
<label <shared-input
for="newUsername" key="newUsername"
class="block mb-2 text-sm font-medium text-skin-primary-muted" inputType="string"
>{{ "settings.security.new-username" | translate }}</label label="settings.security.new-username"
> required="true"
<input
formControlName="newUsername"
type="string"
id="newUsername"
class="bg-skin-primary border border-gray-300 text-skin-primary text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
required
/> />
</div> </div>
<button <shared-submit
(click)="changeUsername()" label="settings.security.change-username"
[disabled]="!changeUsernameForm.valid" (onSubmit)="changeUsername()"
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"
>
{{ "settings.security.change-username" | translate }}
</button>
</form> </form>
</div> </div>
</div> </div>

View File

@ -22,6 +22,14 @@ export class TabSecurityComponent {
constructor(private authService: AuthService) {} constructor(private authService: AuthService) {}
changePassword(): void { changePassword(): void {
console.log({
password: this.changePasswordForm.value.password!,
newPassword: this.changePasswordForm.value.newPassword!,
newPasswordConfirmation:
this.changePasswordForm.value.newPasswordConfirmation!,
});
return;
this.authService.changePassword({ this.authService.changePassword({
password: this.changePasswordForm.value.password!, password: this.changePasswordForm.value.password!,
newPassword: this.changePasswordForm.value.newPassword!, newPassword: this.changePasswordForm.value.newPassword!,

View File

@ -0,0 +1,27 @@
<div>
<label
*ngIf="label !== undefined"
[for]="key"
class="block mb-2 text-sm font-medium text-skin-primary-muted"
>{{ label | translate }}</label
>
<input
*ngIf="required"
[id]="key"
[type]="inputType"
[formControlName]="key"
[placeholder]="placeholder"
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"
required
/>
<input
*ngIf="!required"
[id]="key"
[type]="inputType"
[formControlName]="key"
[placeholder]="placeholder"
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"
/>
</div>

View File

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

View File

@ -0,0 +1,8 @@
<button
(click)="submit()"
[disabled]="!getParentFormGroup().valid"
type="submit"
class="w-full cursor-pointer 9xl:w-auto font-bold text-skin-primary bg-skin-accent hover:bg-skin-accent-muted focus:ring-2 focus:ring-skin-accent rounded-lg text-sm px-5 py-2.5 text-center"
>
{{ label | translate }}
</button>

View File

@ -0,0 +1,34 @@
import { Component, EventEmitter, inject, Input, Output } from "@angular/core";
import { ControlContainer, FormGroup } from "@angular/forms";
@Component({
selector: "shared-submit",
templateUrl: "./submit.component.html",
styleUrl: "./submit.component.scss",
viewProviders: [
{
provide: ControlContainer,
useFactory: () => inject(ControlContainer, { skipSelf: true }),
},
],
})
export class SubmitComponent {
@Output() onSubmit = new EventEmitter();
@Input({ required: true }) label!: string;
constructor(private controlContainer: ControlContainer) {}
getParentFormGroup(): FormGroup {
return this.controlContainer.control as FormGroup;
}
submit(): void {
var formGroup = this.getParentFormGroup();
if (!formGroup.valid) {
console.log("formGroup not Valid");
return;
}
this.onSubmit.emit();
}
}

View File

@ -0,0 +1,28 @@
<div class="">
<div class="mb-4 border-b border-skin-primary">
<ul
role="tablist"
class="flex flex-nowrap -mb-px text-sm font-medium text-center"
data-tabs-toggle="#tab-outlet"
data-tabs-active-classes="text-skin-accent border-skin-accent"
data-tabs-inactive-classes="text-skin-primary-muted hover:text-skin-primary border-skin-primary hover:border-skin-accent"
>
<li
*ngFor="let tab of tabs"
class="me-2 w-full xl:w-auto"
role="presentation"
>
<button
role="tab"
class="block p-4 w-full xl:w-auto border-b-2 rounded-t-lg"
[data-tabs-target]="tab.target"
>
{{ tab.label | translate }}
</button>
</li>
</ul>
</div>
<div id="tab-outlet" class="text-skin-primary">
<ng-content />
</div>
</div>

View File

@ -0,0 +1,16 @@
import { Component, Input, OnInit } from "@angular/core";
import { initFlowbite } from "flowbite";
import { TabItem } from "../../models/tab-item.model";
@Component({
selector: "shared-tab-control",
templateUrl: "./tab-control.component.html",
styleUrl: "./tab-control.component.scss",
})
export class TabControlComponent implements OnInit {
@Input({ required: true }) tabs!: TabItem[];
ngOnInit(): void {
initFlowbite();
}
}

View File

@ -1,55 +1,57 @@
<table class="w-full text-skin-primary"> <table class="w-full table-fixed text-skin-primary">
<thead
<thead class="uppercase bg-skin-accent border-b border-skin-primary text-left"> class="uppercase bg-skin-accent border-b border-skin-primary text-left"
<tr>
<th
*ngFor="let column of columns"
class="px-5 py-2"
> >
{{column.header}} <tr>
<th *ngFor="let column of columns" class="px-5 py-2">
{{ column.label | translate }}
</th> </th>
<th *ngIf="rowActions.length > 0" class="px-5 py-2"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let item of items" class="border-b border-skin-primary"> <tr *ngFor="let item of items" class="border-b border-skin-primary">
<td <td
*ngFor="let column of columns; index as currentIndex" *ngFor="let column of columns; index as currentIndex"
class="px-5 py-2" class="px-5 py-2 text-skin-primary"
[ngClass]="{ [ngClass]="{
'bg-skin-primary': currentIndex % 2 === 0, 'bg-skin-primary': currentIndex % 2 === 0,
'bg-skin-secondary': currentIndex % 2 === 1, 'bg-skin-secondary': currentIndex % 2 === 1,
}" }"
> >
<div *ngIf="column.routerLink !== undefined"> <div *ngIf="column.routerLink !== undefined">
<a [routerLink]="column.routerLink(item)" class="font-bold hover:text-skin-accent hover:underline"> <a [routerLink]="getRouterLink(column, item)">
<div *ngIf="column.columnContent !== undefined"> <div class="text-skin-accent hover:underline">
{{ column.columnContent }} {{ getContent(column, item) }}
</div> </div>
<div
*ngIf="column.columnFunction !== undefined"
[innerHtml]="column.columnFunction(item)"
></div>
</a> </a>
</div> </div>
<div *ngIf="column.routerLink === undefined"> <div *ngIf="column.routerLink === undefined">
<div *ngIf="column.columnContent !== undefined"> {{ getContent(column, item) }}
<div *ngIf="isBoolean(column.columnContent)">
<input type="checkbox" [(ngModel)]="column.columnContent" />
</div>
<div *ngIf="!isBoolean(column.columnContent)">
{{ column.columnContent }}
</div>
</div>
<div
*ngIf="column.columnFunction !== undefined"
[innerHtml]="column.columnFunction(item)"
></div>
</div> </div>
</td> </td>
<td
*ngIf="rowActions.length > 0"
class="text-right w-auto h-full"
[ngClass]="{
'bg-skin-primary': columns.length % 2 === 0,
'bg-skin-secondary': columns.length % 2 === 1,
}"
>
<button
class="p-2 font-bold text-skin-primary-muted hover:text-skin-primary"
*ngFor="let rowAction of rowActions"
(click)="rowAction.action(item)"
>
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6">
<title>{{ rowAction.label | translate }}</title>
<path fill="currentColor" [attr.d]="rowAction.path" />
</svg>
</button>
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -1,10 +1,15 @@
import { Component, Input } from "@angular/core"; import { Component, Input } from "@angular/core";
export interface ColumnDefinition { export interface ColumnDefinition {
header: string; label: string;
columnContent?: string | undefined; content: Function | string | boolean | number;
columnFunction?: Function | undefined; routerLink: Function | string | undefined;
routerLink?: Function | undefined; }
export interface RowAction {
label: string;
action: Function;
path: string;
} }
@Component({ @Component({
@ -15,8 +20,29 @@ export interface ColumnDefinition {
export class TableComponent { export class TableComponent {
@Input() items: any[] = []; @Input() items: any[] = [];
@Input() columns: ColumnDefinition[] = []; @Input() columns: ColumnDefinition[] = [];
@Input() rowActions: RowAction[] = [];
isBoolean(obje: any): boolean { isBoolean(obje: any): boolean {
return typeof obje === "boolean"; return typeof obje === "boolean";
} }
getRouterLink(column: ColumnDefinition, item: any) {
if (column.routerLink === undefined) {
return undefined;
}
if (typeof column.routerLink === "string") {
return column.routerLink;
}
return column.routerLink(item);
}
getContent(column: ColumnDefinition, item: any) {
if (typeof column.content === "function") {
return column.content(item);
}
return column.content;
}
} }

View File

@ -1,4 +1,4 @@
export interface TabItem { export interface TabItem {
id: string; label: string;
title: string; target: string;
} }

View File

@ -4,11 +4,35 @@ import { CommonModule } from "@angular/common";
import { CardComponent } from "./components/card/card.component"; import { CardComponent } from "./components/card/card.component";
import { PaginatorComponent } from "./components/paginator/paginator.component"; import { PaginatorComponent } from "./components/paginator/paginator.component";
import { TableComponent } from "./components/table/table.component"; import { TableComponent } from "./components/table/table.component";
import { FormsModule } from "@angular/forms"; import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { InputComponent } from "./components/input/input.component";
import { TranslateModule } from "@ngx-translate/core";
import { SubmitComponent } from "./components/submit/submit.component";
import { TabControlComponent } from "./components/tab-control/tab-control.component";
@NgModule({ @NgModule({
declarations: [CardComponent, PaginatorComponent, TableComponent], declarations: [
exports: [CardComponent, PaginatorComponent, TableComponent, FormsModule], CardComponent,
imports: [RouterModule, CommonModule, FormsModule], PaginatorComponent,
TableComponent,
TabControlComponent,
InputComponent,
SubmitComponent,
],
exports: [
CardComponent,
PaginatorComponent,
TableComponent,
TabControlComponent,
InputComponent,
SubmitComponent,
],
imports: [
RouterModule,
CommonModule,
FormsModule,
ReactiveFormsModule,
TranslateModule,
],
}) })
export class SharedModule {} export class SharedModule {}

View File

@ -36,7 +36,7 @@
"login-now": "Jetzt anmelden!", "login-now": "Jetzt anmelden!",
"register-now": "Jetzt registrieren!", "register-now": "Jetzt registrieren!",
"forgot-password": "Passwort vergessen", "forgot-password": "Passwort vergessen",
"confirm-registration": "Registrierung bestätigen" "confirm-registration": "Registrierung abschließen"
}, },
"settings": { "settings": {
"tab-security": "Sicherheit", "tab-security": "Sicherheit",
@ -50,5 +50,13 @@
"new-username": "Neuer Benutzername", "new-username": "Neuer Benutzername",
"password-confirmation": "Passwort wiederholen" "password-confirmation": "Passwort wiederholen"
} }
},
"test": {
"product": "Produkt",
"price": "Preis",
"stock": "Lagerbestand",
"edit": "Bearbeiten",
"delete": "Löschen"
} }
} }

View File

@ -50,5 +50,13 @@
"new-username": "New Username", "new-username": "New Username",
"password-confirmation": "Confirm Password" "password-confirmation": "Confirm Password"
} }
},
"test": {
"product": "Product",
"price": "Price",
"stock": "Stock",
"edit": "Edit",
"delete": "Delete"
} }
} }

View File

@ -3,28 +3,41 @@
@tailwind utilities; @tailwind utilities;
@layer base { @layer base {
/* Light Theme */
:root { :root {
--color-text-primary: 0, 0, 0; --color-text-primary: 0, 0, 0;
--color-text-primary-muted: 100, 100, 100; --color-text-primary-muted: 50, 50, 50;
--color-text-secondary: 255, 255, 255; --color-text-secondary: 255, 255, 255;
--color-text-secondary-muted: 170, 170, 170; --color-text-secondary-muted: 200, 200, 200;
--color-text-accent: 250, 183, 0;
--color-text-accent-muted: 250, 183, 0;
--color-primary: 244, 244, 245; --color-text-accent: 255, 180, 0;
--color-secondary: 238, 238, 240; --color-text-accent-muted: 240, 170, 0;
--color-accent: 255, 199, 44;
--color-background-primary: 255, 255, 255;
--color-background-primary-muted: 240, 240, 240;
--color-background-secondary: 245, 245, 245;
--color-background-secondary-muted: 230, 230, 230;
--color-background-accent: 255, 199, 44;
--color-background-accent-muted: 240, 184, 34;
} }
/* Dark Theme */
.theme-dark { .theme-dark {
--color-text-primary: 255, 255, 255; --color-text-primary: 255, 255, 255;
--color-text-primary-muted: 170, 170, 170; --color-text-primary-muted: 170, 170, 170;
--color-text-secondary: 0, 0, 0; --color-text-secondary: 0, 0, 0;
--color-text-secondary-muted: 100, 100, 100; --color-text-secondary-muted: 100, 100, 100;
--color-text-accent: 250, 183, 0;
--color-text-accent-muted: 250, 183, 0;
--color-primary: 24, 24, 27; --color-text-accent: 255, 180, 50;
--color-secondary: 39, 39, 42; --color-text-accent-muted: 230, 160, 40;
--color-accent: 250, 183, 0;
--color-background-primary: 24, 24, 27;
--color-background-primary-muted: 34, 34, 37;
--color-background-secondary: 39, 39, 42;
--color-background-secondary-muted: 49, 49, 52;
--color-background-accent: 250, 183, 0;
--color-background-accent-muted: 225, 158, 0;
} }
} }

View File

@ -37,9 +37,12 @@ module.exports = {
// "backgrounds" // "backgrounds"
backgroundColor: { backgroundColor: {
skin: { skin: {
primary: withOpacity("--color-primary"), primary: withOpacity("--color-background-primary"),
secondary: withOpacity("--color-secondary"), "primary-muted": withOpacity("--color-background-primary-muted"),
accent: withOpacity("--color-accent"), secondary: withOpacity("--color-background-secondary"),
"secondary-muted": withOpacity("--color-background-secondary-muted"),
accent: withOpacity("--color-background-accent"),
"accent-muted": withOpacity("--color-background-accent-muted"),
}, },
}, },
@ -52,6 +55,7 @@ module.exports = {
ringColor: { ringColor: {
skin: { skin: {
primary: withOpacity("--color-text-secondary-muted"), primary: withOpacity("--color-text-secondary-muted"),
accent: withOpacity("--color-text-accent-muted"),
}, },
}, },
divideColor: { divideColor: {