Merge pull request 'feature/init-tailwind-daisyui' (#13) from feature/init-tailwind-daisyui into main

Reviewed-on: #13
This commit is contained in:
Igor Hrenowitsch Propisnov 2024-06-26 19:22:32 +02:00
commit b7f73033f3
17 changed files with 3031 additions and 6103 deletions

View File

@ -29,6 +29,7 @@
"@angular/platform-browser-dynamic": "^17.3.0", "@angular/platform-browser-dynamic": "^17.3.0",
"@angular/router": "^17.3.0", "@angular/router": "^17.3.0",
"@types/dompurify": "^3.0.5", "@types/dompurify": "^3.0.5",
"chroma-js": "^2.4.2",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dompurify": "^3.1.3", "dompurify": "^3.1.3",
"primeicons": "^7.0.0", "primeicons": "^7.0.0",
@ -46,11 +47,14 @@
"@angular-eslint/template-parser": "17.2.1", "@angular-eslint/template-parser": "17.2.1",
"@angular/cli": "^17.3.0", "@angular/cli": "^17.3.0",
"@angular/compiler-cli": "^17.3.0", "@angular/compiler-cli": "^17.3.0",
"@types/chroma-js": "^2.4.4",
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "^4.2.2",
"@types/jest": "^29.5.12", "@types/jest": "^29.5.12",
"@typescript-eslint/eslint-plugin": "6.19.0", "@typescript-eslint/eslint-plugin": "6.19.0",
"@typescript-eslint/parser": "6.19.0", "@typescript-eslint/parser": "6.19.0",
"autoprefixer": "^10.4.19",
"concurrently": "^8.2.2", "concurrently": "^8.2.2",
"daisyui": "^4.12.2",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.29.1", "eslint-plugin-import": "^2.29.1",
@ -59,7 +63,10 @@
"eslint-plugin-unused-imports": "^3.2.0", "eslint-plugin-unused-imports": "^3.2.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-preset-angular": "^14.0.3", "jest-preset-angular": "^14.0.3",
"postcss": "^8.4.38",
"prettier": "3.2.5", "prettier": "3.2.5",
"tailwindcss": "^3.4.4",
"tailwindcss-animated": "^1.1.2",
"typescript": "~5.4.2", "typescript": "~5.4.2",
"wait-on": "^7.2.0" "wait-on": "^7.2.0"
}, },

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
}
}

View File

@ -1,6 +1,8 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router'; import { RouterOutlet } from '@angular/router';
import { ThemeService } from './shared/service';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
standalone: true, standalone: true,
@ -10,5 +12,5 @@ import { RouterOutlet } from '@angular/router';
styleUrl: './app.component.scss', styleUrl: './app.component.scss',
}) })
export class AppComponent { export class AppComponent {
public constructor() {} public constructor(private readonly themeService: ThemeService) {}
} }

View File

