feat(auth): add login and registration modal functionality

This commit is contained in:
Jan-Marlon Leibl 2025-05-14 12:00:33 +02:00
commit 0079ee7bf2
Signed by: jleibl
GPG key ID: 300B2F906DC6F1D5
11 changed files with 212 additions and 55 deletions

View file

@ -1,7 +1,45 @@
<div class="min-h-screen flex flex-col"> <div class="min-h-screen flex flex-col">
<main class="flex-grow"> <main class="flex-grow">
<app-navbar></app-navbar> <app-navbar
(showLogin)="showLoginForm()"
(showRegister)="showRegisterForm()"
></app-navbar>
<router-outlet></router-outlet> <router-outlet></router-outlet>
</main> </main>
<app-footer></app-footer> <app-footer></app-footer>
<!-- Auth Forms Overlay -->
@if (showLogin() || showRegister()) {
<div
class="fixed inset-0 bg-black/50 z-40"
(click)="hideAuthForms()"
(keydown.enter)="hideAuthForms()"
tabindex="0"
role="dialog"
aria-modal="true"
></div>
<div
class="fixed inset-0 flex items-center justify-center z-50 p-4"
role="presentation"
>
<div
class="relative"
role="dialog"
aria-modal="true"
>
@if (showLogin()) {
<app-login
(switchForm)="showRegisterForm()"
(closeDialog)="hideAuthForms()"
></app-login>
}
@if (showRegister()) {
<app-register
(switchForm)="showLoginForm()"
(closeDialog)="hideAuthForms()"
></app-register>
}
</div>
</div>
}
</div> </div>

View file

@ -1,16 +1,44 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'; import { Component, HostListener, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router'; import { RouterOutlet } from '@angular/router';
import { FooterComponent } from '@shared/components/footer/footer.component'; import { NavbarComponent } from './shared/components/navbar/navbar.component';
import { NavbarComponent } from '@shared/components/navbar/navbar.component'; import { FooterComponent } from './shared/components/footer/footer.component';
import { LoginComponent } from './feature/auth/login/login.component';
import { RegisterComponent } from './feature/auth/register/register.component';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
standalone: true, standalone: true,
imports: [CommonModule, RouterOutlet, FooterComponent, NavbarComponent], imports: [RouterOutlet, NavbarComponent, FooterComponent, LoginComponent, RegisterComponent],
providers: [],
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrl: './app.component.css',
changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class AppComponent {} export class AppComponent {
showLogin = signal(false);
showRegister = signal(false);
@HostListener('document:keydown.escape')
handleEscapeKey() {
this.hideAuthForms();
}
showLoginForm() {
this.showLogin.set(true);
this.showRegister.set(false);
document.body.style.overflow = 'hidden';
}
showRegisterForm() {
this.showRegister.set(true);
this.showLogin.set(false);
document.body.style.overflow = 'hidden';
}
hideAuthForms() {
this.showLogin.set(false);
this.showRegister.set(false);
document.body.style.overflow = 'auto';
}
stopPropagation(event: MouseEvent) {
event.stopPropagation();
}
}

View file

