Fertig- next ist dashboard

This commit is contained in:
Tizian.Breuch
2025-09-17 09:43:45 +02:00
parent 1bb9a408b8
commit 7e5939868b
26 changed files with 424 additions and 369 deletions

View File

@@ -9,14 +9,9 @@ export const routes: Routes = [
// wird sofort und ohne Umwege zur Login-Seite weitergeleitet. // wird sofort und ohne Umwege zur Login-Seite weitergeleitet.
{ {
path: '', path: '',
redirectTo: 'auth', // Leitet zur /auth-Route weiter redirectTo: 'auth',
pathMatch: 'full', // Wichtig: Gilt nur für den exakt leeren Pfad pathMatch: 'full',
}, },
// Regel 2: Authentifizierungs-Feature
// Alle URLs, die mit "auth/" beginnen (z.B. "/auth/login", "/auth/register"),
// werden von dieser Regel abgefangen und an die auth.routes.ts zur
// weiteren Verarbeitung übergeben.
{ {
path: 'auth', path: 'auth',
loadChildren: () => loadChildren: () =>
@@ -27,14 +22,11 @@ export const routes: Routes = [
loadChildren: () => loadChildren: () =>
import('./features/demo/demo.routes').then((r) => r.DEMO_ROUTES), import('./features/demo/demo.routes').then((r) => r.DEMO_ROUTES),
}, },
{ {
path: 'access-denied', path: 'access-denied',
component: AccessDeniedComponent, component: AccessDeniedComponent,
title: 'Zugriff verweigert', title: 'Zugriff verweigert',
}, },
{ {
path: '**', path: '**',
component: NotFoundComponent, component: NotFoundComponent,

View File

@@ -1,25 +1,19 @@
<!-- <div class="not-found-container"> <div class="not-found-container">
<div class="not-found-box"> <div class="not-found-box">
<div class="status-code">404</div> <div class="status-code">404</div>
<h1 class="title">Seite nicht gefunden</h1> <h1 class="title">Seite nicht gefunden</h1>
<p class="subtitle"> <p class="subtitle">
Die von Ihnen angeforderte Seite konnte leider nicht gefunden werden. Die von Ihnen angeforderte Seite konnte leider nicht gefunden werden.
Möglicherweise wurde sie verschoben, gelöscht oder die URL ist falsch. Möglicherweise wurde sie verschoben, gelöscht oder die URL ist falsch.
</p> </p>
<div class="actions"> <div class="actions">
<button class="btn btn-stroked" (click)="goBack()"> <div class="button-group">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg> <app-button buttonType="primary" (click)="goBack()">Zurück</app-button>
<span>Zurück</span> <app-button buttonType="secondary" routerLink="/"
</button> >Zur Startseite</app-button
<button class="btn btn-primary" routerLink="/"> >
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg> </div>
<span>Zur Startseite</span>
</button>
</div> </div>
</div> </div>
</div> --> </div>
<p>404</p>

View File

@@ -2,18 +2,16 @@ import { Component } from '@angular/core';
import { CommonModule, Location } from '@angular/common'; import { CommonModule, Location } from '@angular/common';
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
import { ButtonComponent } from '../../../shared/components/ui/button/button.component';
@Component({ @Component({
selector: 'app-not-found', selector: 'app-not-found',
imports: [ imports: [CommonModule, ButtonComponent,RouterLink],
CommonModule
],
templateUrl: './not-found.component.html', templateUrl: './not-found.component.html',
styleUrl: './not-found.component.css' styleUrl: './not-found.component.css',
}) })
export class NotFoundComponent { export class NotFoundComponent {
constructor(private location: Location) {}
constructor(private location: Location) { }
/** /**
* Navigiert den Benutzer eine Seite in der Browser-Historie zurück. * Navigiert den Benutzer eine Seite in der Browser-Historie zurück.
@@ -21,4 +19,4 @@ export class NotFoundComponent {
goBack(): void { goBack(): void {
this.location.back(); this.location.back();
} }
} }

View File

@@ -1,85 +0,0 @@
/* src\app\features\auth\_auth-common.css */
/* =================================================================================
* GEMEINSAME STILE FÜR DAS AUTH-FEATURE
* ================================================================================= */
/* -- Globale Layout-Elemente -- */
.component-header {
text-align: center;
margin-bottom: 2rem;
}
.auth-title {
font-size: 1.75rem;
font-weight: 700;
color: var(--color-text);
margin-bottom: 0.5rem;
}
.auth-subtitle {
color: var(--color-text-light);
font-size: 1rem;
}
/* -- Formular-Basiselemente & Helfer -- */
form {
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.btn-full-width {
width: 100%;
}
.btn-icon-left {
display: flex;
justify-content: center;
align-items: center;
gap: 0.75rem;
}
.error-text {
color: var(--color-danger);
font-size: 0.85rem;
padding-top: 0.25rem;
}
.info-text {
color: var(--color-text-light);
text-align: center;
line-height: 1.6;
}
/* -- Links & Footer -- */
.link {
color: var(--color-primary);
font-weight: 500;
text-decoration: none;
font-size: 0.9rem;
transition: color var(--transition-speed);
}
.link:hover {
text-decoration: underline;
color: var(--color-primary-dark);
}
.footer-link {
text-align: center;
margin-top: 1.5rem;
color: var(--color-text-light);
font-size: 0.95rem;
}
/* -- Visueller Trenner ("oder") -- */
.divider-or {
display: flex;
align-items: center;
text-align: center;
color: var(--color-text-light);
font-size: 0.85rem;
font-weight: 500;
margin: 0.5rem 0;
}
.divider-or::before,
.divider-or::after {
content: '';
flex: 1;
border-bottom: 1px solid var(--color-border);
}
.divider-or:not(:empty)::before { margin-right: 1em; }
.divider-or:not(:empty)::after { margin-left: 1em; }

View File

@@ -6,6 +6,8 @@ import { LoginComponent } from './components/login/login.component';
import { RegisterComponent } from './components/register/register.component'; import { RegisterComponent } from './components/register/register.component';
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 { VerifyEmailComponent } from './components/verify-email/verify-email.component';
import { NotFoundComponent } from '../../core/components/not-found/not-found.component';
export const AUTH_ROUTES: Routes = [ export const AUTH_ROUTES: Routes = [
{ {
@@ -26,6 +28,11 @@ export const AUTH_ROUTES: Routes = [
component: ResetPasswordComponent, component: ResetPasswordComponent,
title: 'Neues Passwort', title: 'Neues Passwort',
}, },
{
path: 'verify-email/:token',
component: VerifyEmailComponent,
title: 'Email bestätigen',
},
], ],
}, },
]; ];

View File

@@ -1,39 +1,56 @@
/* src\app\features\auth\components\auth-layout\auth-layout.component.css */ /* =================================================================================
/* Stile NUR für den äußeren Rahmen aller Auth-Seiten */ FINALES & ZENTRALISIERTES STYLING FÜR DAS AUTH-LAYOUT
================================================================================== */
:host ::ng-deep .form-content-wrapper {
flex-grow: 1; /* Sorgt dafür, dass der Inhalt den Platz füllt */
}
:host ::ng-deep .verify-content {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
:host ::ng-deep .verify-content app-icon {
font-size: 64px;
color: var(--color-primary);
margin-bottom: 1.5rem;
}
:host ::ng-deep .verify-content .info-text.small {
font-size: 0.9rem;
}
/* 1. Basis-Layout für den Container und die Karte */
:host { :host {
display: block; display: block;
width: 100%; width: 100%;
min-height: 100vh; min-height: 100vh;
background-color: var(--color-body-bg); background-color: var(--color-body-bg);
} }
.auth-container { .auth-container {
display: grid; display: grid;
place-items: center; place-items: center;
min-height: 100vh; min-height: 100vh;
padding: 1rem; padding: 1rem;
} }
.auth-card { .auth-card {
padding: 2rem;
width: 100%; width: 100%;
max-width: 450px; max-width: 450px;
animation: fade-in 0.5s ease-out forwards; animation: fade-in 0.5s ease-out forwards;
} }
.auth-header { .auth-header {
text-align: center; text-align: center;
margin-bottom: 0.5rem; margin-bottom: 1.5rem;
} }
.auth-header app-icon {
.auth-logo { font-size: 48px;
display: inline-block; display: inline-block;
padding: 0.75rem; padding: 0.75rem;
background: var(--color-primary-gradient); background: var(--color-primary-gradient);
color: #fff; color: #fff;
border-radius: 50%; border-radius: 50%;
} }
@keyframes fade-in { @keyframes fade-in {
from { from {
opacity: 0; opacity: 0;
@@ -44,3 +61,94 @@
transform: translateY(0); transform: translateY(0);
} }
} }
/* 2. Flexbox-Layout für den Karteninhalt (DER ENTSCHEIDENDE TEIL) */
/* Wir verwenden ::ng-deep, damit diese Layout-Regeln auf die projizierten Inhalte wirken. */
:host ::ng-deep .auth-content {
display: flex;
flex-direction: column;
height: 100%;
}
/* 3. Stile für die gemeinsamen Elemente innerhalb der Auth-Seiten */
:host ::ng-deep .component-header {
text-align: center;
margin-bottom: 2rem;
}
:host ::ng-deep .auth-title {
font-size: 1.75rem;
font-weight: 700;
color: var(--color-text);
margin-bottom: 0.5rem;
}
:host ::ng-deep .auth-subtitle {
color: var(--color-text-light);
font-size: 1rem;
}
/* Der <form>-Tag oder ein anderer Inhalts-Wrapper wird anpassungsfähig */
:host ::ng-deep form {
display: flex;
flex-direction: column;
gap: 1.25rem;
flex-grow: 1; /* WICHTIG: Nimmt den verfügbaren Platz ein */
}
/* Spezifische Formular-Elemente */
:host ::ng-deep .form-actions {
text-align: right;
margin-top: -0.75rem;
margin-bottom: 0.5rem;
}
:host ::ng-deep .error-text {
color: var(--color-danger);
font-size: 0.85rem;
padding-top: 0.25rem;
}
:host ::ng-deep .info-text {
color: var(--color-text-light);
text-align: center;
line-height: 1.6;
margin-bottom: 1.5rem;
}
/* Links & Footer */
:host ::ng-deep .link {
color: var(--color-primary);
font-weight: 500;
text-decoration: none;
font-size: 0.9rem;
transition: color var(--transition-speed);
}
:host ::ng-deep .link:hover {
text-decoration: underline;
color: var(--color-primary-dark);
}
:host ::ng-deep .footer-link {
text-align: center;
margin-top: auto; /* WICHTIG: Schiebt den Footer immer nach ganz unten */
padding-top: 1.5rem; /* Sorgt für Abstand zum Element darüber */
color: var(--color-text-light);
font-size: 0.95rem;
}
/* Visueller Trenner */
:host ::ng-deep .divider-or {
display: flex;
align-items: center;
color: var(--color-text-light);
font-size: 0.85rem;
margin: 0.5rem 0;
}
:host ::ng-deep .divider-or::before,
:host ::ng-deep .divider-or::after {
content: "";
flex: 1;
border-bottom: 1px solid var(--color-border);
}
:host ::ng-deep .divider-or:not(:empty)::before {
margin-right: 1em;
}
:host ::ng-deep .divider-or:not(:empty)::after {
margin-left: 1em;
}

View File

@@ -1,29 +1,11 @@
<div class="auth-container"> <div class="auth-container">
<div class="auth-card card"> <!-- Wir verwenden jetzt eine app-card für den konsistenten Look -->
<div class="auth-header"> <app-card class="auth-card">
<svg <!-- <div class="auth-header">
class="auth-logo"
xmlns="http://www.w3.org/2000/svg" </div> -->
width="48"
height="48"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"></path>
<polyline points="10 17 15 12 10 7"></polyline>
<line x1="15" y1="12" x2="3" y2="12"></line>
</svg>
</div>
<!-- Der RouterOutlet rendert die spezifischen Inhalte (Login, Register, etc.) -->
<div class="auth-content"> <div class="auth-content">
<router-outlet></router-outlet> <router-outlet></router-outlet>
</div> </div>
</div> </app-card>
</div> </div>

View File

@@ -2,13 +2,11 @@ import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router'; import { RouterOutlet } from '@angular/router';
import { CardComponent } from '../../../../shared/components/ui/card/card.component'; import { CardComponent } from '../../../../shared/components/ui/card/card.component';
@Component({ @Component({
selector: 'app-auth-layout', selector: 'app-auth-layout',
imports: [RouterOutlet, CardComponent], imports: [RouterOutlet, CardComponent],
templateUrl: './auth-layout.component.html', templateUrl: './auth-layout.component.html',
styleUrl: './auth-layout.component.css', styleUrl: './auth-layout.component.css',
}) })
export class AuthLayoutComponent { export class AuthLayoutComponent {}
// Diese Komponente benötigt in der Regel keine eigene Logik.
// Sie dient nur als Hülle für die untergeordneten Routen.
}

View File

@@ -1,28 +0,0 @@
/* src\app\features\auth\components\forgot-password\forgot-password.component.css */
@import '../../_auth-common.css';
/* Stile NUR für die Passwort-vergessen-Seite */
:host { display: block; width: 100%; }
form {
gap: 1.5rem; /* Etwas mehr Abstand als beim Login */
}
.info-text {
margin-bottom: 2rem;
}
.success-message {
text-align: center;
}
.success-title {
font-size: 1.25rem;
font-weight: 600;
color: var(--color-text);
margin-bottom: 0.75rem;
}
.footer-link {
margin-top: 2rem;
}

View File

@@ -2,28 +2,39 @@
<h2 class="auth-title">Passwort vergessen?</h2> <h2 class="auth-title">Passwort vergessen?</h2>
</div> </div>
<div *ngIf="!emailSent"> <!-- Container für den Inhalt, damit der Footer nach unten rutscht -->
<p class="info-text"> <div class="form-content-wrapper">
Geben Sie Ihre E-Mail-Adresse ein. Wir senden Ihnen einen Link, mit dem Sie Ihr Passwort zurücksetzen können. <!-- Fall 1: Formular anzeigen -->
</p> <div *ngIf="!emailSent">
<form [formGroup]="forgotPasswordForm" (ngSubmit)="onSubmit()" novalidate> <p class="info-text">
<div class="form-field"> Geben Sie Ihre E-Mail ein, wir senden Ihnen einen Link zum Zurücksetzen.
<input type="email" class="form-input" id="email" placeholder=" " formControlName="email"> </p>
<label for="email" class="form-label">E-Mail-Adresse</label> <form [formGroup]="form" (ngSubmit)="onSubmit()" novalidate>
</div> <app-form-field
<button type="submit" class="btn btn-primary btn-full-width" [disabled]="forgotPasswordForm.invalid"> label="E-Mail-Adresse"
Link anfordern type="email"
</button> formControlName="email"
</form> ></app-form-field>
</div> <app-button
submitType="submit"
buttonType="primary"
[fullWidth]="true"
[disabled]="form.invalid"
>Link anfordern</app-button
>
</form>
</div>
<div *ngIf="emailSent" class="success-message"> <!-- Fall 2: Erfolgsmeldung anzeigen -->
<h3 class="success-title">Prüfen Sie Ihr Postfach</h3> <div *ngIf="emailSent" class="success-message">
<p class="info-text"> <h2 class="auth-title">Prüfen Sie Ihr Postfach</h2>
Wenn ein Konto mit <strong>{{ forgotPasswordForm.value.email }}</strong> existiert, wurde ein Link versendet. <p class="info-text">
</p> Wenn ein Konto existiert, wurde ein Link an
<strong>{{ form.value.email }}</strong> gesendet.
</p>
</div>
</div> </div>
<div class="footer-link"> <div class="footer-link">
<a routerLink="/auth/login" class="link">Zurück zum Login</a> <a routerLink="/auth/login" class="link">Zurück zum Login</a>
</div> </div>

View File

@@ -2,38 +2,39 @@ import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormBuilder, Validators, FormGroup } from '@angular/forms'; import { ReactiveFormsModule, FormBuilder, Validators, FormGroup } from '@angular/forms';
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
import { ButtonComponent } from '../../../../shared/components/ui/button/button.component';
import { FormFieldComponent } from '../../../../shared/components/form/form-field/form-field.component';
@Component({ @Component({
selector: 'app-forgot-password', selector: 'app-forgot-password',
imports: [ imports: [
CommonModule, CommonModule,
ReactiveFormsModule, ReactiveFormsModule,
RouterLink RouterLink,ButtonComponent,FormFieldComponent
], ],
templateUrl: './forgot-password.component.html', templateUrl: './forgot-password.component.html',
styleUrl: './forgot-password.component.css' styleUrl: './forgot-password.component.css'
}) })
export class ForgotPasswordComponent { export class ForgotPasswordComponent {
forgotPasswordForm: FormGroup; form: FormGroup;
// Optional: Eine Eigenschaft, um eine Erfolgsmeldung anzuzeigen
emailSent = false; emailSent = false;
constructor(private fb: FormBuilder) { constructor(private fb: FormBuilder) {
this.forgotPasswordForm = this.fb.group({ this.form = this.fb.group({
email: ['', [Validators.required, Validators.email]] email: ['', [Validators.required, Validators.email]]
}); });
} }
onSubmit() { onSubmit() {
if (this.forgotPasswordForm.valid) { if (this.form.valid) {
// Hier würde die Logik zum Senden der E-Mail an das Backend stehen // Hier würde die Logik zum Senden der E-Mail an das Backend stehen
console.log('Anforderung zum Zurücksetzen des Passworts für:', this.forgotPasswordForm.value.email); console.log('Anforderung zum Zurücksetzen des Passworts für:', this.form.value.email);
// z.B. this.authService.sendPasswordResetEmail(this.forgotPasswordForm.value.email); // z.B. this.authService.sendPasswordResetEmail(this.forgotPasswordForm.value.email);
// Setze den Zustand, um die Erfolgsmeldung anzuzeigen // Setze den Zustand, um die Erfolgsmeldung anzuzeigen
this.emailSent = true; this.emailSent = true;
} else { } else {
this.forgotPasswordForm.markAllAsTouched(); this.form.markAllAsTouched();
} }
} }
} }

View File

@@ -1,36 +1,43 @@
<!-- Seitenspezifischer Titel und Untertitel -->
<div class="component-header"> <div class="component-header">
<h2 class="auth-title">Anmelden</h2> <h2 class="auth-title">Anmelden</h2>
<p class="auth-subtitle">Bitte geben Sie Ihre Daten ein, um fortzufahren.</p> <p class="auth-subtitle">Bitte geben Sie Ihre Daten ein, um fortzufahren.</p>
</div> </div>
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()" novalidate> <form [formGroup]="loginForm" (ngSubmit)="onSubmit()" novalidate>
<div class="form-field"> <app-form-field
<input type="email" class="form-input" id="email" placeholder=" " formControlName="email"> label="E-Mail-Adresse"
<label for="email" class="form-label">E-Mail-Adresse</label> type="email"
</div> formControlName="email">
</app-form-field>
<div class="form-field"> <app-form-field
<input type="password" class="form-input" id="password" placeholder=" " formControlName="password"> label="Passwort"
<label for="password" class="form-label">Passwort</label> type="password"
</div> formControlName="password">
</app-form-field>
<div class="form-actions"> <div class="form-actions">
<a routerLink="/auth/forgot-password" class="link">Passwort vergessen?</a> <a routerLink="/auth/forgot-password" class="link">Passwort vergessen?</a>
</div> </div>
<button type="submit" class="btn btn-primary btn-full-width" [disabled]="loginForm.invalid"> <app-button
submitType="submit"
buttonType="primary"
[fullWidth]="true"
[disabled]="loginForm.invalid">
Anmelden Anmelden
</button> </app-button>
<div class="divider-or"><span>oder</span></div> <div class="divider-or"><span>oder</span></div>
<button type="button" class="btn btn-secondary btn-full-width btn-icon-left"> <app-button
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"><path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/><path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/><path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/><path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/><path d="M1 1h22v22H1z" fill="none"/></svg> buttonType="secondary"
<span>Mit Google anmelden</span> [fullWidth]="true"
</button> iconName="placeholder">
Mit Google anmelden
</app-button>
</form>
<div class="footer-link"> <div class="footer-link">
Noch kein Konto? <a routerLink="/auth/register" class="link">Jetzt registrieren</a> Noch kein Konto? <a routerLink="/auth/register" class="link">Jetzt registrieren</a>
</div> </div>
</form>

View File

@@ -28,6 +28,8 @@ import { FormFieldComponent } from '../../../../shared/components/form/form-fiel
export class LoginComponent { export class LoginComponent {
loginForm: FormGroup; loginForm: FormGroup;
constructor(private fb: FormBuilder) { constructor(private fb: FormBuilder) {
this.loginForm = this.fb.group({ this.loginForm = this.fb.group({
email: ['', [Validators.required, Validators.email]], email: ['', [Validators.required, Validators.email]],

View File

@@ -1,8 +0,0 @@
/* src\app\features\auth\components\register\register.component.css */
@import '../../_auth-common.css';
/* Stile NUR für die Registrierungs-Seite */
:host { display: block; width: 100%; }
/* Diese Komponente hat momentan keine spezifischen Extra-Stile,
aber die Datei ist da, falls welche benötigt werden. */

View File

@@ -4,34 +4,44 @@
</div> </div>
<form [formGroup]="registerForm" (ngSubmit)="onSubmit()" novalidate> <form [formGroup]="registerForm" (ngSubmit)="onSubmit()" novalidate>
<div class="form-field"> <app-form-field
<input type="text" class="form-input" id="fullName" placeholder=" " formControlName="fullName"> label="Vollständiger Name"
<label for="fullName" class="form-label">Vollständiger Name</label> type="text"
</div> formControlName="fullName"
></app-form-field>
<div class="form-field"> <app-form-field
<input type="email" class="form-input" id="email" placeholder=" " formControlName="email"> label="E-Mail-Adresse"
<label for="email" class="form-label">E-Mail-Adresse</label> type="email"
</div> formControlName="email"
></app-form-field>
<div class="form-field"> <app-form-field
<input type="password" class="form-input" id="password" placeholder=" " formControlName="password"> label="Passwort (min. 8 Zeichen)"
<label for="password" class="form-label">Passwort (min. 8 Zeichen)</label> type="password"
formControlName="password"
></app-form-field>
<app-form-field
label="Passwort bestätigen"
type="password"
formControlName="confirmPassword"
></app-form-field>
<div
*ngIf="registerForm.errors?.['passwordMismatch'] && registerForm.get('confirmPassword')?.touched"
class="error-text"
>
Die Passwörter stimmen nicht überein.
</div> </div>
<div class="form-field"> <app-button
<input type="password" class="form-input" id="confirmPassword" placeholder=" " formControlName="confirmPassword"> submitType="submit"
<label for="confirmPassword" class="form-label">Passwort bestätigen</label> buttonType="primary"
<div *ngIf="registerForm.errors?.['passwordMismatch'] && registerForm.get('confirmPassword')?.touched" class="error-text"> [fullWidth]="true"
Die Passwörter stimmen nicht überein. [disabled]="registerForm.invalid"
</div> >
</div>
<button type="submit" class="btn btn-primary btn-full-width" [disabled]="registerForm.invalid">
Registrieren Registrieren
</button> </app-button>
</form> </form>
<div class="footer-link"> <div class="footer-link">
Bereits ein Konto? <a routerLink="/auth/login" class="link">Jetzt anmelden</a> Bereits ein Konto? <a routerLink="/auth/login" class="link">Jetzt anmelden</a>
</div> </div>

View File

@@ -4,10 +4,12 @@ import { ReactiveFormsModule, FormBuilder, Validators, FormGroup } from '@angula
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
// Optional: Ein Custom Validator für den Passwort-Vergleich // Optional: Ein Custom Validator für den Passwort-Vergleich
import { passwordMatchValidator } from '../../../../shared/validators/password-match.validator'; import { passwordMatchValidator } from '../../../../shared/validators/password-match.validator';
import { ButtonComponent } from '../../../../shared/components/ui/button/button.component';
import { FormFieldComponent } from '../../../../shared/components/form/form-field/form-field.component';
@Component({ @Component({
selector: 'app-register', selector: 'app-register',
imports: [CommonModule, ReactiveFormsModule, RouterLink], imports: [CommonModule, ReactiveFormsModule, RouterLink,ButtonComponent,FormFieldComponent],
templateUrl: './register.component.html', templateUrl: './register.component.html',
styleUrl: './register.component.css' styleUrl: './register.component.css'
}) })

View File

@@ -1,5 +0,0 @@
/* src\app\features\auth\components\reset-password\reset-password.component.css */
@import '../../_auth-common.css';
/* Stile NUR für die Passwort-zurücksetzen-Seite */
:host { display: block; width: 100%; }

View File

@@ -4,18 +4,32 @@
</div> </div>
<form [formGroup]="resetPasswordForm" (ngSubmit)="onSubmit()" novalidate> <form [formGroup]="resetPasswordForm" (ngSubmit)="onSubmit()" novalidate>
<div class="form-field"> <app-form-field
<input type="password" class="form-input" id="password" placeholder=" " formControlName="password"> label="Neues Passwort"
<label for="password" class="form-label">Neues Passwort</label> type="password"
formControlName="password"
></app-form-field>
<app-form-field
label="Passwort bestätigen"
type="password"
formControlName="confirmPassword"
></app-form-field>
<div
*ngIf="resetPasswordForm.errors?.['passwordMismatch'] && resetPasswordForm.get('confirmPassword')?.touched"
class="error-text"
>
Die Passwörter stimmen nicht überein.
</div> </div>
<div class="form-field">
<input type="password" class="form-input" id="confirmPassword" placeholder=" " formControlName="confirmPassword"> <app-button
<label for="confirmPassword" class="form-label">Neues Passwort bestätigen</label> submitType="submit"
<div *ngIf="resetPasswordForm.errors?.['passwordMismatch'] && resetPasswordForm.get('confirmPassword')?.touched" class="error-text"> buttonType="primary"
Die Passwörter stimmen nicht überein. [fullWidth]="true"
</div> [disabled]="resetPasswordForm.invalid"
</div> >
<button type="submit" class="btn btn-primary btn-full-width" [disabled]="resetPasswordForm.invalid">
Passwort speichern Passwort speichern
</button> </app-button>
</form> </form>

View File

@@ -1,29 +1,43 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormBuilder, Validators, FormGroup } from '@angular/forms'; import {
ReactiveFormsModule,
FormBuilder,
Validators,
FormGroup,
} from '@angular/forms';
import { Router, ActivatedRoute } from '@angular/router'; import { Router, ActivatedRoute } from '@angular/router';
import { passwordMatchValidator } from '../../../../shared/validators/password-match.validator'; import { passwordMatchValidator } from '../../../../shared/validators/password-match.validator';
import { ButtonComponent } from '../../../../shared/components/ui/button/button.component';
import { FormFieldComponent } from '../../../../shared/components/form/form-field/form-field.component';
@Component({ @Component({
selector: 'app-reset-password', selector: 'app-reset-password',
imports: [CommonModule, ReactiveFormsModule], imports: [
CommonModule,
ReactiveFormsModule,
ButtonComponent,
FormFieldComponent,
],
templateUrl: './reset-password.component.html', templateUrl: './reset-password.component.html',
styleUrl: './reset-password.component.css' styleUrl: './reset-password.component.css',
}) })
export class ResetPasswordComponent implements OnInit { export class ResetPasswordComponent implements OnInit {
resetPasswordForm: FormGroup; resetPasswordForm: FormGroup;
token: string | null = null; token: string | null = null;
constructor( constructor(
private fb: FormBuilder, private fb: FormBuilder,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router private router: Router
) { ) {
this.resetPasswordForm = this.fb.group({ this.resetPasswordForm = this.fb.group(
password: ['', [Validators.required, Validators.minLength(8)]], {
confirmPassword: ['', [Validators.required]] password: ['', [Validators.required, Validators.minLength(8)]],
}, { validators: passwordMatchValidator }); confirmPassword: ['', [Validators.required]],
},
{ validators: passwordMatchValidator }
);
} }
ngOnInit(): void { ngOnInit(): void {
@@ -45,4 +59,4 @@ export class ResetPasswordComponent implements OnInit {
this.resetPasswordForm.markAllAsTouched(); this.resetPasswordForm.markAllAsTouched();
} }
} }
} }

View File

@@ -1,22 +0,0 @@
/* src\app\features\auth\components\register\register.component.css */
@import '../../_auth-common.css';
/* Stile NUR für die E-Mail-verifizieren-Seite */
:host { display: block; width: 100%; }
.verify-content {
text-align: center;
}
.icon {
color: var(--color-primary);
margin-bottom: 1.5rem;
}
.info-text {
margin-bottom: 1rem;
}
.info-text.small {
font-size: 0.9rem;
}
.footer-link {
margin-top: 2rem;
}

View File

@@ -1,12 +1,12 @@
<div class="component-header"> <div class="component-header">
<h2 class="auth-title">Bestätigen Sie Ihre E-Mail</h2> <h2 class="auth-title">Bestätigen Sie Ihre E-Mail</h2>
<p class="auth-subtitle">
Ein Bestätigungslink wurde an Ihre E-Mail gesendet. Bitte aktivieren Sie Ihr
Konto.
</p>
</div> </div>
<div class="verify-content"> <div class="form-content-wrapper verify-content">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>
<p class="info-text">
Ein Bestätigungslink wurde an Ihre E-Mail-Adresse gesendet. Bitte klicken Sie darauf, um Ihr Konto zu aktivieren.
</p>
<p class="info-text small"> <p class="info-text small">
Keine E-Mail erhalten? <a href="#" class="link">Erneut senden</a>. Keine E-Mail erhalten? <a href="#" class="link">Erneut senden</a>.
</p> </p>
@@ -14,4 +14,4 @@
<div class="footer-link"> <div class="footer-link">
<a routerLink="/auth/login" class="link">Zurück zum Login</a> <a routerLink="/auth/login" class="link">Zurück zum Login</a>
</div> </div>

View File

@@ -26,26 +26,43 @@
</div> </div>
<div class="grid-col-span-2"> <div class="grid-col-span-2">
<app-form-group <form [formGroup]="demoForm">
title="Form Components 1" <app-form-group
description="Diese Informationen sind öffentlich sichtbar." title="Form Components 1"
> description="Diese Informationen sind öffentlich sichtbar."
<app-form-select
label="Land"
[options]="countryOptions"
[(ngModel)]="selectedCountry"
> >
</app-form-select> <app-form-select
label="Land"
[options]="countryOptions"
[(ngModel)]="selectedCountry"
[ngModelOptions]="{standalone: true}"> <!-- Wichtig für die Mischung von ngModel und formGroup -->
</app-form-select>
<app-form-field label="Benutzername" type="text" [(value)]="benutzername"> <!-- Diese Felder verwenden jetzt formControlName -->
</app-form-field> <app-form-field
label="Benutzername"
type="text"
formControlName="username">
</app-form-field>
<app-form-field
label="E-Mail-Adresse"
type="email"
formControlName="email">
</app-form-field>
<app-form-field
label="Passwort"
type="password"
formControlName="password">
</app-form-field>
<app-form-field label="E-Mail-Adresse" type="email" [(value)]="email"> <app-form-textarea
</app-form-field> label="Biografie"
[rows]="5"
<app-form-textarea label="Biografie" [rows]="5" [(ngModel)]="biografie"> [(ngModel)]="biografie"
</app-form-textarea> [ngModelOptions]="{standalone: true}">
</app-form-textarea>
</app-form-group> </app-form-group>
</form>
</div> </div>
<div class="grid-col-span-2"> <div class="grid-col-span-2">

View File

@@ -24,7 +24,12 @@ import {
SelectOption, SelectOption,
} from '../../../../shared/components/form/form-select/form-select.component'; } from '../../../../shared/components/form/form-select/form-select.component';
import { FormsModule } from '@angular/forms'; import {
FormBuilder,
FormGroup,
FormsModule,
Validators,
} from '@angular/forms';
import { FormTextareaComponent } from '../../../../shared/components/form/form-textarea/form-textarea.component'; import { FormTextareaComponent } from '../../../../shared/components/form/form-textarea/form-textarea.component';
@@ -40,6 +45,8 @@ import { ButtonComponent } from '../../../../shared/components/ui/button/button.
import { ChipComponent } from '../../../../shared/components/ui/chip/chip.component'; import { ChipComponent } from '../../../../shared/components/ui/chip/chip.component';
import { ReactiveFormsModule } from '@angular/forms';
// Wir definieren ein Interface für unsere KPI-Daten für Typsicherheit // Wir definieren ein Interface für unsere KPI-Daten für Typsicherheit
interface Kpi { interface Kpi {
value: string; value: string;
@@ -65,11 +72,16 @@ interface Kpi {
ExpansionPanelComponent, ExpansionPanelComponent,
PageHeaderComponent, PageHeaderComponent,
ButtonComponent, ButtonComponent,
ChipComponent ChipComponent,
ReactiveFormsModule
], ],
templateUrl: './demo2.component.html', templateUrl: './demo2.component.html',
}) })
export class Demo2Component { export class Demo2Component {
demoForm: FormGroup;
kpiData: Kpi[] = [ kpiData: Kpi[] = [
{ {
value: '€ 14.750', value: '€ 14.750',
@@ -199,8 +211,6 @@ export class Demo2Component {
}, },
]; ];
benutzername: string = '';
email: string = '';
aktuellesPasswort: any = ''; aktuellesPasswort: any = '';
neuesPasswort: any = ''; neuesPasswort: any = '';
@@ -221,11 +231,20 @@ export class Demo2Component {
private readonly darkModeKey = 'app-dark-mode-setting'; private readonly darkModeKey = 'app-dark-mode-setting';
darkModeAktiv: boolean = false; darkModeAktiv: boolean = false;
constructor( constructor(
private renderer: Renderer2, private renderer: Renderer2,
@Inject(DOCUMENT) private document: Document, @Inject(DOCUMENT) private document: Document,
@Inject(PLATFORM_ID) private platformId: Object @Inject(PLATFORM_ID) private platformId: Object,
) {}
private fb: FormBuilder
) {
this.demoForm = this.fb.group({
username: [''], // Entspricht dem [(value)]="benutzername"
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required]]
});
}
ngOnInit(): void { ngOnInit(): void {
this.loadThemeSetting(); this.loadThemeSetting();

View File

@@ -15,13 +15,13 @@ export const DEMO_ROUTES: Routes = [
// Diese Route passt auf '/demo/1' und lädt die Komponente genau einmal. // Diese Route passt auf '/demo/1' und lädt die Komponente genau einmal.
path: '1', path: '1',
component: Demo1Component, component: Demo1Component,
title: 'Demo', title: 'Demo1',
}, },
{ {
// Diese Route passt auf '/demo/1' und lädt die Komponente genau einmal. // Diese Route passt auf '/demo/1' und lädt die Komponente genau einmal.
path: '2', path: '2',
component: Demo2Component, component: Demo2Component,
title: 'Demo', title: 'Demo2',
}, },
// Hier könntest du weitere Routen wie '2', '3' etc. hinzufügen, // Hier könntest du weitere Routen wie '2', '3' etc. hinzufügen,
// die andere Komponenten laden. // die andere Komponenten laden.

View File

@@ -1,11 +1,13 @@
<div class="form-field"> <div class="form-field">
<input <input
[type]="type" [type]="type"
class="form-input" class="form-input"
[id]="label" [id]="controlId"
placeholder=" " placeholder=" "
[value]="value" [disabled]="disabled"
(input)="onInput($event)" [(ngModel)]="value"
[disabled]="disabled"> (ngModelChange)="onChange($event)"
<label [for]="label" class="form-label">{{ label }}</label> (blur)="onTouched()">
<label [for]="controlId" class="form-label">{{ label }}</label>
</div> </div>

View File

@@ -1,32 +1,57 @@
import { Component, Input, Output, EventEmitter } from '@angular/core'; import { Component, Input, forwardRef } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import {
FormsModule,
ControlValueAccessor,
NG_VALUE_ACCESSOR,
} from '@angular/forms';
@Component({ @Component({
selector: 'app-form-field', selector: 'app-form-field',
standalone: true, standalone: true,
imports: [CommonModule], // Kein FormsModule mehr nötig imports: [
CommonModule,
FormsModule, // Wichtig für [(ngModel)] im Template
],
templateUrl: './form-field.component.html', templateUrl: './form-field.component.html',
styleUrl: './form-field.component.css', styleUrl: './form-field.component.css',
// Kein 'providers'-Block für ControlValueAccessor mehr providers: [
{
// Stellt diese Komponente als "Value Accessor" zur Verfügung
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => FormFieldComponent),
multi: true,
},
],
}) })
export class FormFieldComponent { // Die Komponente implementiert die ControlValueAccessor-Schnittstelle
// EINGÄNGE: Werte, die von außen gesetzt werden export class FormFieldComponent implements ControlValueAccessor {
@Input() label: string = ''; @Input() label: string = '';
@Input() type: 'text' | 'email' | 'password' = 'text'; @Input() type: 'text' | 'email' | 'password' = 'text';
@Input() value: string = ''; // Der aktuelle Wert des Feldes
@Input() disabled: boolean = false;
// AUSGANG: Ein Event, das ausgelöst wird, wenn sich der Wert ändert controlId = `form-field-${Math.random().toString(36)}`;
// WICHTIG: Der Name muss `[InputName]Change` sein, also `valueChange`
@Output() valueChange = new EventEmitter<string>();
/** // --- Eigenschaften für ControlValueAccessor ---
* Diese Methode wird bei jeder Tastatureingabe im Input-Feld aufgerufen. value: string = '';
*/ onChange: (value: any) => void = () => {};
onInput(event: Event): void { onTouched: () => void = () => {};
// 1. Hole den neuen Wert aus dem HTML-Input-Element disabled = false;
const newValue = (event.target as HTMLInputElement).value;
// 2. Sende den neuen Wert über den EventEmitter nach außen // --- Methoden, die von Angular Forms aufgerufen werden ---
this.valueChange.emit(newValue);
writeValue(value: any): void {
this.value = value;
} }
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState?(isDisabled: boolean): void {
this.disabled = isDisabled;
}
}