import { Component, inject, signal, computed, OnDestroy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormBuilder, FormGroup, Validators, ReactiveFormsModule, AbstractControl, ValidationErrors } from '@angular/forms'; import { Router, RouterModule } from '@angular/router'; import { MatCardModule } from '@angular/material/card'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressBarModule } from '@angular/material/progress-bar'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { AuthService } from '../../../core/services/auth.service'; import { StorageService } from '../../../core/services/storage.service'; import { Subject, takeUntil } from 'rxjs'; @Component({ selector: 'app-register', imports: [ CommonModule, ReactiveFormsModule, RouterModule, MatCardModule, MatFormFieldModule, MatInputModule, MatButtonModule, MatIconModule, MatProgressBarModule, MatProgressSpinnerModule ], templateUrl: './register.html', styleUrl: './register.scss' }) export class RegisterComponent implements OnDestroy { private fb = inject(FormBuilder); private authService = inject(AuthService); private storageService = inject(StorageService); private router = inject(Router); private destroy$ = new Subject(); // Signals isSubmitting = signal(false); hidePassword = signal(true); hideConfirmPassword = signal(true); // Form registerForm: FormGroup; // Password strength computed signal passwordStrength = computed(() => { const password = this.registerForm?.get('password')?.value || ''; return this.calculatePasswordStrength(password); }); constructor() { // Check if converting from guest const guestToken = this.storageService.getGuestToken(); // Initialize form this.registerForm = this.fb.group({ username: ['', [ Validators.required, Validators.minLength(3), Validators.maxLength(30), Validators.pattern(/^[a-zA-Z0-9_]+$/) ]], email: ['', [ Validators.required, Validators.email ]], password: ['', [ Validators.required, Validators.minLength(8), this.passwordStrengthValidator ]], confirmPassword: ['', [Validators.required]] }, { validators: this.passwordMatchValidator }); // Redirect if already authenticated if (this.authService.isAuthenticated()) { this.router.navigate(['/dashboard']); } } /** * Password strength validator */ private passwordStrengthValidator(control: AbstractControl): ValidationErrors | null { const password = control.value; if (!password) { return null; } const hasUpperCase = /[A-Z]/.test(password); const hasLowerCase = /[a-z]/.test(password); const hasNumber = /[0-9]/.test(password); const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password); const isValid = hasUpperCase && hasLowerCase && hasNumber && hasSpecialChar; return isValid ? null : { weakPassword: true }; } /** * Password match validator */ private passwordMatchValidator(group: AbstractControl): ValidationErrors | null { const password = group.get('password')?.value; const confirmPassword = group.get('confirmPassword')?.value; return password === confirmPassword ? null : { passwordMismatch: true }; } /** * Calculate password strength */ private calculatePasswordStrength(password: string): { score: number; label: string; color: string; } { if (!password) { return { score: 0, label: '', color: '' }; } let score = 0; // Length if (password.length >= 8) score += 25; if (password.length >= 12) score += 25; // Character types if (/[a-z]/.test(password)) score += 15; if (/[A-Z]/.test(password)) score += 15; if (/[0-9]/.test(password)) score += 10; if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) score += 10; let label = ''; let color = ''; if (score < 40) { label = 'Weak'; color = 'warn'; } else if (score < 70) { label = 'Fair'; color = 'accent'; } else if (score < 90) { label = 'Good'; color = 'primary'; } else { label = 'Strong'; color = 'primary'; } return { score, label, color }; } /** * Toggle password visibility */ togglePasswordVisibility(): void { this.hidePassword.update(val => !val); } /** * Toggle confirm password visibility */ toggleConfirmPasswordVisibility(): void { this.hideConfirmPassword.update(val => !val); } /** * Submit registration form */ onSubmit(): void { if (this.registerForm.invalid || this.isSubmitting()) { this.registerForm.markAllAsTouched(); return; } this.isSubmitting.set(true); const { username, email, password } = this.registerForm.value; const guestSessionId = this.storageService.getGuestToken() || undefined; this.authService.register(username, email, password, guestSessionId) .pipe(takeUntil(this.destroy$)) .subscribe({ next: () => { this.isSubmitting.set(false); // Navigation handled by service }, error: () => { this.isSubmitting.set(false); } }); } /** * Get form control error message */ getErrorMessage(controlName: string): string { const control = this.registerForm.get(controlName); if (!control || !control.touched) { return ''; } if (control.hasError('required')) { return `${this.getFieldLabel(controlName)} is required`; } if (control.hasError('email')) { return 'Please enter a valid email address'; } if (control.hasError('minlength')) { const minLength = control.getError('minlength').requiredLength; return `Must be at least ${minLength} characters`; } if (control.hasError('maxlength')) { const maxLength = control.getError('maxlength').requiredLength; return `Must not exceed ${maxLength} characters`; } if (control.hasError('pattern') && controlName === 'username') { return 'Username can only contain letters, numbers, and underscores'; } if (control.hasError('weakPassword')) { return 'Password must include uppercase, lowercase, number, and special character'; } return ''; } /** * Get field label */ private getFieldLabel(controlName: string): string { const labels: { [key: string]: string } = { username: 'Username', email: 'Email', password: 'Password', confirmPassword: 'Confirm Password' }; return labels[controlName] || controlName; } /** * Check if form has password mismatch error */ hasPasswordMismatch(): boolean { const confirmControl = this.registerForm.get('confirmPassword'); return !!confirmControl?.touched && this.registerForm.hasError('passwordMismatch'); } ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); } }