@ -7,16 +7,6 @@ export const routes: Routes = [
path: '', path: '',
component: LandingComponent, component: LandingComponent,
}, },
{
path: 'login',
loadComponent: () =>
import('./feature/auth/login/login.component').then((m) => m.LoginComponent),
},
{
path: 'register',
loadComponent: () =>
import('./feature/auth/register/register.component').then((m) => m.RegisterComponent),
},
{ {
path: 'home', path: 'home',
loadComponent: () => import('./feature/home/home.component'), loadComponent: () => import('./feature/home/home.component'),

View file

@ -1,5 +1,16 @@
<div class="min-h-screen bg-deep-blue flex items-center justify-center"> <div class="min-h-screen bg-deep-blue flex items-center justify-center">
<div class="modal-card max-w-md w-full"> <div class="modal-card max-w-md w-full bg-deep-blue rounded-lg shadow-xl p-6 relative">
<!-- Close Button -->
<button
(click)="closeDialog.emit()"
class="absolute top-4 right-4 text-text-secondary hover:text-white transition-colors"
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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<h2 class="modal-heading text-center">Anmelden</h2> <h2 class="modal-heading text-center">Anmelden</h2>
@if (errorMessage()) { @if (errorMessage()) {
@ -65,12 +76,12 @@
<div class="mt-6 text-center"> <div class="mt-6 text-center">
<p class="text-sm text-text-secondary"> <p class="text-sm text-text-secondary">
Noch kein Konto? Noch kein Konto?
<a <button
routerLink="/register" (click)="switchToRegister()"
class="font-medium text-emerald hover:text-emerald-light transition-all duration-200" class="font-medium text-emerald hover:text-emerald-light transition-all duration-200"
> >
Registrieren Registrieren
</a> </button>
</p> </p>
</div> </div>
</div> </div>

View file

@ -1,6 +1,6 @@
import { Component, signal } from '@angular/core'; import { Component, EventEmitter, Output, signal } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { Router, RouterLink } from '@angular/router'; import { Router } from '@angular/router';
import { LoginRequest } from '../../../model/auth/LoginRequest'; import { LoginRequest } from '../../../model/auth/LoginRequest';
import { AuthService } from '@service/auth.service'; import { AuthService } from '@service/auth.service';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@ -8,13 +8,15 @@ import { CommonModule } from '@angular/common';
@Component({ @Component({
selector: 'app-login', selector: 'app-login',
standalone: true, standalone: true,
imports: [CommonModule, ReactiveFormsModule, RouterLink], imports: [CommonModule, ReactiveFormsModule],
templateUrl: './login.component.html', templateUrl: './login.component.html',
}) })
export class LoginComponent { export class LoginComponent {
loginForm: FormGroup; loginForm: FormGroup;
errorMessage = signal(''); errorMessage = signal('');
isLoading = signal(false); isLoading = signal(false);
@Output() switchForm = new EventEmitter<void>();
@Output() closeDialog = new EventEmitter<void>();
constructor( constructor(
private fb: FormBuilder, private fb: FormBuilder,
@ -31,6 +33,10 @@ export class LoginComponent {
return this.loginForm.controls; return this.loginForm.controls;
} }
switchToRegister(): void {
this.switchForm.emit();
}
onSubmit(): void { onSubmit(): void {
if (this.loginForm.invalid) { if (this.loginForm.invalid) {
return; return;
@ -46,6 +52,7 @@ export class LoginComponent {
this.authService.login(loginRequest).subscribe({ this.authService.login(loginRequest).subscribe({
next: () => { next: () => {
this.closeDialog.emit();
this.router.navigate(['/home']); this.router.navigate(['/home']);
}, },
error: (err) => { error: (err) => {

View file

@ -1,5 +1,16 @@
<div class="min-h-screen bg-deep-blue flex items-center justify-center"> <div class="min-h-screen bg-deep-blue flex items-center justify-center">
<div class="modal-card max-w-md w-full"> <div class="modal-card max-w-md w-full bg-deep-blue rounded-lg shadow-xl p-6 relative">
<!-- Close Button -->
<button
(click)="closeDialog.emit()"
class="absolute top-4 right-4 text-text-secondary hover:text-white transition-colors"
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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<h2 class="modal-heading text-center">Konto erstellen</h2> <h2 class="modal-heading text-center">Konto erstellen</h2>
@if (errorMessage()) { @if (errorMessage()) {
@ -107,12 +118,12 @@
<div class="mt-6 text-center"> <div class="mt-6 text-center">
<p class="text-sm text-text-secondary"> <p class="text-sm text-text-secondary">
Bereits ein Konto? Bereits ein Konto?
<a <button
routerLink="/login" (click)="switchToLogin()"
class="font-medium text-emerald hover:text-emerald-light transition-all duration-200" class="font-medium text-emerald hover:text-emerald-light transition-all duration-200"
> >
Anmelden Anmelden
</a> </button>
</p> </p>
</div> </div>
</div> </div>

View file

@ -1,6 +1,6 @@
import { Component, signal } from '@angular/core'; import { Component, EventEmitter, Output, signal } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { Router, RouterLink } from '@angular/router'; import { Router } from '@angular/router';
import { RegisterRequest } from '../../../model/auth/RegisterRequest'; import { RegisterRequest } from '../../../model/auth/RegisterRequest';
import { AuthService } from '@service/auth.service'; import { AuthService } from '@service/auth.service';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@ -9,7 +9,7 @@ import { HttpErrorResponse } from '@angular/common/http';
@Component({ @Component({
selector: 'app-register', selector: 'app-register',
standalone: true, standalone: true,
imports: [CommonModule, ReactiveFormsModule, RouterLink], imports: [CommonModule, ReactiveFormsModule],
templateUrl: './register.component.html', templateUrl: './register.component.html',
}) })
export class RegisterComponent { export class RegisterComponent {
@ -17,6 +17,8 @@ export class RegisterComponent {
errorMessage = signal<string>(''); errorMessage = signal<string>('');
isLoading = signal<boolean>(false); isLoading = signal<boolean>(false);
fieldErrors = signal<Record<string, string>>({}); fieldErrors = signal<Record<string, string>>({});
@Output() switchForm = new EventEmitter<void>();
@Output() closeDialog = new EventEmitter<void>();
constructor( constructor(
private fb: FormBuilder, private fb: FormBuilder,
@ -34,6 +36,10 @@ export class RegisterComponent {
return this.registerForm.controls; return this.registerForm.controls;
} }
switchToLogin(): void {
this.switchForm.emit();
}
onSubmit(): void { onSubmit(): void {
if (this.registerForm.invalid) { if (this.registerForm.invalid) {
return; return;

View file

@ -1,4 +1,4 @@
<div class="min-h-screen bg-deep-blue text-text-primary"> <div class="min-h-screen bg-deep-blue text-text-primary relative">
<div class="container mx-auto px-4 py-8 sm:py-12"> <div class="container mx-auto px-4 py-8 sm:py-12">
<div class="max-w-5xl mx-auto"> <div class="max-w-5xl mx-auto">
<div class="text-center mb-12 sm:mb-16"> <div class="text-center mb-12 sm:mb-16">
@ -17,18 +17,18 @@
Spiele Spiele
</a> </a>
} @else { } @else {
<a <button
routerLink="/register" (click)="showRegisterForm()"
class="w-full sm:w-auto button-primary px-6 sm:px-8 py-3 shadow-lg" class="w-full sm:w-auto button-primary px-6 sm:px-8 py-3 shadow-lg"
> >
Konto erstellen Konto erstellen
</a> </button>
<a <button
routerLink="/login" (click)="showLoginForm()"
class="w-full sm:w-auto bg-slate-700 text-white hover:bg-slate-600 px-6 sm:px-8 py-3 shadow-lg rounded" class="w-full sm:w-auto bg-slate-700 text-white hover:bg-slate-600 px-6 sm:px-8 py-3 shadow-lg rounded"
> >
Anmelden Anmelden
</a> </button>
} }
</div> </div>
</div> </div>
@ -120,8 +120,8 @@
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6" class="h-6 w-6"
fill="none" fill="none"
viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
viewBox="0 0 24 24"
> >
<path <path
stroke-linecap="round" stroke-linecap="round"
@ -136,8 +136,8 @@
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6" class="h-6 w-6"
fill="none" fill="none"
viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
viewBox="0 0 24 24"
> >
<path <path
stroke-linecap="round" stroke-linecap="round"
@ -181,4 +181,39 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Auth Forms Overlay -->
@if (showLogin() || showRegister()) {
<div
class="fixed inset-0 bg-black/50 z-40"
(click)="hideAuthForms()"
(keydown.enter)="hideAuthForms()"
tabindex="0"
role="dialog"
aria-modal="true"
></div>
<div
class="fixed inset-0 flex items-center justify-center z-50 p-4"
role="presentation"
>
<div
class="relative"
role="dialog"
aria-modal="true"
>
@if (showLogin()) {
<app-login
(switchForm)="showRegisterForm()"
(closeDialog)="hideAuthForms()"
></app-login>
}
@if (showRegister()) {
<app-register
(switchForm)="showLoginForm()"
(closeDialog)="hideAuthForms()"
></app-register>
}
</div>
</div>
}
</div> </div>

View file

@ -1,12 +1,14 @@
import { ChangeDetectionStrategy, Component, inject, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, inject, OnDestroy, OnInit, signal } from '@angular/core';
import { NgFor } from '@angular/common'; import { NgFor } from '@angular/common';
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
import { AuthService } from '@service/auth.service'; import { AuthService } from '@service/auth.service';
import { LoginComponent } from '../auth/login/login.component';
import { RegisterComponent } from '../auth/register/register.component';
@Component({ @Component({
selector: 'app-landing-page', selector: 'app-landing-page',
standalone: true, standalone: true,
imports: [NgFor, RouterLink], imports: [NgFor, RouterLink, LoginComponent, RegisterComponent],
templateUrl: './landing.component.html', templateUrl: './landing.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
@ -14,13 +16,35 @@ export class LandingComponent implements OnInit, OnDestroy {
currentSlide = 0; currentSlide = 0;
private autoplayInterval: ReturnType<typeof setInterval> | undefined; private autoplayInterval: ReturnType<typeof setInterval> | undefined;
authService: AuthService = inject(AuthService); authService: AuthService = inject(AuthService);
showLogin = signal(false);
showRegister = signal(false);
ngOnInit() { ngOnInit() {
this.startAutoplay(); this.startAutoplay();
document.body.style.overflow = 'auto';
} }
ngOnDestroy() { ngOnDestroy() {
this.stopAutoplay(); this.stopAutoplay();
document.body.style.overflow = 'auto';
}
showLoginForm() {
this.showLogin.set(true);
this.showRegister.set(false);
document.body.style.overflow = 'hidden';
}
showRegisterForm() {
this.showRegister.set(true);
this.showLogin.set(false);
document.body.style.overflow = 'hidden';
}
hideAuthForms() {
this.showLogin.set(false);
this.showRegister.set(false);
document.body.style.overflow = 'auto';
} }
prevSlide() { prevSlide() {

View file

@ -12,12 +12,13 @@
<div class="hidden md:flex items-center space-x-4"> <div class="hidden md:flex items-center space-x-4">
@if (!isLoggedIn()) { @if (!isLoggedIn()) {
<a routerLink="/login" class="button-primary px-4 py-1.5">Anmelden</a> <button (click)="showLogin.emit()" class="button-primary px-4 py-1.5">Anmelden</button>
<a <button
routerLink="/register" (click)="showRegister.emit()"
class="bg-emerald-700 text-white hover:bg-emerald-600 px-4 py-1.5 rounded" class="bg-emerald-700 text-white hover:bg-emerald-600 px-4 py-1.5 rounded"
>Registrieren</a
> >
Registrieren
</button>
} }
@if (isLoggedIn()) { @if (isLoggedIn()) {
<div <div
@ -71,14 +72,15 @@
<a routerLink="/games" class="nav-mobile-link">Spiele</a> <a routerLink="/games" class="nav-mobile-link">Spiele</a>
<div class="pt-2 space-y-2"> <div class="pt-2 space-y-2">
@if (!isLoggedIn()) { @if (!isLoggedIn()) {
<a routerLink="/login" class="button-primary w-full py-1.5 block text-center" <button (click)="showLogin.emit()" class="button-primary w-full py-1.5 block text-center">
>Anmelden</a Anmelden
> </button>
<a <button
routerLink="/register" (click)="showRegister.emit()"
class="bg-emerald-700 text-white hover:bg-emerald-600 w-full py-1.5 rounded block text-center" class="bg-emerald-700 text-white hover:bg-emerald-600 w-full py-1.5 rounded block text-center"
>Registrieren</a
> >
Registrieren
</button>
} }
@if (isLoggedIn()) { @if (isLoggedIn()) {
<button (click)="logout()" class="button-primary w-full py-1.5">Abmelden</button> <button (click)="logout()" class="button-primary w-full py-1.5">Abmelden</button>

View file

@ -1,9 +1,11 @@
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
EventEmitter,
inject, inject,
OnDestroy, OnDestroy,
OnInit, OnInit,
Output,
signal, signal,
} from '@angular/core'; } from '@angular/core';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
@ -26,6 +28,9 @@ export class NavbarComponent implements OnInit, OnDestroy {
private authSubscription!: Subscription; private authSubscription!: Subscription;
public balance = signal(0); public balance = signal(0);
@Output() showLogin = new EventEmitter<void>();
@Output() showRegister = new EventEmitter<void>();
ngOnInit() { ngOnInit() {
this.authSubscription = this.authService.userSubject.subscribe({ this.authSubscription = this.authService.userSubject.subscribe({
next: (user) => { next: (user) => {