Merge pull request 'feat(auth): move recover password page to modal' (!196) from 1 into main
All checks were successful
Release / Release (push) Successful in 1m6s
Release / Build Backend Image (push) Successful in 23s
Release / Build Frontend Image (push) Successful in 29s

Reviewed-on: #196
Reviewed-by: Phan Huy Tran <ptran@noreply.localhost>
This commit is contained in:
Constantin Simonis 2025-05-15 11:00:22 +00:00
commit 4f2e7fe712
No known key found for this signature in database
GPG key ID: 944223E4D46B7412
5 changed files with 212 additions and 176 deletions

View file

@ -5,8 +5,8 @@
</main> </main>
<app-footer></app-footer> <app-footer></app-footer>
Auth Forms Overlay --> <!-- Auth Forms Overlay -->
@if (showLogin() || showRegister()) { @if (showLogin() || showRegister() || showRecoverPassword()) {
<div <div
class="fixed inset-0 bg-black/50 z-40" class="fixed inset-0 bg-black/50 z-40"
(click)="hideAuthForms()" (click)="hideAuthForms()"
@ -18,7 +18,11 @@
<div class="fixed inset-0 flex items-center justify-center z-50 p-4" role="presentation"> <div class="fixed inset-0 flex items-center justify-center z-50 p-4" role="presentation">
<div class="relative" role="dialog" aria-modal="true"> <div class="relative" role="dialog" aria-modal="true">
@if (showLogin()) { @if (showLogin()) {
<app-login (switchForm)="showRegisterForm()" (closeDialog)="hideAuthForms()"></app-login> <app-login
(switchForm)="showRegisterForm()"
(closeDialog)="hideAuthForms()"
(forgotPassword)="showRecoverPasswordForm()"
></app-login>
} }
@if (showRegister()) { @if (showRegister()) {
<app-register <app-register
@ -26,6 +30,12 @@
(closeDialog)="hideAuthForms()" (closeDialog)="hideAuthForms()"
></app-register> ></app-register>
} }
@if (showRecoverPassword()) {
<app-recover-password
(closeDialog)="hideAuthForms()"
(switchToLogin)="showLoginForm()"
></app-recover-password>
}
</div> </div>
</div> </div>
} }

View file