@ -9,7 +9,7 @@ const publicRoutes: Routes = [
), ),
}, },
{ {
path: 'signup', path: 'welcome',
loadComponent: () => loadComponent: () =>
import('./pages/register-root/register-root.component').then( import('./pages/register-root/register-root.component').then(
(m) => m.RegisterRootComponent (m) => m.RegisterRootComponent

View File

@ -1,20 +1,39 @@
<div id="background"> <div class="bg-primary w-screen h-screen">
<div class="wrapper"> <div class="modal modal-open">
<div class="content"> <div
[ngStyle]="backgroundStyle"
class="modal-box w-11/12 h-2/6 max-w-5xl flex">
<div class="w-full flex flex-col justify-center items-center">
@if (verifyStatus() === true) { @if (verifyStatus() === true) {
@if (showRedirectMessage()) { @if (showRedirectMessage()) {
<h1>Es geht gleich los!</h1> <div class="text-center">
<h2> <h1 class="font-bold text-3xl pt-5">Your email is verified!</h1>
Danke für das bestätigen der E-Mail - Wir leiten dich zum Login <p class="pt-3 pb-6">
weiter! Your email {{ email() }} has been successfully verified. will
</h2> <br />
You will be automatically redirected in to the login page to
access the application shortly.
</p>
<button
(click)="navigateToWelcomeScreen()"
class="btn btn-primary no-animation">
Go to the App
</button>
</div>
} }
} @else if (verifyStatus() === false) { } @else if (verifyStatus() === false) {
<h1>Oops, da ist etwas schief gelaufen!</h1> <div class="text-center">
<h2>Der Link ist nicht mehr gültig</h2> <h1 class="font-bold text-3xl pt-5">
Oops, something went wrong! :(
</h1>
<p class="pt-3">We couldn't verify your email.</p>
</div>
} @else { } @else {
<h1>Verifizierung wird durchgeführt...</h1> <div class="text-center">
<span class="loading loading-dots loading-lg"></span>
</div>
} }
</div> </div>
</div> </div>
</div> </div>
</div>

View File

@ -1,28 +0,0 @@
#background {
display: flex;
height: 100%;
}
.wrapper {
flex: 1;
background-color: lightsalmon;
display: flex;
.content{
flex-direction: column;
display: flex;
align-items: flex-start;
justify-content: center;
h1 {
font-size: 4rem;
margin-left: 3rem;
line-height: 1rem;
}
h2 {
margin-left: 3rem;
}
p {
margin-left: 3rem;
}
}
}

View File

@ -1,6 +1,8 @@
import { CommonModule } from '@angular/common';
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
ElementRef,
InputSignal, InputSignal,
OnInit, OnInit,
WritableSignal, WritableSignal,
@ -12,11 +14,12 @@ import { Router } from '@angular/router';
import { delay, filter, tap } from 'rxjs'; import { delay, filter, tap } from 'rxjs';
import { VerifyApiService } from '../../api'; import { VerifyApiService } from '../../api';
import { BackgroundPatternService, ThemeService } from '../../shared/service';
@Component({ @Component({
selector: 'app-email-verify-root', selector: 'app-email-verify-root',
standalone: true, standalone: true,
imports: [], imports: [CommonModule],
providers: [], providers: [],
templateUrl: './email-verify-root.component.html', templateUrl: './email-verify-root.component.html',
styleUrl: './email-verify-root.component.scss', styleUrl: './email-verify-root.component.scss',
@ -24,6 +27,8 @@ import { VerifyApiService } from '../../api';
}) })
export class EmailVerifyRootComponent implements OnInit { export class EmailVerifyRootComponent implements OnInit {
public token: InputSignal<string> = input<string>(''); public token: InputSignal<string> = input<string>('');
public email: WritableSignal<string> = signal<string>('');
public backgroundStyle: { 'background-image': string } | null = null;
public verifyStatus: WritableSignal<boolean | null> = signal<boolean | null>( public verifyStatus: WritableSignal<boolean | null> = signal<boolean | null>(
null null
); );
@ -31,19 +36,71 @@ export class EmailVerifyRootComponent implements OnInit {
public constructor( public constructor(
private readonly api: VerifyApiService, private readonly api: VerifyApiService,
private readonly router: Router private readonly router: Router,
private readonly el: ElementRef,
private readonly backgroundPatternService: BackgroundPatternService,
private readonly themeService: ThemeService
) {} ) {}
public ngOnInit(): void { public ngOnInit(): void {
this.verifyEmail(); this.verifyEmail();
this.setBackground();
}
public setBackground(): void {
const theme = this.themeService.getTheme();
let opacity: number;
if (theme === 'dark') {
opacity = 0.05;
} else {
opacity = 0.1;
}
const colorPrimary = getComputedStyle(
this.el.nativeElement
).getPropertyValue('--p');
const svgUrl = this.backgroundPatternService.getWigglePattern(
colorPrimary,
opacity
);
this.backgroundStyle = { 'background-image': `url("${svgUrl}")` };
}
public navigateToWelcomeScreen(): void {
const email: string = this.extractEmail();
this.router.navigate(['/welcome'], {
queryParams: { verified: true, email: email },
});
}
private extractVerifyToken(): string {
const [verifyToken]: string[] = this.token().split('|');
return verifyToken;
}
private extractEmail(): string {
const [, email]: string[] = this.token().split('|');
return email;
} }
private verifyEmail(): void { private verifyEmail(): void {
const [verifyToken, email]: string[] = this.token().split('|'); const verifyToken: string = this.extractVerifyToken();
const email: string = this.extractEmail();
if (verifyToken && email) {
this.email.set(decodeURIComponent(atob(email)));
}
this.api this.api
.verifyControllerVerifyEmail(verifyToken) .verifyControllerVerifyEmail(verifyToken)
.pipe( .pipe(
delay(1500),
tap((isVerified: boolean) => { tap((isVerified: boolean) => {
this.verifyStatus.set(isVerified); this.verifyStatus.set(isVerified);
}), }),
@ -51,12 +108,10 @@ export class EmailVerifyRootComponent implements OnInit {
tap(() => { tap(() => {
this.showRedirectMessage.set(true); this.showRedirectMessage.set(true);
}), }),
delay(5000) delay(10000)
) )
.subscribe(() => { .subscribe(() => {
this.router.navigate(['/signup'], { this.navigateToWelcomeScreen();
queryParams: { verified: true, email: email },
});
}); });
} }
} }

View File

@ -28,7 +28,7 @@ export class HomeComponent implements OnInit {
}, },
(error: HttpErrorResponse) => { (error: HttpErrorResponse) => {
if (error.status === 401) { if (error.status === 401) {
this.router.navigate(['signup'], { this.router.navigate(['welcome'], {
queryParams: { login: true }, queryParams: { login: true },
}); });
} }

View File

@ -1,110 +1,301 @@
<div id="background">
<div class="img-zone">
<div class="img-wrapper">
@if (userSignupSuccess()) {
<div class="success">
<h1>Danke für deine Registrierung!</h1>
<h2>
Wir haben dir eine Mail geschickt an
{{ form?.get('email')?.value }}. Bitte bestätige deine
E-Mail-Adresse um fortzufahren.
</h2>
<p>Du kannst diesen Tab nun schließen</p>
</div>
} @else {
<div class="headline">
<h1>Hi, Welcome to Ticket App.</h1>
</div>
}
</div>
</div>
@if (!userSignupSuccess()) { @if (!userSignupSuccess()) {
<div class="content-zone"> <div class="flex h-screen w-screen">
<h1> <div
@if (isSignupSignal()) { [ngStyle]="leftBackgroundStyle"
Anmelden class="hidden md:flex md:flex-col md:w-1/2 bg-primary">
} @else if (isRegisterSignal()) { <div class="flex-1 flex items-start pt-16 px-12">
Registrieren <h1 class="text-3xl text-base-100">[LOGO] APP-NAME</h1>
} @else { </div>
Erste Schritte <div class="flex-1 flex flex-col justify-end pb-16 px-12">
} <blockquote>
</h1> <p class="text-xl text-base-100 font-semibold">
“This library has saved me countless hours of work and helped me
deliver stunning designs to my clients faster than ever before.”
</p>
<small class="block text-sm font-light text-base-100 mt-4">
— Sofia Davis
</small>
</blockquote>
</div>
</div>
@if (isDisplayButtons()) { <!-- Rechter Bereich, immer sichtbar -->
<div class="action"> <div [ngStyle]="rightBackgroundStyle" class="flex flex-col w-full md:w-1/2">
<button <div class="flex px-12 gap-3">
pButton <div class="flex items-start justify-end pt-16">
type="button" <label class="swap swap-rotate">
label="Anmelden"
(click)="toggleAction('signup')"></button>
<button
pButton
type="button"
label="Registrieren"
(click)="toggleAction('register')"></button>
</div>
}
@if (isSignupSignal() || isRegisterSignal()) {
<div class="register-wrapper">
@if (form) {
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<div class="e-mail">
<div class="label">
<label for="email">E-Mail</label>
</div>
<input <input
pInputText type="checkbox"
id="email" (change)="toggleTheme()"
formControlName="email" [checked]="isDarkMode" />
aria-describedby="e-mail" />
<!-- sun icon -->
<svg
class="swap-on h-10 w-10 fill-current"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<path
d="M5.64,17l-.71.71a1,1,0,0,0,0,1.41,1,1,0,0,0,1.41,0l.71-.71A1,1,0,0,0,5.64,17ZM5,12a1,1,0,0,0-1-1H3a1,1,0,0,0,0,2H4A1,1,0,0,0,5,12Zm7-7a1,1,0,0,0,1-1V3a1,1,0,0,0-2,0V4A1,1,0,0,0,12,5ZM5.64,7.05a1,1,0,0,0,.7.29,1,1,0,0,0,.71-.29,1,1,0,0,0,0-1.41l-.71-.71A1,1,0,0,0,4.93,6.34Zm12,.29a1,1,0,0,0,.7-.29l.71-.71a1,1,0,1,0-1.41-1.41L17,5.64a1,1,0,0,0,0,1.41A1,1,0,0,0,17.66,7.34ZM21,11H20a1,1,0,0,0,0,2h1a1,1,0,0,0,0-2Zm-9,8a1,1,0,0,0-1,1v1a1,1,0,0,0,2,0V20A1,1,0,0,0,12,19ZM18.36,17A1,1,0,0,0,17,18.36l.71.71a1,1,0,0,0,1.41,0,1,1,0,0,0,0-1.41ZM12,6.5A5.5,5.5,0,1,0,17.5,12,5.51,5.51,0,0,0,12,6.5Zm0,9A3.5,3.5,0,1,1,15.5,12,3.5,3.5,0,0,1,12,15.5Z" />
</svg>
<!-- moon icon -->
<svg
class="swap-off h-10 w-10 fill-current"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<path
d="M21.64,13a1,1,0,0,0-1.05-.14,8.05,8.05,0,0,1-3.37.73A8.15,8.15,0,0,1,9.08,5.49a8.59,8.59,0,0,1,.25-2A1,1,0,0,0,8,2.36,10.14,10.14,0,1,0,22,14.05,1,1,0,0,0,21.64,13Zm-9.5,6.69A8.14,8.14,0,0,1,7.08,5.22v.27A10.15,10.15,0,0,0,17.22,15.63a9.79,9.79,0,0,0,2.1-.22A8.11,8.11,0,0,1,12.14,19.73Z" />
</svg>
</label>
</div> </div>
<div class="password"> <div class="flex-1 items-start flex justify-end pt-16">
<div class="label">
<label for="password">Password</label>
</div>
<p-password
class="custom-p-password"
id="password"
formControlName="password"
aria-describedby="password"
[toggleMask]="true"></p-password>
</div>
@if (isRegisterSignal()) {
<div class="terms">
<p-checkbox
formControlName="terms"
label="Ich habe die AGB gelesen und stimme zu."
name="terms"
[binary]="true"></p-checkbox>
</div>
}
<div class="signup">
<button
pButton
type="submit"
[label]="
isSignupSignal()
? 'Anmelden'
: '✨ Jetzt KOSTENFREI loslegen ✨'
"></button>
</div>
<div class="change-mask">
<a
(click)="switchMask()"
(keyup.enter)="switchMask()"
tabindex="0">
@if (isSignupSignal()) { @if (isSignupSignal()) {
Kein Account? Erstellen Sie jetzt KOSTENFREI einen! <button
} @else { (click)="toggleAction('signin')"
Schon einen Account? Hier einloggen class="btn btn-primary btn-outline no-animation">
Login
</button>
} }
@if (isSigninSignal()) {
@if (displaySkeleton()) {
<div class="skeleton w-36 h-12"></div>
} @else {
<button
(click)="toggleAction('signup')"
class="btn btn-primary btn-outline no-animation">
New here - Register now!
</button>
}
}
</div>
</div>
@if (isSignupSignal()) {
<div
class="animate-fade-down animate-once animate-duration-1000 animate-ease-in-out flex-1 flex flex-col justify-center items-center px-12">
<h1 class="text-3xl font-semibold text-center">Create an Account</h1>
<p class="text-center">
Enter your email below to create your Account
</p>
<form
[formGroup]="form"
(ngSubmit)="onSubmit()"
class="flex gap-4 flex-col items-center py-6 w-full max-w-md">
<div class="form-control w-full">
<label
[ngClass]="{
'w-full': true,
'border-error focus:border-error':
form.get('email')?.invalid &&
(form.get('email')?.dirty || form.get('email')?.touched)
}"
class="input input-bordered flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="h-4 w-4 opacity-70">
<path
d="M2.5 3A1.5 1.5 0 0 0 1 4.5v.793c.026.009.051.02.076.032L7.674 8.51c.206.1.446.1.652 0l6.598-3.185A.755.755 0 0 1 15 5.293V4.5A1.5 1.5 0 0 0 13.5 3h-11Z" />
<path
d="M15 6.954 8.978 9.86a2.25 2.25 0 0 1-1.956 0L1 6.954V11.5A1.5 1.5 0 0 0 2.5 13h11a1.5 1.5 0 0 0 1.5-1.5V6.954Z" />
</svg>
<input
formControlName="email"
type="text"
class="grow"
placeholder="name@example.com" />
</label>
</div>
<div class="form-control w-full">
<label
[ngClass]="{
'w-full': true,
'border-error focus:border-error':
form.get('password')?.invalid &&
(form.get('password')?.dirty ||
form.get('password')?.touched)
}"
class="input input-bordered flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="h-4 w-4 opacity-70">
<path
fill-rule="evenodd"
d="M14 6a4 4 0 0 1-4.899 3.899l-1.955 1.955a.5.5 0 0 1-.353.146H5v1.5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-2.293a.5.5 0 0 1 .146-.353l3.955-3.955A4 4 0 1 1 14 6Zm-4-2a.75.75 0 0 0 0 1.5.5.5 0 0 1 .5.5.75.75 0 0 0 1.5 0 2 2 0 0 0-2-2Z"
clip-rule="evenodd" />
</svg>
<input
formControlName="password"
type="password"
class="grow"
value="" />
</label>
</div>
<button class="btn w-full btn-primary font-semibold">
@if (isLoading()) {
<span class="loading loading-spinner"></span>
}
Sign Up with Email
</button>
<p class="text-xs w-full text-center">
By clicking continue, you agree to our
<u class="cursor-pointer">Terms of Service</u>
and
<u class="cursor-pointer">Privacy Policy</u>
.
</p>
</form>
</div>
}
@if (isSigninSignal()) {
<div
class="animate-fade-down animate-once animate-duration-1000 animate-ease-in-out flex-1 flex flex-col justify-center items-center px-12">
@if (displaySkeleton()) {
<div class="flex items-center w-full flex-col max-w-md gap-4">
<div class="skeleton w-36 h-10"></div>
<div class="skeleton w-full h-10 max-w-md"></div>
<div class="skeleton w-full h-10 max-w-md"></div>
<div class="skeleton w-full h-10 max-w-md"></div>
<div class="skeleton w-full h-10 max-w-md"></div>
</div>
} @else {
<h1 class="text-3xl font-semibold text-center">Login</h1>
<form
[formGroup]="form"
(ngSubmit)="onSubmit()"
class="flex gap-4 flex-col items-center py-6 w-full max-w-md">
<div class="form-control w-full">
<label
[ngClass]="{
'w-full': true,
'border-error focus:border-error':
form.get('email')?.invalid &&
(form.get('email')?.dirty || form.get('email')?.touched)
}"
class="input input-bordered flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="h-4 w-4 opacity-70">
<path
d="M2.5 3A1.5 1.5 0 0 0 1 4.5v.793c.026.009.051.02.076.032L7.674 8.51c.206.1.446.1.652 0l6.598-3.185A.755.755 0 0 1 15 5.293V4.5A1.5 1.5 0 0 0 13.5 3h-11Z" />
<path
d="M15 6.954 8.978 9.86a2.25 2.25 0 0 1-1.956 0L1 6.954V11.5A1.5 1.5 0 0 0 2.5 13h11a1.5 1.5 0 0 0 1.5-1.5V6.954Z" />
</svg>
<input
formControlName="email"
type="text"
class="grow"
placeholder="name@example.com" />
</label>
</div>
<div class="form-control w-full">
<label
[ngClass]="{
'w-full': true,
'border-error focus:border-error':
form.get('password')?.invalid &&
(form.get('password')?.dirty ||
form.get('password')?.touched)
}"
class="input input-bordered flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="h-4 w-4 opacity-70">
<path
fill-rule="evenodd"
d="M14 6a4 4 0 0 1-4.899 3.899l-1.955 1.955a.5.5 0 0 1-.353.146H5v1.5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-2.293a.5.5 0 0 1 .146-.353l3.955-3.955A4 4 0 1 1 14 6Zm-4-2a.75.75 0 0 0 0 1.5.5.5 0 0 1 .5.5.75.75 0 0 0 1.5 0 2 2 0 0 0-2-2Z"
clip-rule="evenodd" />
</svg>
<input
formControlName="password"
type="password"
class="grow"
value="" />
</label>
</div>
<div class="form-control w-full">
<div class="flex items-center justify-between">
<label class="label cursor-pointer">
<input
[formControl]="rememberMe"
type="checkbox"
checked="checked"
class="checkbox checkbox-md checkbox-primary" />
<span class="label-text ml-1.5">Remember me</span>
</label>
<a class="text-primary label-text cursor-pointer">
Forgot password?
</a>
</div>
</div>
<button class="btn w-full btn-primary font-semibold">
@if (isLoading()) {
<span class="loading loading-spinner"></span>
}
Sign In
</button>
<div class="flex gap-1">
<span class="text-xs">Not registered yet?</span>
<a
(click)="toggleAction('signup')"
(keypress)="toggleAction('signup')"
tabindex="0"
class="text-primary cursor-pointer text-xs">
Create An Account
</a> </a>
</div> </div>
</form> </form>
} }
</div> </div>
} }
<div class="flex flex-col items-center justify-center py-12">
<footer>
<p class="text-xs">Made with ♥️ in Germany</p>
</footer>
</div>
</div>
</div>
} @else {
<div class="flex h-screen w-screen bg-primary">
<div class="hidden md:flex md:flex-col md:w-1/1"></div>
</div> </div>
} }
<div class="modal modal-open" *ngIf="isDialogOpen()">
<div
[ngStyle]="dialogBackgroundStyle"
class="modal-box w-11/12 h-2/6 max-w-5xl flex">
<div class="w-full flex flex-col justify-center items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1"
stroke="currentColor"
class="size-28 animate-jump animate-once animate-duration-[2000ms] animate-delay-500 animate-ease-in-out animate-normal">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M21.75 9v.906a2.25 2.25 0 0 1-1.183 1.981l-6.478 3.488M2.25 9v.906a2.25 2.25 0 0 0 1.183 1.981l6.478 3.488m8.839 2.51-4.66-2.51m0 0-1.023-.55a2.25 2.25 0 0 0-2.134 0l-1.022.55m0 0-4.661 2.51m16.5 1.615a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V8.844a2.25 2.25 0 0 1 1.183-1.981l7.5-4.039a2.25 2.25 0 0 1 2.134 0l7.5 4.039a2.25 2.25 0 0 1 1.183 1.98V19.5Z" />
</svg>
<h1 class="font-bold text-3xl pt-5">Check your inbox, please!</h1>
<div class="flex flex-col items-center text-center">
<p class="pt-3">
Hey, to start using [APP-NAME], we need to verify your email.
</p>
<p class="pt-1">
We´ve already sent out the verification link. Please check it and
<br />
confirm it´s really you.
</p>
</div>
</div>
</div>
</div> </div>

View File

@ -1,104 +0,0 @@
#background {
display: flex;
height: 100%;
}
.img-zone {
flex: 65;
background-color: lightsalmon;
display: flex;
.img-wrapper {
display: flex;
align-items: center;
.success {
margin-left: 4em;
h1 {
font-size: 4em;
}
}
.headline {
h1 {
font-size: 4em;
margin-left: 1em;
}
}
}
}
.content-zone {
flex: 35;
background-color: lightcyan;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
.action {
display: flex;
justify-content: center;
gap: 1em;
button {
min-width: 200px;
}
}
.register-wrapper {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
h1 {
font-size: 3em;
}
.label {
padding: 0 0 0.5em;
}
.e-mail,
.password,
.terms {
.label {
font-size: 1;
}
input {
min-width: 500px;
}
::ng-deep p-password.custom-p-password div input {
min-width: 500px;
}
}
.terms {
padding-top: 1.33em;
}
.password {
padding-top: 2em;
}
.signup {
padding-top: 3em;
button {
min-width: 500px;
}
}
.change-mask {
padding-top: 1.33em;
display: flex;
justify-content: center;
a {
cursor: pointer;
}
}
}
}

View File

@ -9,6 +9,7 @@ import {
effect, effect,
InputSignal, InputSignal,
input, input,
ElementRef,
} from '@angular/core'; } from '@angular/core';
import { import {
FormBuilder, FormBuilder,
@ -24,6 +25,7 @@ import { ButtonModule } from 'primeng/button';
import { CheckboxModule } from 'primeng/checkbox'; import { CheckboxModule } from 'primeng/checkbox';
import { InputTextModule } from 'primeng/inputtext'; import { InputTextModule } from 'primeng/inputtext';
import { PasswordModule } from 'primeng/password'; import { PasswordModule } from 'primeng/password';
import { delay, finalize, tap } from 'rxjs';
import { import {
Configuration, Configuration,
@ -32,13 +34,18 @@ import {
UserCredentialsDtoApiModel, UserCredentialsDtoApiModel,
} from '../../api'; } from '../../api';
import { ApiConfiguration } from '../../config/api-configuration'; import { ApiConfiguration } from '../../config/api-configuration';
import { AuthService, SessionStorageService } from '../../shared/service'; import {
AuthService,
BackgroundPatternService,
ThemeService,
} from '../../shared/service';
import { LocalStorageService } from '../../shared/service/local-storage.service';
import { import {
customEmailValidator, customEmailValidator,
customPasswordValidator, customPasswordValidator,
} from '../../shared/validator'; } from '../../shared/validator';
type AuthAction = 'register' | 'signup'; type AuthAction = 'signin' | 'signup';
@Component({ @Component({
selector: 'app-register-root', selector: 'app-register-root',
@ -65,47 +72,68 @@ type AuthAction = 'register' | 'signup';
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class RegisterRootComponent implements OnInit { export class RegisterRootComponent implements OnInit {
public dialogBackgroundStyle: { 'background-image': string } | null = null;
public leftBackgroundStyle: { 'background-image': string } | null = null;
public rightBackgroundStyle: { 'background-image': string } | null = null;
public verified: InputSignal<boolean> = input<boolean>(false); public verified: InputSignal<boolean> = input<boolean>(false);
public login: InputSignal<boolean> = input<boolean>(false); public login: InputSignal<boolean> = input<boolean>(false);
public email: InputSignal<string> = input<string>(''); public email: InputSignal<string> = input<string>('');
public form: FormGroup | undefined; public form!: FormGroup;
public isRegisterSignal: WritableSignal<boolean> = signal(false); public rememberMe: FormControl = new FormControl(false);
public isSignupSignal: WritableSignal<boolean> = signal(false); public isSigninSignal: WritableSignal<boolean> = signal(false);
public isDisplayButtons: WritableSignal<boolean> = signal(true); public isSignupSignal: WritableSignal<boolean> = signal(true);
public emailInvalid: WritableSignal<string | null> = signal(null); public isSignUpSuccess: WritableSignal<boolean> = signal(false);
public passwordInvalid: WritableSignal<string | null> = signal(null);
public termsInvalid: WritableSignal<string | null> = signal(null);
public userSignupSuccess: WritableSignal<boolean> = signal(false); public userSignupSuccess: WritableSignal<boolean> = signal(false);
public isDialogOpen: WritableSignal<boolean> = signal(false);
public isLoading: WritableSignal<boolean> = signal(false);
public displaySkeleton: WritableSignal<boolean> = signal(true);
private removeQueryParams: WritableSignal<boolean> = signal(false); private removeQueryParams: WritableSignal<boolean> = signal(false);
public get isDarkMode(): boolean {
return this.themeService.getTheme() === 'dark';
}
public constructor( public constructor(
private readonly formBuilder: FormBuilder, private readonly formBuilder: FormBuilder,
private readonly authService: AuthService, private readonly authService: AuthService,
private readonly router: Router, private readonly router: Router,
private readonly sessionStorageService: SessionStorageService private readonly themeService: ThemeService,
private readonly el: ElementRef,
private readonly backgroundPatternService: BackgroundPatternService,
private readonly localStorageService: LocalStorageService
) { ) {
effect(() => { effect(() => {
if (this.form) {
if (this.isRegisterSignal()) {
this.form.addControl(
'terms',
new FormControl(false, Validators.requiredTrue)
);
} else {
this.form.removeControl('terms');
}
}
if (this.removeQueryParams()) { if (this.removeQueryParams()) {
this.clearRouteParams(); this.clearRouteParams();
} }
}); });
const rememberMe = this.localStorageService.getItem<boolean>('remember-me');
if (rememberMe) {
this.authService
.status()
.pipe(delay(1500))
.subscribe({
next: (response: SuccessDtoApiModel) => {
if (response.success) {
this.router.navigate(['/dashboard']);
} else {
this.displaySkeleton.set(false);
}
},
error: () => {
this.displaySkeleton.set(false);
},
});
} else {
this.displaySkeleton.set(false);
}
} }
public ngOnInit(): void { public ngOnInit(): void {
this.setBackground();
this.initializeForm(); this.initializeForm();
this.setupValueChanges(); this.setupValueChanges();
this.preselectForm();
if ((this.email() && this.verified()) || this.login()) { if ((this.email() && this.verified()) || this.login()) {
this.handleRedirect(); this.handleRedirect();
@ -113,52 +141,116 @@ export class RegisterRootComponent implements OnInit {
} }
} }
public preselectForm(): void { public setBackground(): void {
if (!this.email() || !this.verified()) { const theme = this.themeService.getTheme();
const email = this.sessionStorageService.getItem('email'); let opacity: number;
this.form?.get('email')?.setValue(email); if (theme === 'dark') {
opacity = 0.05;
} else {
opacity = 0.1;
} }
const colorPrimary = getComputedStyle(
this.el.nativeElement
).getPropertyValue('--p');
const colorPrimaryC = getComputedStyle(
this.el.nativeElement
).getPropertyValue('--pc');
const svgUrlforDialog = this.backgroundPatternService.getWigglePattern(
colorPrimary,
opacity
);
const svgUrlForLeft = this.backgroundPatternService.getBankNotePattern(
colorPrimaryC,
opacity
);
const svgUrlForRight = this.backgroundPatternService.getHideoutPattern(
colorPrimary,
opacity
);
this.dialogBackgroundStyle = {
'background-image': `url("${svgUrlforDialog}")`,
};
this.leftBackgroundStyle = {
'background-image': `url("${svgUrlForLeft}")`,
};
this.rightBackgroundStyle = {
'background-image': `url("${svgUrlForRight}")`,
};
}
public openModal(): void {
this.isDialogOpen.set(true);
}
public closeModal(): void {
this.isDialogOpen.set(false);
}
public toggleTheme(): void {
this.themeService.toggleTheme();
this.setBackground();
} }
public toggleAction(action: AuthAction): void { public toggleAction(action: AuthAction): void {
if (action === 'register') { this.resetFormValidation();
this.isRegisterSignal.set(true);
if (action === 'signin') {
this.handlePreselect();
this.isSigninSignal.set(true);
this.isSignupSignal.set(false); this.isSignupSignal.set(false);
} else { } else {
this.isRegisterSignal.set(false); this.isSigninSignal.set(false);
this.isSignupSignal.set(true); this.isSignupSignal.set(true);
} }
this.isDisplayButtons.set(false);
} }
public onSubmit(): void { public onSubmit(): void {
this.markControlsAsTouchedAndDirty(['email', 'password', 'terms']); this.markControlsAsTouchedAndDirty(['email', 'password']);
if (this.form?.valid) { if (this.form?.valid) {
if (this.isRegisterSignal()) { if (this.isSigninSignal()) {
this.signup(this.form.value);
} else {
this.signin(this.form.value); this.signin(this.form.value);
} else {
this.signup(this.form.value);
} }
} }
} }
public switchMask(): void { private handlePreselect(): void {
this.resetFormValidation(); const rememberMe = this.localStorageService.getItem<boolean>('remember-me');
const email = this.localStorageService.getItem<string>('email');
if (this.isSignupSignal()) { if (rememberMe) {
this.isSigninSignal.set(true);
this.isSignupSignal.set(false); this.isSignupSignal.set(false);
this.isRegisterSignal.set(true);
} else if (this.isRegisterSignal()) {
this.isSignupSignal.set(true);
this.isRegisterSignal.set(false);
} }
if (email) {
this.form?.get('email')?.setValue(email);
}
this.rememberMe.setValue(rememberMe);
} }
private initializeForm(): void { private initializeForm(): void {
const rememberMeValue =
this.localStorageService.getItem<boolean>('remember-me');
const email = this.localStorageService.getItem<string>('email');
if (rememberMeValue) {
this.isSigninSignal.set(true);
this.isSignupSignal.set(false);
}
const emailValue = rememberMeValue && email ? email : '';
this.form = this.formBuilder.group({ this.form = this.formBuilder.group({
email: new FormControl('', { email: new FormControl(emailValue, {
validators: [Validators.required, customEmailValidator()], validators: [Validators.required, customEmailValidator()],
updateOn: 'change', updateOn: 'change',
}), }),
@ -166,18 +258,15 @@ export class RegisterRootComponent implements OnInit {
validators: [Validators.required, customPasswordValidator()], validators: [Validators.required, customPasswordValidator()],
updateOn: 'change', updateOn: 'change',
}), }),
terms: new FormControl(false, {
validators: [Validators.requiredTrue],
updateOn: 'change',
}),
}); });
this.rememberMe.setValue(rememberMeValue);
} }
private handleRedirect(): void { private handleRedirect(): void {
if (this.verified()) { if (this.verified()) {
this.isDisplayButtons.set(false); this.isSigninSignal.set(true);
this.isRegisterSignal.set(false); this.isSignupSignal.set(false);
this.isSignupSignal.set(true);
} }
if (this.email()) { if (this.email()) {
this.form?.get('email')?.setValue(decodeURIComponent(atob(this.email()))); this.form?.get('email')?.setValue(decodeURIComponent(atob(this.email())));
@ -185,8 +274,7 @@ export class RegisterRootComponent implements OnInit {
if (this.login()) { if (this.login()) {
this.isSignupSignal.set(true); this.isSignupSignal.set(true);
this.isDisplayButtons.set(false); this.isSigninSignal.set(false);
this.isRegisterSignal.set(false);
} }
} }
@ -250,7 +338,7 @@ export class RegisterRootComponent implements OnInit {
} }
private resetFormValidation(): void { private resetFormValidation(): void {
['email', 'password', 'terms'].forEach((controlName: string) => { ['email', 'password'].forEach((controlName: string) => {
this.resetControlValidation(controlName); this.resetControlValidation(controlName);
}); });
} }
@ -267,8 +355,20 @@ export class RegisterRootComponent implements OnInit {
} }
private signin(logiCredentials: UserCredentialsDtoApiModel): void { private signin(logiCredentials: UserCredentialsDtoApiModel): void {
const rememberMe: boolean = this.rememberMe.value;
if (rememberMe) {
this.localStorageService.setItem<string>('email', logiCredentials.email);
this.localStorageService.setItem<boolean>('remember-me', rememberMe);
}
this.authService this.authService
.signin(logiCredentials) .signin(logiCredentials)
.pipe(
tap(() => this.isLoading.set(true)),
delay(1000),
finalize(() => this.isLoading.set(false))
)
.subscribe((response: SigninResponseDtoApiModel) => { .subscribe((response: SigninResponseDtoApiModel) => {
if (response) { if (response) {
this.router.navigate(['/dashboard']); this.router.navigate(['/dashboard']);
@ -277,10 +377,17 @@ export class RegisterRootComponent implements OnInit {
} }
private signup(logiCredentials: UserCredentialsDtoApiModel): void { private signup(logiCredentials: UserCredentialsDtoApiModel): void {
this.isLoading.set(true);
this.authService this.authService
.signup(logiCredentials) .signup(logiCredentials)
.pipe(
delay(1000),
tap(() => this.isLoading.set(true)),
finalize(() => this.isLoading.set(false))
)
.subscribe((response: SuccessDtoApiModel) => { .subscribe((response: SuccessDtoApiModel) => {
if (response.success) { if (response.success) {
this.openModal();
this.userSignupSuccess.set(true); this.userSignupSuccess.set(true);
} }
}); });

View File

@ -0,0 +1,137 @@
import { Injectable } from '@angular/core';
import chroma from 'chroma-js';
@Injectable({
providedIn: 'root',
})
export class BackgroundPatternService {
public getWigglePattern(color: string, opacity: number): string {
const colorHex = this.convertOklchStringToHex(color);
const encodedHex = encodeURIComponent(colorHex);
const pattern = `
data:image/svg+xml,
%3Csvg width='52' height='26' viewBox='0 0 52 26' xmlns='http://www.w3.org/2000/svg'%3E
%3Cg fill='none' fill-rule='evenodd'%3E
%3Cg fill='${encodedHex}' fill-opacity='${opacity}'%3E
%3Cpath d='M10 10c0-2.21-1.79-4-4-4-3.314 0-6-2.686-6-6h2c0 2.21 1.79 4 4 4 3.314 0 6 2.686 6 6 0 2.21 1.79 4 4 4 3.314 0 6 2.686 6 6 0 2.21 1.79 4 4 4v2c-3.314 0-6-2.686-6-6 0-2.21-1.79-4-4-4-3.314 0-6-2.686-6-6zm25.464-1.95l8.486 8.486-1.414 1.414-8.486-8.486 1.414-1.414z' /%3E
%3C/g%3E
%3C/g%3E
%3C/svg%3E`;
return pattern.replace(/[\n\r]+|[\s]{2,}/g, ' ').trim();
}
public getTexturePattern(color: string, opacity: number): string {
const colorHex = this.convertOklchStringToHex(color);
const encodedHex = encodeURIComponent(colorHex);
const pattern = `
data:image/svg+xml,
%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3E
%3Cpath fill='${encodedHex}' fill-opacity='${opacity}' d='M1 3h1v1H1V3zm2-2h1v1H3V1z'%3E
%3C/path%3E
%3C/svg%3E`;
return pattern.replace(/[\n\r]+|[\s]{2,}/g, ' ').trim();
}
public getBubblesPattern(color: string, opacity: number): string {
const colorHex = this.convertOklchStringToHex(color);
const encodedHex = encodeURIComponent(colorHex);
const pattern = `
data:image/svg+xml,
%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E
%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z' fill='${encodedHex}' fill-opacity='${opacity}' fill-rule='evenodd'/%3E
%3C/svg%3E`;
return pattern.replace(/[\n\r]+|[\s]{2,}/g, ' ').trim();
}
public getBankNotePattern(color: string, opacity: number): string {
const colorHex = this.convertOklchStringToHex(color);
const encodedHex = encodeURIComponent(colorHex);
const pattern = `
data:image/svg+xml,
%3Csvg width='100' height='20' viewBox='0 0 100 20' xmlns='http://www.w3.org/2000/svg'%3E
%3Cpath d='M21.184 20c.357-.13.72-.264 1.088-.402l1.768-.661C33.64 15.347 39.647 14 50 14c10.271 0 15.362 1.222 24.629 4.928.955.383 1.869.74 2.75 1.072h6.225c-2.51-.73-5.139-1.691-8.233-2.928C65.888 13.278 60.562 12 50 12c-10.626 0-16.855 1.397-26.66 5.063l-1.767.662c-2.475.923-4.66 1.674-6.724 2.275h6.335zm0-20C13.258 2.892 8.077 4 0 4V2c5.744 0 9.951-.574 14.85-2h6.334zM77.38 0C85.239 2.966 90.502 4 100 4V2c-6.842 0-11.386-.542-16.396-2h-6.225zM0 14c8.44 0 13.718-1.21 22.272-4.402l1.768-.661C33.64 5.347 39.647 4 50 4c10.271 0 15.362 1.222 24.629 4.928C84.112 12.722 89.438 14 100 14v-2c-10.271 0-15.362-1.222-24.629-4.928C65.888 3.278 60.562 2 50 2 39.374 2 33.145 3.397 23.34 7.063l-1.767.662C13.223 10.84 8.163 12 0 12v2z' fill='${encodedHex}' fill-opacity='${opacity}' fill-rule='evenodd'/%3E
%3C/svg%3E`;
return pattern.replace(/[\n\r]+|[\s]{2,}/g, ' ').trim();
}
public getHideoutPattern(color: string, opacity: number): string {
const colorHex = this.convertOklchStringToHex(color);
const encodedHex = encodeURIComponent(colorHex);
const pattern = `
data:image/svg+xml,
%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' viewBox='0 0 40 40'%3E
%3Cg fill-rule='evenodd'%3E
%3Cg fill='${encodedHex}' fill-opacity='${opacity}'%3E
%3Cpath d='M0 38.59l2.83-2.83 1.41 1.41L1.41 40H0v-1.41zM0 1.4l2.83 2.83 1.41-1.41L1.41 0H0v1.41zM38.59 40l-2.83-2.83 1.41-1.41L40 38.59V40h-1.41zM40 1.41l-2.83 2.83-1.41-1.41L38.59 0H40v1.41zM20 18.6l2.83-2.83 1.41 1.41L21.41 20l2.83 2.83-1.41 1.41L20 21.41l-2.83 2.83-1.41-1.41L18.59 20l-2.83-2.83 1.41-1.41L20 18.59z'/%3E
%3C/g%3E
%3C/g%3E
%3C/svg%3E`;
return pattern.replace(/[\n\r]+|[\s]{2,}/g, ' ').trim();
}
public getFourPointsStarsPattern(color: string, opacity: number): string {
const colorHex = this.convertOklchStringToHex(color);
const encodedHex = encodeURIComponent(colorHex);
const pattern = `
data:image/svg+xml,
%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E
%3Cg fill='${encodedHex}' fill-opacity='${opacity}'%3E
%3Cpolygon fill-rule='evenodd' points='8 4 12 6 8 8 6 12 4 8 0 6 4 4 6 0 8 4'/%3E
%3C/g%3E
%3C/svg%3E`;
return pattern.replace(/[\n\r]+|[\s]{2,}/g, ' ').trim();
}
public getPlusPattern(color: string, opacity: number): string {
const colorHex = this.convertOklchStringToHex(color);
const encodedHex = encodeURIComponent(colorHex);
const pattern = `
data:image/svg+xml,
%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E
%3Cg fill='none' fill-rule='evenodd'%3E
%3Cg fill='${encodedHex}' fill-opacity='${opacity}'%3E
%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E
%3C/g%3E
%3C/g%3E
%3C/svg%3E`;
return pattern.replace(/[\n\r]+|[\s]{2,}/g, ' ').trim();
}
public getPolkaDotsPattern(color: string, opacity: number = 0.4): string {
const colorHex = this.convertOklchStringToHex(color);
const encodedHex = encodeURIComponent(colorHex);
const pattern = `
data:image/svg+xml,
%3Csvg width='20' height='20' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3E
%3Cg fill='${encodedHex}' fill-opacity='${opacity}' fill-rule='evenodd'%3E
%3Ccircle cx='3' cy='3' r='3'/%3E
%3Ccircle cx='13' cy='13' r='3'/%3E
%3C/g%3E
%3C/svg%3E`;
return pattern.replace(/[\n\r]+|[\s]{2,}/g, ' ').trim();
}
private convertOklchStringToHex(oklchString: string): string {
const parts = oklchString.split(' ');
const l = parseFloat(parts[0]);
const c = parseFloat(parts[1]);
const h = parseFloat(parts[2]);
return this.convertOklchToHex(l / 100, c, h);
}
private convertOklchToHex(l: number, c: number, h: number): string {
const rgb = chroma.oklch(l, c, h).rgb();
return chroma(rgb).hex();
}
}

View File

@ -1,3 +1,5 @@
export * from './auth.service'; export * from './auth.service';
export * from './local-storage.service'; export * from './local-storage.service';
export * from './session-storage.service'; export * from './session-storage.service';
export * from './theme.service';
export * from './background-pattern.service';

View File

@ -0,0 +1,40 @@
import { Injectable } from '@angular/core';
import { LocalStorageService } from './local-storage.service';
@Injectable({
providedIn: 'root',
})
export class ThemeService {
private currentTheme: string = '';
public constructor(private storageService: LocalStorageService) {
this.loadInitialTheme();
}
public getTheme(): string {
return this.currentTheme;
}
public setTheme(theme: string): void {
this.currentTheme = theme;
this.storageService.setItem('theme', theme);
this.applyTheme(theme);
}
public toggleTheme(): void {
this.currentTheme = this.currentTheme === 'light' ? 'dark' : 'light';
this.setTheme(this.currentTheme);
}
private applyTheme(theme: string): void {
document.body.setAttribute('data-theme', theme);
}
private loadInitialTheme(): void {
const savedTheme = this.storageService.getItem<string>('theme');
this.currentTheme = savedTheme ? savedTheme : 'light';
this.applyTheme(this.currentTheme);
}
}

View File

@ -1,12 +1,10 @@
// Import PrimeNG styles // Import PrimeNG styles
@import 'primeng/resources/themes/lara-light-blue/theme.css'; // @import 'primeng/resources/themes/lara-light-blue/theme.css';
@import 'primeng/resources/primeng.css'; // @import 'primeng/resources/primeng.css';
// PrimeNG icons // // PrimeNG icons
@import 'primeicons/primeicons.css'; // @import 'primeicons/primeicons.css';
html, @tailwind base;
body { @tailwind components;
height: 100%; @tailwind utilities;
margin: 0;
}

View File

@ -0,0 +1,15 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{html,ts}",
],
theme: {
extend: {},
},
plugins: [require('daisyui'), require('tailwindcss-animated')],
daisyui: {
themes: ["light", "dark"],
darkMode: ['class', '[data-theme="dark"]']
}
}