@ -4,16 +4,25 @@ import { NavbarComponent } from './shared/components/navbar/navbar.component';
import { FooterComponent } from './shared/components/footer/footer.component'; import { FooterComponent } from './shared/components/footer/footer.component';
import { LoginComponent } from './feature/auth/login/login.component'; import { LoginComponent } from './feature/auth/login/login.component';
import { RegisterComponent } from './feature/auth/register/register.component'; import { RegisterComponent } from './feature/auth/register/register.component';
import { RecoverPasswordComponent } from './feature/auth/recover-password/recover-password.component';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
standalone: true, standalone: true,
imports: [RouterOutlet, NavbarComponent, FooterComponent, LoginComponent, RegisterComponent], imports: [
RouterOutlet,
NavbarComponent,
FooterComponent,
LoginComponent,
RegisterComponent,
RecoverPasswordComponent,
],
templateUrl: './app.component.html', templateUrl: './app.component.html',
}) })
export class AppComponent { export class AppComponent {
showLogin = signal(false); showLogin = signal(false);
showRegister = signal(false); showRegister = signal(false);
showRecoverPassword = signal(false);
@HostListener('document:keydown.escape') @HostListener('document:keydown.escape')
handleEscapeKey() { handleEscapeKey() {
@ -23,18 +32,28 @@ export class AppComponent {
showLoginForm() { showLoginForm() {
this.showLogin.set(true); this.showLogin.set(true);
this.showRegister.set(false); this.showRegister.set(false);
this.showRecoverPassword.set(false);
document.body.style.overflow = 'hidden'; document.body.style.overflow = 'hidden';
} }
showRegisterForm() { showRegisterForm() {
this.showRegister.set(true); this.showRegister.set(true);
this.showLogin.set(false); this.showLogin.set(false);
this.showRecoverPassword.set(false);
document.body.style.overflow = 'hidden';
}
showRecoverPasswordForm() {
this.showRecoverPassword.set(true);
this.showLogin.set(false);
this.showRegister.set(false);
document.body.style.overflow = 'hidden'; document.body.style.overflow = 'hidden';
} }
hideAuthForms() { hideAuthForms() {
this.showLogin.set(false); this.showLogin.set(false);
this.showRegister.set(false); this.showRegister.set(false);
this.showRecoverPassword.set(false);
document.body.style.overflow = 'auto'; document.body.style.overflow = 'auto';
} }

View file

@ -17,6 +17,7 @@ export class LoginComponent {
isLoading = signal(false); isLoading = signal(false);
@Output() switchForm = new EventEmitter<void>(); @Output() switchForm = new EventEmitter<void>();
@Output() closeDialog = new EventEmitter<void>(); @Output() closeDialog = new EventEmitter<void>();
@Output() forgotPassword = new EventEmitter<void>();
constructor( constructor(
private fb: FormBuilder, private fb: FormBuilder,
@ -65,7 +66,6 @@ export class LoginComponent {
} }
switchToForgotPassword() { switchToForgotPassword() {
this.closeDialog.emit(); this.forgotPassword.emit();
this.router.navigate(['/recover-password']);
} }
} }

View file

@ -1,172 +1,170 @@
<div class="min-h-screen bg-deep-blue flex items-center justify-center"> <div class="modal-card max-w-md w-full bg-deep-blue rounded-lg shadow-xl p-6 relative">
<div class="modal-card max-w-md w-full bg-deep-blue rounded-lg shadow-xl p-6 relative"> <button
<button (click)="closeDialog.emit()"
(click)="closeDialog.emit()" class="absolute top-4 right-4 text-text-secondary hover:text-white transition-colors"
class="absolute top-4 right-4 text-text-secondary hover:text-white transition-colors" aria-label="Dialog schließen"
aria-label="Dialog schließen" >
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
> >
<svg <path
xmlns="http://www.w3.org/2000/svg" stroke-linecap="round"
class="h-6 w-6" stroke-linejoin="round"
fill="none" stroke-width="2"
viewBox="0 0 24 24" d="M6 18L18 6M6 6l12 12"
stroke="currentColor" />
> </svg>
<path </button>
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
<h2 class="modal-heading text-center">
@if (isResetMode()) {
Passwort zurücksetzen
} @else {
Passwort vergessen
}
</h2>
@if (errorMessage()) {
<div class="bg-accent-red text-white p-4 rounded mb-4">
{{ errorMessage() }}
</div>
}
@if (successMessage()) {
<div class="bg-emerald text-white p-4 rounded mb-4">
{{ successMessage() }}
</div>
}
@if (!isResetMode()) {
<!-- Request Password Reset Form -->
<form [formGroup]="emailForm" (ngSubmit)="onSubmitEmail()" class="space-y-4">
<div class="mb-6">
<p class="text-text-secondary text-sm mb-4">
Gib deine E-Mail-Adresse ein, und wir senden dir einen Link zum Zurücksetzen deines
Passworts.
</p>
</div>
<div>
<label for="email" class="text-text-secondary text-sm font-medium mb-1 block">
E-Mail
</label>
<input
id="email"
type="email"
formControlName="email"
class="w-full px-4 py-2.5 bg-deep-blue-light/50 text-white rounded-lg my-1 border border-deep-blue-light/30 focus:border-emerald/50 focus:ring-1 focus:ring-emerald/50 outline-none transition-all duration-200"
placeholder="Gib deine E-Mail-Adresse ein"
/>
@if (emailFormControls['email'].touched && emailFormControls['email'].errors) {
<div class="text-accent-red mt-1 text-sm">
@if (emailFormControls['email'].errors['required']) {
<span>E-Mail ist erforderlich</span>
}
@if (emailFormControls['email'].errors['email']) {
<span>Bitte gib eine gültige E-Mail-Adresse ein</span>
}
</div>
}
</div>
<div class="pt-2">
<button
type="submit"
[disabled]="emailForm.invalid || isLoading()"
class="button-primary w-full py-2.5 rounded"
>
{{ isLoading() ? 'Wird gesendet...' : 'Link zum Zurücksetzen senden' }}
</button>
</div>
</form>
}
<h2 class="modal-heading text-center">
@if (isResetMode()) { @if (isResetMode()) {
<!-- Reset Password Form --> Passwort zurücksetzen
<form [formGroup]="resetPasswordForm" (ngSubmit)="onSubmitReset()" class="space-y-4"> } @else {
<div class="mb-6"> Passwort vergessen
<p class="text-text-secondary text-sm mb-4">Gib dein neues Passwort ein.</p>
</div>
<div>
<label for="password" class="text-text-secondary text-sm font-medium mb-1 block">
Neues Passwort
</label>
<input
id="password"
type="password"
formControlName="password"
class="w-full px-4 py-2.5 bg-deep-blue-light/50 text-white rounded-lg my-1 border border-deep-blue-light/30 focus:border-emerald/50 focus:ring-1 focus:ring-emerald/50 outline-none transition-all duration-200"
placeholder="Gib dein neues Passwort ein"
/>
@if (resetFormControls['password'].touched && resetFormControls['password'].errors) {
<div class="text-accent-red mt-1 text-sm">
@if (resetFormControls['password'].errors['required']) {
<span>Passwort ist erforderlich</span>
}
@if (resetFormControls['password'].errors['minlength']) {
<span>Passwort muss mindestens 8 Zeichen lang sein</span>
}
</div>
}
</div>
<div>
<label for="confirmPassword" class="text-text-secondary text-sm font-medium mb-1 block">
Passwort bestätigen
</label>
<input
id="confirmPassword"
type="password"
formControlName="confirmPassword"
class="w-full px-4 py-2.5 bg-deep-blue-light/50 text-white rounded-lg my-1 border border-deep-blue-light/30 focus:border-emerald/50 focus:ring-1 focus:ring-emerald/50 outline-none transition-all duration-200"
placeholder="Bestätige dein neues Passwort"
/>
@if (
resetFormControls['confirmPassword'].touched &&
(resetFormControls['confirmPassword'].errors ||
resetPasswordForm.errors?.['passwordMismatch'])
) {
<div class="text-accent-red mt-1 text-sm">
@if (resetFormControls['confirmPassword'].errors?.['required']) {
<span>Passwortbestätigung ist erforderlich</span>
}
@if (resetPasswordForm.errors?.['passwordMismatch']) {
<span>Passwörter stimmen nicht überein</span>
}
</div>
}
</div>
<div class="pt-2">
<button
type="submit"
[disabled]="resetPasswordForm.invalid || isLoading()"
class="button-primary w-full py-2.5 rounded"
>
{{ isLoading() ? 'Wird aktualisiert...' : 'Passwort aktualisieren' }}
</button>
</div>
</form>
} }
</h2>
<div class="mt-6 text-center"> @if (errorMessage()) {
<p class="text-sm text-text-secondary"> <div class="bg-accent-red text-white p-4 rounded mb-4">
<a {{ errorMessage() }}
routerLink="/"
class="font-medium text-emerald hover:text-emerald-light transition-all duration-200"
>
Zurück zur Startseite
</a>
</p>
</div> </div>
}
@if (successMessage()) {
<div class="bg-emerald text-white p-4 rounded mb-4">
{{ successMessage() }}
</div>
}
@if (!isResetMode()) {
<!-- Request Password Reset Form -->
<form [formGroup]="emailForm" (ngSubmit)="onSubmitEmail()" class="space-y-4">
<div class="mb-6">
<p class="text-text-secondary text-sm mb-4">
Gib deine E-Mail-Adresse ein, und wir senden dir einen Link zum Zurücksetzen deines
Passworts.
</p>
</div>
<div>
<label for="email" class="text-text-secondary text-sm font-medium mb-1 block">
E-Mail
</label>
<input
id="email"
type="email"
formControlName="email"
class="w-full px-4 py-2.5 bg-deep-blue-light/50 text-white rounded-lg my-1 border border-deep-blue-light/30 focus:border-emerald/50 focus:ring-1 focus:ring-emerald/50 outline-none transition-all duration-200"
placeholder="Gib deine E-Mail-Adresse ein"
/>
@if (emailFormControls['email'].touched && emailFormControls['email'].errors) {
<div class="text-accent-red mt-1 text-sm">
@if (emailFormControls['email'].errors['required']) {
<span>E-Mail ist erforderlich</span>
}
@if (emailFormControls['email'].errors['email']) {
<span>Bitte gib eine gültige E-Mail-Adresse ein</span>
}
</div>
}
</div>
<div class="pt-2">
<button
type="submit"
[disabled]="emailForm.invalid || isLoading()"
class="button-primary w-full py-2.5 rounded"
>
{{ isLoading() ? 'Wird gesendet...' : 'Link zum Zurücksetzen senden' }}
</button>
</div>
</form>
}
@if (isResetMode()) {
<!-- Reset Password Form -->
<form [formGroup]="resetPasswordForm" (ngSubmit)="onSubmitReset()" class="space-y-4">
<div class="mb-6">
<p class="text-text-secondary text-sm mb-4">Gib dein neues Passwort ein.</p>
</div>
<div>
<label for="password" class="text-text-secondary text-sm font-medium mb-1 block">
Neues Passwort
</label>
<input
id="password"
type="password"
formControlName="password"
class="w-full px-4 py-2.5 bg-deep-blue-light/50 text-white rounded-lg my-1 border border-deep-blue-light/30 focus:border-emerald/50 focus:ring-1 focus:ring-emerald/50 outline-none transition-all duration-200"
placeholder="Gib dein neues Passwort ein"
/>
@if (resetFormControls['password'].touched && resetFormControls['password'].errors) {
<div class="text-accent-red mt-1 text-sm">
@if (resetFormControls['password'].errors['required']) {
<span>Passwort ist erforderlich</span>
}
@if (resetFormControls['password'].errors['minlength']) {
<span>Passwort muss mindestens 8 Zeichen lang sein</span>
}
</div>
}
</div>
<div>
<label for="confirmPassword" class="text-text-secondary text-sm font-medium mb-1 block">
Passwort bestätigen
</label>
<input
id="confirmPassword"
type="password"
formControlName="confirmPassword"
class="w-full px-4 py-2.5 bg-deep-blue-light/50 text-white rounded-lg my-1 border border-deep-blue-light/30 focus:border-emerald/50 focus:ring-1 focus:ring-emerald/50 outline-none transition-all duration-200"
placeholder="Bestätige dein neues Passwort"
/>
@if (
resetFormControls['confirmPassword'].touched &&
(resetFormControls['confirmPassword'].errors ||
resetPasswordForm.errors?.['passwordMismatch'])
) {
<div class="text-accent-red mt-1 text-sm">
@if (resetFormControls['confirmPassword'].errors?.['required']) {
<span>Passwortbestätigung ist erforderlich</span>
}
@if (resetPasswordForm.errors?.['passwordMismatch']) {
<span>Passwörter stimmen nicht überein</span>
}
</div>
}
</div>
<div class="pt-2">
<button
type="submit"
[disabled]="resetPasswordForm.invalid || isLoading()"
class="button-primary w-full py-2.5 rounded"
>
{{ isLoading() ? 'Wird aktualisiert...' : 'Passwort aktualisieren' }}
</button>
</div>
</form>
}
<div class="mt-6 text-center">
<p class="text-sm text-text-secondary">
<button
(click)="goBackToLogin()"
class="font-medium text-emerald hover:text-emerald-light transition-all duration-200"
>
Zurück zum Login
</button>
</p>
</div> </div>
</div> </div>

View file

@ -1,16 +1,16 @@
import { Component, EventEmitter, Output, signal } from '@angular/core'; import { Component, EventEmitter, Output, signal, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { AuthService } from '@service/auth.service'; import { AuthService } from '@service/auth.service';
@Component({ @Component({
selector: 'app-recover-password', selector: 'app-recover-password',
standalone: true, standalone: true,
imports: [CommonModule, ReactiveFormsModule], imports: [CommonModule, ReactiveFormsModule, RouterModule],
templateUrl: './recover-password.component.html', templateUrl: './recover-password.component.html',
}) })
export class RecoverPasswordComponent { export class RecoverPasswordComponent implements OnInit {
emailForm: FormGroup; emailForm: FormGroup;
resetPasswordForm: FormGroup; resetPasswordForm: FormGroup;
errorMessage = signal(''); errorMessage = signal('');
@ -20,6 +20,7 @@ export class RecoverPasswordComponent {
isResetMode = signal(false); isResetMode = signal(false);
@Output() closeDialog = new EventEmitter<void>(); @Output() closeDialog = new EventEmitter<void>();
@Output() switchToLogin = new EventEmitter<void>();
constructor( constructor(
private fb: FormBuilder, private fb: FormBuilder,
@ -40,8 +41,11 @@ export class RecoverPasswordComponent {
validators: this.passwordMatchValidator, validators: this.passwordMatchValidator,
} }
); );
}
// Check if we're in reset mode ngOnInit(): void {
// Check if we're in reset mode via URL parameters
// This is still needed for direct access via URLs with token
this.route.queryParamMap.subscribe((params) => { this.route.queryParamMap.subscribe((params) => {
const token = params.get('token'); const token = params.get('token');
if (token) { if (token) {
@ -111,7 +115,8 @@ export class RecoverPasswordComponent {
'Dein Passwort wurde erfolgreich zurückgesetzt. Du kannst dich jetzt anmelden.' 'Dein Passwort wurde erfolgreich zurückgesetzt. Du kannst dich jetzt anmelden.'
); );
setTimeout(() => { setTimeout(() => {
this.router.navigate([''], { queryParams: { login: true } }); this.closeDialog.emit();
this.switchToLogin.emit();
}, 3000); }, 3000);
}, },
error: (err) => { error: (err) => {
@ -122,4 +127,8 @@ export class RecoverPasswordComponent {
}, },
}); });
} }
goBackToLogin(): void {
this.switchToLogin.emit();
}
} }