init commit

This commit is contained in:
David Melendez
2026-01-14 22:32:13 +01:00
parent 29b2e1438c
commit 00cb087b68
84 changed files with 29665 additions and 1 deletions

196
README.md
View File

@@ -1,2 +1,196 @@
# Angular /**
* Professional Angular Resume Builder
* Created by David Valera Melendez
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
# Professional Angular Resume Builder
**Created by David Valera Melendez** | david@valera-melendez.de | Made in Germany 🇩🇪
A modern, professional resume builder application built with Angular 17, TypeScript, and Angular Material UI. This project follows the same design patterns and architecture as the Next.js version, adapted for Angular ecosystem.
## 🚀 Features
- **Professional Design**: Clean, modern interface with Angular Material UI
- **Real-time Preview**: See your resume as you build it
- **Multiple Sections**: Personal info, experience, education, skills, languages, certifications, and projects
- **Responsive Design**: Works perfectly on desktop and mobile devices
- **Export Options**: Generate PDF versions of your resume
- **TypeScript**: Fully typed for better development experience
- **Angular Material**: Modern Material Design components and theming
## 🛠️ Tech Stack
- **Framework**: Angular 17+ with standalone components
- **Language**: TypeScript
- **UI Library**: Angular Material UI
- **Styling**: SCSS with Angular Material theming
- **Icons**: Material Icons
- **Forms**: Angular Reactive Forms
- **State Management**: Angular Services with RxJS
- **Export**: HTML to PDF conversion
## 📦 Installation
1. **Prerequisites**: Node.js 18+ and npm 9+
2. **Install dependencies**:
```bash
# Navigate to project directory
cd Angular
# Install dependencies
npm install
# Start development server
npm start
```
3. **Development server**:
```bash
# Start the development server
ng serve
# Navigate to http://localhost:4200/
```
## 🚀 Available Scripts
```bash
npm start # Start development server
npm run build # Build for production
npm run build:prod # Build for production with optimizations
npm test # Run unit tests
npm run test:coverage # Run tests with coverage
npm run lint # Run ESLint
npm run format # Format code with Prettier
```
## 📁 Project Structure
```
Angular/
├── src/
│ ├── app/
│ │ ├── components/ # Reusable UI components
│ │ │ ├── layout/ # Layout components (header, footer)
│ │ │ ├── resume/ # Resume-specific components
│ │ │ ├── ui/ # Generic UI components
│ │ │ └── shared/ # Shared components
│ │ ├── pages/ # Page components (route components)
│ │ ├── services/ # Angular services
│ │ ├── models/ # TypeScript interfaces and types
│ │ ├── utils/ # Utility functions
│ │ ├── shared/ # Shared modules and pipes
│ │ └── app.component.ts # Root component
│ ├── assets/ # Static assets
│ ├── styles/ # Global styles and themes
│ └── index.html # Main HTML file
├── angular.json # Angular CLI configuration
├── package.json # Dependencies and scripts
└── tsconfig.json # TypeScript configuration
```
## 🎨 Design System
### Angular Material Theme
- **Primary**: Professional blue theme (#1976d2) - Material Blue
- **Accent**: Complementary color for highlights
- **Warn**: Error and warning states
- **Typography**: Roboto font family (Material Design standard)
### Component Architecture
- **Material Components**: Using Angular Material's comprehensive component library
- **Responsive Layout**: CSS Grid and Flexbox with Angular Flex Layout
- **Custom Theming**: Professional color scheme with Material Design principles
- **Consistent Spacing**: Material Design spacing system
## 🎯 Usage
1. **Start the application**: `npm start`
2. **Fill in your information** using the step-by-step form
3. **Preview your resume** in real-time
4. **Export to PDF** when ready
### Resume Sections
- **Personal Information**: Contact details and professional summary
- **Work Experience**: Job history with achievements
- **Education**: Academic background
- **Skills**: Technical and soft skills with proficiency levels
- **Languages**: Language skills with proficiency
- **Certifications**: Professional certifications
- **Projects**: Notable projects and achievements
## 🔧 Development Guidelines
### Code Standards
- **Angular Style Guide**: Following official Angular style guide
- **TypeScript Strict**: Strict TypeScript configuration
- **Material Design**: Consistent with Material Design principles
- **Professional Quality**: Production-ready code with proper documentation
- **Author Attribution**: David Valera Melendez signature on all files
### Architecture Principles
- **Modular Design**: Feature-based modules
- **Reactive Programming**: RxJS for state management
- **Dependency Injection**: Angular's DI system
- **Component Communication**: Input/Output properties and services
- **Form Validation**: Angular Reactive Forms with Material validators
## 🔒 Professional Features
- **Type Safety**: Full TypeScript implementation
- **Error Handling**: Comprehensive error handling and user feedback
- **Accessibility**: WCAG compliant with Material CDK
- **Performance**: OnPush change detection and lazy loading
- **Testing**: Unit tests with Jasmine and Karma
- **Documentation**: Comprehensive inline documentation
## 📱 Browser Support
- Chrome (recommended)
- Firefox
- Safari
- Edge
## 🚀 Deployment
Build for production:
```bash
npm run build:prod
```
Deploy to:
- Firebase Hosting
- Netlify
- Vercel
- GitHub Pages
- Any static hosting service
## 👨‍💻 Author
**David Valera Melendez**
- Email: david@valera-melendez.de
- Location: Germany 🇩🇪
- Created: August 8, 2025
## 📄 License
MIT License - Feel free to use this project for your professional resume needs.
## 🤝 Contributing
This project is designed as a professional showcase. If you'd like to contribute or suggest improvements, please reach out via email.
---
**Professional Angular Resume Builder** - Helping professionals create outstanding resumes with Material Design | Made in Germany 🇩🇪

124
angular.json Normal file
View File

@@ -0,0 +1,124 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"david-valera-resume-builder": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
},
"@schematics/angular:application": {
"strict": true
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/david-valera-resume-builder",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"@angular/material/prebuilt-themes/indigo-pink.css",
"src/styles.scss"
],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "david-valera-resume-builder:build:production"
},
"development": {
"buildTarget": "david-valera-resume-builder:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"buildTarget": "david-valera-resume-builder:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"@angular/material/prebuilt-themes/indigo-pink.css",
"src/styles.scss"
],
"scripts": []
}
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": [
"src/**/*.ts",
"src/**/*.html"
]
}
}
}
}
},
"cli": {
"analytics": false,
"schematicCollections": [
"@angular-eslint/schematics"
]
}
}

15984
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

86
package.json Normal file
View File

@@ -0,0 +1,86 @@
{
"name": "david-valera-angular-resume-builder",
"version": "1.0.0",
"description": "Professional Angular Resume Builder with Material UI - Created by David Valera Melendez",
"author": {
"name": "David Valera Melendez",
"email": "david@valera-melendez.de",
"url": "https://valera-melendez.de"
},
"private": true,
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"build:prod": "ng build --configuration production",
"watch": "ng build --watch --configuration development",
"test": "ng test",
"test:coverage": "ng test --code-coverage",
"lint": "ng lint",
"e2e": "ng e2e",
"format": "prettier --write .",
"format:check": "prettier --check ."
},
"dependencies": {
"@angular/animations": "^17.0.0",
"@angular/cdk": "^17.3.10",
"@angular/common": "^17.0.0",
"@angular/compiler": "^17.0.0",
"@angular/core": "^17.0.0",
"@angular/forms": "^17.0.0",
"@angular/material": "^17.0.0",
"@angular/platform-browser": "^17.0.0",
"@angular/platform-browser-dynamic": "^17.0.0",
"@angular/router": "^17.0.0",
"@types/crypto-js": "^4.2.2",
"crypto-js": "^4.2.0",
"html2canvas": "^1.4.1",
"jspdf": "^2.5.1",
"ngx-loading-bar": "^0.0.9",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.2"
},
"devDependencies": {
"@angular-devkit/build-angular": "^17.0.0",
"@angular-eslint/builder": "20.1.1",
"@angular-eslint/eslint-plugin": "20.1.1",
"@angular-eslint/eslint-plugin-template": "20.1.1",
"@angular-eslint/schematics": "20.1.1",
"@angular-eslint/template-parser": "20.1.1",
"@angular/cli": "^17.0.0",
"@angular/compiler-cli": "^17.0.0",
"@types/jasmine": "~5.1.0",
"@types/node": "^18.18.0",
"@typescript-eslint/eslint-plugin": "8.39.0",
"@typescript-eslint/parser": "8.39.0",
"eslint": "8.57.1",
"jasmine-core": "~5.1.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"prettier": "^3.0.3",
"typescript": "~5.2.0"
},
"keywords": [
"angular",
"resume",
"cv",
"builder",
"material-ui",
"typescript",
"professional",
"david-valera-melendez"
],
"repository": {
"type": "git",
"url": "https://github.com/davidvalera/angular-resume-builder.git"
},
"license": "MIT",
"engines": {
"node": ">=18.17.0",
"npm": ">=9.6.0"
}
}

19
src/app/app.component.css Normal file
View File

@@ -0,0 +1,19 @@
/**
* Root Application Component Styles
* Professional Angular Resume Builder - Application Root
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
* @description Root application component styles and global host element configuration
*/
/* ==========================================================================
Root Host Element
========================================================================== */
:host {
display: block;
width: 100%;
height: 100vh;
}

View File

@@ -0,0 +1,14 @@
<!--
Root Application Template
Professional Angular Resume Builder
@author David Valera Melendez <david@valera-melendez.de>
@created 2025-08-08
@location Made in Germany 🇩🇪
-->
<!-- Global Progress Bar -->
<app-global-progress-bar></app-global-progress-bar>
<!-- Main Application Content -->
<router-outlet></router-outlet>

84
src/app/app.component.ts Normal file
View File

@@ -0,0 +1,84 @@
/**
* Root Application Component
* Professional Angular Resume Builder
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { AuthService } from './services/auth.service';
import { NavigationService } from './core/services/navigation.service';
import { GlobalProgressBarComponent } from './components/ui/global-progress-bar/global-progress-bar.component';
/**
* Root application component - simplified to handle routing only
* Layout components handle the UI structure
*/
@Component({
selector: 'app-root',
standalone: true,
imports: [
CommonModule,
RouterOutlet,
GlobalProgressBarComponent
],
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit, OnDestroy {
/**
* Application title - Professional Resume Builder by David Valera Melendez
*/
title = 'Professional Resume Builder - David Valera Melendez';
private readonly destroy$ = new Subject<void>();
constructor(
private authService: AuthService,
private navigationService: NavigationService
) {}
ngOnInit(): void {
// Initialize authentication state watcher for automatic redirects
this.initializeAuthenticationWatcher();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
/**
* Initialize authentication state watcher for automatic redirects
*/
private initializeAuthenticationWatcher(): void {
this.authService.isAuthenticated$
.pipe(takeUntil(this.destroy$))
.subscribe(isAuthenticated => {
if (!isAuthenticated && !this.isOnPublicRoute()) {
this.handleUnauthenticatedRedirect();
}
});
}
/**
* Check if current route is public (doesn't require authentication)
*/
private isOnPublicRoute(): boolean {
return this.navigationService.isPublicRoute();
}
/**
* Handle redirect for unauthenticated users
*/
private async handleUnauthenticatedRedirect(): Promise<void> {
await this.navigationService.navigateToLogin();
}
}

196
src/app/app.routes.ts Normal file
View File

@@ -0,0 +1,196 @@
/**
* Application Routes Configuration
* Professional Angular Resume Builder - Enterprise Grade Routing
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
import { Routes } from '@angular/router';
import { AuthGuard, GuestGuard } from './guards/auth.guard';
/**
* Enterprise-grade application routing configuration
* Defines all application routes with proper lazy loading, guards, and SEO-friendly titles
*/
export const routes: Routes = [
// Default route redirect
{
path: '',
redirectTo: '/login',
pathMatch: 'full'
},
// Main layout routes (with navigation header)
{
path: '',
loadComponent: () => import('./layouts/main-layout/main-layout.component').then(m => m.MainLayoutComponent),
children: [
// Protected home/dashboard route (requires authentication)
{
path: 'home',
loadComponent: () => import('./pages/home/home.component').then(m => m.HomeComponent),
canActivate: [AuthGuard],
title: 'Dashboard - Professional Resume Builder',
data: {
description: 'Your personal resume builder dashboard',
keywords: 'resume builder, dashboard, CV creator, professional resume',
breadcrumb: 'Dashboard',
requiresAuth: true
}
},
// Protected routes (require authentication)
{
path: 'builder',
loadComponent: () => import('./pages/resume-builder/resume-builder.component').then(m => m.ResumeBuilderComponent),
canActivate: [AuthGuard],
title: 'Resume Builder - Create Your Professional CV',
data: {
description: 'Build your professional resume with our advanced tools',
breadcrumb: 'Builder',
requiresAuth: true
}
},
{
path: 'dashboard',
loadComponent: () => import('./pages/home/home.component').then(m => m.HomeComponent),
canActivate: [AuthGuard],
title: 'Dashboard - Resume Builder',
data: {
description: 'Your personal resume builder dashboard',
breadcrumb: 'Dashboard',
requiresAuth: true
}
},
{
path: 'preview',
loadComponent: () => import('./pages/preview/preview.component').then(m => m.PreviewComponent),
canActivate: [AuthGuard],
title: 'Resume Preview - Professional CV Display',
data: {
description: 'Preview your professional resume before download',
breadcrumb: 'Preview',
requiresAuth: true
}
},
{
path: 'settings',
loadComponent: () => import('./pages/settings/settings.component').then(m => m.SettingsComponent),
canActivate: [AuthGuard],
title: 'Account Settings - Resume Builder',
data: {
description: 'Manage your account settings and preferences',
breadcrumb: 'Settings',
requiresAuth: true,
robots: 'noindex, nofollow'
}
},
// Profile routes
{
path: 'profile',
redirectTo: '/settings',
pathMatch: 'full'
},
// Templates and showcase routes
{
path: 'templates',
loadComponent: () => import('./pages/templates/templates.component').then(m => m.TemplatesComponent),
title: 'Resume Templates - Professional Designs',
data: {
description: 'Choose from our collection of professional resume templates',
breadcrumb: 'Templates'
}
}
]
},
// Auth layout routes (without navigation header)
{
path: '',
loadComponent: () => import('./layouts/auth-layout/auth-layout.component').then(m => m.AuthLayoutComponent),
children: [
{
path: 'login',
loadComponent: () => import('./pages/login/login.component').then(m => m.LoginComponent),
canActivate: [GuestGuard],
title: 'Sign In - Resume Builder',
data: {
description: 'Sign in to access your resume builder dashboard',
robots: 'noindex, nofollow'
}
},
{
path: 'two-factor-auth',
loadComponent: () => import('./components/auth/two-factor-auth/two-factor-auth.component').then(m => m.TwoFactorAuthComponent),
title: '2FA Verification - Resume Builder',
data: {
description: 'Complete two-factor authentication for device verification',
robots: 'noindex, nofollow'
}
},
{
path: 'register',
loadComponent: () => import('./auth/register/register.component').then(m => m.RegisterComponent),
canActivate: [GuestGuard],
title: 'Create Account - Resume Builder',
data: {
description: 'Create a new account to start building your professional resume',
robots: 'noindex, nofollow'
}
},
{
path: 'forgot-password',
loadComponent: () => import('./auth/forgot-password/forgot-password.component').then(m => m.ForgotPasswordComponent),
canActivate: [GuestGuard],
title: 'Reset Password - Resume Builder',
data: {
description: 'Reset your password to regain access to your account',
robots: 'noindex, nofollow'
}
}
]
},
// Legacy auth redirects
{
path: 'auth/login',
redirectTo: '/login',
pathMatch: 'full'
},
// Error handling routes
{
path: '404',
loadComponent: () => import('./pages/error/not-found/not-found.component').then(m => m.NotFoundComponent),
title: 'Page Not Found - Resume Builder',
data: {
robots: 'noindex, nofollow'
}
},
{
path: '403',
loadComponent: () => import('./pages/error/forbidden/forbidden.component').then(m => m.ForbiddenComponent),
title: 'Access Denied - Resume Builder',
data: {
robots: 'noindex, nofollow'
}
},
{
path: '500',
loadComponent: () => import('./pages/error/server-error/server-error.component').then(m => m.ServerErrorComponent),
title: 'Server Error - Resume Builder',
data: {
robots: 'noindex, nofollow'
}
},
// Wildcard route - must be last
{
path: '**',
redirectTo: '/404'
}
];

View File

@@ -0,0 +1,247 @@
/**
* Forgot Password Component Styles
* Professional Angular Resume Builder - Password Recovery System
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
* @description Password recovery form with email validation and reset functionality
*/
/* ==========================================================================
Forgot Password Container & Layout
========================================================================== */
.forgot-password-container {
min-height: 100vh;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
background: var(--gradient-primary);
padding: 2rem;
box-sizing: border-box;
position: relative;
}
.forgot-password-card-wrapper {
width: 100%;
max-width: 450px;
}
.forgot-password-card {
width: 100%;
padding: 0;
box-shadow: 0 8px 25px var(--color-shadow-medium);
border-radius: 12px;
}
.forgot-password-header {
text-align: center;
padding: 2rem 2rem 1rem 2rem;
}
.forgot-password-header h1 {
margin: 0 0 0.5rem 0;
color: var(--color-text-primary);
font-weight: 500;
font-size: 1.6rem;
}
.forgot-password-header p {
margin: 0;
color: var(--color-text-secondary);
font-size: 0.95rem;
line-height: 1.4;
}
.forgot-password-form {
padding: 0 2rem 1rem 2rem;
}
.full-width {
width: 100%;
margin-bottom: 1.5rem;
}
.reset-button {
width: 100%;
height: 48px;
font-size: 1rem;
font-weight: 500;
border-radius: 6px;
}
.reset-button:disabled {
opacity: 0.6;
}
.email-sent-content {
padding: 0 2rem 1rem 2rem;
text-align: center;
}
.success-icon {
margin-bottom: 1rem;
}
.success-icon mat-icon {
font-size: 3rem;
height: 3rem;
width: 3rem;
color: var(--color-success);
}
.email-sent-content h3 {
margin: 0 0 1rem 0;
color: var(--color-text-primary);
font-weight: 500;
}
.email-sent-content p {
margin: 0 0 2rem 0;
color: var(--color-text-secondary);
line-height: 1.5;
font-size: 0.95rem;
}
.email-sent-actions {
margin-top: 1.5rem;
}
.resend-button {
border-radius: 6px;
font-weight: 500;
}
.back-to-login {
padding: 1rem 2rem 2rem 2rem;
text-align: center;
}
.back-button {
font-weight: 500;
text-transform: none;
border-radius: 6px;
}
/* Material Form Field Styling */
::ng-deep .mat-mdc-form-field {
font-size: 14px;
}
::ng-deep .mat-mdc-form-field-outline {
border-radius: 6px;
}
::ng-deep .mat-mdc-text-field-wrapper {
border-radius: 6px;
}
/* Error Styling */
::ng-deep .mat-mdc-form-field.mat-form-field-invalid .mat-mdc-form-field-outline-thick {
border-color: var(--color-error);
}
/* Success Snackbar */
::ng-deep .success-snackbar {
background-color: var(--color-success);
color: white;
}
/* Info Snackbar */
::ng-deep .info-snackbar {
background-color: var(--color-primary);
color: white;
}
/* Error Snackbar */
::ng-deep .error-snackbar {
background-color: var(--color-error);
color: white;
}
/* Responsive Design */
@media (max-width: 768px) {
.forgot-password-container {
padding: 1rem;
}
.forgot-password-card {
margin: 0;
border-radius: 8px;
}
.forgot-password-header {
padding: 1.5rem 1.5rem 1rem 1.5rem;
}
.forgot-password-header h1 {
font-size: 1.4rem;
}
.forgot-password-form,
.email-sent-content {
padding: 0 1.5rem 1rem 1.5rem;
}
.back-to-login {
padding: 1rem 1.5rem 1.5rem 1.5rem;
}
}
@media (max-width: 480px) {
.forgot-password-container {
padding: 0.5rem;
}
.forgot-password-header h1 {
font-size: 1.3rem;
}
.forgot-password-header p {
font-size: 0.9rem;
}
}
/* Loading State */
.reset-button mat-spinner {
margin-right: 8px;
}
/* Focus States */
::ng-deep .mat-mdc-form-field.mat-focused .mat-mdc-form-field-outline-thick {
border-color: var(--color-primary);
}
/* Animation */
.forgot-password-card {
animation: slideInUp 0.3s ease-out;
}
@keyframes slideInUp {
from {
transform: translateY(30px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
/* Email Sent Animation */
.email-sent-content {
animation: fadeIn 0.4s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

View File

@@ -0,0 +1,77 @@
<div class="forgot-password-container">
<div class="forgot-password-card-wrapper">
<mat-card class="forgot-password-card">
<mat-card-header>
<div class="forgot-password-header">
<h1>Reset Your Password</h1>
<p *ngIf="!emailSent">Enter your email address and we'll send you a link to reset your password</p>
<p *ngIf="emailSent">Check your email for reset instructions</p>
</div>
</mat-card-header>
<mat-card-content>
<!-- Email Form -->
<form *ngIf="!emailSent"
[formGroup]="forgotPasswordForm"
(ngSubmit)="onForgotPassword()"
class="forgot-password-form">
<mat-form-field appearance="outline" class="full-width">
<mat-label>Email Address</mat-label>
<input matInput
type="email"
formControlName="email"
placeholder="Enter your email address"
autocomplete="email">
<mat-icon matSuffix>email</mat-icon>
<mat-error *ngIf="hasFieldError('email')">
{{ getFieldError('email') }}
</mat-error>
</mat-form-field>
<button mat-raised-button
color="primary"
type="submit"
class="reset-button"
[disabled]="isLoading || forgotPasswordForm.invalid">
<mat-spinner *ngIf="isLoading" diameter="20"></mat-spinner>
<span *ngIf="!isLoading">Send Reset Link</span>
<span *ngIf="isLoading">Sending...</span>
</button>
</form>
<!-- Success State -->
<div *ngIf="emailSent" class="email-sent-content">
<div class="success-icon">
<mat-icon>mark_email_read</mat-icon>
</div>
<h3>Check Your Email</h3>
<p>We've sent password reset instructions to your email address.
If you don't receive an email within a few minutes, please check your spam folder.</p>
<div class="email-sent-actions">
<button mat-stroked-button
color="primary"
(click)="resendEmail()"
class="resend-button">
<mat-icon>refresh</mat-icon>
Resend Email
</button>
</div>
</div>
</mat-card-content>
<mat-card-actions>
<div class="back-to-login">
<button mat-button
color="primary"
(click)="navigateToLogin()"
class="back-button">
<mat-icon>arrow_back</mat-icon>
Back to Sign In
</button>
</div>
</mat-card-actions>
</mat-card>
</div>
</div>

View File

@@ -0,0 +1,212 @@
/**
* Forgot Password Component
* Professional password reset form with email validation
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule, Router } from '@angular/router';
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { MatCardModule } from '@angular/material/card';
import { MatInputModule } from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSnackBarModule, MatSnackBar } from '@angular/material/snack-bar';
import { Subject } from 'rxjs';
import { takeUntil, finalize } from 'rxjs/operators';
import { AuthService } from '../../services/auth.service';
@Component({
selector: 'app-forgot-password',
standalone: true,
imports: [
CommonModule,
RouterModule,
ReactiveFormsModule,
MatCardModule,
MatInputModule,
MatButtonModule,
MatIconModule,
MatProgressSpinnerModule,
MatSnackBarModule
],
templateUrl: './forgot-password.component.html',
styleUrls: ['./forgot-password.component.css']
})
export class ForgotPasswordComponent implements OnInit, OnDestroy {
forgotPasswordForm!: FormGroup;
isLoading = false;
emailSent = false;
private destroy$ = new Subject<void>();
constructor(
private fb: FormBuilder,
private authService: AuthService,
private router: Router,
private snackBar: MatSnackBar
) {}
ngOnInit(): void {
this.initializeForm();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
/**
* Initialize the forgot password form with validation
*/
private initializeForm(): void {
this.forgotPasswordForm = this.fb.group({
email: ['', [
Validators.required,
Validators.email,
Validators.maxLength(255)
]]
});
}
/**
* Get form field error message
*/
getFieldError(fieldName: string): string {
const field = this.forgotPasswordForm.get(fieldName);
if (field?.hasError('required')) {
return 'Email address is required';
}
if (field?.hasError('email')) {
return 'Please enter a valid email address';
}
if (field?.hasError('maxlength')) {
return 'Email address cannot exceed 255 characters';
}
return '';
}
/**
* Check if form field has error
*/
hasFieldError(fieldName: string): boolean {
const field = this.forgotPasswordForm.get(fieldName);
return !!(field?.invalid && (field?.dirty || field?.touched));
}
/**
* Handle forgot password request
*/
onForgotPassword(): void {
if (this.forgotPasswordForm.invalid) {
this.markFormGroupTouched();
return;
}
this.isLoading = true;
const email = this.forgotPasswordForm.value.email;
// For now, we'll simulate the API call since we don't have a backend
// In a real implementation, this would call this.authService.forgotPassword(email)
setTimeout(() => {
this.isLoading = false;
this.emailSent = true;
this.showSuccessMessage(
'If an account with this email exists, you will receive password reset instructions shortly.'
);
}, 2000);
// Real implementation would be:
/*
this.authService.forgotPassword(email)
.pipe(
takeUntil(this.destroy$),
finalize(() => {
this.isLoading = false;
})
)
.subscribe({
next: (response) => {
this.emailSent = true;
this.showSuccessMessage(
'If an account with this email exists, you will receive password reset instructions shortly.'
);
},
error: (error) => {
console.error('Forgot password error:', error);
// Show generic message for security
this.showInfoMessage(
'If an account with this email exists, you will receive password reset instructions shortly.'
);
}
});
*/
}
/**
* Mark all form fields as touched to show validation errors
*/
private markFormGroupTouched(): void {
Object.keys(this.forgotPasswordForm.controls).forEach(key => {
const control = this.forgotPasswordForm.get(key);
control?.markAsTouched();
});
}
/**
* Navigate to login page
*/
navigateToLogin(): void {
this.router.navigate(['/login']);
}
/**
* Resend reset email
*/
resendEmail(): void {
this.emailSent = false;
this.onForgotPassword();
}
/**
* Show success message to user
*/
private showSuccessMessage(message: string): void {
this.snackBar.open(message, 'Close', {
duration: 8000,
panelClass: ['success-snackbar'],
horizontalPosition: 'end',
verticalPosition: 'top'
});
}
/**
* Show info message to user
*/
private showInfoMessage(message: string): void {
this.snackBar.open(message, 'Close', {
duration: 8000,
panelClass: ['info-snackbar'],
horizontalPosition: 'end',
verticalPosition: 'top'
});
}
/**
* Show error message to user
*/
private showErrorMessage(message: string): void {
this.snackBar.open(message, 'Close', {
duration: 8000,
panelClass: ['error-snackbar'],
horizontalPosition: 'end',
verticalPosition: 'top'
});
}
}

View File

@@ -0,0 +1,242 @@
/**
* User Registration Component Styles
* Professional Angular Resume Builder - User Account Registration
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
* @description User registration form with validation, social auth, and professional styling
*/
/* ==========================================================================
Registration Container & Layout
========================================================================== */
.register-container {
min-height: 100vh;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
background: var(--gradient-primary);
padding: 2rem;
box-sizing: border-box;
position: relative;
}
.register-card-wrapper {
width: 100%;
max-width: 500px;
}
.register-card {
width: 100%;
padding: 0;
box-shadow: 0 8px 25px var(--color-shadow-medium);
border-radius: 12px;
}
.register-header {
text-align: center;
padding: 2rem 2rem 1rem 2rem;
}
.register-header h1 {
margin: 0 0 0.5rem 0;
color: var(--color-text-primary);
font-weight: 500;
font-size: 1.8rem;
}
.register-header p {
margin: 0;
color: var(--color-text-secondary);
font-size: 0.95rem;
}
.register-form {
padding: 0 2rem 1rem 2rem;
}
.name-row {
display: flex;
gap: 1rem;
margin-bottom: 0.5rem;
}
.name-field {
flex: 1;
}
.full-width {
width: 100%;
margin-bottom: 0.5rem;
}
.checkbox-section {
margin: 1.5rem 0;
}
.terms-checkbox,
.newsletter-checkbox {
width: 100%;
}
.terms-link {
color: var(--color-primary);
text-decoration: none;
}
.terms-link:hover {
text-decoration: underline;
}
.register-button {
width: 100%;
height: 48px;
margin-top: 1rem;
font-size: 1rem;
font-weight: 500;
border-radius: 6px;
}
.register-button:disabled {
opacity: 0.6;
}
.login-link {
padding: 1rem 2rem 2rem 2rem;
text-align: center;
}
.login-link p {
margin: 0;
color: var(--color-text-secondary);
font-size: 0.9rem;
}
.login-button {
font-weight: 500;
text-transform: none;
padding: 0;
min-width: auto;
}
/* Material Form Field Styling */
::ng-deep .mat-mdc-form-field {
font-size: 14px;
}
::ng-deep .mat-mdc-form-field-outline {
border-radius: 6px;
}
::ng-deep .mat-mdc-text-field-wrapper {
border-radius: 6px;
}
/* Error Styling */
::ng-deep .mat-mdc-form-field.mat-form-field-invalid .mat-mdc-form-field-outline-thick {
border-color: var(--color-error);
}
/* Success Snackbar */
::ng-deep .success-snackbar {
background-color: var(--color-success);
color: white;
}
/* Error Snackbar */
::ng-deep .error-snackbar {
background-color: var(--color-error);
color: white;
}
/* Responsive Design */
@media (max-width: 768px) {
.register-container {
padding: 1rem;
min-height: 100vh;
}
.register-card {
margin: 0;
border-radius: 8px;
}
.register-header {
padding: 1.5rem 1.5rem 1rem 1.5rem;
}
.register-header h1 {
font-size: 1.5rem;
}
.register-form {
padding: 0 1.5rem 1rem 1.5rem;
}
.name-row {
flex-direction: column;
gap: 0;
}
.name-field {
margin-bottom: 0.5rem;
}
.login-link {
padding: 1rem 1.5rem 1.5rem 1.5rem;
}
}
@media (max-width: 480px) {
.register-container {
padding: 0.5rem;
}
.register-header h1 {
font-size: 1.3rem;
}
.register-header p {
font-size: 0.9rem;
}
}
/* Loading State */
.register-button mat-spinner {
margin-right: 8px;
}
/* Focus States */
::ng-deep .mat-mdc-form-field.mat-focused .mat-mdc-form-field-outline-thick {
border-color: var(--color-primary);
}
/* Checkbox Styling */
::ng-deep .mat-mdc-checkbox {
margin-bottom: 0.5rem;
}
::ng-deep .mat-mdc-checkbox .mdc-form-field {
color: var(--color-text-primary);
font-size: 0.9rem;
line-height: 1.4;
}
/* Animation */
.register-card {
animation: slideInUp 0.3s ease-out;
}
@keyframes slideInUp {
from {
transform: translateY(30px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}

View File

@@ -0,0 +1,146 @@
<div class="register-container">
<div class="register-card-wrapper">
<mat-card class="register-card">
<mat-card-header>
<div class="register-header">
<h1>Ihr Konto erstellen</h1>
<p>Werden Sie Teil von Tausenden von Fachkräften, die beeindruckende Lebensläufe erstellen</p>
</div>
</mat-card-header>
<mat-card-content>
<form [formGroup]="registerForm" (ngSubmit)="onRegister()" class="register-form">
<!-- Name Fields -->
<div class="name-row">
<mat-form-field appearance="outline" class="name-field">
<mat-label>Vorname</mat-label>
<input matInput
formControlName="firstName"
placeholder="Geben Sie Ihren Vornamen ein"
autocomplete="given-name">
<mat-icon matSuffix>person</mat-icon>
<mat-error *ngIf="hasFieldError('firstName')">
{{ getFieldError('firstName') }}
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="name-field">
<mat-label>Nachname</mat-label>
<input matInput
formControlName="lastName"
placeholder="Geben Sie Ihren Nachnamen ein"
autocomplete="family-name">
<mat-icon matSuffix>person</mat-icon>
<mat-error *ngIf="hasFieldError('lastName')">
{{ getFieldError('lastName') }}
</mat-error>
</mat-form-field>
</div>
<!-- Email Field -->
<mat-form-field appearance="outline" class="full-width">
<mat-label>E-Mail-Adresse</mat-label>
<input matInput
type="email"
formControlName="email"
placeholder="Geben Sie Ihre E-Mail-Adresse ein"
autocomplete="email">
<mat-icon matSuffix>email</mat-icon>
<mat-error *ngIf="hasFieldError('email')">
{{ getFieldError('email') }}
</mat-error>
</mat-form-field>
<!-- Password Field -->
<mat-form-field appearance="outline" class="full-width">
<mat-label>Password</mat-label>
<input matInput
[type]="hidePassword ? 'password' : 'text'"
formControlName="password"
placeholder="Create a secure password"
autocomplete="new-password">
<button mat-icon-button
matSuffix
type="button"
(click)="hidePassword = !hidePassword"
[attr.aria-label]="'Hide password'"
[attr.aria-pressed]="hidePassword">
<mat-icon>{{ hidePassword ? 'visibility_off' : 'visibility' }}</mat-icon>
</button>
<mat-error *ngIf="hasFieldError('password')">
{{ getFieldError('password') }}
</mat-error>
<mat-hint>Must contain uppercase, lowercase, number, and special character</mat-hint>
</mat-form-field>
<!-- Confirm Password Field -->
<mat-form-field appearance="outline" class="full-width">
<mat-label>Confirm Password</mat-label>
<input matInput
[type]="hideConfirmPassword ? 'password' : 'text'"
formControlName="confirmPassword"
placeholder="Confirm your password"
autocomplete="new-password">
<button mat-icon-button
matSuffix
type="button"
(click)="hideConfirmPassword = !hideConfirmPassword"
[attr.aria-label]="'Hide confirm password'"
[attr.aria-pressed]="hideConfirmPassword">
<mat-icon>{{ hideConfirmPassword ? 'visibility_off' : 'visibility' }}</mat-icon>
</button>
<mat-error *ngIf="hasFieldError('confirmPassword')">
{{ getFieldError('confirmPassword') }}
</mat-error>
</mat-form-field>
<!-- Terms and Conditions -->
<div class="checkbox-section">
<mat-checkbox formControlName="acceptTerms" class="terms-checkbox">
I agree to the
<a href="/terms" target="_blank" class="terms-link">Terms of Service</a>
and
<a href="/privacy" target="_blank" class="terms-link">Privacy Policy</a>
</mat-checkbox>
<mat-error *ngIf="registerForm.get('acceptTerms')?.hasError('required') && registerForm.get('acceptTerms')?.touched">
You must accept the terms and conditions
</mat-error>
</div>
<!-- Newsletter Subscription -->
<div class="checkbox-section">
<mat-checkbox formControlName="newsletter" class="newsletter-checkbox">
Send me tips and updates about resume building
</mat-checkbox>
</div>
<!-- Submit Button -->
<app-loading-button
text="Create Account"
loadingText="Creating Account..."
formId="register-form"
icon="person_add"
color="primary"
size="large"
[disabled]="registerForm.invalid"
(clicked)="onRegister()"
class="register-button">
</app-loading-button>
</form>
</mat-card-content>
<mat-card-actions>
<div class="login-link">
<p>Already have an account?
<button mat-button
color="primary"
(click)="navigateToLogin()"
class="login-button">
Sign In
</button>
</p>
</div>
</mat-card-actions>
</mat-card>
</div>
</div>

View File

@@ -0,0 +1,253 @@
/**
* Register Component
* Professional user registration form with validation
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule, Router } from '@angular/router';
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { MatCardModule } from '@angular/material/card';
import { MatInputModule } from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSnackBarModule, MatSnackBar } from '@angular/material/snack-bar';
import { Subject } from 'rxjs';
import { takeUntil, finalize } from 'rxjs/operators';
import { AuthService } from '../../services/auth.service';
import { RegisterData } from '../../models/auth.model';
import { LoadingButtonComponent } from '../../components/ui/loading-button/loading-button.component';
@Component({
selector: 'app-register',
standalone: true,
imports: [
CommonModule,
RouterModule,
ReactiveFormsModule,
MatCardModule,
MatInputModule,
MatButtonModule,
MatIconModule,
MatCheckboxModule,
MatProgressSpinnerModule,
MatSnackBarModule,
LoadingButtonComponent
],
templateUrl: './register.component.html',
styleUrls: ['./register.component.css']
})
export class RegisterComponent implements OnInit, OnDestroy {
registerForm!: FormGroup;
hidePassword = true;
hideConfirmPassword = true;
private destroy$ = new Subject<void>();
constructor(
private fb: FormBuilder,
private authService: AuthService,
private router: Router,
private snackBar: MatSnackBar
) {}
ngOnInit(): void {
this.initializeForm();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
/**
* Initialize the registration form with validation
*/
private initializeForm(): void {
this.registerForm = this.fb.group({
firstName: ['', [
Validators.required,
Validators.minLength(2),
Validators.maxLength(50)
]],
lastName: ['', [
Validators.required,
Validators.minLength(2),
Validators.maxLength(50)
]],
email: ['', [
Validators.required,
Validators.email,
Validators.maxLength(255)
]],
password: ['', [
Validators.required,
Validators.minLength(8),
Validators.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/)
]],
confirmPassword: ['', [
Validators.required
]],
acceptTerms: [false, [
Validators.requiredTrue
]],
newsletter: [false]
}, {
validators: this.passwordMatchValidator
});
}
/**
* Custom validator to check if passwords match
*/
private passwordMatchValidator(form: FormGroup) {
const password = form.get('password');
const confirmPassword = form.get('confirmPassword');
if (password && confirmPassword && password.value !== confirmPassword.value) {
confirmPassword.setErrors({ passwordMismatch: true });
return { passwordMismatch: true };
}
if (confirmPassword?.hasError('passwordMismatch')) {
delete confirmPassword.errors?.['passwordMismatch'];
confirmPassword.updateValueAndValidity();
}
return null;
}
/**
* Get form field error message
*/
getFieldError(fieldName: string): string {
const field = this.registerForm.get(fieldName);
if (field?.hasError('required')) {
return `${this.getFieldLabel(fieldName)} is required`;
}
if (field?.hasError('email')) {
return 'Please enter a valid email address';
}
if (field?.hasError('minlength')) {
const minLength = field.errors?.['minlength']?.requiredLength;
return `${this.getFieldLabel(fieldName)} must be at least ${minLength} characters`;
}
if (field?.hasError('maxlength')) {
const maxLength = field.errors?.['maxlength']?.requiredLength;
return `${this.getFieldLabel(fieldName)} cannot exceed ${maxLength} characters`;
}
if (field?.hasError('pattern') && fieldName === 'password') {
return 'Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character';
}
if (field?.hasError('passwordMismatch')) {
return 'Passwords do not match';
}
return '';
}
/**
* Get user-friendly field label
*/
private getFieldLabel(fieldName: string): string {
const labels: { [key: string]: string } = {
firstName: 'First name',
lastName: 'Last name',
email: 'Email',
password: 'Password',
confirmPassword: 'Confirm password'
};
return labels[fieldName] || fieldName;
}
/**
* Check if form field has error
*/
hasFieldError(fieldName: string): boolean {
const field = this.registerForm.get(fieldName);
return !!(field?.invalid && (field?.dirty || field?.touched));
}
/**
* Handle user registration
*/
onRegister(): void {
if (this.registerForm.invalid) {
this.markFormGroupTouched();
return;
}
const formValue = this.registerForm.value;
const registerData: RegisterData = {
firstName: formValue.firstName,
lastName: formValue.lastName,
email: formValue.email,
password: formValue.password,
confirmPassword: formValue.confirmPassword,
acceptTerms: formValue.acceptTerms
};
this.authService.register(registerData)
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (response) => {
this.showSuccessMessage('Registration successful! Welcome to Resume Builder.');
this.router.navigate(['/builder']);
},
error: (error) => {
console.error('Registration error:', error);
this.showErrorMessage(
error?.error?.message || 'Registration failed. Please try again.'
);
}
});
}
/**
* Mark all form fields as touched to show validation errors
*/
private markFormGroupTouched(): void {
Object.keys(this.registerForm.controls).forEach(key => {
const control = this.registerForm.get(key);
control?.markAsTouched();
});
}
/**
* Navigate to login page
*/
navigateToLogin(): void {
this.router.navigate(['/login']);
}
/**
* Show success message to user
*/
private showSuccessMessage(message: string): void {
this.snackBar.open(message, 'Close', {
duration: 5000,
panelClass: ['success-snackbar'],
horizontalPosition: 'end',
verticalPosition: 'top'
});
}
/**
* Show error message to user
*/
private showErrorMessage(message: string): void {
this.snackBar.open(message, 'Close', {
duration: 8000,
panelClass: ['error-snackbar'],
horizontalPosition: 'end',
verticalPosition: 'top'
});
}
}

View File

@@ -0,0 +1,475 @@
/**
* Two-Factor Authentication Component Styles
* Professional Angular Resume Builder - Device Registration 2FA
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
/* ==========================================================================
Page Layout (for standalone page)
========================================================================== */
.two-factor-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: var(--gradient-primary);
padding: var(--spacing-lg);
}
.page-container {
width: 100%;
max-width: 600px;
}
/* ==========================================================================
Container & Layout
========================================================================== */
.two-factor-container {
width: 100%;
max-width: 500px;
padding: var(--spacing-xl);
box-sizing: border-box;
background: var(--color-surface);
border-radius: var(--border-radius-lg);
box-shadow: var(--shadow-medium);
}
/* ==========================================================================
Header Section
========================================================================== */
.two-factor-header {
text-align: center;
margin-bottom: 24px;
}
.header-icon {
margin-bottom: var(--spacing-md);
}
.security-icon {
font-size: 48px;
width: 48px;
height: 48px;
color: var(--color-primary);
}
.header-title {
font-size: 24px;
font-weight: 600;
color: var(--color-text-primary);
margin: 0 0 var(--spacing-xs) 0;
}
.header-subtitle {
font-size: 14px;
color: var(--color-text-secondary);
margin: 0;
}
/* ==========================================================================
Device Information Card
========================================================================== */
.device-info-card {
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-light);
border-radius: var(--border-radius-md);
padding: var(--spacing-md);
margin-bottom: var(--spacing-xl);
}
.device-info-header {
display: flex;
align-items: center;
margin-bottom: var(--spacing-sm);
gap: var(--spacing-xs);
}
.device-icon {
font-size: 20px;
color: var(--color-indigo);
}
.device-title {
font-weight: 600;
color: var(--color-text-primary);
}
.device-details {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
}
.device-detail {
display: flex;
justify-content: space-between;
align-items: center;
}
.detail-label {
font-weight: 500;
color: var(--color-text-secondary);
min-width: 80px;
}
.detail-value {
color: var(--color-text-primary);
font-weight: 600;
text-align: right;
}
/* ==========================================================================
Step Content
========================================================================== */
.step-content {
margin-bottom: var(--spacing-xl);
}
/* Security Message */
.security-message {
background: var(--gradient-warning-light);
border: 1px solid var(--color-warning-border-light);
border-radius: var(--border-radius-md);
padding: var(--spacing-lg);
margin-bottom: var(--spacing-xl);
display: flex;
align-items: flex-start;
gap: var(--spacing-md);
}
.warning-icon {
font-size: 24px;
color: var(--color-warning);
margin-top: 4px;
}
.message-content h3 {
font-size: 18px;
font-weight: 600;
color: var(--color-text-primary);
margin: 0 0 8px 0;
}
.message-content p {
font-size: 14px;
color: var(--color-text-secondary);
margin: 0 0 8px 0;
line-height: 1.5;
}
.message-content p:last-child {
margin-bottom: 0;
}
/* Verification Info */
.verification-info {
background: var(--gradient-success-light);
border: 1px solid var(--color-success-border-light);
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
display: flex;
align-items: flex-start;
gap: 16px;
}
.check-icon {
font-size: 24px;
color: var(--color-success);
margin-top: 4px;
}
.info-content h3 {
font-size: 18px;
font-weight: 600;
color: var(--color-text-primary);
margin: 0 0 8px 0;
}
.info-content p {
font-size: 14px;
color: var(--color-text-secondary);
margin: 0 0 12px 0;
line-height: 1.5;
}
/* Countdown Timer */
.countdown-timer {
display: flex;
align-items: center;
gap: 8px;
background: var(--color-surface-overlay);
border-radius: 8px;
padding: 8px 12px;
font-size: 14px;
font-weight: 600;
color: var(--color-indigo);
}
.timer-icon {
font-size: 18px;
}
/* ==========================================================================
Demo Information Card
========================================================================== */
.demo-info-card {
background: var(--gradient-info-light);
border: 1px solid var(--color-info-border-light);
border-radius: 12px;
padding: 16px;
margin-bottom: 20px;
}
.demo-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.demo-icon {
font-size: 20px;
color: var(--color-info);
}
.demo-title {
font-weight: 600;
color: var(--color-text-primary);
}
.demo-content p {
font-size: 14px;
color: var(--color-text-secondary);
margin: 0 0 12px 0;
}
.demo-content strong {
color: var(--color-primary);
font-family: 'Courier New', monospace;
background: var(--color-surface-overlay);
padding: 2px 6px;
border-radius: 4px;
}
/* Verification Code Display */
.verification-code-display {
text-align: center;
margin: 16px 0;
}
.verification-label {
display: block;
font-size: 14px;
color: var(--color-text-secondary);
margin-bottom: 8px;
font-weight: 500;
}
.verification-code {
font-family: 'Courier New', monospace;
font-size: 32px;
font-weight: 700;
color: var(--color-primary);
background: var(--color-surface-overlay);
border: 2px solid var(--color-primary-light);
border-radius: 12px;
padding: 16px 24px;
letter-spacing: 4px;
text-align: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin: 0 auto;
display: inline-block;
min-width: 200px;
}
.fill-demo-button {
font-size: 12px;
height: 36px;
min-width: auto;
}
/* ==========================================================================
Forms & Buttons
========================================================================== */
.verification-form {
margin-bottom: 20px;
}
.full-width {
width: 100%;
}
.action-buttons,
.form-actions {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 20px;
}
.request-button,
.verify-button {
height: 48px;
font-size: 16px;
font-weight: 600;
letter-spacing: 0.5px;
}
.request-button {
background: var(--gradient-primary);
box-shadow: 0 4px 12px var(--color-purple-glow-medium);
}
.verify-button {
background: var(--gradient-success);
box-shadow: 0 4px 12px var(--color-green-glow-medium);
}
.back-button,
.cancel-button {
height: 40px;
font-size: 14px;
}
.cancel-button {
color: var(--color-error);
}
.button-spinner {
margin-right: 8px;
}
/* ==========================================================================
Environment Information
========================================================================== */
.environment-info {
margin-top: 20px;
}
.env-details {
margin-top: 16px;
}
.env-chips {
justify-content: center;
gap: 8px;
}
.env-chip {
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-light);
color: var(--color-text-secondary);
font-size: 12px;
}
/* ==========================================================================
Security Features
========================================================================== */
.security-features {
display: flex;
justify-content: space-around;
gap: 16px;
margin-top: 24px;
padding-top: 20px;
border-top: 1px solid var(--color-border-light);
}
.feature-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
flex: 1;
text-align: center;
}
.feature-icon {
font-size: 20px;
color: var(--color-indigo);
}
.feature-item span {
font-size: 12px;
color: var(--color-text-secondary);
font-weight: 500;
}
/* ==========================================================================
Responsive Design
========================================================================== */
@media (max-width: 600px) {
.two-factor-container {
padding: 16px;
}
.header-title {
font-size: 20px;
}
.device-details {
font-size: 14px;
}
.device-detail {
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.detail-label {
min-width: unset;
}
.detail-value {
text-align: left;
}
.security-features {
gap: 12px;
}
.feature-item span {
font-size: 11px;
}
}
@media (max-width: 480px) {
.action-buttons,
.form-actions {
gap: 8px;
}
.request-button,
.verify-button {
height: 44px;
font-size: 15px;
}
.security-message,
.verification-info,
.demo-info-card {
flex-direction: column;
text-align: center;
}
.warning-icon,
.check-icon,
.demo-icon {
margin-top: 0;
align-self: center;
}
}

View File

@@ -0,0 +1,228 @@
<!--
Two-Factor Authent <div class="device-detail">
<span class="detail-label">User:</span>
<span class="detail-value">{{ data.userName || 'Unknown User' }}</span>
</div>
<div class="device-detail">
<span class="detail-label">Email:</span>
<span class="detail-value">{{ data.userEmail }}</span>
</div>
<div class="device-detail" *ngIf="data.reason">
<span class="detail-label">Reason:</span>
<span class="detail-value">{{ data.reason }}</span>
</div>og Template
Professional Angular Resume Builder - Device Registration 2FA
@author David Valera Melendez <david@valera-melendez.de>
@created 2025-08-08
@location Made in Germany 🇩🇪
-->
<!-- Two-Factor Authentication Page -->
<div class="two-factor-page">
<div class="page-container">
<div class="two-factor-container">
<!-- Header -->
<div class="two-factor-header">
<div class="header-icon">
<mat-icon class="security-icon">security</mat-icon>
</div>
<h2 class="header-title">
{{ data.flowType === 'device_registration' ? 'New Device Detected' : 'Two-Factor Authentication' }}
</h2>
<p class="header-subtitle">
{{ data.flowType === 'device_registration' ? 'Secure your account with device registration' : 'Complete authentication to continue' }}
</p>
</div>
<!-- Device Information -->
<div class="device-info-card">
<div class="device-info-header">
<mat-icon class="device-icon">devices</mat-icon>
<span class="device-title">Device Information</span>
</div>
<div class="device-details">
<div class="device-detail">
<span class="detail-label">User:</span>
<span class="detail-value">{{ data.userName || 'Unknown User' }}</span>
</div>
<div class="device-detail">
<span class="detail-label">Email:</span>
<span class="detail-value">{{ data.userEmail }}</span>
</div>
<div class="device-detail" *ngIf="data.reason">
<span class="detail-label">Reason:</span>
<span class="detail-value">{{ data.reason }}</span>
</div>
</div>
</div>
<!-- Step 1: Request 2FA Code -->
<div *ngIf="step === 'request'" class="step-content">
<!-- New Device Registration Flow -->
<div *ngIf="data.flowType === 'device_registration'" class="security-message">
<mat-icon class="warning-icon">warning</mat-icon>
<div class="message-content">
<h3>New Device Detected</h3>
<p>You are logging in from a new browser or device. For security reasons, we need to verify and register this device.</p>
<p>Click the button below to request a two-factor authentication code.</p>
</div>
</div>
<!-- Existing Device 2FA Code Required -->
<div *ngIf="data.flowType === 'code_required'" class="security-message">
<mat-icon class="warning-icon">security</mat-icon>
<div class="message-content">
<h3>Two-Factor Authentication Required</h3>
<p>For security reasons, you need to complete two-factor authentication to continue.</p>
<p>Click the button below to request your authentication code.</p>
</div>
</div>
<div class="action-buttons">
<button
mat-raised-button
color="primary"
class="request-button"
[disabled]="isInitiating"
(click)="requestTwoFactorCode()"
>
<mat-spinner
*ngIf="isInitiating"
diameter="20"
class="button-spinner">
</mat-spinner>
<mat-icon *ngIf="!isInitiating">send</mat-icon>
<span *ngIf="!isInitiating">Request Verification Code</span>
<span *ngIf="isInitiating">Requesting Code...</span>
</button>
<button
mat-button
color="warn"
class="cancel-button"
[disabled]="isInitiating"
(click)="navigateToLogin()"
>
Cancel Login
</button>
</div>
</div>
<!-- Step 2: Verify 2FA Code -->
<div *ngIf="step === 'verify' && twoFactorResponse" class="step-content">
<div class="verification-info">
<mat-icon class="check-icon">mark_email_read</mat-icon>
<div class="info-content">
<p>{{ twoFactorResponse.message }}</p>
<!-- Countdown Timer -->
<div *ngIf="countdown > 0" class="countdown-timer">
<mat-icon class="timer-icon">timer</mat-icon>
<span>Code expires in: {{ getFormattedCountdown() }}</span>
</div>
</div>
</div>
<!-- Demo Information (if available) -->
<div *ngIf="isDemoMode() && twoFactorResponse.verificationCode" class="demo-info-card">
<div class="demo-content">
<div class="verification-code-display">
<span class="verification-label">Verification code:</span>
<div class="verification-code">{{ twoFactorResponse.verificationCode }}</div>
</div>
<button
mat-button
color="accent"
class="fill-demo-button"
(click)="fillDemoCode()"
>
<mat-icon>auto_fix_high</mat-icon>
Fill Code
</button>
</div>
</div>
<!-- Verification Form -->
<form [formGroup]="twoFactorForm" (ngSubmit)="submitVerificationCode()" class="verification-form">
<mat-form-field appearance="outline" class="full-width">
<mat-label>6-Digit Verification Code</mat-label>
<input
matInput
type="text"
formControlName="verificationCode"
placeholder="000000"
maxlength="6"
autocomplete="one-time-code"
[class.error]="hasFieldError('verificationCode')"
>
<mat-icon matSuffix>lock</mat-icon>
<mat-error *ngIf="hasFieldError('verificationCode')">
{{ getFieldError('verificationCode') }}
</mat-error>
<mat-hint>Enter the 6-digit code you received</mat-hint>
</mat-form-field>
<!-- Submit Buttons -->
<div class="form-actions">
<button
mat-raised-button
color="primary"
type="submit"
class="verify-button"
[disabled]="!canSubmitVerification"
>
<mat-spinner
*ngIf="isVerifying"
diameter="20"
class="button-spinner">
</mat-spinner>
<mat-icon *ngIf="!isVerifying">verified_user</mat-icon>
<span *ngIf="!isVerifying">Verify & Register Device</span>
<span *ngIf="isVerifying">Verifying...</span>
</button>
<button
mat-button
type="button"
class="back-button"
[disabled]="isVerifying"
(click)="step = 'request'"
>
<mat-icon>arrow_back</mat-icon>
Back
</button>
<button
mat-button
color="warn"
type="button"
class="cancel-button"
[disabled]="isVerifying"
(click)="navigateToLogin()"
>
Cancel Login
</button>
</div>
</form>
</div>
<!-- Security Features Info -->
<div class="security-features">
<div class="feature-item">
<mat-icon class="feature-icon">shield</mat-icon>
<span>End-to-end encryption</span>
</div>
<div class="feature-item">
<mat-icon class="feature-icon">fingerprint</mat-icon>
<span>Device fingerprinting</span>
</div>
<div class="feature-item">
<mat-icon class="feature-icon">verified</mat-icon>
<span>GDPR compliant</span>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,441 @@
/**
* Two-Factor Authentication Component
* Professional Angular Resume Builder - Device Registration 2FA
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router, ActivatedRoute } from '@angular/router';
import { Subject, timer } from 'rxjs';
import { takeUntil, finalize, tap } from 'rxjs/operators';
// Angular Material Modules
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 { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSnackBarModule, MatSnackBar } from '@angular/material/snack-bar';
import { MatDividerModule } from '@angular/material/divider';
import { MatChipsModule } from '@angular/material/chips';
// Services and Models
import { AuthService } from '../../../services/auth.service';
import { TwoFactorInitiationResponse } from '../../../models/auth.model';
interface TwoFactorPageData {
userId: number;
userEmail: string;
userName: string;
reason?: string;
riskScore?: number;
flowType?: 'device_registration' | 'code_required';
deviceInfo?: {
isTrusted: boolean;
deviceName?: string;
lastUsed?: Date;
};
tempToken?: string | null;
timestamp?: number; // For data expiration checking
}
/**
* Two-Factor Authentication Page Component
*/
@Component({
selector: 'app-two-factor-auth',
standalone: true,
imports: [
CommonModule,
ReactiveFormsModule,
MatCardModule,
MatFormFieldModule,
MatInputModule,
MatButtonModule,
MatIconModule,
MatProgressSpinnerModule,
MatSnackBarModule,
MatDividerModule,
MatChipsModule
],
templateUrl: './two-factor-auth.component.html',
styleUrls: ['./two-factor-auth.component.css']
})
export class TwoFactorAuthComponent implements OnInit, OnDestroy {
// Form Management
twoFactorForm!: FormGroup;
// State Management
isInitiating = false;
isVerifying = false;
step: 'request' | 'verify' = 'request';
twoFactorResponse: TwoFactorInitiationResponse | null = null;
countdown = 0;
// Computed Properties
get isVerificationCodeValid(): boolean {
const codeControl = this.twoFactorForm?.get('verificationCode');
return !!(codeControl && codeControl.valid && codeControl.value && codeControl.value.length === 6);
}
get canSubmitVerification(): boolean {
return this.isVerificationCodeValid && !this.isVerifying && !!this.twoFactorResponse;
}
// Page Data
data: TwoFactorPageData = {
userId: 0,
userEmail: '',
userName: ''
};
// Cleanup
private destroy$ = new Subject<void>();
private countdownTimer$ = new Subject<void>();
constructor(
private formBuilder: FormBuilder,
private authService: AuthService,
private snackBar: MatSnackBar,
private router: Router,
private route: ActivatedRoute
) {
this.initializeForms();
}
ngOnInit(): void {
// Get data from sessionStorage (more secure than URL parameters)
const storedData = sessionStorage.getItem('twoFactorData');
if (storedData) {
try {
const parsedData = JSON.parse(storedData);
// Check if data is not too old (10 minutes max)
const maxAge = 10 * 60 * 1000; // 10 minutes in milliseconds
if (Date.now() - parsedData.timestamp > maxAge) {
sessionStorage.removeItem('twoFactorData');
this.router.navigate(['/login']);
return;
}
this.data = parsedData;
} catch (error) {
this.router.navigate(['/login']);
return;
}
} else {
// Fallback: try to get from query params (backward compatibility)
this.route.queryParams.subscribe(params => {
if (params['userId'] || params['email']) {
this.data = {
userId: +params['userId'] || 0,
userEmail: params['email'] || '',
userName: params['name'] || '',
reason: params['reason'] || 'New device detected',
riskScore: +params['riskScore'] || 0,
flowType: params['flowType'] || 'device_registration',
tempToken: params['tempToken'] || null,
timestamp: Date.now()
};
} else {
// No data found, redirect to login
this.snackBar.open('Invalid 2FA session. Please login again.', 'Close', {
duration: 5000,
panelClass: ['error-snackbar']
});
this.router.navigate(['/login']);
return;
}
});
}
// If no valid data, redirect back to login
if (!this.data.userId || !this.data.userEmail) {
this.snackBar.open('Invalid 2FA session. Please login again.', 'Close', {
duration: 5000,
panelClass: ['error-snackbar']
});
this.router.navigate(['/login']);
}
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
this.countdownTimer$.next();
this.countdownTimer$.complete();
// Clean up sensitive data when component is destroyed
sessionStorage.removeItem('twoFactorData');
}
/**
* Initialize reactive forms
*/
private initializeForms(): void {
this.twoFactorForm = this.formBuilder.group({
verificationCode: ['', [
Validators.required,
Validators.pattern(/^\d{6}$/)
]]
});
}
/**
* Request 2FA code from backend
*/
requestTwoFactorCode(): void {
if (this.isInitiating) return;
this.isInitiating = true;
this.authService.initiateTwoFactor(this.data.userId)
.pipe(
takeUntil(this.destroy$),
finalize(() => {
this.isInitiating = false;
})
)
.subscribe({
next: (response) => {
this.twoFactorResponse = response;
this.step = 'verify';
this.startCountdown(response.durationMinutes * 60);
this.snackBar.open(response.message, 'Close', {
duration: 5000,
panelClass: ['info-snackbar']
});
},
error: (error) => {
this.snackBar.open('Failed to request verification code. Please try again.', 'Close', {
duration: 5000,
panelClass: ['error-snackbar']
});
}
});
}
/**
* Submit verification code
*/
submitVerificationCode(): void {
// Enhanced validation
if (!this.canSubmitVerification) {
this.twoFactorForm.markAllAsTouched();
// Show specific error message
const codeControl = this.twoFactorForm.get('verificationCode');
if (!codeControl?.value) {
this.snackBar.open('Please enter the verification code', 'Close', {
duration: 3000,
panelClass: ['error-snackbar']
});
} else if (codeControl.value.length !== 6) {
this.snackBar.open('Verification code must be 6 digits', 'Close', {
duration: 3000,
panelClass: ['error-snackbar']
});
}
return;
}
this.isVerifying = true;
const code = this.twoFactorForm.get('verificationCode')?.value;
this.authService.verifyTwoFactor(code, this.twoFactorResponse!.verificationId, this.data.userId, this.data.tempToken || undefined)
.pipe(
takeUntil(this.destroy$),
finalize(() => {
this.isVerifying = false;
})
)
.subscribe({
next: (response) => {
if (response.success) {
// Clear sensitive data from sessionStorage on success
sessionStorage.removeItem('twoFactorData');
if (this.data.flowType === 'device_registration') {
this.handleDeviceRegistrationSuccess(response);
} else if (this.data.flowType === 'code_required' && this.data.tempToken) {
this.handleLoginCompletionSuccess(response);
}
} else {
this.handleVerificationError(response.message || 'Verification failed');
}
},
error: (error) => {
this.handleVerificationError(error.error?.message || 'Verification failed. Please try again.');
}
});
}
/**
* Handle successful device registration
*/
private handleDeviceRegistrationSuccess(response: any): void {
this.snackBar.open('Device registered successfully! You can now login.', 'Close', {
duration: 5000,
panelClass: ['success-snackbar']
});
// Navigate back to login after successful device registration
this.router.navigate(['/login'], {
queryParams: {
twoFactorComplete: 'true',
email: this.data.userEmail
}
});
}
/**
* Handle successful login completion
*/
private handleLoginCompletionSuccess(response: any): void {
// Check if authentication was completed by backend
if (response.authCompleted && response.accessToken) {
// Store the access token using the correct key that AuthService expects
localStorage.setItem('david_auth_access_token', response.accessToken);
// Store user data if available using the correct key
if (response.user) {
const userData = {
id: response.user.id,
email: response.user.email,
firstName: response.user.firstName || '',
lastName: response.user.lastName || ''
};
localStorage.setItem('david_auth_user_data', JSON.stringify(userData));
}
this.snackBar.open('Authentication successful! Redirecting to dashboard...', 'Close', {
duration: 3000,
panelClass: ['success-snackbar']
});
this.authService.refreshAuthState();
// Navigate to dashboard
setTimeout(() => {
this.router.navigate(['/dashboard']);
}, 1000);
} else {
// Authentication not completed - redirect to login
this.snackBar.open('2FA verification successful. Please complete login.', 'Close', {
duration: 3000,
panelClass: ['success-snackbar']
});
this.router.navigate(['/login'], {
queryParams: {
twoFactorVerified: 'true',
email: this.data.userEmail
}
});
}
}
/**
* Handle verification error
*/
private handleVerificationError(message: string): void {
this.snackBar.open(message, 'Close', {
duration: 4000,
panelClass: ['error-snackbar']
});
// Clear the form to let user try again
this.twoFactorForm.get('verificationCode')?.setValue('');
this.twoFactorForm.get('verificationCode')?.markAsUntouched();
}
/**
* Auto-fill demo code if available
*/
fillDemoCode(): void {
if (this.twoFactorResponse?.verificationCode) {
this.twoFactorForm.patchValue({
verificationCode: this.twoFactorResponse.verificationCode
});
this.snackBar.open('Demo verification code filled', 'Close', {
duration: 2000,
panelClass: ['info-snackbar']
});
}
}
/**
* Start countdown timer for code expiration
*/
private startCountdown(seconds: number): void {
this.countdown = seconds;
timer(0, 1000)
.pipe(
takeUntil(this.countdownTimer$),
takeUntil(this.destroy$),
tap(() => {
this.countdown--;
if (this.countdown <= 0) {
this.countdownTimer$.next();
}
})
)
.subscribe();
}
/**
* Format countdown time as MM:SS
*/
getFormattedCountdown(): string {
const minutes = Math.floor(this.countdown / 60);
const seconds = this.countdown % 60;
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}
/**
* Navigate back to login
*/
navigateToLogin(): void {
this.router.navigate(['/login']);
}
/**
* Get field error message
*/
getFieldError(fieldName: string): string {
const field = this.twoFactorForm.get(fieldName);
if (!field || !field.errors || !field.touched) return '';
const errors = field.errors;
if (errors['required']) return 'Verification code is required';
if (errors['pattern']) return 'Please enter a 6-digit code';
return 'Invalid input';
}
/**
* Check if field has error
*/
hasFieldError(fieldName: string): boolean {
const field = this.twoFactorForm.get(fieldName);
return !!(field && field.invalid && field.touched);
}
/**
* Check if demo mode is enabled
*/
isDemoMode(): boolean {
return this.twoFactorResponse?.environmentInfo.demoModeEnabled || false;
}
}

View File

@@ -0,0 +1,11 @@
/**
* Components Export Index
* Professional Angular Resume Builder - Component Exports
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
// Auth Components
export { TwoFactorAuthComponent } from './auth/two-factor-auth/two-factor-auth.component';

View File

@@ -0,0 +1,94 @@
/**
* Global Progress Bar Component Styles
* Professional Angular Resume Builder - GitHub-style Progress Bar
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
.global-progress-bar {
position: fixed;
top: 0;
left: 0;
height: 3px;
background: var(--gradient-progress);
background-size: 200% 100%;
z-index: 9999;
width: 0%;
opacity: 0;
transition: opacity 0.2s ease-in-out, width 0.3s ease-out;
animation: shimmer 2s infinite;
}
.global-progress-bar.active {
opacity: 1;
}
@keyframes shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
/* GitHub-style alternative */
.global-progress-bar::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(90deg,
transparent 0%,
var(--color-surface-overlay-medium) 50%,
transparent 100%
);
animation: loading-shimmer 1.5s infinite;
}
@keyframes loading-shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
/* Professional blue theme */
.global-progress-bar {
background: linear-gradient(90deg,
var(--color-primary),
var(--color-primary),
var(--color-blue-400),
var(--color-primary),
var(--color-primary)
);
background-size: 300% 100%;
box-shadow: 0 0 10px var(--color-blue-glow);
}
/* Dark mode support */
@media (prefers-color-scheme: dark) {
.global-progress-bar {
background: linear-gradient(90deg,
var(--color-blue-300),
var(--color-blue-200),
var(--color-blue-100),
var(--color-blue-200),
var(--color-blue-300)
);
box-shadow: 0 0 10px var(--color-blue-200-glow);
}
}
/* Mobile responsiveness */
@media (max-width: 768px) {
.global-progress-bar {
height: 2px;
}
}

View File

@@ -0,0 +1,14 @@
<!--
Global Progress Bar Component Template
Professional Angular Resume Builder - GitHub-style Progress Bar
@author David Valera Melendez <david@valera-melendez.de>
@created 2025-08-08
@location Made in Germany 🇩🇪
-->
<div
class="global-progress-bar"
[class.active]="isLoading"
[style.width.%]="progress">
</div>

View File

@@ -0,0 +1,51 @@
/**
* Global Progress Bar Component
* Professional Angular Resume Builder - GitHub-style Progress Bar
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { LoadingService } from '../../../services/loading.service';
@Component({
selector: 'app-global-progress-bar',
standalone: true,
imports: [CommonModule],
templateUrl: './global-progress-bar.component.html',
styleUrls: ['./global-progress-bar.component.css']
})
export class GlobalProgressBarComponent implements OnInit, OnDestroy {
isLoading = false;
progress = 0;
private destroy$ = new Subject<void>();
constructor(private loadingService: LoadingService) {}
ngOnInit(): void {
// Subscribe to global loading state
this.loadingService.globalLoading$
.pipe(takeUntil(this.destroy$))
.subscribe(loading => {
this.isLoading = loading;
});
// Subscribe to progress updates
this.loadingService.progress$
.pipe(takeUntil(this.destroy$))
.subscribe(progress => {
this.progress = progress;
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}

View File

@@ -0,0 +1,140 @@
/**
* Loading Button Component Styles
* Professional Angular Resume Builder - Enterprise Button with Spinner
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
.button-content {
display: flex;
align-items: center;
justify-content: center;
position: relative;
min-height: 24px;
transition: var(--transition-slow);
}
.button-content.loading {
pointer-events: none;
}
.spinner-container {
position: absolute;
left: 8px;
display: flex;
align-items: center;
z-index: 1;
}
.button-text {
transition: var(--transition-slow);
font-weight: 500;
}
.button-text.loading-text {
margin-left: 32px;
opacity: 0.8;
}
.icon-margin {
margin-right: 8px;
}
/* Professional button styles */
button {
min-width: 120px;
height: 44px;
border-radius: var(--border-radius-md);
font-size: 14px;
font-weight: 500;
text-transform: none;
letter-spacing: 0.5px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
button:not([disabled]):hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px var(--color-shadow-light);
}
button[disabled] {
opacity: 0.7;
cursor: not-allowed;
}
/* Ripple effect */
button::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: var(--color-surface-overlay-light);
transition: width 0.6s, height 0.6s, top 0.6s, left 0.6s;
transform: translate(-50%, -50%);
}
button:active::before {
width: 300px;
height: 300px;
}
/* Size variations */
.small {
min-width: 80px;
height: 36px;
font-size: 12px;
}
.large {
min-width: 160px;
height: 52px;
font-size: 16px;
}
/* Color themes */
.success {
background-color: var(--color-success);
color: white;
}
.danger {
background-color: var(--color-error);
color: white;
}
.warning {
background-color: var(--color-warning);
color: white;
}
/* Loading animation */
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
}
.loading {
animation: pulse 2s infinite ease-in-out;
}
/* Dark mode support */
@media (prefers-color-scheme: dark) {
button {
background-color: var(--color-background-darker);
color: var(--color-text-primary);
}
}

View File

@@ -0,0 +1,34 @@
<!--
Loading Button Component Template
Professional Angular Resume Builder - Enterprise Button with Spinner
@author David Valera Melendez <david@valera-melendez.de>
@created 2025-08-08
@location Made in Germany 🇩🇪
-->
<button
mat-raised-button
[color]="color"
[disabled]="isLoading || disabled"
[class]="buttonClass"
(click)="onButtonClick()"
type="submit">
<div class="button-content" [class.loading]="isLoading">
<!-- Loading Spinner -->
<div class="spinner-container" *ngIf="isLoading">
<mat-spinner [diameter]="spinnerSize" [color]="spinnerColor"></mat-spinner>
</div>
<!-- Button Icon -->
<mat-icon *ngIf="icon && !isLoading" [class.icon-margin]="text">
{{ icon }}
</mat-icon>
<!-- Button Text -->
<span class="button-text" [class.loading-text]="isLoading">
{{ isLoading ? loadingText : text }}
</span>
</div>
</button>

View File

@@ -0,0 +1,73 @@
/**
* Loading Button Component
* Professional Angular Resume Builder - Enterprise Button with Spinner
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
import { Component, Input, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { LoadingService } from '../../../services/loading.service';
@Component({
selector: 'app-loading-button',
standalone: true,
imports: [
CommonModule,
MatButtonModule,
MatIconModule,
MatProgressSpinnerModule
],
templateUrl: './loading-button.component.html',
styleUrls: ['./loading-button.component.css']
})
export class LoadingButtonComponent implements OnInit, OnDestroy {
@Input() text: string = 'Submit';
@Input() loadingText: string = 'Processing...';
@Input() formId: string = 'default';
@Input() icon: string = '';
@Input() color: 'primary' | 'accent' | 'warn' = 'primary';
@Input() disabled: boolean = false;
@Input() size: 'small' | 'medium' | 'large' = 'medium';
@Input() spinnerSize: number = 20;
@Input() spinnerColor: 'primary' | 'accent' | 'warn' = 'primary';
@Output() clicked = new EventEmitter<void>();
isLoading = false;
private destroy$ = new Subject<void>();
constructor(private loadingService: LoadingService) {}
ngOnInit(): void {
// Subscribe to form-specific loading state
this.loadingService.isFormLoading(this.formId)
.pipe(takeUntil(this.destroy$))
.subscribe((loading: boolean) => {
this.isLoading = loading;
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
get buttonClass(): string {
const classes = [this.size];
return classes.join(' ');
}
onButtonClick(): void {
if (!this.isLoading && !this.disabled) {
this.clicked.emit();
}
}
}

View File

@@ -0,0 +1,179 @@
/**
* Main Application Routing Module
* Enterprise-grade routing configuration with guards and proper error handling
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
import { NgModule } from '@angular/core';
import { RouterModule, Routes, PreloadAllModules } from '@angular/router';
import { AuthGuard, GuestGuard } from '../../guards/auth.guard';
/**
* Application routes with proper guards and data
* Following enterprise patterns with clear separation of concerns
*/
const routes: Routes = [
// Default redirect
{
path: '',
redirectTo: '/home',
pathMatch: 'full'
},
// Public routes
{
path: 'home',
loadComponent: () => import('../../pages/home/home.component').then(c => c.HomeComponent),
data: {
title: 'Home - Professional Resume Builder',
description: 'Create professional resumes with our enterprise-grade builder'
}
},
// Authentication routes (only accessible when not logged in)
{
path: 'login',
canActivate: [GuestGuard],
loadComponent: () => import('../../pages/login/login.component').then(c => c.LoginComponent),
data: {
title: 'Login - Resume Builder',
description: 'Sign in to access your resume builder dashboard'
}
},
{
path: 'two-factor-auth',
loadComponent: () => import('../../components/auth/two-factor-auth/two-factor-auth.component').then(c => c.TwoFactorAuthComponent),
data: {
title: '2FA Verification - Resume Builder',
description: 'Complete two-factor authentication for device verification'
}
},
{
path: 'test-2fa',
loadComponent: () => import('../../components/auth/two-factor-auth/two-factor-auth.component').then(c => c.TwoFactorAuthComponent),
data: {
title: 'Test 2FA - Resume Builder'
}
},
{
path: 'register',
canActivate: [GuestGuard],
loadComponent: () => import('../../auth/register/register.component').then(c => c.RegisterComponent),
data: {
title: 'Register - Resume Builder'
}
},
{
path: 'forgot-password',
canActivate: [GuestGuard],
loadComponent: () => import('../../auth/forgot-password/forgot-password.component').then(c => c.ForgotPasswordComponent),
data: {
title: 'Forgot Password - Resume Builder'
}
},
// Protected routes (require authentication)
{
path: 'dashboard',
canActivate: [AuthGuard],
redirectTo: '/builder',
pathMatch: 'full'
},
{
path: 'builder',
canActivate: [AuthGuard],
loadComponent: () => import('../../pages/resume-builder/resume-builder.component').then(c => c.ResumeBuilderComponent),
data: {
title: 'Resume Builder',
breadcrumb: 'Builder'
}
},
{
path: 'templates',
loadComponent: () => import('../../pages/templates/templates.component').then(c => c.TemplatesComponent),
data: {
title: 'Resume Templates',
breadcrumb: 'Templates'
}
},
{
path: 'settings',
canActivate: [AuthGuard],
loadComponent: () => import('../../pages/settings/settings.component').then(c => c.SettingsComponent),
data: {
title: 'Account Settings',
breadcrumb: 'Settings'
}
},
{
path: 'profile',
canActivate: [AuthGuard],
redirectTo: '/settings',
pathMatch: 'full'
},
// Preview routes (can be public for sharing)
{
path: 'preview',
loadComponent: () => import('../../pages/preview/preview.component').then(c => c.PreviewComponent),
data: {
title: 'Resume Preview'
}
},
{
path: 'preview/:id',
loadComponent: () => import('../../pages/preview/preview.component').then(c => c.PreviewComponent),
data: {
title: 'Resume Preview'
}
},
// Error pages
{
path: '404',
loadComponent: () => import('../../pages/error/not-found/not-found.component').then(c => c.NotFoundComponent),
data: { title: 'Page Not Found' }
},
{
path: '403',
loadComponent: () => import('../../pages/error/forbidden/forbidden.component').then(c => c.ForbiddenComponent),
data: { title: 'Access Denied' }
},
{
path: '500',
loadComponent: () => import('../../pages/error/server-error/server-error.component').then(c => c.ServerErrorComponent),
data: { title: 'Server Error' }
},
// Wildcard route - must be last
{
path: '**',
redirectTo: '/404'
}
];
@NgModule({
imports: [
RouterModule.forRoot(routes, {
// Enterprise routing configuration
enableTracing: false, // Set to true for debugging in development
preloadingStrategy: PreloadAllModules,
initialNavigation: 'enabledBlocking',
scrollPositionRestoration: 'top',
anchorScrolling: 'enabled',
scrollOffset: [0, 64], // Offset for fixed header
errorHandler: (error) => {
console.error('Router error:', error);
},
onSameUrlNavigation: 'reload',
paramsInheritanceStrategy: 'emptyOnly',
urlUpdateStrategy: 'deferred',
canceledNavigationResolution: 'replace'
})
],
exports: [RouterModule]
})
export class AppRoutingModule { }

View File

@@ -0,0 +1,282 @@
/**
* Professional Navigation Service
* Enterprise-grade navigation handling with error management and user feedback
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
import { Injectable } from '@angular/core';
import { Router, NavigationEnd, NavigationError, NavigationCancel } from '@angular/router';
import { Location } from '@angular/common';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Observable, BehaviorSubject } from 'rxjs';
import { filter } from 'rxjs/operators';
export interface NavigationState {
isNavigating: boolean;
currentUrl: string;
previousUrl: string | null;
error: string | null;
}
@Injectable({
providedIn: 'root'
})
export class NavigationService {
private navigationStateSubject = new BehaviorSubject<NavigationState>({
isNavigating: false,
currentUrl: '/',
previousUrl: null,
error: null
});
public readonly navigationState$: Observable<NavigationState> = this.navigationStateSubject.asObservable();
constructor(
private router: Router,
private location: Location,
private snackBar: MatSnackBar
) {
this.initializeNavigationTracking();
}
/**
* Initialize navigation event tracking
*/
private initializeNavigationTracking(): void {
// Track navigation start
this.router.events.subscribe(event => {
if (event.constructor.name === 'NavigationStart') {
this.updateNavigationState({
...this.navigationStateSubject.value,
isNavigating: true,
error: null
});
}
});
// Track successful navigation
this.router.events.subscribe(event => {
if (event instanceof NavigationEnd) {
this.updateNavigationState({
previousUrl: this.navigationStateSubject.value.currentUrl,
currentUrl: event.url,
isNavigating: false,
error: null
});
}
});
// Track navigation errors
this.router.events.subscribe(event => {
if (event instanceof NavigationError) {
console.error('Navigation error:', event.error);
this.updateNavigationState({
...this.navigationStateSubject.value,
isNavigating: false,
error: event.error?.message || 'Navigation failed'
});
this.showErrorMessage('Navigation failed. Please try again.');
}
});
// Track navigation cancellations
this.router.events.subscribe(event => {
if (event instanceof NavigationCancel) {
console.warn('Navigation cancelled:', event.reason);
this.updateNavigationState({
...this.navigationStateSubject.value,
isNavigating: false,
error: null
});
}
});
}
/**
* Update navigation state
*/
private updateNavigationState(state: NavigationState): void {
this.navigationStateSubject.next(state);
}
/**
* Navigate to a route with comprehensive error handling
*/
async navigateToRoute(route: string | string[], options?: {
replaceUrl?: boolean;
queryParams?: any;
fragment?: string;
state?: any;
skipLocationChange?: boolean;
preserveFragment?: boolean;
preserveQueryParams?: boolean;
}): Promise<boolean> {
try {
const navigationResult = await this.router.navigate(
Array.isArray(route) ? route : [route],
{
replaceUrl: options?.replaceUrl || false,
queryParams: options?.queryParams,
fragment: options?.fragment,
state: options?.state,
skipLocationChange: options?.skipLocationChange || false,
preserveFragment: options?.preserveFragment || false,
queryParamsHandling: options?.preserveQueryParams ? 'merge' : undefined
}
);
if (!navigationResult) {
console.error('Navigation failed for route:', route);
this.showErrorMessage('Unable to navigate to the requested page.');
return false;
}
return true;
} catch (error) {
console.error('Navigation exception:', error);
this.showErrorMessage('An unexpected error occurred during navigation.');
return false;
}
}
/**
* Navigate to login page with return URL
*/
async navigateToLogin(returnUrl?: string): Promise<boolean> {
const queryParams = returnUrl ? { returnUrl } : undefined;
return await this.navigateToRoute('/auth/login', { queryParams });
}
/**
* Navigate to dashboard
*/
async navigateToDashboard(): Promise<boolean> {
return await this.navigateToRoute('/dashboard');
}
/**
* Navigate to resume builder
*/
async navigateToBuilder(): Promise<boolean> {
return await this.navigateToRoute('/builder');
}
/**
* Navigate to user profile
*/
async navigateToProfile(): Promise<boolean> {
return await this.navigateToRoute('/profile');
}
/**
* Navigate to settings
*/
async navigateToSettings(): Promise<boolean> {
return await this.navigateToRoute('/settings');
}
/**
* Navigate to home page
*/
async navigateToHome(): Promise<boolean> {
return await this.navigateToRoute('/home', { replaceUrl: true });
}
/**
* Navigate back to previous page
*/
navigateBack(): void {
const previousUrl = this.navigationStateSubject.value.previousUrl;
if (previousUrl && previousUrl !== this.navigationStateSubject.value.currentUrl) {
this.location.back();
} else {
this.navigateToHome();
}
}
/**
* Navigate forward in browser history
*/
navigateForward(): void {
this.location.forward();
}
/**
* Get current URL
*/
getCurrentUrl(): string {
return this.router.url;
}
/**
* Check if currently on a specific route
*/
isCurrentRoute(route: string): boolean {
return this.router.url === route || this.router.url.startsWith(route + '/');
}
/**
* Check if route is public (doesn't require authentication)
*/
isPublicRoute(url?: string): boolean {
const currentUrl = url || this.router.url;
const publicRoutes = ['/home', '/login', '/register', '/forgot-password', '/auth', '/404', '/403', '/500'];
return publicRoutes.some(route => currentUrl.startsWith(route)) || currentUrl === '/';
}
/**
* Force reload current page
*/
reloadCurrentPage(): void {
const currentUrl = this.router.url;
this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => {
this.router.navigate([currentUrl]);
});
}
/**
* Open external URL in new tab
*/
openExternalUrl(url: string): void {
window.open(url, '_blank', 'noopener,noreferrer');
}
/**
* Show success message to user
*/
private showSuccessMessage(message: string): void {
this.snackBar.open(message, 'Close', {
duration: 4000,
panelClass: ['success-snackbar'],
horizontalPosition: 'end',
verticalPosition: 'top'
});
}
/**
* Show error message to user
*/
private showErrorMessage(message: string): void {
this.snackBar.open(message, 'Close', {
duration: 6000,
panelClass: ['error-snackbar'],
horizontalPosition: 'end',
verticalPosition: 'top'
});
}
/**
* Show info message to user
*/
private showInfoMessage(message: string): void {
this.snackBar.open(message, 'Close', {
duration: 3000,
panelClass: ['info-snackbar'],
horizontalPosition: 'end',
verticalPosition: 'top'
});
}
}

View File

@@ -0,0 +1,181 @@
/**
* Authentication Guards
* Professional Angular Resume Builder - Enterprise Auth System
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
import { Injectable } from '@angular/core';
import {
CanActivate,
CanActivateChild,
CanLoad,
Router,
ActivatedRouteSnapshot,
RouterStateSnapshot,
Route,
UrlSegment
} from '@angular/router';
import { Observable, of } from 'rxjs';
import { map, take, tap } from 'rxjs/operators';
import { AuthService } from '../services/auth.service';
import { UserRole } from '../models';
/**
* Authentication Guard - Protects routes that require authentication
*/
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate, CanActivateChild, CanLoad {
constructor(
private authService: AuthService,
private router: Router
) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> {
return this.checkAuthStatus(state.url);
}
canActivateChild(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> {
return this.canActivate(route, state);
}
canLoad(route: Route, segments: UrlSegment[]): Observable<boolean> {
const url = segments.map(segment => segment.path).join('/');
return this.checkAuthStatus(`/${url}`);
}
/**
* Check authentication status and redirect if necessary
*/
private checkAuthStatus(redirectUrl: string): Observable<boolean> {
return this.authService.isAuthenticated$.pipe(
take(1),
tap(isAuthenticated => {
if (!isAuthenticated) {
this.router.navigate(['/login'], {
queryParams: { returnUrl: redirectUrl },
replaceUrl: true
});
}
})
);
}
}
/**
* Role-based Authorization Guard - Protects routes based on user roles
*/
@Injectable({
providedIn: 'root'
})
export class RoleGuard implements CanActivate {
constructor(
private authService: AuthService,
private router: Router
) {}
canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
const requiredRoles = route.data['roles'] as UserRole[];
if (!requiredRoles || requiredRoles.length === 0) {
return of(true);
}
return this.authService.currentUser$.pipe(
take(1),
map(user => {
if (!user) {
this.router.navigate(['/login']);
return false;
}
const hasRequiredRole = requiredRoles.some(role => user.roles.includes(role));
if (!hasRequiredRole) {
this.router.navigate(['/access-denied']);
return false;
}
return true;
})
);
}
}
/**
* Permission-based Authorization Guard - Protects routes based on user permissions
*/
@Injectable({
providedIn: 'root'
})
export class PermissionGuard implements CanActivate {
constructor(
private authService: AuthService,
private router: Router
) {}
canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
const requiredPermissions = route.data['permissions'] as string[];
if (!requiredPermissions || requiredPermissions.length === 0) {
return of(true);
}
return this.authService.currentUser$.pipe(
take(1),
map(user => {
if (!user) {
this.router.navigate(['/login']);
return false;
}
const hasRequiredPermission = requiredPermissions.some(permission =>
(user.permissions || []).includes(permission)
);
if (!hasRequiredPermission) {
this.router.navigate(['/access-denied']);
return false;
}
return true;
})
);
}
}
/**
* Guest Guard - Redirects authenticated users away from auth pages
*/
@Injectable({
providedIn: 'root'
})
export class GuestGuard implements CanActivate {
constructor(
private authService: AuthService,
private router: Router
) {}
canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
return this.authService.isAuthenticated$.pipe(
take(1),
tap(isAuthenticated => {
if (isAuthenticated) {
const returnUrl = route.queryParams['returnUrl'];
this.router.navigate([returnUrl || '/dashboard']);
}
}),
map(isAuthenticated => !isAuthenticated)
);
}
}

View File

@@ -0,0 +1,118 @@
/**
* JWT Authentication Interceptor Function
* Professional Angular Resume Builder - Enterprise Auth System
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
import { HttpInterceptorFn, HttpErrorResponse, HttpRequest, HttpHandlerFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, switchMap, throwError, BehaviorSubject, filter, take, Observable } from 'rxjs';
import { AuthService } from '../services/auth.service';
let isRefreshing = false;
const refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);
/**
* JWT Authentication Interceptor Function
* Automatically adds JWT tokens to requests and handles token refresh
*/
export const jwtInterceptor: HttpInterceptorFn = (req, next) => {
const authService = inject(AuthService);
// Skip interceptor for authentication endpoints
if (req.url.includes('/auth/login') || req.url.includes('/auth/register')) {
return next(req);
}
// Add JWT token to request if user is authenticated
const token = getTokenFromStorage();
let authReq = req;
if (token) {
authReq = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
}
return next(authReq).pipe(
catchError((error) => {
if (error instanceof HttpErrorResponse && error.status === 401) {
return handle401Error(authReq, next, authService);
}
return throwError(() => error);
})
);
};
/**
* Handle 401 Unauthorized errors with token refresh
*/
function handle401Error(request: HttpRequest<any>, next: HttpHandlerFn, authService: AuthService): Observable<any> {
if (!isRefreshing) {
isRefreshing = true;
refreshTokenSubject.next(null);
const refreshToken = getRefreshTokenFromStorage();
if (refreshToken) {
return authService.refreshToken().pipe(
switchMap((tokens: any) => {
isRefreshing = false;
refreshTokenSubject.next(tokens.accessToken);
// Retry the original request with new token
const newAuthReq = request.clone({
setHeaders: {
Authorization: `Bearer ${tokens.accessToken}`,
'Content-Type': 'application/json'
}
});
return next(newAuthReq);
}),
catchError((error) => {
isRefreshing = false;
authService.logout();
return throwError(() => error);
})
);
}
}
// If refresh is in progress, wait for it to complete
return refreshTokenSubject.pipe(
filter(token => token !== null),
take(1),
switchMap((token) => {
const newAuthReq = request.clone({
setHeaders: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
return next(newAuthReq);
})
);
}
/**
* Get access token from storage
*/
function getTokenFromStorage(): string | null {
return localStorage.getItem('david_auth_access_token') ||
sessionStorage.getItem('david_auth_access_token');
}
/**
* Get refresh token from storage
*/
function getRefreshTokenFromStorage(): string | null {
return localStorage.getItem('david_auth_refresh_token') ||
sessionStorage.getItem('david_auth_refresh_token');
}

View File

@@ -0,0 +1,122 @@
/**
* JWT Authentication Interceptor
* Professional Angular Resume Builder - Enterprise Auth System
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
import { Injectable } from '@angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor,
HttpErrorResponse
} from '@angular/common/http';
import { Observable, throwError, BehaviorSubject } from 'rxjs';
import { catchError, filter, take, switchMap } from 'rxjs/operators';
import { AuthService } from '../services/auth.service';
/**
* Enterprise-grade JWT Authentication Interceptor
* Automatically adds JWT tokens to requests and handles token refresh
*/
@Injectable()
export class JwtInterceptor implements HttpInterceptor {
private isRefreshing = false;
private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);
constructor(private authService: AuthService) {}
/**
* Intercept HTTP requests to add JWT token and handle authentication errors
*/
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// Add JWT token to request if user is authenticated
if (this.authService.isAuthenticated()) {
request = this.addTokenToRequest(request);
}
return next.handle(request).pipe(
catchError(error => {
if (error instanceof HttpErrorResponse && error.status === 401) {
return this.handle401Error(request, next);
}
return throwError(() => error);
})
);
}
/**
* Add JWT token to request headers
*/
private addTokenToRequest(request: HttpRequest<any>): HttpRequest<any> {
const token = this.getTokenFromStorage();
if (token) {
return request.clone({
setHeaders: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
});
}
return request;
}
/**
* Handle 401 Unauthorized errors with token refresh
*/
private handle401Error(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (!this.isRefreshing) {
this.isRefreshing = true;
this.refreshTokenSubject.next(null);
const refreshToken = this.getRefreshTokenFromStorage();
if (refreshToken) {
return this.authService.refreshToken().pipe(
switchMap((tokens: any) => {
this.isRefreshing = false;
this.refreshTokenSubject.next(tokens.accessToken);
// Retry the original request with new token
return next.handle(this.addTokenToRequest(request));
}),
catchError((error) => {
this.isRefreshing = false;
this.authService.logout();
return throwError(() => error);
})
);
}
}
// If refresh is in progress, wait for it to complete
return this.refreshTokenSubject.pipe(
filter(token => token !== null),
take(1),
switchMap(() => next.handle(this.addTokenToRequest(request)))
);
}
/**
* Get access token from storage
*/
private getTokenFromStorage(): string | null {
return localStorage.getItem('david_auth_access_token') ||
sessionStorage.getItem('david_auth_access_token');
}
/**
* Get refresh token from storage
*/
private getRefreshTokenFromStorage(): string | null {
return localStorage.getItem('david_auth_refresh_token') ||
sessionStorage.getItem('david_auth_refresh_token');
}
}

View File

@@ -0,0 +1,20 @@
/**
* Authentication Layout Component Styles
* Professional Angular Resume Builder - Authentication Pages Layout
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
* @description Layout wrapper for authentication pages (login, register, forgot password)
*/
/* ==========================================================================
Auth Layout Container
========================================================================== */
.auth-layout-container {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
}

View File

@@ -0,0 +1,12 @@
<!--
Auth Layout Template
Clean layout for authentication pages without navigation
@author David Valera Melendez <david@valera-melendez.de>
@created 2025-08-08
@location Made in Germany 🇩🇪
-->
<div class="auth-layout-container">
<router-outlet></router-outlet>
</div>

View File

@@ -0,0 +1,26 @@
/**
* Auth Layout Component
* Clean layout for authentication pages (login, register, forgot-password)
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-auth-layout',
standalone: true,
imports: [
CommonModule,
RouterOutlet
],
templateUrl: './auth-layout.component.html',
styleUrls: ['./auth-layout.component.css']
})
export class AuthLayoutComponent {
// Simple layout component for auth pages - no logic needed
}

View File

@@ -0,0 +1,264 @@
/**
* Main Layout Component Styles
* Professional Angular Resume Builder - Application Shell & Navigation
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
* @description Main application layout with header, navigation, and content areas
*/
/* ==========================================================================
Main Layout Container
========================================================================== */
.main-layout-container {
display: flex;
flex-direction: column;
min-height: 100vh;
}
/* ==========================================================================
Application Toolbar
========================================================================== */
.app-toolbar {
position: sticky;
top: 0;
z-index: var(--z-index-fixed);
box-shadow: var(--color-shadow-light) 0px 2px 4px;
}
.toolbar-content {
display: flex;
align-items: center;
width: 100%;
max-width: 1200px;
margin: 0 auto;
padding: 0 16px;
}
.brand-section {
display: flex;
align-items: center;
gap: 12px;
font-weight: 500;
font-size: 1.2rem;
}
.brand-icon {
font-size: 28px;
width: 28px;
height: 28px;
}
.brand-text {
font-size: 1.1rem;
font-weight: 500;
}
.nav-links {
display: flex;
gap: 8px;
margin-left: 32px;
}
.nav-button {
display: flex;
align-items: center;
gap: 4px;
}
.nav-button.active-link {
background-color: var(--color-surface-overlay);
}
.spacer {
flex: 1;
}
.user-section {
display: flex;
align-items: center;
gap: 16px;
}
/* User Display Styles */
.user-display {
display: flex;
align-items: center;
gap: 10px;
padding: 6px 12px;
border-radius: 6px;
background: var(--color-surface-overlay);
transition: background-color 0.2s ease;
min-width: 200px;
}
.user-display:hover {
background: var(--color-background-white-subtle);
}
.user-avatar {
color: white;
font-size: 32px;
width: 32px;
height: 32px;
flex-shrink: 0;
}
.user-info-display {
display: flex;
flex-direction: column;
align-items: flex-start;
min-width: 0;
flex: 1;
line-height: 1.2;
}
.user-name-display {
color: white;
font-weight: 500;
font-size: 0.875rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
margin-bottom: 2px;
}
.user-email-display {
color: var(--color-text-white-light);
font-size: 0.75rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
.menu-button {
color: white;
margin-left: 8px;
}
.main-dropdown {
min-width: 220px;
}
.loading-indicator {
display: flex;
align-items: center;
color: var(--color-text-white-subtle);
}
.loading-icon {
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.user-menu-button {
color: white;
}
.user-dropdown {
min-width: 240px;
}
.user-info {
padding: 16px;
background: var(--color-background-dark-subtle);
}
.user-name {
font-weight: 500;
color: var(--color-text-primary);
}
.user-email {
font-size: 0.875rem;
color: var(--color-text-secondary);
margin-top: 4px;
}
.logout-item {
color: var(--color-error);
}
.logout-item mat-icon {
color: var(--color-error);
}
.main-content {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
position: relative;
}
.main-content router-outlet + * {
flex: 1;
}
.app-footer {
background-color: var(--color-surface);
padding: 16px 0;
border-top: 1px solid var(--color-border);
}
.footer-content {
text-align: center;
color: var(--color-text-secondary);
font-size: 0.875rem;
}
.footer-content strong {
color: var(--color-primary);
}
/* Responsive Design */
@media (max-width: 768px) {
.toolbar-content {
padding: 0 12px;
}
.brand-section {
gap: 8px;
}
.brand-text {
display: none;
}
.user-info-display {
display: none;
}
.user-display {
padding: 4px 8px;
background: transparent;
}
.user-name-display,
.user-email-display {
display: none;
}
}
@media (max-width: 480px) {
.brand-icon {
font-size: 24px;
width: 24px;
height: 24px;
}
.user-avatar {
font-size: 28px;
width: 28px;
height: 28px;
}
}

View File

@@ -0,0 +1,103 @@
<!--
Main Layout Template
Layout wrapper for authenticated pages with navigation header
@author David Valera Melendez <david@valera-melendez.de>
@created 2025-08-08
@location Made in Germany 🇩🇪
-->
<div class="main-layout-container">
<!-- Top Navigation Toolbar -->
<mat-toolbar color="primary" class="app-toolbar">
<div class="toolbar-content">
<!-- Brand Section -->
<div class="brand-section" (click)="navigateToDashboard()" style="cursor: pointer;">
<mat-icon class="brand-icon">description</mat-icon>
<span class="brand-text">Lebenslauf Generator</span>
</div>
<!-- Spacer -->
<div class="spacer"></div>
<!-- User Section with Name/Email Display -->
<div class="user-section" *ngIf="isAuthenticated$ | async">
<!-- Loading Indicator -->
<div *ngIf="isLoading$ | async" class="loading-indicator">
<mat-icon class="loading-icon">hourglass_empty</mat-icon>
</div>
<!-- User Info Display -->
<div *ngIf="!(isLoading$ | async) && (currentUser$ | async) as user" class="user-display">
<mat-icon class="user-avatar">account_circle</mat-icon>
<div class="user-info-display">
<div class="user-name-display">{{ user.firstName }} {{ user.lastName }}</div>
<div class="user-email-display">{{ user.email }}</div>
</div>
</div>
<!-- Navigation Menu Button -->
<button
mat-icon-button
[matMenuTriggerFor]="mainMenu"
class="menu-button"
[attr.aria-label]="'Main menu'"
*ngIf="!(isLoading$ | async)"
>
<mat-icon>menu</mat-icon>
</button>
<mat-menu #mainMenu="matMenu" class="main-dropdown">
<!-- Dashboard Link -->
<button mat-menu-item (click)="navigateToDashboard()">
<mat-icon>dashboard</mat-icon>
<span>Dashboard</span>
</button>
<mat-divider></mat-divider>
<!-- Navigation Items -->
<button mat-menu-item routerLink="/builder">
<mat-icon>edit</mat-icon>
<span>Lebenslauf Generator</span>
</button>
<button mat-menu-item routerLink="/preview">
<mat-icon>visibility</mat-icon>
<span>Lebenslauf Vorschau</span>
</button>
<mat-divider></mat-divider>
<!-- Settings -->
<button mat-menu-item (click)="navigateToProfile()">
<mat-icon>settings</mat-icon>
<span>Einstellungen</span>
</button>
<mat-divider></mat-divider>
<!-- Logout -->
<button mat-menu-item (click)="logout()" class="logout-item">
<mat-icon>logout</mat-icon>
<span>Abmelden</span>
</button>
</mat-menu>
</div>
</div>
</mat-toolbar>
<!-- Main Content Area -->
<main class="main-content">
<router-outlet></router-outlet>
</main>
<!-- Footer -->
<footer class="app-footer" *ngIf="!(isAuthenticated$ | async)">
<div class="footer-content">
<p class="footer-text">
© 2025 David Valera Melendez | Professioneller Lebenslauf Generator | Made in Germany 🇩🇪
</p>
</div>
</footer>
</div>

View File

@@ -0,0 +1,160 @@
/**
* Main Layout Component
* Layout wrapper for authenticated pages with navigation header
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet, RouterModule, Router } from '@angular/router';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { MatDividerModule } from '@angular/material/divider';
import { MatSnackBarModule, MatSnackBar } from '@angular/material/snack-bar';
import { Observable, Subject } from 'rxjs';
import { takeUntil, finalize } from 'rxjs/operators';
import { AuthService } from '../../services/auth.service';
import { AuthUser } from '../../models/auth.model';
import { NavigationService } from '../../core/services/navigation.service';
@Component({
selector: 'app-main-layout',
standalone: true,
imports: [
CommonModule,
RouterOutlet,
RouterModule,
MatToolbarModule,
MatButtonModule,
MatIconModule,
MatMenuModule,
MatDividerModule,
MatSnackBarModule
],
templateUrl: './main-layout.component.html',
styleUrls: ['./main-layout.component.css']
})
export class MainLayoutComponent implements OnInit, OnDestroy {
// Authentication state observables
isAuthenticated$: Observable<boolean>;
currentUser$: Observable<AuthUser | null>;
isLoading$: Observable<boolean>;
// Component state
private isLoggingOut = false;
private readonly destroy$ = new Subject<void>();
constructor(
private authService: AuthService,
private router: Router,
private snackBar: MatSnackBar,
private navigationService: NavigationService
) {
this.isAuthenticated$ = this.authService.isAuthenticated$;
this.currentUser$ = this.authService.currentUser$;
this.isLoading$ = this.authService.isLoading$;
}
ngOnInit(): void {
// Layout component doesn't need auth watcher - that's handled at app level
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
/**
* Handle user logout with comprehensive error handling and user feedback
*/
logout(): void {
if (this.isLoggingOut) {
return;
}
this.isLoggingOut = true;
const logoutMessage = this.snackBar.open('Signing out...', undefined, {
duration: 0,
panelClass: ['info-snackbar']
});
this.authService.logout()
.pipe(
takeUntil(this.destroy$),
finalize(() => {
this.isLoggingOut = false;
logoutMessage.dismiss();
})
)
.subscribe({
next: async () => {
this.showSuccessMessage('Successfully signed out. See you next time!');
const success = await this.navigationService.navigateToHome();
if (!success) {
window.location.href = '/';
}
},
error: async (error) => {
console.error('Logout error:', error);
this.showErrorMessage('Logout encountered an issue, but you have been signed out.');
const success = await this.navigationService.navigateToHome();
if (!success) {
window.location.href = '/';
}
}
});
}
/**
* Navigate to user profile/settings
*/
async navigateToProfile(): Promise<void> {
const success = await this.navigationService.navigateToSettings();
if (!success) {
this.showErrorMessage('Unable to open settings page.');
}
}
/**
* Navigate to dashboard
*/
async navigateToDashboard(): Promise<void> {
try {
await this.router.navigate(['/home']);
} catch (error) {
console.error('Navigation error:', error);
this.showErrorMessage('Unable to open dashboard.');
}
}
/**
* Display success message to user
*/
private showSuccessMessage(message: string): void {
this.snackBar.open(message, 'Close', {
duration: 4000,
panelClass: ['success-snackbar'],
horizontalPosition: 'end',
verticalPosition: 'top'
});
}
/**
* Display error message to user
*/
private showErrorMessage(message: string): void {
this.snackBar.open(message, 'Close', {
duration: 6000,
panelClass: ['error-snackbar'],
horizontalPosition: 'end',
verticalPosition: 'top'
});
}
}

View File

@@ -0,0 +1,282 @@
/**
* Authentication Models
* Professional Angular Resume Builder - Enterprise Auth System
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
/**
* User authentication credentials interface
*/
export interface LoginCredentials {
email: string;
password: string;
rememberMe?: boolean;
}
/**
* User registration data interface
*/
export interface RegisterData {
firstName: string;
lastName: string;
email: string;
password: string;
confirmPassword: string;
acceptTerms: boolean;
}
/**
* User registration request interface (for API calls)
*/
export interface RegisterRequest {
firstName: string;
lastName: string;
email: string;
password: string;
acceptTerms: boolean;
newsletter?: boolean;
}
/**
* JWT token response interface
* Compatible with both simple and full token responses
*/
export interface AuthTokens {
accessToken: string;
refreshToken?: string;
expiresIn?: number;
tokenType?: string;
}
/**
* Authenticated user profile interface
*/
export interface AuthUser {
id: number;
email: string;
firstName: string;
lastName: string;
roles: string[];
permissions?: string[];
emailVerified?: boolean;
profileImage?: string;
lastLoginAt?: Date;
createdAt?: Date;
}
/**
* User role enumeration
*/
export enum UserRole {
ADMIN = 'admin',
USER = 'user',
MODERATOR = 'moderator',
GUEST = 'guest'
}
/**
* Authentication response interface
*/
export interface AuthResponse {
message?: string;
accessToken?: string;
user: AuthUser;
requiresTwoFactor: boolean;
reason?: string;
riskScore?: number;
tempToken?: string | null;
deviceInfo?: {
isTrusted: boolean;
deviceName?: string;
lastUsed?: Date;
};
}
/**
* Password reset request interface
*/
export interface PasswordResetRequest {
email: string;
}
/**
* Password reset confirmation interface
*/
export interface PasswordResetConfirmation {
token: string;
newPassword: string;
confirmPassword: string;
}
/**
* Authentication error interface
*/
export interface AuthError {
code: string;
message: string;
details?: any;
timestamp: Date;
}
/**
* JWT token payload interface
*/
export interface JwtPayload {
sub: number; // User ID
email: string;
roles?: string[];
iat: number; // Issued at
exp: number; // Expires at
iss?: string; // Issuer
}
/**
* Session information interface
*/
export interface SessionInfo {
sessionId: string;
userId: string;
deviceInfo: string;
ipAddress: string;
userAgent: string;
createdAt: Date;
expiresAt: Date;
isActive: boolean;
}
/**
* Two-factor authentication setup interface
*/
export interface TwoFactorSetup {
secret: string;
qrCode: string;
backupCodes: string[];
}
/**
* Two-factor authentication verification interface
*/
export interface TwoFactorVerification {
token: string;
backupCode?: string;
}
/**
* Device fingerprint interface
*/
export interface DeviceFingerprint {
userAgent: string;
acceptLanguage: string;
screenResolution: string;
timezone: string;
platform: string;
cookieEnabled: boolean;
doNotTrack: boolean;
fontsHash?: string;
canvasFingerprint?: string;
webglFingerprint?: string;
}
/**
* Device verification request interface
*/
export interface DeviceVerificationRequest {
userId: number;
fingerprint: DeviceFingerprint;
}
/**
* Device verification response interface
*/
export interface DeviceVerificationResponse {
deviceId: string;
isKnownDevice: boolean;
isTrustedDevice: boolean;
requiresTwoFactor: boolean;
device: {
name: string;
platform: string;
registeredAt: Date;
lastUsedAt?: Date;
};
user: {
firstName: string;
lastName: string;
email: string;
fullName: string;
};
}
/**
* Two-factor authentication initiation request interface
*/
export interface TwoFactorInitiationRequest {
userId: number;
method?: 'sms' | 'email' | 'totp'; // Optional - backend can default to email
}
/**
* Two-factor authentication initiation response interface
*/
export interface TwoFactorInitiationResponse {
verificationId: string;
method: string;
message: string;
expiresAt: Date;
attemptsRemaining: number;
durationMinutes: number;
mode: string;
verificationCode?: string; // Only in demo mode
userInfo?: {
firstName: string;
lastName: string;
email: string;
fullName: string;
};
environmentInfo: {
environment: string;
demoModeEnabled: boolean;
timestamp: string;
securityLevel: string;
purpose: string;
};
securityInfo: {
rateLimitingEnabled: boolean;
auditLoggingEnabled: boolean;
sessionId: string;
ipAddress: string;
};
}
/**
* Two-factor authentication verification request interface
*/
export interface TwoFactorVerificationRequest {
code: string;
verificationId: string;
userId: number;
tempToken?: string; // Optional temp token for completing authentication
}
/**
* Two-factor authentication page data interface
* Used for passing secure data to the 2FA page component
*/
export interface TwoFactorPageData {
userId: number;
userEmail: string;
userName: string;
reason?: string;
riskScore?: number;
flowType?: 'device_registration' | 'code_required';
deviceInfo?: {
isTrusted: boolean;
deviceName?: string;
lastUsed?: Date;
};
tempToken?: string | null;
timestamp?: number; // For data expiration checking
}

14
src/app/models/index.ts Normal file
View File

@@ -0,0 +1,14 @@
/**
* Models Barrel Export
* Professional Angular Resume Builder - Centralized Model Exports
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
// Authentication Models
export * from './auth.model';
// Resume Models
export * from './resume.model';

View File

@@ -0,0 +1,241 @@
/**
* Resume Data Models
* Professional Angular Resume Builder
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
/**
* Personal Information Interface
* Contains all personal and contact details
*/
export interface PersonalInfo {
// Basic Identity
firstName: string;
lastName: string;
jobTitle?: string;
profileImage?: string;
// Contact Information
email: string;
phone: string;
street?: string;
houseNumber?: string;
zipCode?: string;
city: string;
country?: string;
location: string; // Keep for backward compatibility
// Personal Details (German CV style)
nationality?: string;
dateOfBirth?: string;
placeOfBirth?: string;
maritalStatus?: string;
// Professional Links
website?: string;
linkedin?: string;
github?: string;
portfolio?: string;
// Professional Summary
summary?: string;
}
/**
* Work Experience Interface
* Professional work history with achievements
*/
export interface Experience {
id: string;
position: string;
company: string;
location?: string;
startDate: string;
endDate?: string; // null/undefined for current position
isCurrentPosition?: boolean;
description: string;
achievements?: string[];
technologies?: string[];
}
/**
* Education Interface
* Academic background and qualifications
*/
export interface Education {
id: string;
degree: string;
institution: string;
location?: string;
startDate: string;
endDate?: string;
grade?: string;
gpa?: string;
description?: string;
achievements?: string[];
relevantCourses?: string[];
}
/**
* Skill Interface with proficiency levels
*/
export interface Skill {
id: string;
name: string;
category: 'technical' | 'soft' | 'language' | 'other';
proficiency?: 'beginner' | 'intermediate' | 'advanced' | 'expert';
proficiencyLevel?: number; // 1-10 scale
yearsOfExperience?: number;
description?: string;
}
/**
* Language Skill Interface
*/
export interface Language {
id: string;
name: string;
proficiency: 'native' | 'fluent' | 'conversational' | 'basic';
level?: string; // e.g., "C2", "B2", "A1"
certifications?: string[];
}
/**
* Certification Interface
*/
export interface Certification {
id: string;
name: string;
issuer: string;
issueDate: string;
expirationDate?: string;
credentialId?: string;
credentialUrl?: string;
description?: string;
}
/**
* Project Interface
*/
export interface Project {
id: string;
name: string;
description: string;
role?: string;
startDate?: string;
endDate?: string;
technologies?: string[];
url?: string;
githubUrl?: string;
achievements?: string[];
status?: 'completed' | 'ongoing' | 'paused';
}
/**
* Award/Achievement Interface
*/
export interface Award {
id: string;
title: string;
issuer: string;
date: string;
description?: string;
}
/**
* Reference Interface
*/
export interface Reference {
id: string;
name: string;
position: string;
company: string;
email?: string;
phone?: string;
relationship: string;
}
/**
* Complete Resume Data Interface
* Main data structure for the entire resume
*/
export interface ResumeData {
// Personal Information
personalInfo: PersonalInfo;
// Professional Experience
experience: Experience[];
// Education
education: Education[];
// Skills
skills: Skill[];
// Languages
languages: Language[];
// Certifications
certifications: Certification[];
// Projects
projects: Project[];
// Awards
awards: Award[];
// References
references: Reference[];
// Metadata
createdAt?: Date;
updatedAt?: Date;
version?: string;
template?: string;
}
/**
* Form Step Interface for Resume Builder
*/
export interface ResumeStep {
id: string;
title: string;
description: string;
icon: string;
completed: boolean;
optional?: boolean;
}
/**
* Template Configuration Interface
*/
export interface ResumeTemplate {
id: string;
name: string;
description: string;
preview?: string;
colors: {
primary: string;
secondary: string;
accent?: string;
};
fonts: {
heading: string;
body: string;
};
layout: 'modern' | 'classic' | 'creative' | 'minimal';
}
/**
* Export options for PDF generation
*/
export interface ExportOptions {
format: 'pdf' | 'png' | 'jpg';
template?: string;
includeSections?: string[];
pageSize?: 'A4' | 'Letter';
orientation?: 'portrait' | 'landscape';
}

View File

@@ -0,0 +1,74 @@
/**
* 403 Forbidden Error Component Styles
* Professional Angular Resume Builder - Access Forbidden Page
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
* @description Professional 403 error page for unauthorized access attempts
*/
/* ==========================================================================
Error Container & Layout
========================================================================== */
.error-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 60vh;
padding: 2rem;
}
.error-card {
max-width: 500px;
width: 100%;
text-align: center;
padding: 2rem;
}
.error-icon {
margin-bottom: 1rem;
}
.error-icon mat-icon {
font-size: 4rem;
height: 4rem;
width: 4rem;
color: var(--color-warning);
}
h1 {
color: var(--color-text-primary);
margin-bottom: 1rem;
font-weight: 500;
}
.error-message {
color: var(--color-text-secondary);
margin-bottom: 2rem;
line-height: 1.5;
}
.error-actions {
display: flex;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
}
.ml-2 {
margin-left: 0.5rem;
}
@media (max-width: 768px) {
.error-actions {
flex-direction: column;
align-items: center;
}
.ml-2 {
margin-left: 0;
margin-top: 0.5rem;
}
}

View File

@@ -0,0 +1,23 @@
<div class="error-container">
<mat-card class="error-card">
<mat-card-content>
<div class="error-icon">
<mat-icon>block</mat-icon>
</div>
<h1>403 - Access Denied</h1>
<p class="error-message">
You don't have permission to access this resource. Please contact an administrator if you believe this is an error.
</p>
<div class="error-actions">
<button mat-raised-button color="primary" routerLink="/home">
<mat-icon>home</mat-icon>
Go Home
</button>
<button mat-stroked-button routerLink="/login" class="ml-2">
<mat-icon>login</mat-icon>
Sign In
</button>
</div>
</mat-card-content>
</mat-card>
</div>

View File

@@ -0,0 +1,30 @@
/**
* Forbidden Error Component (403)
* Professional error page for access denied scenarios
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatCardModule } from '@angular/material/card';
@Component({
selector: 'app-forbidden',
standalone: true,
imports: [
CommonModule,
RouterModule,
MatButtonModule,
MatIconModule,
MatCardModule
],
templateUrl: './forbidden.component.html',
styleUrls: ['./forbidden.component.css']
})
export class ForbiddenComponent { }

View File

@@ -0,0 +1,74 @@
/**
* 404 Not Found Error Component Styles
* Professional Angular Resume Builder - 404 Error Page
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
* @description Professional 404 error page with navigation options and user-friendly messaging
*/
/* ==========================================================================
Error Container & Layout
========================================================================== */
.error-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 60vh;
padding: 2rem;
}
.error-card {
max-width: 500px;
width: 100%;
text-align: center;
padding: 2rem;
}
.error-icon {
margin-bottom: 1rem;
}
.error-icon mat-icon {
font-size: 4rem;
height: 4rem;
width: 4rem;
color: var(--color-error);
}
h1 {
color: var(--color-text-primary);
margin-bottom: 1rem;
font-weight: 500;
}
.error-message {
color: var(--color-text-secondary);
margin-bottom: 2rem;
line-height: 1.5;
}
.error-actions {
display: flex;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
}
.ml-2 {
margin-left: 0.5rem;
}
@media (max-width: 768px) {
.error-actions {
flex-direction: column;
align-items: center;
}
.ml-2 {
margin-left: 0;
margin-top: 0.5rem;
}
}

View File

@@ -0,0 +1,23 @@
<div class="error-container">
<mat-card class="error-card">
<mat-card-content>
<div class="error-icon">
<mat-icon>error_outline</mat-icon>
</div>
<h1>404 - Seite nicht gefunden</h1>
<p class="error-message">
Entschuldigung, die gesuchte Seite existiert nicht oder wurde verschoben.
</p>
<div class="error-actions">
<button mat-raised-button color="primary" routerLink="/home">
<mat-icon>home</mat-icon>
Zur Startseite
</button>
<button mat-stroked-button routerLink="/builder" class="ml-2">
<mat-icon>build</mat-icon>
Lebenslauf Generator
</button>
</div>
</mat-card-content>
</mat-card>
</div>

View File

@@ -0,0 +1,30 @@
/**
* Not Found Error Component (404)
* Professional error page for missing routes
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatCardModule } from '@angular/material/card';
@Component({
selector: 'app-not-found',
standalone: true,
imports: [
CommonModule,
RouterModule,
MatButtonModule,
MatIconModule,
MatCardModule
],
templateUrl: './not-found.component.html',
styleUrls: ['./not-found.component.css']
})
export class NotFoundComponent { }

View File

@@ -0,0 +1,74 @@
/**
* 500 Server Error Component Styles
* Professional Angular Resume Builder - Server Error Page
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
* @description Professional 500 error page for server-side errors with recovery options
*/
/* ==========================================================================
Error Container & Layout
========================================================================== */
.error-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 60vh;
padding: 2rem;
}
.error-card {
max-width: 500px;
width: 100%;
text-align: center;
padding: 2rem;
}
.error-icon {
margin-bottom: 1rem;
}
.error-icon mat-icon {
font-size: 4rem;
height: 4rem;
width: 4rem;
color: var(--color-error);
}
h1 {
color: var(--color-text-primary);
margin-bottom: 1rem;
font-weight: 500;
}
.error-message {
color: var(--color-text-secondary);
margin-bottom: 2rem;
line-height: 1.5;
}
.error-actions {
display: flex;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
}
.ml-2 {
margin-left: 0.5rem;
}
@media (max-width: 768px) {
.error-actions {
flex-direction: column;
align-items: center;
}
.ml-2 {
margin-left: 0;
margin-top: 0.5rem;
}
}

View File

@@ -0,0 +1,23 @@
<div class="error-container">
<mat-card class="error-card">
<mat-card-content>
<div class="error-icon">
<mat-icon>warning</mat-icon>
</div>
<h1>500 - Server Error</h1>
<p class="error-message">
We're experiencing technical difficulties. Our team has been notified and is working to resolve the issue.
</p>
<div class="error-actions">
<button mat-raised-button color="primary" routerLink="/home">
<mat-icon>home</mat-icon>
Go Home
</button>
<button mat-stroked-button (click)="reloadPage()" class="ml-2">
<mat-icon>refresh</mat-icon>
Try Again
</button>
</div>
</mat-card-content>
</mat-card>
</div>

View File

@@ -0,0 +1,37 @@
/**
* Server Error Component (500)
* Professional error page for internal server errors
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatCardModule } from '@angular/material/card';
@Component({
selector: 'app-server-error',
standalone: true,
imports: [
CommonModule,
RouterModule,
MatButtonModule,
MatIconModule,
MatCardModule
],
templateUrl: './server-error.component.html',
styleUrls: ['./server-error.component.css']
})
export class ServerErrorComponent {
/**
* Reload the current page
*/
reloadPage(): void {
window.location.reload();
}
}

View File

@@ -0,0 +1,230 @@
/**
* Home Page Component Styles
* Professional Angular Resume Builder - Landing Page & Dashboard Overview
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
* @description Main landing page with hero section, feature highlights, and call-to-action
*/
/* ==========================================================================
Home Container & Layout
========================================================================== */
.home-container {
max-width: 1200px;
margin: 0 auto;
padding: 32px 24px 0 24px;
}
/* ==========================================================================
Hero Section
========================================================================== */
.hero-section {
margin-bottom: 48px;
}
.hero-card {
text-align: center;
padding: 48px 24px;
background: var(--gradient-primary);
color: white;
border-radius: var(--border-radius-xl);
}
.hero-content {
max-width: 600px;
margin: 0 auto;
}
.hero-icon .large-icon {
font-size: 72px;
width: 72px;
height: 72px;
margin-bottom: 24px;
color: var(--color-text-white-muted);
}
.hero-title {
font-size: 2.5rem;
font-weight: 300;
margin-bottom: 16px;
line-height: 1.2;
}
.hero-subtitle {
font-size: 1.25rem;
margin-bottom: 16px;
opacity: 0.9;
line-height: 1.5;
}
.author-signature {
font-size: 1rem;
margin-bottom: 32px;
opacity: 0.8;
}
.hero-actions {
display: flex;
gap: 16px;
justify-content: center;
flex-wrap: wrap;
}
.primary-cta, .secondary-cta {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
font-size: 1rem;
}
.features-section {
margin-bottom: 48px;
}
.section-header {
text-align: center;
margin-bottom: 32px;
}
.section-header h2 {
font-size: 2rem;
font-weight: 400;
margin-bottom: 8px;
color: var(--color-text-primary);
}
.section-header p {
font-size: 1.125rem;
color: var(--color-text-secondary);
}
.features-grid {
margin-bottom: 24px;
}
.feature-card {
height: 100%;
padding: 24px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.feature-avatar {
background-color: var(--color-primary);
color: white;
}
.feature-highlights {
list-style: none;
padding: 0;
margin: 16px 0 0;
}
.feature-highlights li {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
font-size: 0.9rem;
}
.check-icon {
color: var(--color-success);
font-size: 16px;
}
.tech-section {
margin-bottom: 48px;
}
.tech-card {
padding: 24px;
}
.tech-stack {
display: flex;
flex-wrap: wrap;
gap: 24px;
margin: 24px 0;
justify-content: center;
}
.tech-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 16px;
border-radius: 8px;
background-color: var(--color-surface);
}
.tech-item mat-icon {
color: var(--color-primary);
font-size: 32px;
width: 32px;
height: 32px;
}
.tech-description {
text-align: center;
color: var(--color-text-secondary);
margin-top: 24px;
line-height: 1.6;
}
/* Responsive Design */
@media (max-width: 768px) {
.home-container {
padding: 0 16px;
}
.hero-card {
padding: 32px 16px;
}
.hero-title {
font-size: 2rem;
}
.hero-actions {
flex-direction: column;
align-items: center;
}
.features-grid {
grid-template-columns: 1fr !important;
}
.tech-stack {
justify-content: space-around;
}
}
@media (max-width: 960px) {
.features-grid {
grid-template-columns: repeat(2, 1fr) !important;
}
}
@media (max-width: 768px) {
.home-container {
padding: 24px 16px 0 16px;
}
.hero-card {
padding: 32px 16px;
}
}
@media (max-width: 480px) {
.home-container {
padding: 16px 12px 0 12px;
}
}

View File

@@ -0,0 +1,86 @@
<!-- Professional Home Page -->
<div class="home-container">
<!-- Hero Section -->
<section class="hero-section">
<mat-card class="hero-card">
<mat-card-content>
<div class="hero-content">
<div class="hero-icon">
<mat-icon class="large-icon">assignment_ind</mat-icon>
</div>
<h1 class="hero-title">Professioneller Lebenslauf Generator</h1>
<p class="hero-subtitle">
Erstellen Sie beeindruckende, professionelle Lebensläufe mit modernem Design und fachkundiger Anleitung
</p>
<p class="author-signature">
<strong>Created by David Valera Melendez</strong> | Made in Germany 🇩🇪
</p>
<div class="hero-actions">
<button mat-raised-button color="primary" routerLink="/builder" class="primary-cta">
<mat-icon>edit</mat-icon>
Starten Sie Ihren Lebenslauf
</button>
<button mat-stroked-button routerLink="/preview" class="secondary-cta">
<mat-icon>visibility</mat-icon>
Beispiel-Lebenslauf ansehen
</button>
</div>
</div>
</mat-card-content>
</mat-card>
</section>
<!-- Features Section -->
<section class="features-section">
<div class="section-header">
<h2>Warum unseren Lebenslauf Generator wählen?</h2>
<p>Professionelle Funktionen für moderne Jobsuchende</p>
</div>
<mat-grid-list cols="3" rowHeight="300px" gutterSize="24px" class="features-grid">
<mat-grid-tile *ngFor="let feature of features">
<mat-card class="feature-card">
<mat-card-header>
<div mat-card-avatar class="feature-avatar">
<mat-icon>{{ feature.icon }}</mat-icon>
</div>
<mat-card-title>{{ feature.title }}</mat-card-title>
</mat-card-header>
<mat-card-content>
<p>{{ feature.description }}</p>
<ul class="feature-highlights">
<li *ngFor="let highlight of feature.highlights">
<mat-icon class="check-icon">check_circle</mat-icon>
{{ highlight }}
</li>
</ul>
</mat-card-content>
</mat-card>
</mat-grid-tile>
</mat-grid-list>
</section>
<!-- Technology Stack Section -->
<section class="tech-section">
<mat-card class="tech-card">
<mat-card-header>
<mat-card-title>
<mat-icon>code</mat-icon>
Gebaut mit moderner Technologie
</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="tech-stack">
<div class="tech-item" *ngFor="let tech of techStack">
<mat-icon>{{ tech.icon }}</mat-icon>
<span>{{ tech.name }}</span>
</div>
</div>
<p class="tech-description">
Dieser Lebenslauf Generator wurde mit Angular 17, Material UI und TypeScript entwickelt,
um eine moderne, responsive und professionelle Benutzererfahrung zu gewährleisten.
</p>
</mat-card-content>
</mat-card>
</section>
</div>

View File

@@ -0,0 +1,85 @@
/**
* Home Page Component
* Professional Angular Resume Builder
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatGridListModule } from '@angular/material/grid-list';
/**
* Home page component - Landing page for the resume builder
* Features professional introduction and navigation to builder
*/
@Component({
selector: 'app-home',
standalone: true,
imports: [
CommonModule,
RouterModule,
MatCardModule,
MatButtonModule,
MatIconModule,
MatGridListModule
],
templateUrl: './home.component.html',
styleUrls: ['./home.component.css']
})
export class HomeComponent {
/**
* Feature list for the home page
*/
features = [
{
title: 'Professionelle Vorlagen',
icon: 'design_services',
description: 'Wählen Sie aus professionell gestalteten Vorlagen, die für ATS-Systeme und moderne Personalgewinnung optimiert sind.',
highlights: [
'ATS-freundliche Formatierung',
'Moderne, saubere Designs',
'Anpassbare Themen',
'Druckfertige Layouts'
]
},
{
title: 'Intelligenter Generator',
icon: 'auto_awesome',
description: 'Schritt-für-Schritt-Anleitung mit intelligenten Vorschlägen und Echtzeit-Validierung.',
highlights: [
'Schritt-für-Schritt-Prozess',
'Live-Vorschau',
'Formular-Validierung',
'Intelligente Vorschläge'
]
},
{
title: 'Export & Teilen',
icon: 'cloud_download',
description: 'Exportieren Sie Ihren Lebenslauf als PDF oder teilen Sie ihn online mit professioneller Formatierung.',
highlights: [
'PDF-Export',
'Mehrere Formate',
'Cloud-Speicher',
'Einfaches Teilen'
]
}
];
/**
* Technology stack used in the application
*/
techStack = [
{ name: 'Angular 17', icon: 'web' },
{ name: 'Material UI', icon: 'palette' },
{ name: 'TypeScript', icon: 'code' },
{ name: 'Responsive Design', icon: 'devices' }
];
}

View File

@@ -0,0 +1,707 @@
/**
* Login Page Styles
* Professional Angular Resume Builder - Enterprise Auth System
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
/* ==========================================================================
Login Container & Layout
========================================================================== */
.login-container {
min-height: 100vh;
width: 100%;
background: var(--gradient-primary);
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
box-sizing: border-box;
position: relative;
}
.login-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grid" width="10" height="10" patternUnits="userSpaceOnUse"><path d="M 10 0 L 0 0 0 10" fill="none" stroke="var(--color-surface-overlay)" stroke-width="0.5"/></pattern></defs><rect width="100" height="100" fill="url(%23grid)"/></svg>');
pointer-events: none;
}
.login-wrapper {
width: 100%;
max-width: 480px;
position: relative;
}
/* ==========================================================================
Brand Header
========================================================================== */
.brand-header {
text-align: center;
margin-bottom: 32px;
color: white;
}
.brand-logo {
margin-bottom: 16px;
}
.brand-icon {
font-size: 48px;
height: 48px;
width: 48px;
background: var(--color-surface-overlay);
border-radius: 50%;
padding: 12px;
backdrop-filter: blur(10px);
border: 1px solid var(--color-surface-overlay-light);
}
.brand-title {
margin: 0 0 8px 0;
font-size: 32px;
font-weight: 600;
text-shadow: 0 2px 4px var(--color-text-shadow);
letter-spacing: -0.5px;
}
.brand-subtitle {
margin: 0;
font-size: 16px;
opacity: 0.9;
font-weight: 300;
}
/* ==========================================================================
Login Card
========================================================================== */
.login-card {
background: var(--color-background-white-overlay);
backdrop-filter: blur(20px);
border-radius: 16px;
box-shadow:
0 20px 40px var(--color-shadow-light),
0 4px 8px var(--color-shadow-subtle);
border: 1px solid var(--color-border-white-subtle);
overflow: hidden;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.login-card:hover {
transform: translateY(-2px);
box-shadow:
0 24px 48px var(--color-shadow-deep),
0 8px 16px var(--color-shadow-card);
}
/* ==========================================================================
Card Header
========================================================================== */
.login-card .mat-mdc-card-header {
padding: 32px 32px 16px 32px;
background: var(--gradient-background);
border-bottom: 1px solid var(--color-border-dark-subtle);
}
.login-avatar {
background: var(--gradient-primary);
color: white;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
width: 56px;
height: 56px;
}
.login-avatar mat-icon {
font-size: 28px;
height: 28px;
width: 28px;
}
.mat-mdc-card-title {
font-size: 28px !important;
font-weight: 700 !important;
color: var(--color-text-gray-900) !important;
margin-bottom: 4px !important;
letter-spacing: -0.5px;
}
.mat-mdc-card-subtitle {
font-size: 16px !important;
color: var(--color-text-slate) !important;
font-weight: 400 !important;
}
/* ==========================================================================
Card Content & Form
========================================================================== */
.mat-mdc-card-content {
padding: 32px;
}
.login-form {
display: flex;
flex-direction: column;
gap: 24px;
}
.full-width {
width: 100%;
}
/* ==========================================================================
Form Fields
========================================================================== */
.mat-mdc-form-field {
font-family: 'Roboto', sans-serif;
}
.mat-mdc-form-field.mat-form-field-appearance-outline .mat-mdc-form-field-outline {
color: var(--color-disabled-overlay);
}
.mat-mdc-form-field.mat-focused .mat-mdc-form-field-outline-thick {
color: var(--color-purple);
}
.mat-mdc-form-field-label {
color: var(--color-text-slate);
font-weight: 500;
}
.mat-mdc-input-element {
font-size: 16px;
font-weight: 400;
color: var(--color-text-gray-900);
}
.mat-mdc-input-element::placeholder {
color: var(--color-text-slate-light);
font-weight: 400;
}
/* Error States */
.mat-mdc-form-field.mat-form-field-invalid .mat-mdc-form-field-outline-thick {
color: var(--color-error);
}
.mat-mdc-form-field-error {
font-size: 13px;
font-weight: 500;
}
.error {
border-color: var(--color-error) !important;
}
/* ==========================================================================
Form Options
========================================================================== */
.form-options {
display: flex;
justify-content: space-between;
align-items: center;
margin: 8px 0;
}
.mat-mdc-checkbox {
margin-right: 16px;
}
.mat-mdc-checkbox-label {
color: var(--color-text-slate-dark);
font-weight: 500;
}
.forgot-password-link {
color: var(--color-purple);
text-decoration: none;
font-weight: 500;
font-size: 14px;
cursor: pointer;
transition: color 0.2s ease;
}
.forgot-password-link:hover {
color: var(--color-indigo);
text-decoration: underline;
}
/* ==========================================================================
Login Button
========================================================================== */
.login-button {
height: 48px;
font-size: 16px;
font-weight: 600;
letter-spacing: 0.5px;
background: var(--gradient-primary);
border: none;
box-shadow: 0 4px 12px var(--color-purple-glow-medium);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.login-button:hover:not([disabled]) {
transform: translateY(-2px);
box-shadow: 0 6px 20px var(--color-purple-glow-strong);
}
.login-button:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
box-shadow: 0 2px 4px var(--color-purple-glow-light);
}
.button-spinner {
margin-right: 8px;
}
/* ==========================================================================
Demo Login Button
========================================================================== */
.demo-button {
height: 42px;
font-size: 14px;
font-weight: 500;
letter-spacing: 0.25px;
margin-top: 12px;
border: 2px solid var(--color-indigo-light);
color: var(--color-indigo);
background: var(--color-surface-elevated);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.demo-button:hover:not([disabled]) {
background: var(--color-indigo-light);
color: var(--color-text-on-primary);
transform: translateY(-1px);
box-shadow: 0 4px 12px var(--color-indigo-glow-light);
}
.demo-button:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.demo-icon {
font-size: 18px;
margin-right: 8px;
}
.demo-info {
text-align: center;
color: var(--color-text-secondary);
font-size: 12px;
margin: 8px 0 0 0;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.demo-info-icon {
font-size: 14px;
opacity: 0.7;
}
/* ==========================================================================
Warnings & Alerts
========================================================================== */
.lockout-warning {
background: var(--gradient-error-light);
border: 1px solid var(--color-error-border-light);
border-radius: 12px;
padding: 20px;
margin-bottom: 24px;
display: flex;
align-items: flex-start;
gap: 12px;
}
.warning-icon {
color: var(--color-error);
font-size: 24px;
height: 24px;
width: 24px;
margin-top: 2px;
}
.lockout-message h4 {
margin: 0 0 8px 0;
color: var(--color-error-text-dark);
font-size: 16px;
font-weight: 600;
}
.lockout-message p {
margin: 0;
color: var(--color-error);
font-size: 14px;
line-height: 1.5;
}
.attempts-warning {
background: var(--gradient-warning-light);
border: 1px solid var(--color-warning-border-light);
border-radius: 8px;
padding: 12px 16px;
margin-top: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.attempts-warning .warning-icon {
color: var(--color-warning-text);
font-size: 20px;
height: 20px;
width: 20px;
}
.attempts-warning span {
color: var(--color-warning-text-dark);
font-size: 13px;
font-weight: 500;
}
/* ==========================================================================
Card Actions
========================================================================== */
.card-actions {
padding: 0;
display: flex;
flex-direction: column;
}
/* ==========================================================================
Social Login Section
========================================================================== */
.social-login {
padding: 24px 32px;
text-align: center;
border-bottom: 1px solid var(--color-border-dark-subtle);
}
.social-text {
margin: 0 0 16px 0;
color: var(--color-text-slate);
font-size: 14px;
font-weight: 500;
}
.social-buttons {
display: flex;
gap: 12px;
margin-bottom: 12px;
}
.social-button {
flex: 1;
height: 44px;
border: 2px solid var(--color-slate-300);
border-radius: 8px;
font-weight: 500;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.social-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.google-button:not(:disabled):hover {
border-color: var(--color-google);
color: var(--color-google);
background: var(--color-google-bg);
}
.microsoft-button:not(:disabled):hover {
border-color: var(--color-microsoft);
color: var(--color-microsoft);
background: var(--color-microsoft-bg);
}
.social-icon {
font-size: 18px;
height: 18px;
width: 18px;
}
.social-disabled-note {
margin: 0;
color: var(--color-text-slate-light);
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
.info-icon {
font-size: 14px;
height: 14px;
width: 14px;
}
/* ==========================================================================
Register Section
========================================================================== */
.register-section {
padding: 24px 32px;
text-align: center;
background: var(--color-slate-50);
}
.register-text {
margin: 0 0 12px 0;
color: var(--color-text-slate);
font-size: 14px;
font-weight: 500;
}
.register-button {
font-weight: 600;
letter-spacing: 0.5px;
background: transparent;
color: var(--color-purple);
border: 2px solid var(--color-purple-brand);
height: 40px;
border-radius: 8px;
transition: all 0.2s ease;
}
.register-button:hover {
background: var(--color-purple-brand);
color: white;
transform: translateY(-1px);
}
/* ==========================================================================
Security Info
========================================================================== */
.security-info {
margin-top: 24px;
display: flex;
justify-content: center;
gap: 24px;
flex-wrap: wrap;
padding: 0 16px;
}
.security-item {
display: flex;
align-items: center;
gap: 8px;
color: var(--color-text-white-muted);
font-size: 13px;
font-weight: 500;
}
.security-icon {
font-size: 16px;
height: 16px;
width: 16px;
opacity: 0.8;
}
/* ==========================================================================
Footer
========================================================================== */
.login-footer {
margin-top: 32px;
text-align: center;
color: var(--color-text-white-light);
}
.footer-text {
margin: 0 0 12px 0;
font-size: 13px;
}
.footer-links {
display: flex;
justify-content: center;
gap: 16px;
flex-wrap: wrap;
}
.footer-link {
color: var(--color-text-white-subtle);
text-decoration: none;
font-size: 12px;
transition: color 0.2s ease;
}
.footer-link:hover {
color: white;
text-decoration: underline;
}
/* ==========================================================================
Responsive Design
========================================================================== */
@media (max-width: 640px) {
.login-container {
padding: 16px;
}
.login-wrapper {
max-width: 100%;
}
.brand-title {
font-size: 28px;
}
.brand-subtitle {
font-size: 14px;
}
.mat-mdc-card-content {
padding: 24px;
}
.login-card .mat-mdc-card-header {
padding: 24px 24px 16px 24px;
}
.social-login,
.register-section {
padding: 20px 24px;
}
.social-buttons {
flex-direction: column;
gap: 8px;
}
.security-info {
flex-direction: column;
align-items: center;
gap: 12px;
}
.footer-links {
flex-direction: column;
gap: 8px;
}
}
@media (max-width: 480px) {
.form-options {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.mat-mdc-card-title {
font-size: 24px !important;
}
}
/* ==========================================================================
Dark Mode Support (Future Enhancement)
========================================================================== */
@media (prefers-color-scheme: dark) {
.login-card {
background: var(--color-overlay-dark);
color: white;
}
.mat-mdc-card-title {
color: white !important;
}
.mat-mdc-card-subtitle {
color: var(--color-text-slate-light) !important;
}
.login-card .mat-mdc-card-header {
background: linear-gradient(135deg, var(--color-text-slate-700) 0%, var(--color-text-slate-600) 100%);
}
}
/* ==========================================================================
Animation Enhancements
========================================================================== */
@keyframes slideInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.login-wrapper {
animation: slideInUp 0.6s ease-out;
}
/* ==========================================================================
Focus Styles for Accessibility
========================================================================== */
.forgot-password-link:focus,
.register-button:focus,
.footer-link:focus {
outline: 2px solid var(--color-purple-brand);
outline-offset: 2px;
border-radius: 4px;
}
/* ==========================================================================
High Contrast Mode Support
========================================================================== */
@media (prefers-contrast: high) {
.login-card {
border: 2px solid var(--color-black);
}
.mat-mdc-form-field.mat-focused .mat-mdc-form-field-outline-thick {
color: var(--color-black);
}
.login-button {
background: var(--color-black);
color: var(--color-background);
}
}

View File

@@ -0,0 +1,221 @@
<!--
Login Page Template
Professional Angular Resume Builder - Enterprise Auth System
@author David Valera Melendez <david@valera-melendez.de>
@created 2025-08-08
@location Made in Germany 🇩🇪
-->
<div class="login-container">
<div class="login-wrapper">
<!-- Brand Header -->
<div class="brand-header">
<div class="brand-logo">
<mat-icon class="brand-icon">account_circle</mat-icon>
</div>
<h1 class="brand-title">Lebenslauf Generator</h1>
<p class="brand-subtitle">Professionelle Lebenslauf-Erstellungsplattform</p>
</div>
<!-- Login Form Card -->
<mat-card class="login-card">
<mat-card-header>
<div mat-card-avatar class="login-avatar">
<mat-icon>login</mat-icon>
</div>
<mat-card-title>Willkommen zurück</mat-card-title>
<mat-card-subtitle>Melden Sie sich bei Ihrem Konto an</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<!-- Account Lockout Warning -->
<div *ngIf="isLocked" class="lockout-warning">
<mat-icon class="warning-icon">lock</mat-icon>
<div class="lockout-message">
<h4>Konto vorübergehend gesperrt</h4>
<p>Zu viele fehlgeschlagene Anmeldeversuche. Bitte versuchen Sie es in {{ getRemainingLockoutTime() }} Minuten erneut.</p>
</div>
</div>
<!-- Login Form -->
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()" class="login-form">
<!-- Email Field -->
<mat-form-field appearance="outline" class="full-width">
<mat-label>E-Mail-Adresse</mat-label>
<input
matInput
type="email"
formControlName="email"
placeholder="Geben Sie Ihre E-Mail ein"
autocomplete="email"
[class.error]="hasFieldError('email')"
>
<mat-icon matSuffix>email</mat-icon>
<mat-error *ngIf="hasFieldError('email')">
{{ getFieldError('email') }}
</mat-error>
</mat-form-field>
<!-- Password Field -->
<mat-form-field appearance="outline" class="full-width">
<mat-label>Passwort</mat-label>
<input
matInput
[type]="hidePassword ? 'password' : 'text'"
formControlName="password"
placeholder="Geben Sie Ihr Passwort ein"
autocomplete="current-password"
[class.error]="hasFieldError('password')"
>
<button
type="button"
mat-icon-button
matSuffix
(click)="togglePasswordVisibility()"
[attr.aria-label]="'Hide password'"
[attr.aria-pressed]="hidePassword"
>
<mat-icon>{{ hidePassword ? 'visibility_off' : 'visibility' }}</mat-icon>
</button>
<mat-error *ngIf="hasFieldError('password')">
{{ getFieldError('password') }}
</mat-error>
</mat-form-field>
<!-- Remember Me & Forgot Password -->
<div class="form-options">
<mat-checkbox formControlName="rememberMe">
Angemeldet bleiben
</mat-checkbox>
<a
class="forgot-password-link"
(click)="navigateToPasswordReset()"
tabindex="0"
>
Passwort vergessen?
</a>
</div>
<!-- Login Button -->
<button
mat-raised-button
color="primary"
type="submit"
class="login-button full-width"
[disabled]="isLoading || isLocked || loginForm.invalid"
>
<mat-spinner
*ngIf="isLoading"
diameter="20"
class="button-spinner">
</mat-spinner>
<span *ngIf="!isLoading">Anmelden</span>
<span *ngIf="isLoading">Anmelden...</span>
</button>
<!-- Demo Login Button -->
<button
*ngIf="isDemoEnabled()"
mat-stroked-button
color="accent"
type="button"
class="demo-button full-width"
[disabled]="isLoading || isLocked"
(click)="fillDemoCredentials()"
matTooltip="Formular mit Demo-Daten füllen"
matTooltipPosition="above"
>
<mat-icon class="demo-icon">auto_fix_high</mat-icon>
<span>Demo-Daten ausfüllen</span>
</button>
<!-- Demo Info Text -->
<p *ngIf="isDemoEnabled()" class="demo-info">
<mat-icon class="demo-info-icon">info_outline</mat-icon>
Klicken Sie zum Ausfüllen des Formulars und drücken Sie dann "Anmelden" zum Einloggen
</p>
<!-- Failed Attempts Warning -->
<div *ngIf="loginAttempts > 0 && loginAttempts < maxLoginAttempts" class="attempts-warning">
<mat-icon class="warning-icon">warning</mat-icon>
<span>{{ loginAttempts }}/{{ maxLoginAttempts }} fehlgeschlagene Versuche.
{{ maxLoginAttempts - loginAttempts }} Versuche verbleibend.</span>
</div>
</form>
</mat-card-content>
<mat-divider></mat-divider>
<mat-card-actions class="card-actions">
<!-- Social Login Options (Future Enhancement) -->
<div class="social-login">
<p class="social-text">Oder fortfahren mit</p>
<div class="social-buttons">
<button
mat-button
class="social-button google-button"
disabled
>
<mat-icon class="social-icon">login</mat-icon>
Google
</button>
<button
mat-button
class="social-button microsoft-button"
disabled
>
<mat-icon class="social-icon">business</mat-icon>
Microsoft
</button>
</div>
<p class="social-disabled-note">
<mat-icon class="info-icon">info</mat-icon>
Social Login demnächst verfügbar
</p>
</div>
<!-- Registration Link -->
<div class="register-section">
<p class="register-text">Haben Sie noch kein Konto?</p>
<button
mat-button
color="accent"
(click)="navigateToRegister()"
class="register-button"
>
Konto erstellen
</button>
</div>
</mat-card-actions>
</mat-card>
<!-- Security Features Info -->
<div class="security-info">
<div class="security-item">
<mat-icon class="security-icon">security</mat-icon>
<span>256-bit SSL-Verschlüsselung</span>
</div>
<div class="security-item">
<mat-icon class="security-icon">verified_user</mat-icon>
<span>Zwei-Faktor-Authentifizierung</span>
</div>
<div class="security-item">
<mat-icon class="security-icon">privacy_tip</mat-icon>
<span>DSGVO-konform</span>
</div>
</div>
<!-- Footer -->
<div class="login-footer">
<p class="footer-text">
© 2025 David Valera Melendez. Made in Germany 🇩🇪
</p>
<div class="footer-links">
<a href="#" class="footer-link">Datenschutzerklärung</a>
<a href="#" class="footer-link">Nutzungsbedingungen</a>
<a href="#" class="footer-link">Support</a>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,525 @@
/**
* Login Page Component
* Professional Angular Resume Builder - Enterprise Auth System
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule, Router, ActivatedRoute } from '@angular/router';
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Subject } from 'rxjs';
import { takeUntil, finalize, filter, take } from 'rxjs/operators';
// Angular Material Modules
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 { MatCheckboxModule } from '@angular/material/checkbox';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSnackBarModule, MatSnackBar } from '@angular/material/snack-bar';
import { MatDividerModule } from '@angular/material/divider';
import { MatTooltipModule } from '@angular/material/tooltip';
// Auth Models and Services
import { AuthService } from '../../services/auth.service';
import { LoginCredentials, AuthError, AuthResponse } from '../../models';
// Environment Configuration
import { environment } from '../../../environments/environment';
/**
* Professional Login Component with Enterprise Security Features
* Includes form validation, error handling, and security best practices
*/
@Component({
selector: 'app-login',
standalone: true,
imports: [
CommonModule,
RouterModule,
ReactiveFormsModule,
MatCardModule,
MatFormFieldModule,
MatInputModule,
MatButtonModule,
MatIconModule,
MatCheckboxModule,
MatProgressSpinnerModule,
MatSnackBarModule,
MatDividerModule,
MatTooltipModule
],
templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit, OnDestroy {
// Form Management
loginForm!: FormGroup;
// State Management
isLoading = false;
hidePassword = true;
loginAttempts = 0;
maxLoginAttempts = 5;
isLocked = false;
lockoutEndTime: Date | null = null;
// Navigation
returnUrl = '/dashboard';
// Cleanup
private destroy$ = new Subject<void>();
constructor(
private formBuilder: FormBuilder,
private authService: AuthService,
private router: Router,
private route: ActivatedRoute,
private snackBar: MatSnackBar
) {
this.initializeForm();
}
ngOnInit(): void {
this.setupReturnUrl();
this.subscribeToAuthState();
this.checkAccountLockout();
this.handleTwoFactorReturn();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
/**
* Initialize reactive form with validators
*/
private initializeForm(): void {
this.loginForm = this.formBuilder.group({
email: ['', [
Validators.required,
Validators.email,
Validators.maxLength(255)
]],
password: ['', [
Validators.required,
Validators.minLength(8),
Validators.maxLength(128)
]],
rememberMe: [false]
});
}
/**
* Setup return URL from query parameters
*/
private setupReturnUrl(): void {
this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/dashboard';
}
/**
* Subscribe to authentication state changes
*/
private subscribeToAuthState(): void {
this.authService.isLoading$
.pipe(takeUntil(this.destroy$))
.subscribe(loading => {
this.isLoading = loading;
});
}
/**
* Handle return from 2FA completion
*/
private handleTwoFactorReturn(): void {
this.route.queryParams.subscribe(params => {
if (params['twoFactorComplete'] === 'true' && params['email']) {
// Pre-fill the email field and show success message
this.loginForm.patchValue({ email: params['email'] });
this.snackBar.open('Device registered successfully! Please login again.', 'Close', {
duration: 5000,
panelClass: ['success-snackbar']
});
// Clear the query parameters
this.router.navigate([], {
relativeTo: this.route,
queryParams: {},
replaceUrl: true
});
}
});
}
/**
* Check if account is locked due to failed attempts
*/
private checkAccountLockout(): void {
const lockoutData = localStorage.getItem('david_auth_lockout');
if (lockoutData) {
const { endTime, attempts } = JSON.parse(lockoutData);
this.lockoutEndTime = new Date(endTime);
if (new Date() < this.lockoutEndTime) {
this.isLocked = true;
this.loginAttempts = attempts;
this.startLockoutTimer();
} else {
// Lockout expired, clear it
localStorage.removeItem('david_auth_lockout');
this.loginAttempts = 0;
}
}
}
/**
* Handle form submission with device verification and 2FA support
*/
onSubmit(): void {
if (this.loginForm.invalid || this.isLocked || this.isLoading) {
this.markFormGroupTouched();
return;
}
this.isLoading = true;
const credentials: LoginCredentials = this.loginForm.value;
this.authService.login(credentials)
.pipe(
takeUntil(this.destroy$),
finalize(() => {
this.isLoading = false;
})
)
.subscribe({
next: (response) => {
if (response.requiresTwoFactor) {
// Determine the type of 2FA required based on reason/device info
this.handle2FARequired(response, credentials);
} else {
// Normal login success - no 2FA required
this.handleSuccessfulLogin();
}
},
error: (error: AuthError) => {
this.handleLoginError(error);
}
});
}
/**
* Handle 2FA requirement - different scenarios based on reason
*/
private handle2FARequired(response: AuthResponse, credentials: LoginCredentials): void {
const reason = response.reason?.toLowerCase() || '';
// Check for specific failure reasons
if (reason.includes('verification process failed') || reason.includes('verification failed')) {
// Device fingerprint verification failed - treat as new device registration
this.navigateTo2FAPage(response, credentials, 'device_registration');
return;
}
// Check if it's a new device scenario
const isNewDevice = reason.includes('new device') ||
reason.includes('unknown device') ||
reason.includes('unrecognized device') ||
(response.deviceInfo && !response.deviceInfo.isTrusted);
if (isNewDevice) {
// New/Unknown Device - Navigate to 2FA page for device registration
this.navigateTo2FAPage(response, credentials, 'device_registration');
} else {
// Existing device but requires 2FA code - Navigate to 2FA page for code verification
this.navigateTo2FAPage(response, credentials, 'code_required');
}
}
/**
* Navigate to 2FA page with appropriate context
*/
private navigateTo2FAPage(response: AuthResponse, credentials: LoginCredentials, flowType: 'device_registration' | 'code_required'): void {
// Store 2FA data securely in sessionStorage (not in URL)
const twoFactorData = {
userId: response.user?.id || 0,
userEmail: response.user?.email || credentials.email,
userName: `${response.user?.firstName || ''} ${response.user?.lastName || ''}`.trim(),
reason: response.reason || (flowType === 'device_registration' ? 'New device detected' : '2FA code required'),
riskScore: response.riskScore || 0,
flowType: flowType,
deviceInfo: response.deviceInfo,
tempToken: response.tempToken, // For continuing auth after 2FA
timestamp: Date.now() // For expiration checking
};
// Store in sessionStorage (cleared when browser tab closes)
sessionStorage.setItem('twoFactorData', JSON.stringify(twoFactorData));
// Navigate to 2FA page without query parameters
const navigationPromise = this.router.navigate(['/two-factor-auth']);
navigationPromise.then(
(success) => {
}
).catch(
(error) => {
// Clear data if navigation fails
sessionStorage.removeItem('twoFactorData');
}
);
}
/**
* Proceed with login after successful 2FA device registration
*/
private proceedWithLoginAfter2FA(credentials: LoginCredentials): void {
this.isLoading = true;
// The device should now be trusted, so login should work normally
this.authService.login(credentials)
.pipe(
takeUntil(this.destroy$),
finalize(() => {
this.isLoading = false;
})
)
.subscribe({
next: (response) => {
if (response.requiresTwoFactor) {
// This shouldn't happen after successful 2FA, but handle it just in case
this.snackBar.open('Additional verification required. Please contact support.', 'Close', {
duration: 5000,
panelClass: ['warning-snackbar']
});
} else {
this.handleSuccessfulLogin();
}
},
error: (error: AuthError) => {
this.handleLoginError(error);
}
});
}
/**
* Handle successful login
*/
private handleSuccessfulLogin(): void {
// Clear failed attempts
this.loginAttempts = 0;
localStorage.removeItem('david_auth_lockout');
// Show success message
this.snackBar.open('Login successful! Welcome back.', 'Close', {
duration: 3000,
panelClass: ['success-snackbar']
});
// Wait for auth state to be set before navigating
this.authService.isAuthenticated$.pipe(
takeUntil(this.destroy$),
// Wait for authenticated state
filter(isAuthenticated => isAuthenticated === true),
// Take only the first emission
take(1)
).subscribe(() => {
// Navigate to return URL once authenticated
this.router.navigate([this.returnUrl]);
});
}
/**
* Handle login errors with security measures
*/
private handleLoginError(error: AuthError): void {
this.loginAttempts++;
let errorMessage = 'Login failed. Please check your credentials.';
switch (error.code) {
case 'INVALID_CREDENTIALS':
errorMessage = 'Invalid email or password.';
break;
case 'ACCOUNT_LOCKED':
errorMessage = 'Your account has been temporarily locked. Please try again later.';
break;
case 'EMAIL_NOT_VERIFIED':
errorMessage = 'Please verify your email address before logging in.';
break;
case 'TOO_MANY_ATTEMPTS':
errorMessage = 'Too many failed attempts. Please try again later.';
break;
case 'NETWORK_ERROR':
errorMessage = 'Network error. Please check your connection.';
break;
default:
errorMessage = error.message || errorMessage;
}
// Check if we should lock the account
if (this.loginAttempts >= this.maxLoginAttempts) {
this.lockAccount();
errorMessage = `Too many failed attempts. Account locked for 15 minutes.`;
}
this.snackBar.open(errorMessage, 'Close', {
duration: 5000,
panelClass: ['error-snackbar']
});
// Clear password field for security
this.loginForm.patchValue({ password: '' });
}
/**
* Lock account temporarily
*/
private lockAccount(): void {
this.isLocked = true;
this.lockoutEndTime = new Date(Date.now() + 15 * 60 * 1000); // 15 minutes
localStorage.setItem('david_auth_lockout', JSON.stringify({
endTime: this.lockoutEndTime.toISOString(),
attempts: this.loginAttempts
}));
this.startLockoutTimer();
}
/**
* Start lockout countdown timer
*/
private startLockoutTimer(): void {
const timer = setInterval(() => {
if (this.lockoutEndTime && new Date() >= this.lockoutEndTime) {
this.isLocked = false;
this.lockoutEndTime = null;
this.loginAttempts = 0;
localStorage.removeItem('david_auth_lockout');
clearInterval(timer);
this.snackBar.open('Account unlocked. You can now try logging in again.', 'Close', {
duration: 3000,
panelClass: ['info-snackbar']
});
}
}, 1000);
}
/**
* Get remaining lockout time in minutes
*/
getRemainingLockoutTime(): number {
if (!this.lockoutEndTime) return 0;
const remaining = Math.ceil((this.lockoutEndTime.getTime() - Date.now()) / (1000 * 60));
return Math.max(0, remaining);
}
/**
* Toggle password visibility
*/
togglePasswordVisibility(): void {
this.hidePassword = !this.hidePassword;
}
/**
* Navigate to registration page
*/
navigateToRegister(): void {
this.router.navigate(['/register']);
}
/**
* Navigate to password reset page
*/
navigateToPasswordReset(): void {
this.router.navigate(['/forgot-password']);
}
/**
* Fill form with demo credentials (no automatic login)
*/
fillDemoCredentials(): void {
if (this.isLocked || this.isLoading || !environment.demo.enabled) {
return;
}
// Demo credentials from environment configuration
const demoCredentials = {
email: environment.demo.credentials.email,
password: environment.demo.credentials.password,
rememberMe: false
};
// Fill the form without submitting
this.loginForm.patchValue(demoCredentials);
// Show info message
this.snackBar.open('Demo credentials loaded. Click "Sign In" to login.', 'Close', {
duration: 3000,
panelClass: ['info-snackbar']
});
}
/**
* Check if demo login is available
*/
isDemoEnabled(): boolean {
return environment.demo.enabled;
}
/**
* Get field error message
*/
getFieldError(fieldName: string): string {
const field = this.loginForm.get(fieldName);
if (!field || !field.errors || !field.touched) return '';
const errors = field.errors;
if (errors['required']) return `${this.getFieldDisplayName(fieldName)} is required.`;
if (errors['email']) return 'Please enter a valid email address.';
if (errors['minlength']) return `${this.getFieldDisplayName(fieldName)} must be at least ${errors['minlength'].requiredLength} characters.`;
if (errors['maxlength']) return `${this.getFieldDisplayName(fieldName)} must not exceed ${errors['maxlength'].requiredLength} characters.`;
return 'Invalid input.';
}
/**
* Get display name for field
*/
private getFieldDisplayName(fieldName: string): string {
const names: { [key: string]: string } = {
email: 'Email',
password: 'Password'
};
return names[fieldName] || fieldName;
}
/**
* Check if field has error
*/
hasFieldError(fieldName: string): boolean {
const field = this.loginForm.get(fieldName);
return !!(field && field.invalid && field.touched);
}
/**
* Mark all form fields as touched
*/
private markFormGroupTouched(): void {
Object.keys(this.loginForm.controls).forEach(key => {
const control = this.loginForm.get(key);
if (control) {
control.markAsTouched();
}
});
}
}

View File

@@ -0,0 +1,403 @@
/**
* Resume Preview Component Styles
* Professional Angular Resume Builder - Live Resume Preview & Export
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
* @description Resume preview page with print layout, export options, and real-time rendering
*/
/* ==========================================================================
Preview Container & Layout
========================================================================== */
.preview-container {
max-width: 900px;
margin: 0 auto;
padding: 0 16px;
}
/* ==========================================================================
Action Bar & Controls
========================================================================== */
.action-bar {
display: flex;
gap: 16px;
margin-bottom: 24px;
padding: 16px 0;
border-bottom: 1px solid var(--color-border);
}
.action-bar button {
display: flex;
align-items: center;
gap: 8px;
}
.resume-preview {
background: white;
box-shadow: 0 4px 12px var(--color-shadow-light);
border-radius: 8px;
overflow: hidden;
}
.resume-card {
padding: 48px;
font-family: 'Roboto', sans-serif;
line-height: 1.6;
color: var(--color-text-primary);
}
/* Header Section */
.resume-header {
margin-bottom: 32px;
}
.full-name {
font-size: 2.5rem;
font-weight: 300;
color: var(--color-primary);
margin: 0 0 8px 0;
line-height: 1.2;
}
.job-title {
font-size: 1.5rem;
font-weight: 400;
color: var(--color-text-secondary);
margin: 0 0 24px 0;
}
.contact-info {
display: flex;
flex-wrap: wrap;
gap: 24px;
}
.contact-item {
display: flex;
align-items: center;
gap: 8px;
color: var(--color-text-muted);
}
.contact-item mat-icon {
color: var(--color-primary);
font-size: 18px;
width: 18px;
height: 18px;
}
/* Section Styling */
.resume-section {
margin: 32px 0;
}
.section-title {
display: flex;
align-items: center;
gap: 12px;
font-size: 1.5rem;
font-weight: 500;
color: var(--color-primary);
margin: 0 0 20px 0;
padding-bottom: 8px;
border-bottom: 2px solid var(--color-primary-light);
}
.section-title mat-icon {
color: var(--color-primary);
}
/* Professional Summary */
.summary-text {
font-size: 1.1rem;
line-height: 1.7;
color: var(--color-text-dark);
margin: 0;
}
/* Experience Section */
.experience-list {
display: flex;
flex-direction: column;
gap: 32px;
}
.experience-item {
border-left: 4px solid var(--color-primary-light);
padding-left: 24px;
}
.experience-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
}
.position-title {
font-size: 1.3rem;
font-weight: 500;
color: var(--color-text-primary);
margin: 0 0 4px 0;
}
.company-name {
font-size: 1.1rem;
font-weight: 400;
color: var(--color-primary);
margin: 0 0 4px 0;
}
.location {
font-size: 0.9rem;
color: var(--color-text-secondary);
margin: 0;
}
.date-range {
text-align: right;
}
.date-text {
font-size: 0.9rem;
color: var(--color-text-secondary);
font-weight: 500;
}
.experience-description {
margin: 12px 0;
color: var(--color-text-muted);
line-height: 1.6;
}
.achievements {
margin: 16px 0;
}
.achievements h6 {
font-weight: 500;
color: var(--color-text-primary);
margin: 0 0 8px 0;
}
.achievements ul {
margin: 0;
padding-left: 20px;
}
.achievements li {
margin-bottom: 4px;
color: var(--color-text-muted);
}
.technologies {
margin-top: 12px;
}
.technologies mat-chip {
margin-right: 8px;
margin-bottom: 4px;
}
/* Education Section */
.education-list {
display: flex;
flex-direction: column;
gap: 24px;
}
.education-item {
border-left: 4px solid var(--color-success-light);
padding-left: 24px;
}
.education-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 8px;
}
.degree-title {
font-size: 1.2rem;
font-weight: 500;
color: var(--color-text-primary);
margin: 0 0 4px 0;
}
.institution-name {
font-size: 1rem;
color: var(--color-success);
margin: 0 0 4px 0;
}
.grade {
font-weight: 500;
color: var(--color-text-primary);
margin: 0 0 8px 0;
}
.education-description {
color: var(--color-text-muted);
margin: 0;
}
/* Skills Section */
.skills-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 24px;
}
.skill-category {
background: var(--color-background);
padding: 20px;
border-radius: 8px;
border-left: 4px solid var(--color-warning);
}
.category-title {
font-size: 1.1rem;
font-weight: 500;
color: var(--color-text-primary);
margin: 0 0 16px 0;
text-transform: capitalize;
}
.skill-item {
margin-bottom: 16px;
}
.skill-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
.skill-name {
font-weight: 500;
color: var(--color-text-primary);
}
.skill-level {
font-size: 0.875rem;
color: var(--color-text-secondary);
text-transform: capitalize;
}
.skill-progress {
height: 6px;
border-radius: 3px;
}
/* Languages Section */
.languages-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.language-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
background: var(--color-surface);
border-radius: 6px;
}
.language-name {
font-weight: 500;
color: var(--color-text-primary);
}
.language-level {
font-size: 0.875rem;
color: var(--color-text-secondary);
text-transform: capitalize;
}
/* Footer */
.resume-footer {
margin-top: 48px;
text-align: center;
}
.footer-text {
color: var(--color-text-light);
font-size: 0.8rem;
margin: 16px 0 0 0;
}
/* Print Styles */
@media print {
.action-bar {
display: none;
}
.preview-container {
max-width: none;
padding: 0;
}
.resume-preview {
box-shadow: none;
}
.resume-card {
padding: 20px;
}
.full-name {
font-size: 2rem;
}
.section-title {
font-size: 1.2rem;
}
}
/* Responsive Design */
@media (max-width: 768px) {
.resume-card {
padding: 24px 16px;
}
.action-bar {
flex-direction: column;
gap: 8px;
}
.action-bar button {
width: 100%;
justify-content: center;
}
.contact-info {
flex-direction: column;
gap: 12px;
}
.experience-header,
.education-header {
flex-direction: column;
gap: 8px;
}
.date-range {
text-align: left;
}
.skills-grid {
grid-template-columns: 1fr;
}
.full-name {
font-size: 2rem;
}
}

View File

@@ -0,0 +1,192 @@
<!-- Professional Resume Preview -->
<div class="preview-container">
<!-- Action Bar -->
<div class="action-bar">
<button mat-raised-button color="primary" (click)="exportToPDF()">
<mat-icon>picture_as_pdf</mat-icon>
PDF exportieren
</button>
<button mat-stroked-button (click)="printResume()">
<mat-icon>print</mat-icon>
Drucken
</button>
<button mat-button routerLink="/builder">
<mat-icon>edit</mat-icon>
Lebenslauf bearbeiten
</button>
</div>
<!-- Resume Content -->
<div class="resume-preview" id="resume-content">
<mat-card class="resume-card">
<!-- Header Section -->
<div class="resume-header">
<div class="personal-info">
<h1 class="full-name">
{{ resumeData.personalInfo.firstName }} {{ resumeData.personalInfo.lastName }}
</h1>
<h2 class="job-title" *ngIf="resumeData.personalInfo.jobTitle">
{{ resumeData.personalInfo.jobTitle }}
</h2>
<div class="contact-info">
<div class="contact-item" *ngIf="resumeData.personalInfo.email">
<mat-icon>email</mat-icon>
<span>{{ resumeData.personalInfo.email }}</span>
</div>
<div class="contact-item" *ngIf="resumeData.personalInfo.phone">
<mat-icon>phone</mat-icon>
<span>{{ resumeData.personalInfo.phone }}</span>
</div>
<div class="contact-item" *ngIf="resumeData.personalInfo.location">
<mat-icon>location_on</mat-icon>
<span>{{ resumeData.personalInfo.location }}</span>
</div>
</div>
</div>
</div>
<!-- Professional Summary -->
<div class="resume-section" *ngIf="resumeData.personalInfo.summary">
<h3 class="section-title">
<mat-icon>person</mat-icon>
Berufliche Zusammenfassung
</h3>
<p class="summary-text">{{ resumeData.personalInfo.summary }}</p>
</div>
<mat-divider></mat-divider>
<!-- Experience Section -->
<div class="resume-section" *ngIf="resumeData.experience.length > 0">
<h3 class="section-title">
<mat-icon>work</mat-icon>
Berufserfahrung
</h3>
<div class="experience-list">
<div class="experience-item" *ngFor="let exp of resumeData.experience">
<div class="experience-header">
<div class="position-info">
<h4 class="position-title">{{ exp.position }}</h4>
<h5 class="company-name">{{ exp.company }}</h5>
<p class="location" *ngIf="exp.location">{{ exp.location }}</p>
</div>
<div class="date-range">
<span class="date-text">
{{ formatDate(exp.startDate) }} -
{{ exp.isCurrentPosition ? 'Aktuell' : formatDate(exp.endDate) }}
</span>
</div>
</div>
<p class="experience-description" *ngIf="exp.description">
{{ exp.description }}
</p>
<div class="achievements" *ngIf="exp.achievements && exp.achievements.length > 0">
<h6>Wichtige Erfolge:</h6>
<ul>
<li *ngFor="let achievement of exp.achievements">{{ achievement }}</li>
</ul>
</div>
<div class="technologies" *ngIf="exp.technologies && exp.technologies.length > 0">
<mat-chip-listbox>
<mat-chip *ngFor="let tech of exp.technologies">{{ tech }}</mat-chip>
</mat-chip-listbox>
</div>
</div>
</div>
</div>
<mat-divider *ngIf="resumeData.education.length > 0"></mat-divider>
<!-- Education Section -->
<div class="resume-section" *ngIf="resumeData.education.length > 0">
<h3 class="section-title">
<mat-icon>school</mat-icon>
Education
</h3>
<div class="education-list">
<div class="education-item" *ngFor="let edu of resumeData.education">
<div class="education-header">
<div class="degree-info">
<h4 class="degree-title">{{ edu.degree }}</h4>
<h5 class="institution-name">{{ edu.institution }}</h5>
<p class="location" *ngIf="edu.location">{{ edu.location }}</p>
</div>
<div class="date-range">
<span class="date-text">
{{ formatDate(edu.startDate) }} - {{ formatDate(edu.endDate) }}
</span>
</div>
</div>
<div class="education-details">
<p *ngIf="edu.grade" class="grade">Grade: {{ edu.grade }}</p>
<p *ngIf="edu.description" class="education-description">{{ edu.description }}</p>
</div>
</div>
</div>
</div>
<mat-divider *ngIf="resumeData.skills.length > 0"></mat-divider>
<!-- Skills Section -->
<div class="resume-section" *ngIf="resumeData.skills.length > 0">
<h3 class="section-title">
<mat-icon>psychology</mat-icon>
Skills & Competencies
</h3>
<div class="skills-grid">
<div class="skill-category" *ngFor="let category of getSkillCategories()">
<h6 class="category-title">{{ category.name }}</h6>
<div class="skills-in-category">
<div class="skill-item" *ngFor="let skill of category.skills">
<div class="skill-info">
<span class="skill-name">{{ skill.name }}</span>
<span class="skill-level">{{ skill.proficiency }}</span>
</div>
<mat-progress-bar
mode="determinate"
[value]="getSkillPercentage(skill.proficiencyLevel)"
class="skill-progress">
</mat-progress-bar>
</div>
</div>
</div>
</div>
</div>
<mat-divider *ngIf="resumeData.languages.length > 0"></mat-divider>
<!-- Languages Section -->
<div class="resume-section" *ngIf="resumeData.languages.length > 0">
<h3 class="section-title">
<mat-icon>language</mat-icon>
Languages
</h3>
<div class="languages-list">
<div class="language-item" *ngFor="let lang of resumeData.languages">
<span class="language-name">{{ lang.name }}</span>
<span class="language-level">{{ lang.proficiency }} ({{ lang.level }})</span>
</div>
</div>
</div>
<!-- Footer -->
<div class="resume-footer">
<mat-divider></mat-divider>
<p class="footer-text">
Created with Professional Resume Builder by David Valera Melendez | Made in Germany 🇩🇪
</p>
</div>
</mat-card>
</div>
</div>

View File

@@ -0,0 +1,165 @@
/**
* Resume Preview Page Component
* Professional Angular Resume Builder
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatDividerModule } from '@angular/material/divider';
import { MatChipsModule } from '@angular/material/chips';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { ResumeData } from '../../models/resume.model';
import { ResumeService } from '../../services/resume.service';
/**
* Resume Preview Component - Professional CV Display
* Renders a complete, professional resume preview with print support
*/
@Component({
selector: 'app-preview',
standalone: true,
imports: [
CommonModule,
RouterModule,
MatCardModule,
MatButtonModule,
MatIconModule,
MatDividerModule,
MatChipsModule,
MatProgressBarModule
],
templateUrl: './preview.component.html',
styleUrls: ['./preview.component.css']
})
export class PreviewComponent implements OnInit {
resumeData: ResumeData = this.getEmptyResumeData();
constructor(private resumeService: ResumeService) {}
ngOnInit(): void {
this.loadResumeData();
}
/**
* Load resume data from service
*/
private loadResumeData(): void {
const data = this.resumeService.getResumeData();
if (data && this.hasContent(data)) {
this.resumeData = data;
} else {
// Load sample data if no real data exists
this.resumeData = this.resumeService.getSampleResumeData();
}
}
/**
* Check if resume has any meaningful content
*/
private hasContent(data: ResumeData): boolean {
return !!(
data.personalInfo.firstName ||
data.personalInfo.lastName ||
data.experience.length > 0 ||
data.education.length > 0
);
}
/**
* Format date string for display
*/
formatDate(dateString: string | undefined): string {
if (!dateString) return '';
try {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short'
});
} catch {
return dateString;
}
}
/**
* Get skill categories with grouped skills
*/
getSkillCategories(): Array<{name: string, skills: any[]}> {
const categories: {[key: string]: any[]} = {};
this.resumeData.skills.forEach(skill => {
const category = skill.category || 'other';
if (!categories[category]) {
categories[category] = [];
}
categories[category].push(skill);
});
return Object.keys(categories).map(key => ({
name: key.charAt(0).toUpperCase() + key.slice(1),
skills: categories[key]
}));
}
/**
* Convert proficiency level to percentage
*/
getSkillPercentage(level: number | undefined): number {
if (!level) return 50;
return Math.min(100, Math.max(0, (level / 10) * 100));
}
/**
* Export resume to PDF
*/
exportToPDF(): void {
// TODO: Implementation would use libraries like jsPDF with html2canvas
// For now, use browser's print functionality
window.print();
}
/**
* Print resume
*/
printResume(): void {
window.print();
}
/**
* Get empty resume data structure
*/
private getEmptyResumeData(): ResumeData {
return {
personalInfo: {
firstName: '',
lastName: '',
jobTitle: '',
email: '',
phone: '',
city: '',
country: '',
location: '',
summary: ''
},
experience: [],
education: [],
skills: [],
languages: [],
certifications: [],
projects: [],
awards: [],
references: []
};
}
}

View File

@@ -0,0 +1,172 @@
/**
* Resume Builder Component Styles
* Professional Angular Resume Builder - Interactive Resume Creation Interface
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
* @description Main resume building interface with form sections, drag-and-drop, and real-time preview
*/
/* ==========================================================================
Builder Container & Layout
========================================================================== */
.builder-container {
max-width: 1000px;
margin: 0 auto;
padding: 0 16px;
}
/* ==========================================================================
Builder Header
========================================================================== */
.builder-header {
text-align: center;
margin-bottom: 32px;
}
.builder-header h1 {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
font-size: 2rem;
font-weight: 300;
color: var(--color-text-primary);
margin-bottom: 8px;
}
.builder-header p {
color: var(--color-text-secondary);
font-size: 1.125rem;
}
.stepper-card {
padding: 32px;
border-radius: 12px;
}
.resume-stepper {
background: transparent;
}
.step-form {
padding: 24px 0;
max-width: 800px;
}
.step-form h3 {
color: var(--color-text-primary);
font-weight: 400;
margin-bottom: 16px;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 16px;
}
.full-width {
width: 100%;
margin-bottom: 16px;
}
mat-form-field {
margin-bottom: 16px;
}
.step-actions {
display: flex;
justify-content: space-between;
margin-top: 32px;
padding-top: 16px;
border-top: 1px solid var(--color-border);
}
.step-actions button {
display: flex;
align-items: center;
gap: 8px;
}
.experience-item,
.education-item,
.skill-item {
margin-bottom: 16px;
}
.skill-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.skill-info h4 {
margin: 0 0 4px 0;
font-weight: 500;
}
.skill-category {
font-size: 0.875rem;
color: var(--color-text-secondary);
text-transform: capitalize;
}
.skill-level {
font-size: 0.875rem;
color: var(--color-primary);
font-weight: 500;
text-transform: capitalize;
}
.add-button {
width: 100%;
padding: 16px;
margin: 16px 0;
border: 2px dashed var(--color-border);
background: transparent;
}
.add-button:hover {
border-color: var(--color-primary);
background: var(--color-primary-bg);
}
/* Responsive Design */
@media (max-width: 768px) {
.stepper-card {
padding: 16px;
}
.form-row {
grid-template-columns: 1fr;
gap: 0;
}
.step-actions {
flex-direction: column;
gap: 16px;
}
.step-actions button {
width: 100%;
justify-content: center;
}
}
/* Material Stepper Custom Styles */
::ng-deep .mat-stepper-horizontal {
margin-top: 8px;
}
::ng-deep .mat-step-header .mat-step-icon {
background-color: var(--color-primary);
}
::ng-deep .mat-step-header .mat-step-icon-selected {
background-color: var(--color-primary);
}

View File

@@ -0,0 +1,214 @@
<!-- Professional Resume Builder -->
<div class="builder-container">
<div class="builder-header">
<h1>
<mat-icon>edit</mat-icon>
Professional Lebenslauf Generator
</h1>
<p>Erstellen Sie Ihren professionellen Lebenslauf Schritt für Schritt</p>
</div>
<mat-card class="stepper-card">
<mat-stepper [linear]="true" #stepper class="resume-stepper">
<!-- Step 1: Personal Information -->
<mat-step [stepControl]="personalInfoForm" label="Persönliche Info" state="person">
<ng-template matStepLabel>Persönliche Informationen</ng-template>
<form [formGroup]="personalInfoForm" class="step-form">
<h3>Persönliche Informationen</h3>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Vorname</mat-label>
<input matInput formControlName="firstName" required>
<mat-icon matSuffix>person</mat-icon>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Nachname</mat-label>
<input matInput formControlName="lastName" required>
</mat-form-field>
</div>
<mat-form-field appearance="outline" class="full-width">
<mat-label>Berufsbezeichnung</mat-label>
<input matInput formControlName="jobTitle"
placeholder="z.B. Senior Frontend Developer">
<mat-icon matSuffix>work</mat-icon>
</mat-form-field>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>E-Mail</mat-label>
<input matInput type="email" formControlName="email" required>
<mat-icon matSuffix>email</mat-icon>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Telefon</mat-label>
<input matInput type="tel" formControlName="phone" required>
<mat-icon matSuffix>phone</mat-icon>
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Stadt</mat-label>
<input matInput formControlName="city" required>
<mat-icon matSuffix>location_city</mat-icon>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Land</mat-label>
<input matInput formControlName="country">
</mat-form-field>
</div>
<mat-form-field appearance="outline" class="full-width">
<mat-label>Berufliche Zusammenfassung</mat-label>
<textarea matInput formControlName="summary"
rows="4"
placeholder="Kurze berufliche Zusammenfassung..."></textarea>
</mat-form-field>
<div class="step-actions">
<button mat-raised-button color="primary" matStepperNext>
Weiter
<mat-icon>arrow_forward</mat-icon>
</button>
</div>
</form>
</mat-step>
<!-- Step 2: Experience -->
<mat-step [stepControl]="experienceForm" label="Berufserfahrung" state="work">
<ng-template matStepLabel>Berufserfahrung</ng-template>
<div class="step-form">
<h3>Berufserfahrung</h3>
<p>Fügen Sie Ihre Berufserfahrung hinzu, beginnend mit Ihrer aktuellsten Position.</p>
<div *ngFor="let exp of experiences; let i = index" class="experience-item">
<mat-card>
<mat-card-header>
<mat-card-title>{{ exp.position || 'Neue Position' }}</mat-card-title>
<mat-card-subtitle>{{ exp.company }}</mat-card-subtitle>
</mat-card-header>
<mat-card-actions align="end">
<button mat-icon-button (click)="editExperience(i)">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button (click)="removeExperience(i)" color="warn">
<mat-icon>delete</mat-icon>
</button>
</mat-card-actions>
</mat-card>
</div>
<button mat-stroked-button (click)="addExperience()" class="add-button">
<mat-icon>add</mat-icon>
Berufserfahrung hinzufügen
</button>
<div class="step-actions">
<button mat-button matStepperPrevious>
<mat-icon>arrow_back</mat-icon>
Zurück
</button>
<button mat-raised-button color="primary" matStepperNext>
Weiter
<mat-icon>arrow_forward</mat-icon>
</button>
</div>
</div>
</mat-step>
<!-- Step 3: Education -->
<mat-step [stepControl]="educationForm" label="Bildung" state="school">
<ng-template matStepLabel>Bildung</ng-template>
<div class="step-form">
<h3>Bildungshintergrund</h3>
<p>Fügen Sie Ihre Bildungsqualifikationen hinzu.</p>
<div *ngFor="let edu of education; let i = index" class="education-item">
<mat-card>
<mat-card-header>
<mat-card-title>{{ edu.degree || 'Neuer Abschluss' }}</mat-card-title>
<mat-card-subtitle>{{ edu.institution }}</mat-card-subtitle>
</mat-card-header>
<mat-card-actions align="end">
<button mat-icon-button (click)="editEducation(i)">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button (click)="removeEducation(i)" color="warn">
<mat-icon>delete</mat-icon>
</button>
</mat-card-actions>
</mat-card>
</div>
<button mat-stroked-button (click)="addEducation()" class="add-button">
<mat-icon>add</mat-icon>
Bildung hinzufügen
</button>
<div class="step-actions">
<button mat-button matStepperPrevious>
<mat-icon>arrow_back</mat-icon>
Back
</button>
<button mat-raised-button color="primary" matStepperNext>
Next
<mat-icon>arrow_forward</mat-icon>
</button>
</div>
</div>
</mat-step>
<!-- Step 4: Skills -->
<mat-step [stepControl]="skillsForm" label="Fähigkeiten" state="psychology">
<ng-template matStepLabel>Fähigkeiten & Kompetenzen</ng-template>
<div class="step-form">
<h3>Fähigkeiten & Kompetenzen</h3>
<p>Fügen Sie Ihre technischen und sozialen Kompetenzen hinzu.</p>
<div *ngFor="let skill of skills; let i = index" class="skill-item">
<mat-card>
<mat-card-content>
<div class="skill-content">
<div class="skill-info">
<h4>{{ skill.name }}</h4>
<span class="skill-category">{{ skill.category }}</span>
</div>
<div class="skill-level">
<span>{{ skill.proficiency }}</span>
</div>
</div>
</mat-card-content>
<mat-card-actions align="end">
<button mat-icon-button (click)="editSkill(i)">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button (click)="removeSkill(i)" color="warn">
<mat-icon>delete</mat-icon>
</button>
</mat-card-actions>
</mat-card>
</div>
<button mat-stroked-button (click)="addSkill()" class="add-button">
<mat-icon>add</mat-icon>
Fähigkeit hinzufügen
</button>
<div class="step-actions">
<button mat-button matStepperPrevious>
<mat-icon>arrow_back</mat-icon>
Zurück
</button>
<button mat-raised-button color="accent" (click)="saveResume()">
<mat-icon>save</mat-icon>
Lebenslauf speichern
</button>
</div>
</div>
</mat-step>
</mat-stepper>
</mat-card>
</div>

View File

@@ -0,0 +1,275 @@
/**
* Resume Builder Page Component
* Professional Angular Resume Builder
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatStepperModule } from '@angular/material/stepper';
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 { MatSelectModule } from '@angular/material/select';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDividerModule } from '@angular/material/divider';
import { ResumeData, PersonalInfo, Experience, Education, Skill } from '../../models/resume.model';
import { ResumeService } from '../../services/resume.service';
/**
* Resume Builder Component with Material Stepper
* Professional multi-step form for creating resumes
*/
@Component({
selector: 'app-resume-builder',
standalone: true,
imports: [
CommonModule,
ReactiveFormsModule,
MatStepperModule,
MatCardModule,
MatFormFieldModule,
MatInputModule,
MatButtonModule,
MatIconModule,
MatSelectModule,
MatDatepickerModule,
MatCheckboxModule,
MatDividerModule
],
templateUrl: './resume-builder.component.html',
styleUrls: ['./resume-builder.component.css']
})
export class ResumeBuilderComponent implements OnInit {
// Form Groups
personalInfoForm!: FormGroup;
experienceForm!: FormGroup;
educationForm!: FormGroup;
skillsForm!: FormGroup;
// Resume Data
resumeData: ResumeData = this.getEmptyResumeData();
experiences: Experience[] = [];
education: Education[] = [];
skills: Skill[] = [];
constructor(
private formBuilder: FormBuilder,
private resumeService: ResumeService
) {}
ngOnInit(): void {
this.initializeForms();
this.loadExistingData();
}
/**
* Initialize all form groups with validation
*/
private initializeForms(): void {
this.personalInfoForm = this.formBuilder.group({
firstName: ['', Validators.required],
lastName: ['', Validators.required],
jobTitle: [''],
email: ['', [Validators.required, Validators.email]],
phone: ['', Validators.required],
city: ['', Validators.required],
country: [''],
summary: ['']
});
this.experienceForm = this.formBuilder.group({
// Dynamic form controls will be added for experiences
});
this.educationForm = this.formBuilder.group({
// Dynamic form controls will be added for education
});
this.skillsForm = this.formBuilder.group({
// Dynamic form controls will be added for skills
});
}
/**
* Load existing resume data from service
*/
private loadExistingData(): void {
const existingData = this.resumeService.getResumeData();
if (existingData.personalInfo) {
this.personalInfoForm.patchValue(existingData.personalInfo);
}
this.experiences = existingData.experience || [];
this.education = existingData.education || [];
this.skills = existingData.skills || [];
}
/**
* Add new experience entry
*/
addExperience(): void {
const newExperience: Experience = {
id: this.generateId(),
company: '',
position: '',
location: '',
startDate: new Date().toISOString().split('T')[0],
endDate: undefined,
isCurrentPosition: false,
description: ''
};
this.experiences.push(newExperience);
}
/**
* Edit experience entry
* @param index Index of experience to edit
*/
editExperience(index: number): void {
// TODO: Implementation for editing experience
}
/**
* Remove experience entry
* @param index Index of experience to remove
*/
removeExperience(index: number): void {
if (index >= 0 && index < this.experiences.length) {
this.experiences.splice(index, 1);
}
}
/**
* Add new education entry
*/
addEducation(): void {
const newEducation: Education = {
id: this.generateId(),
institution: '',
degree: '',
location: '',
startDate: new Date().toISOString().split('T')[0],
endDate: undefined
};
this.education.push(newEducation);
}
/**
* Edit education entry
* @param index Index of education to edit
*/
editEducation(index: number): void {
// TODO: Implementation for editing education
}
/**
* Remove education entry
* @param index Index of education to remove
*/
removeEducation(index: number): void {
if (index >= 0 && index < this.education.length) {
this.education.splice(index, 1);
}
}
/**
* Add new skill entry
*/
addSkill(): void {
const newSkill: Skill = {
id: this.generateId(),
name: '',
category: 'technical',
proficiency: 'intermediate'
};
this.skills.push(newSkill);
}
/**
* Edit skill entry
* @param index Index of skill to edit
*/
editSkill(index: number): void {
// TODO: Implementation for editing skill
}
/**
* Remove skill entry
* @param index Index of skill to remove
*/
removeSkill(index: number): void {
if (index >= 0 && index < this.skills.length) {
this.skills.splice(index, 1);
}
}
/**
* Save complete resume data
*/
saveResume(): void {
const personalInfo: PersonalInfo = this.personalInfoForm.value;
const resumeData: ResumeData = {
personalInfo,
experience: this.experiences,
education: this.education,
skills: this.skills,
languages: [],
certifications: [],
projects: [],
awards: [],
references: [],
createdAt: this.resumeData.createdAt || new Date(),
updatedAt: new Date()
};
this.resumeService.saveResumeData(resumeData);
// Resume saved successfully
}
/**
* Get empty resume data structure
*/
private getEmptyResumeData(): ResumeData {
return {
personalInfo: {
firstName: '',
lastName: '',
jobTitle: '',
email: '',
phone: '',
city: '',
country: '',
location: '',
summary: ''
},
experience: [],
education: [],
skills: [],
languages: [],
certifications: [],
projects: [],
awards: [],
references: [],
createdAt: new Date(),
updatedAt: new Date()
};
}
/**
* Generate unique ID for resume
*/
private generateId(): string {
return 'resume_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
}
}

View File

@@ -0,0 +1,223 @@
/**
* Settings Page Component Styles
* Professional Angular Resume Builder - User Settings & Preferences
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
* @description User settings page for theme management, profile preferences, and account options
*/
/* ==========================================================================
Settings Container & Layout
========================================================================== */
.settings-container {
max-width: 800px;
margin: 0 auto;
padding: 0 16px;
}
/* ==========================================================================
Settings Header
========================================================================== */
.settings-header {
text-align: center;
margin-bottom: 32px;
}
.settings-header h1 {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
font-size: 2rem;
font-weight: 300;
color: var(--color-text-primary);
margin-bottom: 8px;
}
.settings-header p {
color: var(--color-text-secondary);
font-size: 1.125rem;
}
.settings-content {
display: flex;
flex-direction: column;
gap: 24px;
}
.settings-card {
padding: 24px;
}
.settings-card mat-card-title {
display: flex;
align-items: center;
gap: 12px;
}
.settings-form {
margin-top: 16px;
}
.form-section {
margin-bottom: 32px;
}
.form-section h3 {
font-size: 1.2rem;
font-weight: 500;
color: var(--color-text-primary);
margin: 0 0 16px 0;
}
.form-row {
margin-bottom: 16px;
}
.form-actions {
display: flex;
justify-content: flex-end;
padding-top: 16px;
border-top: 1px solid var(--color-border);
}
.form-actions button {
display: flex;
align-items: center;
gap: 8px;
}
.data-actions {
display: flex;
flex-direction: column;
gap: 32px;
margin-top: 16px;
}
.action-group {
padding: 20px;
border: 1px solid var(--color-border);
border-radius: var(--border-radius-md);
background: var(--color-background);
}
.action-group.danger-zone {
border-color: var(--color-error);
background: var(--color-error-light);
background: var(--color-error-bg);
}
.action-group h3 {
font-size: 1.1rem;
font-weight: 500;
color: var(--color-text-primary);
margin: 0 0 8px 0;
}
.action-group p {
color: var(--color-text-secondary);
margin: 0 0 16px 0;
line-height: 1.5;
}
.warning-text {
color: var(--color-error) !important;
font-weight: 500;
}
.action-group button {
display: flex;
align-items: center;
gap: 8px;
}
.about-content {
margin-top: 16px;
}
.about-section {
margin-bottom: 24px;
}
.about-section h3 {
font-size: 1.2rem;
font-weight: 500;
color: var(--color-text-primary);
margin: 0 0 12px 0;
}
.about-section p {
color: var(--color-text-secondary);
line-height: 1.6;
margin: 0 0 8px 0;
}
.author-info {
background: var(--color-surface);
padding: 16px;
border-radius: 8px;
border-left: 4px solid var(--color-primary);
}
.tech-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.tech-item {
background: var(--color-primary);
color: white;
padding: 4px 12px;
border-radius: 16px;
font-size: 0.875rem;
font-weight: 500;
}
.features-list {
list-style: none;
padding: 0;
margin: 0;
}
.features-list li {
padding: 4px 0;
color: var(--color-text-secondary);
position: relative;
padding-left: 20px;
}
.features-list li::before {
content: '✓';
position: absolute;
left: 0;
color: var(--color-success);
font-weight: bold;
}
/* Responsive Design */
@media (max-width: 768px) {
.settings-container {
padding: 0 8px;
}
.settings-card {
padding: 16px;
}
.data-actions {
gap: 24px;
}
.action-group {
padding: 16px;
}
.tech-list {
justify-content: center;
}
}

View File

@@ -0,0 +1,173 @@
<!-- Settings & Preferences Page -->
<div class="settings-container">
<div class="settings-header">
<h1>
<mat-icon>settings</mat-icon>
Einstellungen & Präferenzen
</h1>
<p>Verwalten Sie Ihre Lebenslauf-Generator-Einstellungen und Daten</p>
</div>
<div class="settings-content">
<!-- Application Settings -->
<mat-card class="settings-card">
<mat-card-header>
<mat-card-title>
<mat-icon>tune</mat-icon>
Anwendungseinstellungen
</mat-card-title>
<mat-card-subtitle>Konfigurieren Sie Ihre Lebenslauf-Generator-Einstellungen</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<form [formGroup]="settingsForm" class="settings-form">
<div class="form-section">
<h3>Anzeigeeinstellungen</h3>
<div class="form-row">
<mat-slide-toggle formControlName="darkMode">
Dunkler Modus
</mat-slide-toggle>
</div>
<div class="form-row">
<mat-slide-toggle formControlName="showTooltips">
Hilfreiche Tipps anzeigen
</mat-slide-toggle>
</div>
</div>
<div class="form-section">
<h3>Export-Einstellungen</h3>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Standard-Papierformat</mat-label>
<mat-select formControlName="defaultPaperSize">
<mat-option value="A4">A4 (Europäischer Standard)</mat-option>
<mat-option value="Letter">Letter (US-Standard)</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="form-actions">
<button mat-raised-button color="primary" (click)="saveSettings()">
<mat-icon>save</mat-icon>
Save Settings
</button>
</div>
</form>
</mat-card-content>
</mat-card>
<!-- Data Management -->
<mat-card class="settings-card">
<mat-card-header>
<mat-card-title>
<mat-icon>storage</mat-icon>
Data Management
</mat-card-title>
<mat-card-subtitle>Import, export, and manage your resume data</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<div class="data-actions">
<div class="action-group">
<h3>Export Data</h3>
<p>Download your resume data as a backup file</p>
<button mat-stroked-button (click)="exportData()">
<mat-icon>download</mat-icon>
Export Resume Data
</button>
</div>
<div class="action-group">
<h3>Import Data</h3>
<p>Upload a previously exported resume data file</p>
<input #fileInput type="file" accept=".json" (change)="importData($event)" hidden>
<button mat-stroked-button (click)="fileInput.click()">
<mat-icon>upload</mat-icon>
Import Resume Data
</button>
</div>
<div class="action-group">
<h3>Sample Data</h3>
<p>Load sample resume data to see how the builder works</p>
<button mat-stroked-button color="accent" (click)="loadSampleData()">
<mat-icon>preview</mat-icon>
Load Sample Data
</button>
</div>
<div class="action-group danger-zone">
<h3>Reset Data</h3>
<p class="warning-text">⚠️ This will permanently delete all your resume data</p>
<button mat-stroked-button color="warn" (click)="clearAllData()">
<mat-icon>delete_forever</mat-icon>
Clear All Data
</button>
</div>
</div>
</mat-card-content>
</mat-card>
<!-- About Section -->
<mat-card class="settings-card">
<mat-card-header>
<mat-card-title>
<mat-icon>info</mat-icon>
About
</mat-card-title>
<mat-card-subtitle>Professional Resume Builder Information</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<div class="about-content">
<div class="about-section">
<h3>Professional Resume Builder</h3>
<p>Version 1.0.0</p>
<p>
A modern, professional resume builder application built with Angular 17,
TypeScript, and Angular Material UI. Designed to help professionals create
outstanding resumes with ease.
</p>
</div>
<div class="about-section">
<h3>Author Information</h3>
<div class="author-info">
<p><strong>Created by:</strong> David Valera Melendez</p>
<p><strong>Email:</strong> david&#64;valera-melendez.de</p>
<p><strong>Location:</strong> Made in Germany 🇩🇪</p>
<p><strong>Created:</strong> August 8, 2025</p>
</div>
</div>
<div class="about-section">
<h3>Technology Stack</h3>
<div class="tech-list">
<span class="tech-item">Angular 17</span>
<span class="tech-item">TypeScript</span>
<span class="tech-item">Angular Material</span>
<span class="tech-item">SCSS</span>
<span class="tech-item">RxJS</span>
</div>
</div>
<div class="about-section">
<h3>Features</h3>
<ul class="features-list">
<li>Professional Material Design UI</li>
<li>Step-by-step resume builder</li>
<li>Real-time preview</li>
<li>PDF export functionality</li>
<li>Data import/export</li>
<li>Responsive design</li>
<li>Local data storage</li>
</ul>
</div>
</div>
</mat-card-content>
</mat-card>
</div>
</div>

View File

@@ -0,0 +1,165 @@
/**
* Settings Page Component
* Professional Angular Resume Builder
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormBuilder, FormGroup } from '@angular/forms';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSelectModule } from '@angular/material/select';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatSnackBarModule, MatSnackBar } from '@angular/material/snack-bar';
import { MatDialogModule, MatDialog } from '@angular/material/dialog';
import { ResumeService } from '../../services/resume.service';
/**
* Settings Component - Application configuration and data management
*/
@Component({
selector: 'app-settings',
standalone: true,
imports: [
CommonModule,
ReactiveFormsModule,
MatCardModule,
MatButtonModule,
MatIconModule,
MatFormFieldModule,
MatSelectModule,
MatSlideToggleModule,
MatSnackBarModule,
MatDialogModule
],
templateUrl: './settings.component.html',
styleUrls: ['./settings.component.css']
})
export class SettingsComponent implements OnInit {
settingsForm!: FormGroup;
constructor(
private formBuilder: FormBuilder,
private resumeService: ResumeService,
private snackBar: MatSnackBar,
private dialog: MatDialog
) {}
ngOnInit(): void {
this.initializeForm();
}
/**
* Initialize settings form with default values
*/
private initializeForm(): void {
this.settingsForm = this.formBuilder.group({
darkMode: [false],
showTooltips: [true],
defaultPaperSize: ['A4']
});
}
/**
* Save application settings
*/
saveSettings(): void {
const settings = this.settingsForm.value;
// Save to localStorage for persistence
localStorage.setItem('resumeBuilderSettings', JSON.stringify(settings));
this.snackBar.open('Settings saved successfully!', 'Close', {
duration: 3000,
horizontalPosition: 'end',
verticalPosition: 'top'
});
}
/**
* Export resume data as JSON file
*/
exportData(): void {
const resumeData = this.resumeService.getResumeData();
const dataStr = JSON.stringify(resumeData, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const link = document.createElement('a');
link.href = URL.createObjectURL(dataBlob);
link.download = 'resume-data.json';
link.click();
this.snackBar.open('Resume data exported successfully!', 'Close', {
duration: 3000,
horizontalPosition: 'end',
verticalPosition: 'top'
});
}
/**
* Import resume data from JSON file
*/
importData(event: Event): void {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target?.result as string);
this.resumeService.saveResumeData(data);
this.snackBar.open('Resume data imported successfully!', 'Close', {
duration: 3000,
horizontalPosition: 'end',
verticalPosition: 'top'
});
} catch (error) {
this.snackBar.open('Error importing data. Please check file format.', 'Close', {
duration: 3000,
horizontalPosition: 'end',
verticalPosition: 'top'
});
}
};
reader.readAsText(file);
}
}
/**
* Load sample resume data
*/
loadSampleData(): void {
const sampleData = this.resumeService.getSampleResumeData();
this.resumeService.saveResumeData(sampleData);
this.snackBar.open('Sample data loaded successfully!', 'Close', {
duration: 3000,
horizontalPosition: 'end',
verticalPosition: 'top'
});
}
/**
* Clear all resume data
*/
clearAllData(): void {
if (confirm('⚠️ Are you sure you want to delete all resume data? This action cannot be undone.')) {
this.resumeService.clearResumeData();
this.snackBar.open('All data cleared successfully!', 'Close', {
duration: 3000,
horizontalPosition: 'end',
verticalPosition: 'top'
});
}
}
}

View File

@@ -0,0 +1,161 @@
/**
* Templates Page Component Styles
* Professional Angular Resume Builder - Resume Template Gallery
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
* @description Template selection page with preview cards, filtering, and template management
*/
/* ==========================================================================
Templates Container & Layout
========================================================================== */
.templates-container {
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
}
/* ==========================================================================
Templates Header
========================================================================== */
.templates-header {
text-align: center;
margin-bottom: 3rem;
}
.templates-header h1 {
color: var(--color-text-primary);
margin-bottom: 1rem;
font-weight: 500;
}
.templates-header p {
color: var(--color-text-secondary);
font-size: 1.1rem;
}
.templates-grid {
margin-bottom: 3rem;
}
.template-card {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.template-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 25px var(--color-shadow-light);
}
.template-preview {
position: relative;
flex: 1;
overflow: hidden;
border-radius: 4px 4px 0 0;
}
.preview-image {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.template-card:hover .preview-image {
transform: scale(1.05);
}
.preview-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--color-overlay);
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
opacity: 0;
transition: opacity 0.3s ease;
}
.template-card:hover .preview-overlay {
opacity: 1;
}
.template-info {
padding: 1rem;
}
.template-info h3 {
margin: 0 0 0.5rem 0;
color: var(--color-text-primary);
font-weight: 500;
}
.template-info p {
margin: 0 0 1rem 0;
color: var(--color-text-secondary);
font-size: 0.9rem;
line-height: 1.4;
}
.template-tags {
margin-top: 0.5rem;
}
.templates-actions {
text-align: center;
padding-top: 2rem;
border-top: 1px solid var(--color-border);
}
.ml-2 {
margin-left: 0.5rem;
}
/* Responsive Design */
@media (max-width: 768px) {
.templates-container {
padding: 1rem;
}
.templates-grid mat-grid-list {
columns: 1;
}
.preview-overlay {
opacity: 1;
background: var(--color-overlay-light);
}
.preview-overlay {
flex-direction: column;
gap: 0.5rem;
}
.ml-2 {
margin-left: 0;
margin-top: 0.5rem;
}
}
@media (max-width: 480px) {
.templates-header h1 {
font-size: 1.5rem;
}
.templates-header p {
font-size: 1rem;
}
}

View File

@@ -0,0 +1,46 @@
<div class="templates-container">
<div class="templates-header">
<h1>Professionelle Lebenslauf-Vorlagen</h1>
<p>Wählen Sie aus unserer Sammlung professionell gestalteter Lebenslauf-Vorlagen</p>
</div>
<div class="templates-grid">
<mat-grid-list cols="2" rowHeight="400px" gutterSize="24px">
<mat-grid-tile *ngFor="let template of templates">
<mat-card class="template-card">
<div class="template-preview">
<img [src]="template.preview" [alt]="template.name" class="preview-image">
<div class="preview-overlay">
<button mat-raised-button color="primary" (click)="selectTemplate(template.id)">
<mat-icon>brush</mat-icon>
Vorlage verwenden
</button>
<button mat-stroked-button (click)="previewTemplate(template.id)" class="ml-2">
<mat-icon>visibility</mat-icon>
Vorschau
</button>
</div>
</div>
<mat-card-content class="template-info">
<h3>{{ template.name }}</h3>
<p>{{ template.description }}</p>
<div class="template-tags">
<mat-chip-set>
<mat-chip *ngFor="let tag of template.tags">{{ tag }}</mat-chip>
</mat-chip-set>
</div>
</mat-card-content>
</mat-card>
</mat-grid-tile>
</mat-grid-list>
</div>
<div class="templates-actions">
<button mat-raised-button color="primary" routerLink="/builder">
<mat-icon>add</mat-icon>
Von Grund auf beginnen
</button>
</div>
</div>

View File

@@ -0,0 +1,89 @@
/**
* Templates Component
* Professional resume templates showcase and selection
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatCardModule } from '@angular/material/card';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatChipsModule } from '@angular/material/chips';
@Component({
selector: 'app-templates',
standalone: true,
imports: [
CommonModule,
RouterModule,
MatButtonModule,
MatIconModule,
MatCardModule,
MatGridListModule,
MatChipsModule
],
templateUrl: './templates.component.html',
styleUrls: ['./templates.component.css']
})
export class TemplatesComponent implements OnInit {
templates = [
{
id: 1,
name: 'Professional Classic',
description: 'Clean and professional design perfect for corporate environments',
preview: '/assets/templates/professional-classic.png',
category: 'Professional',
tags: ['Corporate', 'Clean', 'Modern']
},
{
id: 2,
name: 'Creative Modern',
description: 'Bold and creative design for creative professionals',
preview: '/assets/templates/creative-modern.png',
category: 'Creative',
tags: ['Creative', 'Bold', 'Artistic']
},
{
id: 3,
name: 'Executive Minimalist',
description: 'Elegant minimalist design for executive positions',
preview: '/assets/templates/executive-minimalist.png',
category: 'Executive',
tags: ['Minimalist', 'Executive', 'Elegant']
},
{
id: 4,
name: 'Tech Innovator',
description: 'Modern tech-focused design for IT professionals',
preview: '/assets/templates/tech-innovator.png',
category: 'Technology',
tags: ['Tech', 'Innovation', 'Modern']
}
];
ngOnInit(): void {
// Initialize component
}
/**
* Select a template and navigate to builder
*/
selectTemplate(templateId: number): void {
// TODO: Navigate to builder with selected template
// Implementation would go here
}
/**
* Preview template in full screen
*/
previewTemplate(templateId: number): void {
// TODO: Open template preview
// Implementation would go here
}
}

View File

@@ -0,0 +1,667 @@
/**
* Authentication Service
* Professional Angular Resume Builder - Enterprise Auth System
*
* ✅ PROFESSIONAL IMPLEMENTATION HIGHLIGHTS:
* • Centralized API endpoints via ApiEndpoints utility
* • Environment-based configuration (dev/prod)
* • Consistent error handling and retries
* • Professional code organization and separation of concerns
* • Type-safe endpoint management
* • Scalable architecture for future endpoints
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { BehaviorSubject, Observable, throwError, timer, of } from 'rxjs';
import { map, catchError, switchMap, tap, retry, timeout } from 'rxjs/operators';
import { Router } from '@angular/router';
import { environment } from '../../environments/environment';
import { ApiEndpoints } from '../utils';
import { LoadingService } from './loading.service';
import { EncryptionService } from './encryption.service';
import { DeviceFingerprintService } from './device-fingerprint.service';
import {
LoginCredentials,
RegisterData,
AuthResponse,
AuthUser,
AuthTokens,
PasswordResetRequest,
PasswordResetConfirmation,
AuthError,
JwtPayload,
SessionInfo,
TwoFactorSetup,
TwoFactorVerification,
DeviceVerificationResponse,
TwoFactorInitiationResponse
} from '../models';
/**
* Enterprise-grade Authentication Service
* Handles JWT authentication, session management, and security features
*/
@Injectable({
providedIn: 'root'
})
export class AuthService {
// Configuration Constants
private readonly TOKEN_REFRESH_THRESHOLD = 5 * 60 * 1000; // 5 minutes in milliseconds
private readonly MAX_RETRY_ATTEMPTS = 3;
private readonly REQUEST_TIMEOUT = 30000; // 30 seconds
// Local Storage Keys
private readonly ACCESS_TOKEN_KEY = 'david_auth_access_token';
private readonly REFRESH_TOKEN_KEY = 'david_auth_refresh_token';
private readonly USER_DATA_KEY = 'david_auth_user_data';
private readonly REMEMBER_ME_KEY = 'david_auth_remember_me';
// Reactive State Management
private currentUserSubject = new BehaviorSubject<AuthUser | null>(null);
private isAuthenticatedSubject = new BehaviorSubject<boolean>(false);
private isLoadingSubject = new BehaviorSubject<boolean>(false);
private deviceVerificationSubject = new BehaviorSubject<DeviceVerificationResponse | null>(null);
private requiresTwoFactorSubject = new BehaviorSubject<boolean>(false);
private tokenRefreshTimer: any;
// Public Observables
public readonly currentUser$ = this.currentUserSubject.asObservable();
public readonly isAuthenticated$ = this.isAuthenticatedSubject.asObservable();
public readonly isLoading$ = this.isLoadingSubject.asObservable();
public readonly deviceVerification$ = this.deviceVerificationSubject.asObservable();
public readonly requiresTwoFactor$ = this.requiresTwoFactorSubject.asObservable();
constructor(
private http: HttpClient,
private router: Router,
private loadingService: LoadingService,
private encryptionService: EncryptionService,
private deviceFingerprintService: DeviceFingerprintService
) {
this.initializeAuthState();
this.setupTokenRefreshTimer();
}
/**
* Initialize authentication state from stored tokens
*/
private initializeAuthState(): void {
const accessToken = this.getAccessToken();
const userData = this.getUserData();
if (accessToken && this.isTokenValid(accessToken)) {
// Set temporary auth state with stored data
this.isAuthenticatedSubject.next(true);
if (userData) {
this.currentUserSubject.next(userData);
}
// Fetch fresh profile data from backend
this.fetchUserProfile().subscribe({
next: (user) => {
// Profile loaded successfully
},
error: (error) => {
// Keep using stored data if backend request fails
if (!userData) {
this.clearAuthState();
}
}
});
this.scheduleTokenRefresh();
} else {
this.clearAuthState();
}
}
/**
* Public method to refresh authentication state
* Can be called after manual token storage to update auth state
*/
public refreshAuthState(): void {
this.initializeAuthState();
}
/**
* Fetch user profile from backend (internal method)
*/
private fetchUserProfile(): Observable<AuthUser> {
return this.getCurrentUser();
}
/**
* User login with integrated device fingerprint verification and 2FA support
*/
login(credentials: LoginCredentials): Observable<AuthResponse> {
// Start both global and form loading
this.loadingService.startGlobalLoading();
this.loadingService.startFormLoading('login-form');
this.isLoadingSubject.next(true);
// Generate device fingerprint
const deviceFingerprint = this.deviceFingerprintService.generateFingerprint();
// Create secure encrypted payload
const securePayload = this.encryptionService.createSecurePayload(credentials.password);
// Prepare login payload with encrypted password and device fingerprint
const loginPayload = {
email: credentials.email,
deviceFingerprint: {
userAgent: deviceFingerprint.userAgent,
acceptLanguage: deviceFingerprint.acceptLanguage,
screenResolution: deviceFingerprint.screenResolution,
timezone: deviceFingerprint.timezone,
platform: deviceFingerprint.platform,
fontsHash: deviceFingerprint.fontsHash,
canvasFingerprint: deviceFingerprint.canvasFingerprint,
webglFingerprint: deviceFingerprint.webglFingerprint,
cookieEnabled: deviceFingerprint.cookieEnabled,
doNotTrack: deviceFingerprint.doNotTrack,
touchSupport: 'ontouchstart' in window || navigator.maxTouchPoints > 0,
colorDepth: screen.colorDepth || 24,
},
...securePayload
};
return this.http.post<AuthResponse>(ApiEndpoints.AUTH.LOGIN, loginPayload)
.pipe(
timeout(this.REQUEST_TIMEOUT),
retry(this.MAX_RETRY_ATTEMPTS),
tap(response => {
if (response.requiresTwoFactor) {
// 2FA is required - don't set tokens yet
this.requiresTwoFactorSubject.next(true);
} else {
// Login successful - handle normal authentication
this.handleSuccessfulAuth(response, credentials.rememberMe);
}
}),
catchError(error => this.handleAuthError(error)),
tap(() => this.stopLoading())
);
}
/**
* Proceed with normal login after device verification
*/
private proceedWithLogin(credentials: LoginCredentials): Observable<AuthResponse> {
// Create secure encrypted payload
const securePayload = this.encryptionService.createSecurePayload(credentials.password);
// Prepare login payload with encrypted password
const loginPayload = {
email: credentials.email,
...securePayload
};
return this.http.post<AuthResponse>(ApiEndpoints.AUTH.LOGIN, loginPayload)
.pipe(
timeout(this.REQUEST_TIMEOUT),
retry(this.MAX_RETRY_ATTEMPTS),
tap(response => this.handleSuccessfulAuth(response, credentials.rememberMe)),
catchError(error => this.handleAuthError(error)),
tap(() => this.stopLoading())
);
}
/**
* Initiate 2FA for device registration
*/
initiateTwoFactor(userId: number, deviceName?: string): Observable<TwoFactorInitiationResponse> {
return this.deviceFingerprintService.initiateTwoFactor(userId, deviceName);
}
/**
* Verify 2FA code and complete device registration
*/
verifyTwoFactor(code: string, verificationId: string, userId: number, tempToken?: string): Observable<any>;
verifyTwoFactor(verification: TwoFactorVerification): Observable<AuthResponse>;
verifyTwoFactor(
codeOrVerification: string | TwoFactorVerification,
verificationId?: string,
userId?: number,
tempToken?: string
): Observable<any> {
if (typeof codeOrVerification === 'string' && verificationId && userId) {
// New device registration 2FA
return this.deviceFingerprintService.verifyTwoFactor(codeOrVerification, verificationId, userId, tempToken)
.pipe(
tap(response => {
if (response.success) {
// Device is now trusted, clear 2FA requirement
this.requiresTwoFactorSubject.next(false);
this.deviceVerificationSubject.next(null);
}
})
);
} else {
// Legacy 2FA verification
const verification = codeOrVerification as TwoFactorVerification;
return this.http.post<AuthResponse>(ApiEndpoints.AUTH.TWO_FACTOR.VERIFY, verification)
.pipe(
timeout(this.REQUEST_TIMEOUT),
tap(response => this.handleSuccessfulAuth(response, false)),
catchError(error => this.handleAuthError(error))
);
}
}
/**
* Clear device verification state
*/
clearDeviceVerification(): void {
this.deviceVerificationSubject.next(null);
this.requiresTwoFactorSubject.next(false);
this.deviceFingerprintService.clearTwoFactorSession();
}
/**
* Stop loading states
*/
private stopLoading(): void {
this.loadingService.stopGlobalLoading();
this.loadingService.stopFormLoading('login-form');
this.isLoadingSubject.next(false);
}
/**
* User registration with validation and secure password transmission
*/
register(registerData: RegisterData): Observable<AuthResponse> {
// Start both global and form loading
this.loadingService.startGlobalLoading();
this.loadingService.startFormLoading('register-form');
this.isLoadingSubject.next(true);
// Create secure encrypted payloads for both password fields
const passwordPayload = this.encryptionService.createSecurePayload(registerData.password);
const confirmPasswordPayload = this.encryptionService.createSecurePayload(registerData.confirmPassword);
// Transform RegisterData to match backend with encrypted passwords
const registerRequest = {
firstName: registerData.firstName,
lastName: registerData.lastName,
email: registerData.email,
encryptedPassword: passwordPayload.encryptedPassword,
passwordEncryptionMeta: passwordPayload.encryptionMeta,
encryptedConfirmPassword: confirmPasswordPayload.encryptedPassword,
confirmPasswordEncryptionMeta: confirmPasswordPayload.encryptionMeta
};
return this.http.post<AuthResponse>(ApiEndpoints.AUTH.REGISTER, registerRequest)
.pipe(
timeout(this.REQUEST_TIMEOUT),
retry(this.MAX_RETRY_ATTEMPTS),
tap(response => this.handleSuccessfulAuth(response, false)),
catchError(error => this.handleAuthError(error)),
tap(() => {
// Stop all loading states
this.loadingService.stopGlobalLoading();
this.loadingService.stopFormLoading('register-form');
this.isLoadingSubject.next(false);
})
);
}
/**
* Secure logout with token invalidation
*/
logout(): Observable<any> {
const refreshToken = this.getRefreshToken();
// Clear local state immediately
this.clearAuthState();
this.clearTokenRefreshTimer();
// Notify server to invalidate tokens
if (refreshToken) {
return this.http.post(ApiEndpoints.AUTH.LOGOUT, { refreshToken })
.pipe(
catchError(() => of(null)), // Continue even if server request fails
tap(() => this.router.navigate(['/login']))
);
}
this.router.navigate(['/login']);
return of(null);
}
/**
* Refresh authentication token
*/
refreshToken(): Observable<AuthTokens> {
const refreshToken = this.getRefreshToken();
if (!refreshToken) {
return throwError(() => new Error('No refresh token available'));
}
return this.http.post<AuthTokens>(ApiEndpoints.AUTH.REFRESH_TOKEN, { refreshToken })
.pipe(
tap(tokens => this.storeTokens(tokens)),
catchError(error => {
this.clearAuthState();
this.router.navigate(['/login']);
return throwError(() => error);
})
);
}
/**
* Request password reset
*/
requestPasswordReset(request: PasswordResetRequest): Observable<{ message: string }> {
return this.http.post<{ message: string }>(ApiEndpoints.AUTH.PASSWORD_RESET.REQUEST, request)
.pipe(
timeout(this.REQUEST_TIMEOUT),
catchError(error => this.handleAuthError(error))
);
}
/**
* Confirm password reset
*/
confirmPasswordReset(confirmation: PasswordResetConfirmation): Observable<{ message: string }> {
return this.http.post<{ message: string }>(ApiEndpoints.AUTH.PASSWORD_RESET.CONFIRM, confirmation)
.pipe(
timeout(this.REQUEST_TIMEOUT),
catchError(error => this.handleAuthError(error))
);
}
/**
* Setup two-factor authentication
*/
setupTwoFactor(): Observable<TwoFactorSetup> {
return this.http.post<TwoFactorSetup>(ApiEndpoints.AUTH.TWO_FACTOR.SETUP, {})
.pipe(
timeout(this.REQUEST_TIMEOUT),
catchError(error => this.handleAuthError(error))
);
}
/**
* Get current user profile
*/
getCurrentUser(): Observable<AuthUser> {
return this.http.get<AuthUser>(ApiEndpoints.AUTH.PROFILE)
.pipe(
tap(user => {
this.currentUserSubject.next(user);
this.storeUserData(user);
}),
catchError(error => this.handleAuthError(error))
);
}
/**
* Update user profile
*/
updateProfile(userData: Partial<AuthUser>): Observable<AuthUser> {
return this.http.put<AuthUser>(ApiEndpoints.AUTH.PROFILE, userData)
.pipe(
tap(user => {
this.currentUserSubject.next(user);
this.storeUserData(user);
}),
catchError(error => this.handleAuthError(error))
);
}
/**
* Check if user has specific role
*/
hasRole(role: string): boolean {
const user = this.currentUserSubject.value;
return user ? (user.roles || []).includes(role) : false;
}
/**
* Check if user has specific permission
*/
hasPermission(permission: string): boolean {
const user = this.currentUserSubject.value;
return user ? (user.permissions || []).includes(permission) : false;
}
/**
* Get current authentication status
*/
isAuthenticated(): boolean {
return this.isAuthenticatedSubject.value;
}
/**
* Get current user
*/
getCurrentUserValue(): AuthUser | null {
return this.currentUserSubject.value;
}
/**
* Handle successful authentication
*/
private handleSuccessfulAuth(response: AuthResponse, rememberMe: boolean = false): void {
// Only handle successful auth if we have an access token
if (!response.accessToken) {
throw new Error('Invalid authentication response: missing access token');
}
// Create tokens object from NestJS response
const tokens: AuthTokens = {
accessToken: response.accessToken
// refreshToken, expiresIn, tokenType are optional and not provided by NestJS initially
};
this.storeTokens(tokens);
this.storeUserData(response.user);
if (rememberMe) {
localStorage.setItem(this.REMEMBER_ME_KEY, 'true');
}
this.currentUserSubject.next(response.user);
this.isAuthenticatedSubject.next(true);
// Note: Token refresh scheduling may need to be adjusted for single token approach
}
/**
* Handle authentication errors
*/
private handleAuthError(error: HttpErrorResponse): Observable<never> {
let authError: AuthError;
if (error.error instanceof ErrorEvent) {
// Client-side error
authError = {
code: 'CLIENT_ERROR',
message: 'Network error occurred. Please check your connection.',
details: error.error.message,
timestamp: new Date()
};
} else {
// Server-side error
authError = {
code: error.error?.code || `HTTP_${error.status}`,
message: error.error?.message || 'An authentication error occurred.',
details: error.error?.details,
timestamp: new Date()
};
}
console.error('Authentication Error:', authError);
return throwError(() => authError);
}
/**
* Store authentication tokens securely
* Compatible with both simple and full token responses
*/
private storeTokens(tokens: AuthTokens): void {
if (localStorage.getItem(this.REMEMBER_ME_KEY)) {
localStorage.setItem(this.ACCESS_TOKEN_KEY, tokens.accessToken);
if (tokens.refreshToken) {
localStorage.setItem(this.REFRESH_TOKEN_KEY, tokens.refreshToken);
}
} else {
sessionStorage.setItem(this.ACCESS_TOKEN_KEY, tokens.accessToken);
if (tokens.refreshToken) {
sessionStorage.setItem(this.REFRESH_TOKEN_KEY, tokens.refreshToken);
}
}
}
/**
* Store user data
*/
private storeUserData(user: AuthUser): void {
const storage = localStorage.getItem(this.REMEMBER_ME_KEY) ? localStorage : sessionStorage;
storage.setItem(this.USER_DATA_KEY, JSON.stringify(user));
}
/**
* Get stored access token
*/
private getAccessToken(): string | null {
return localStorage.getItem(this.ACCESS_TOKEN_KEY) ||
sessionStorage.getItem(this.ACCESS_TOKEN_KEY);
}
/**
* Get stored refresh token
*/
private getRefreshToken(): string | null {
return localStorage.getItem(this.REFRESH_TOKEN_KEY) ||
sessionStorage.getItem(this.REFRESH_TOKEN_KEY);
}
/**
* Get stored user data
*/
private getUserData(): AuthUser | null {
const userData = localStorage.getItem(this.USER_DATA_KEY) ||
sessionStorage.getItem(this.USER_DATA_KEY);
return userData ? JSON.parse(userData) : null;
}
/**
* Clear authentication state
*/
private clearAuthState(): void {
// Clear from both storages
[localStorage, sessionStorage].forEach(storage => {
storage.removeItem(this.ACCESS_TOKEN_KEY);
storage.removeItem(this.REFRESH_TOKEN_KEY);
storage.removeItem(this.USER_DATA_KEY);
});
localStorage.removeItem(this.REMEMBER_ME_KEY);
this.currentUserSubject.next(null);
this.isAuthenticatedSubject.next(false);
}
/**
* Check if token is valid and not expired
*/
private isTokenValid(token: string): boolean {
try {
const payload = this.decodeJwtPayload(token);
const currentTime = Math.floor(Date.now() / 1000);
return payload.exp > currentTime;
} catch {
return false;
}
}
/**
* Decode JWT token payload
*/
private decodeJwtPayload(token: string): JwtPayload {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent(
atob(base64)
.split('')
.map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
.join('')
);
return JSON.parse(jsonPayload);
}
/**
* Setup automatic token refresh
*/
private setupTokenRefreshTimer(): void {
// Check token validity every minute
timer(0, 60000).subscribe(() => {
if (this.isAuthenticated()) {
const token = this.getAccessToken();
if (token && this.shouldRefreshToken(token)) {
this.refreshToken().subscribe({
error: () => this.logout()
});
}
}
});
}
/**
* Check if token should be refreshed
*/
private shouldRefreshToken(token: string): boolean {
try {
const payload = this.decodeJwtPayload(token);
const currentTime = Math.floor(Date.now() / 1000);
const timeUntilExpiry = (payload.exp - currentTime) * 1000;
return timeUntilExpiry < this.TOKEN_REFRESH_THRESHOLD;
} catch {
return true;
}
}
/**
* Schedule token refresh
*/
private scheduleTokenRefresh(): void {
this.clearTokenRefreshTimer();
const token = this.getAccessToken();
if (token) {
try {
const payload = this.decodeJwtPayload(token);
const currentTime = Math.floor(Date.now() / 1000);
const refreshTime = (payload.exp - currentTime - 300) * 1000; // 5 minutes before expiry
if (refreshTime > 0) {
this.tokenRefreshTimer = setTimeout(() => {
this.refreshToken().subscribe({
error: () => this.logout()
});
}, refreshTime);
}
} catch {
// Token is invalid, logout
this.logout();
}
}
}
/**
* Clear token refresh timer
*/
private clearTokenRefreshTimer(): void {
if (this.tokenRefreshTimer) {
clearTimeout(this.tokenRefreshTimer);
this.tokenRefreshTimer = null;
}
}
}

View File

@@ -0,0 +1,249 @@
/**
* Device Fingerprinting Service
* Professional Angular Resume Builder - Browser Fingerprinting System
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, BehaviorSubject, throwError } from 'rxjs';
import { map, catchError, tap } from 'rxjs/operators';
import { environment } from '../../environments/environment';
import {
DeviceFingerprint,
DeviceVerificationRequest,
DeviceVerificationResponse,
TwoFactorInitiationRequest,
TwoFactorInitiationResponse,
TwoFactorVerificationRequest
} from '../models/auth.model';
/**
* Service for generating device fingerprints and managing device trust
*/
@Injectable({
providedIn: 'root'
})
export class DeviceFingerprintService {
private readonly apiUrl = `${environment.apiUrl}/device-fingerprint`;
// State management for 2FA process
private currentTwoFactorSession = new BehaviorSubject<TwoFactorInitiationResponse | null>(null);
public readonly currentTwoFactorSession$ = this.currentTwoFactorSession.asObservable();
constructor(private http: HttpClient) {}
/**
* Generate browser fingerprint from available browser APIs
*/
generateFingerprint(): DeviceFingerprint {
try {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
let canvasFingerprint = '';
if (ctx) {
ctx.textBaseline = 'top';
ctx.font = '14px Arial';
ctx.fillText('Device fingerprint test 🔒', 2, 2);
canvasFingerprint = canvas.toDataURL().slice(-50);
}
// Collect available fonts (simplified approach)
const testFonts = ['Arial', 'Helvetica', 'Times', 'Georgia', 'Verdana', 'Courier'];
const availableFonts = testFonts.filter(font => this.isFontAvailable(font));
const fontsHash = this.hashString(availableFonts.join(','));
const fingerprint: DeviceFingerprint = {
userAgent: navigator.userAgent,
acceptLanguage: navigator.language || (navigator as any).userLanguage || '',
screenResolution: `${screen.width}x${screen.height}`,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
platform: navigator.platform,
cookieEnabled: navigator.cookieEnabled,
doNotTrack: navigator.doNotTrack === '1',
fontsHash: fontsHash,
canvasFingerprint: canvasFingerprint,
webglFingerprint: this.getWebGLFingerprint()
};
return fingerprint;
} catch (error) {
console.error('Error generating fingerprint:', error);
// Return minimal fingerprint on error
return {
userAgent: navigator.userAgent,
acceptLanguage: navigator.language || '',
screenResolution: `${screen.width}x${screen.height}`,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
platform: navigator.platform,
cookieEnabled: navigator.cookieEnabled,
doNotTrack: false
};
}
}
/**
* Verify device trust status with backend
*/
verifyDevice(userId: number): Observable<DeviceVerificationResponse> {
const fingerprint = this.generateFingerprint();
const request: DeviceVerificationRequest = {
userId,
fingerprint
};
return this.http.post<DeviceVerificationResponse>(`${this.apiUrl}/verify`, request)
.pipe(
catchError(error => {
console.error('Device verification failed:', error);
return throwError(() => error);
})
);
}
/**
* Initiate two-factor authentication for device registration
*/
initiateTwoFactor(userId: number, deviceName?: string): Observable<TwoFactorInitiationResponse> {
const request: TwoFactorInitiationRequest = {
userId,
method: 'email' // Default method for demo mode
};
return this.http.post<TwoFactorInitiationResponse>(`${this.apiUrl}/two-factor/initiate`, request)
.pipe(
tap(response => {
// Store the 2FA session
this.currentTwoFactorSession.next(response);
}),
catchError(error => {
console.error('2FA initiation failed:', error);
return throwError(() => error);
})
);
}
/**
* Verify two-factor authentication code
*/
verifyTwoFactor(code: string, verificationId: string, userId: number, tempToken?: string): Observable<any> {
const request: TwoFactorVerificationRequest = {
code,
verificationId,
userId,
tempToken // Include temp token if provided
};
return this.http.post<any>(`${this.apiUrl}/two-factor/verify`, request)
.pipe(
tap(response => {
// Clear the 2FA session on success
if (response.success) {
this.currentTwoFactorSession.next(null);
}
}),
catchError(error => {
console.error('2FA verification failed:', error);
return throwError(() => error);
})
);
}
/**
* Clear current 2FA session
*/
clearTwoFactorSession(): void {
this.currentTwoFactorSession.next(null);
}
/**
* Check if a font is available by measuring text width
*/
private isFontAvailable(fontName: string): boolean {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) return false;
const baselineText = 'abcdefghijklmnopqrstuvwxyz';
ctx.font = '32px monospace';
const baselineWidth = ctx.measureText(baselineText).width;
ctx.font = `32px ${fontName}, monospace`;
const testWidth = ctx.measureText(baselineText).width;
return baselineWidth !== testWidth;
}
/**
* Get WebGL fingerprint for additional uniqueness
*/
private getWebGLFingerprint(): string {
try {
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl') as WebGLRenderingContext;
if (!gl) return '';
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
if (debugInfo) {
const vendor = gl.getParameter((debugInfo as any).UNMASKED_VENDOR_WEBGL);
const renderer = gl.getParameter((debugInfo as any).UNMASKED_RENDERER_WEBGL);
return this.hashString(`${vendor}|${renderer}`).slice(0, 16);
}
return '';
} catch (error) {
return '';
}
}
/**
* Generate device name based on browser and platform info
*/
private getDeviceName(): string {
const userAgent = navigator.userAgent;
let deviceName = 'Unknown Device';
if (userAgent.includes('Windows')) {
deviceName = 'Windows Device';
} else if (userAgent.includes('Mac')) {
deviceName = 'Mac Device';
} else if (userAgent.includes('Linux')) {
deviceName = 'Linux Device';
} else if (userAgent.includes('Android')) {
deviceName = 'Android Device';
} else if (userAgent.includes('iOS') || userAgent.includes('iPhone') || userAgent.includes('iPad')) {
deviceName = 'iOS Device';
}
// Add browser info
if (userAgent.includes('Chrome')) {
deviceName += ' (Chrome)';
} else if (userAgent.includes('Firefox')) {
deviceName += ' (Firefox)';
} else if (userAgent.includes('Safari') && !userAgent.includes('Chrome')) {
deviceName += ' (Safari)';
} else if (userAgent.includes('Edge')) {
deviceName += ' (Edge)';
}
return deviceName;
}
/**
* Simple string hashing function
*/
private hashString(str: string): string {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32-bit integer
}
return Math.abs(hash).toString(16);
}
}

View File

@@ -0,0 +1,137 @@
/**
* Professional Encryption Service
* Author: David Valera Melendez
* Email: david@valera-melendez.de
*
* Handles secure password encryption for frontend-backend communication
* Uses AES-256-GCM encryption for maximum security
*/
import { Injectable } from '@angular/core';
import * as CryptoJS from 'crypto-js';
import { environment } from '../../environments/environment';
/**
* Encryption configuration interface
*/
interface EncryptionResult {
encryptedData: string;
iv: string;
salt: string;
}
@Injectable({
providedIn: 'root'
})
export class EncryptionService {
// Professional encryption configuration from environment
private readonly ENCRYPTION_KEY: string;
private readonly KEY_SIZE: number;
private readonly IV_SIZE = 16;
private readonly SALT_SIZE = 16;
private readonly ITERATIONS: number;
private readonly ALGORITHM: string;
constructor() {
// Get encryption configuration from environment
this.ENCRYPTION_KEY = environment.security.encryptionKey;
this.KEY_SIZE = environment.security.keySize;
this.ITERATIONS = environment.security.iterations;
this.ALGORITHM = environment.security.encryptionAlgorithm;
// Validate environment configuration
if (!this.ENCRYPTION_KEY) {
throw new Error('Encryption key not found in environment configuration');
}
if (this.ENCRYPTION_KEY.length < 32) {
throw new Error('Encryption key must be at least 32 characters long');
}
}
/**
* Encrypts password using AES-256-GCM with random IV and salt
* @param password The plain text password to encrypt
* @returns Encryption result with encrypted data, IV, and salt
*/
encryptPassword(password: string): EncryptionResult {
try {
// Generate random salt and IV for each encryption
const salt = CryptoJS.lib.WordArray.random(this.SALT_SIZE);
const iv = CryptoJS.lib.WordArray.random(this.IV_SIZE);
// Derive key using PBKDF2
const key = CryptoJS.PBKDF2(this.ENCRYPTION_KEY, salt, {
keySize: this.KEY_SIZE / 32,
iterations: this.ITERATIONS
});
// Encrypt the password
const encrypted = CryptoJS.AES.encrypt(password, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return {
encryptedData: encrypted.toString(),
iv: CryptoJS.enc.Base64.stringify(iv),
salt: CryptoJS.enc.Base64.stringify(salt)
};
} catch (error) {
console.error('❌ Encryption failed:', error);
throw new Error('Password encryption failed');
}
}
/**
* Creates a secure payload for backend transmission
* @param password The plain text password
* @returns Secure payload object
*/
createSecurePayload(password: string): {
encryptedPassword: string;
encryptionMeta: {
iv: string;
salt: string;
algorithm: string;
keySize: number;
iterations: number;
}
} {
const encryptionResult = this.encryptPassword(password);
return {
encryptedPassword: encryptionResult.encryptedData,
encryptionMeta: {
iv: encryptionResult.iv,
salt: encryptionResult.salt,
algorithm: this.ALGORITHM,
keySize: this.KEY_SIZE,
iterations: this.ITERATIONS
}
};
}
/**
* Generates a secure hash for password verification (client-side only)
* This is NOT sent to backend, only for local validation
*/
generatePasswordHash(password: string): string {
return CryptoJS.SHA256(password + this.ENCRYPTION_KEY).toString();
}
/**
* Validates encryption parameters
* @param encryptionMeta Encryption metadata to validate
* @returns boolean indicating if parameters are valid
*/
validateEncryptionMeta(encryptionMeta: any): boolean {
return encryptionMeta &&
encryptionMeta.iv &&
encryptionMeta.salt &&
encryptionMeta.algorithm === this.ALGORITHM &&
encryptionMeta.keySize === this.KEY_SIZE;
}
}

View File

@@ -0,0 +1,251 @@
/**
* HTTP Service
* Professional Angular Resume Builder - HTTP Client Wrapper
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpParams, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, timeout, retry } from 'rxjs/operators';
import { ApiEndpoints, HttpMethod, ApiRequestConfig } from '../utils';
/**
* Professional HTTP Service with consistent error handling and configuration
*/
@Injectable({
providedIn: 'root'
})
export class HttpService {
private readonly DEFAULT_TIMEOUT = 30000; // 30 seconds
private readonly DEFAULT_RETRY_ATTEMPTS = 3;
constructor(private http: HttpClient) {}
/**
* Generic request method with professional configuration
*/
request<T>(config: ApiRequestConfig): Observable<T> {
const {
endpoint,
method,
body,
params,
headers,
timeout: requestTimeout = this.DEFAULT_TIMEOUT,
retries = this.DEFAULT_RETRY_ATTEMPTS
} = config;
// Build HTTP options
const httpOptions = this.buildHttpOptions(headers, params);
// Validate endpoint
if (!ApiEndpoints.isValidEndpoint(endpoint)) {
return throwError(() => new Error('Invalid endpoint URL'));
}
// Make the request
let request$: Observable<T>;
switch (method) {
case HttpMethod.GET:
request$ = this.http.get<T>(endpoint, httpOptions);
break;
case HttpMethod.POST:
request$ = this.http.post<T>(endpoint, body, httpOptions);
break;
case HttpMethod.PUT:
request$ = this.http.put<T>(endpoint, body, httpOptions);
break;
case HttpMethod.PATCH:
request$ = this.http.patch<T>(endpoint, body, httpOptions);
break;
case HttpMethod.DELETE:
request$ = this.http.delete<T>(endpoint, httpOptions);
break;
default:
return throwError(() => new Error(`Unsupported HTTP method: ${method}`));
}
return request$.pipe(
timeout(requestTimeout),
retry(retries),
catchError(error => this.handleError(error))
);
}
/**
* Convenience method for GET requests
*/
get<T>(endpoint: string, params?: Record<string, any>): Observable<T> {
const url = params ? ApiEndpoints.withQuery(endpoint, params) : endpoint;
return this.request<T>({
endpoint: url,
method: HttpMethod.GET
});
}
/**
* Convenience method for POST requests
*/
post<T>(endpoint: string, body: any, headers?: Record<string, string>): Observable<T> {
return this.request<T>({
endpoint,
method: HttpMethod.POST,
body,
headers
});
}
/**
* Convenience method for PUT requests
*/
put<T>(endpoint: string, body: any, headers?: Record<string, string>): Observable<T> {
return this.request<T>({
endpoint,
method: HttpMethod.PUT,
body,
headers
});
}
/**
* Convenience method for PATCH requests
*/
patch<T>(endpoint: string, body: any, headers?: Record<string, string>): Observable<T> {
return this.request<T>({
endpoint,
method: HttpMethod.PATCH,
body,
headers
});
}
/**
* Convenience method for DELETE requests
*/
delete<T>(endpoint: string, headers?: Record<string, string>): Observable<T> {
return this.request<T>({
endpoint,
method: HttpMethod.DELETE,
headers
});
}
/**
* Upload file with progress tracking
*/
uploadFile<T>(endpoint: string, file: File, additionalData?: Record<string, any>): Observable<T> {
const formData = new FormData();
formData.append('file', file, file.name);
// Add additional form data if provided
if (additionalData) {
Object.entries(additionalData).forEach(([key, value]) => {
formData.append(key, String(value));
});
}
return this.post<T>(endpoint, formData);
}
/**
* Download file from endpoint
*/
downloadFile(endpoint: string, filename?: string): Observable<Blob> {
return this.http.get(endpoint, { responseType: 'blob' });
}
/**
* Build HTTP options from headers and params
*/
private buildHttpOptions(headers?: Record<string, string>, params?: Record<string, any>) {
let httpHeaders = new HttpHeaders({
'Content-Type': 'application/json'
});
// Add custom headers
if (headers) {
Object.entries(headers).forEach(([key, value]) => {
httpHeaders = httpHeaders.set(key, value);
});
}
// Build HTTP params
let httpParams = new HttpParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== null && value !== undefined && value !== '') {
httpParams = httpParams.set(key, String(value));
}
});
}
return {
headers: httpHeaders,
params: httpParams
};
}
/**
* Professional error handling
*/
private handleError(error: HttpErrorResponse): Observable<never> {
let errorMessage = 'An unknown error occurred';
if (error.error instanceof ErrorEvent) {
// Client-side error
errorMessage = `Client Error: ${error.error.message}`;
} else {
// Server-side error
switch (error.status) {
case 400:
errorMessage = 'Bad Request: ' + (error.error?.message || 'Invalid request');
break;
case 401:
errorMessage = 'Unauthorized: Please check your credentials';
break;
case 403:
errorMessage = 'Forbidden: You do not have permission to access this resource';
break;
case 404:
errorMessage = 'Not Found: The requested resource was not found';
break;
case 409:
errorMessage = 'Conflict: ' + (error.error?.message || 'Resource conflict');
break;
case 422:
errorMessage = 'Validation Error: ' + (error.error?.message || 'Invalid data provided');
break;
case 429:
errorMessage = 'Too Many Requests: Please wait before making another request';
break;
case 500:
errorMessage = 'Internal Server Error: Please try again later';
break;
case 503:
errorMessage = 'Service Unavailable: The server is currently unavailable';
break;
default:
errorMessage = `HTTP Error ${error.status}: ${error.error?.message || error.message}`;
}
}
console.error('HTTP Service Error:', {
status: error.status,
message: errorMessage,
url: error.url,
error: error.error
});
return throwError(() => ({
status: error.status,
message: errorMessage,
originalError: error
}));
}
}

View File

@@ -0,0 +1,134 @@
/**
* Global Loading Service
* Professional Angular Resume Builder - Enterprise Loading System
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class LoadingService {
private formLoadingSubject = new BehaviorSubject<{ [key: string]: boolean }>({});
private globalLoadingSubject = new BehaviorSubject<boolean>(false);
private progressSubject = new BehaviorSubject<number>(0);
// Active loading operations counter
private activeOperations = 0;
// Form-specific loading states
public formLoading$ = this.formLoadingSubject.asObservable();
public globalLoading$ = this.globalLoadingSubject.asObservable();
public progress$ = this.progressSubject.asObservable();
/**
* Start global loading with progress bar
*/
startGlobalLoading(): void {
this.activeOperations++;
if (this.activeOperations === 1) {
this.globalLoadingSubject.next(true);
this.animateProgressBar();
}
}
/**
* Stop global loading and hide progress bar
*/
stopGlobalLoading(): void {
this.activeOperations = Math.max(0, this.activeOperations - 1);
if (this.activeOperations === 0) {
this.completeProgressBar();
}
}
/**
* Animate progress bar from 0 to 90% then wait
*/
private animateProgressBar(): void {
this.progressSubject.next(0);
let progress = 0;
const increment = 2;
const animation = setInterval(() => {
progress += increment;
if (progress >= 90) {
progress = 90;
clearInterval(animation);
}
this.progressSubject.next(progress);
}, 20);
}
/**
* Complete progress bar animation
*/
private completeProgressBar(): void {
this.progressSubject.next(100);
setTimeout(() => {
this.globalLoadingSubject.next(false);
this.progressSubject.next(0);
}, 200);
}
/**
* Set progress bar value manually (0-100)
*/
setProgress(value: number): void {
this.progressSubject.next(Math.min(100, Math.max(0, value)));
}
/**
* Start form-specific loading
*/
startFormLoading(formId: string): void {
const currentState = this.formLoadingSubject.value;
this.formLoadingSubject.next({
...currentState,
[formId]: true
});
}
/**
* Stop form-specific loading
*/
stopFormLoading(formId: string): void {
const currentState = this.formLoadingSubject.value;
const newState = { ...currentState };
delete newState[formId];
this.formLoadingSubject.next(newState);
}
/**
* Check if specific form is loading
*/
isFormLoading(formId: string): Observable<boolean> {
return new Observable(observer => {
this.formLoading$.subscribe(loadingState => {
observer.next(!!loadingState[formId]);
});
});
}
/**
* Get current global loading state
*/
isGlobalLoading(): boolean {
return this.globalLoadingSubject.value;
}
/**
* Reset all loading states
*/
resetAllLoading(): void {
this.activeOperations = 0;
this.globalLoadingSubject.next(false);
this.formLoadingSubject.next({});
this.progressSubject.next(0);
}
}

View File

@@ -0,0 +1,365 @@
/**
* Resume Service
* Professional Angular Resume Builder
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { ResumeData, PersonalInfo, Experience, Education, Skill } from '../models/resume.model';
/**
* Resume Service - Central data management for resume builder
* Handles data persistence, state management, and data operations
*/
@Injectable({
providedIn: 'root'
})
export class ResumeService {
private readonly STORAGE_KEY = 'david_valera_resume_data';
// Reactive state management
private resumeDataSubject = new BehaviorSubject<ResumeData>(this.getEmptyResumeData());
public resumeData$ = this.resumeDataSubject.asObservable();
constructor() {
// Load existing data on service initialization
this.loadFromStorage();
}
/**
* Get current resume data
* @returns Current resume data
*/
getResumeData(): ResumeData {
return this.resumeDataSubject.value;
}
/**
* Update resume data and persist to storage
* @param resumeData Complete resume data object
*/
saveResumeData(resumeData: ResumeData): void {
resumeData.updatedAt = new Date();
this.resumeDataSubject.next(resumeData);
this.saveToStorage(resumeData);
}
/**
* Update personal information
* @param personalInfo Personal information object
*/
updatePersonalInfo(personalInfo: PersonalInfo): void {
const currentData = this.getResumeData();
currentData.personalInfo = { ...personalInfo };
this.saveResumeData(currentData);
}
/**
* Add new work experience
* @param experience Experience object to add
*/
addExperience(experience: Experience): void {
const currentData = this.getResumeData();
currentData.experience.push(experience);
this.saveResumeData(currentData);
}
/**
* Update existing work experience
* @param index Index of experience to update
* @param experience Updated experience object
*/
updateExperience(index: number, experience: Experience): void {
const currentData = this.getResumeData();
if (index >= 0 && index < currentData.experience.length) {
currentData.experience[index] = { ...experience };
this.saveResumeData(currentData);
}
}
/**
* Remove work experience
* @param index Index of experience to remove
*/
removeExperience(index: number): void {
const currentData = this.getResumeData();
if (index >= 0 && index < currentData.experience.length) {
currentData.experience.splice(index, 1);
this.saveResumeData(currentData);
}
}
/**
* Add new education entry
* @param education Education object to add
*/
addEducation(education: Education): void {
const currentData = this.getResumeData();
currentData.education.push(education);
this.saveResumeData(currentData);
}
/**
* Update existing education entry
* @param index Index of education to update
* @param education Updated education object
*/
updateEducation(index: number, education: Education): void {
const currentData = this.getResumeData();
if (index >= 0 && index < currentData.education.length) {
currentData.education[index] = { ...education };
this.saveResumeData(currentData);
}
}
/**
* Remove education entry
* @param index Index of education to remove
*/
removeEducation(index: number): void {
const currentData = this.getResumeData();
if (index >= 0 && index < currentData.education.length) {
currentData.education.splice(index, 1);
this.saveResumeData(currentData);
}
}
/**
* Add new skill
* @param skill Skill object to add
*/
addSkill(skill: Skill): void {
const currentData = this.getResumeData();
currentData.skills.push(skill);
this.saveResumeData(currentData);
}
/**
* Update existing skill
* @param index Index of skill to update
* @param skill Updated skill object
*/
updateSkill(index: number, skill: Skill): void {
const currentData = this.getResumeData();
if (index >= 0 && index < currentData.skills.length) {
currentData.skills[index] = { ...skill };
this.saveResumeData(currentData);
}
}
/**
* Remove skill
* @param index Index of skill to remove
*/
removeSkill(index: number): void {
const currentData = this.getResumeData();
if (index >= 0 && index < currentData.skills.length) {
currentData.skills.splice(index, 1);
this.saveResumeData(currentData);
}
}
/**
* Export resume data as JSON
* @returns Resume data as JSON string
*/
exportAsJson(): string {
const currentData = this.getResumeData();
return JSON.stringify(currentData, null, 2);
}
/**
* Import resume data from JSON
* @param jsonData JSON string of resume data
* @returns Success status
*/
importFromJson(jsonData: string): boolean {
try {
const resumeData: ResumeData = JSON.parse(jsonData);
this.validateResumeData(resumeData);
this.saveResumeData(resumeData);
return true;
} catch (error) {
console.error('Error importing resume data:', error);
return false;
}
}
/**
* Clear all resume data
*/
clearResumeData(): void {
const emptyData = this.getEmptyResumeData();
this.saveResumeData(emptyData);
}
/**
* Get sample resume data for demonstration
* @returns Sample resume data
*/
getSampleResumeData(): ResumeData {
return {
personalInfo: {
firstName: 'David',
lastName: 'Valera Melendez',
jobTitle: 'Senior Full-Stack Developer',
email: 'david@valera-melendez.de',
phone: '+49 123 456 7890',
city: 'Munich',
country: 'Germany',
location: 'Munich, Germany',
summary: 'Experienced Full-Stack Developer with expertise in Angular, React, Node.js, and cloud technologies. Passionate about creating scalable web applications and leading development teams.',
linkedin: 'https://linkedin.com/in/david-valera-melendez',
github: 'https://github.com/davidvalera',
website: 'https://valera-melendez.de'
},
experience: [
{
id: '1',
position: 'Senior Full-Stack Developer',
company: 'TechCorp GmbH',
location: 'Munich, Germany',
startDate: '2022-01',
endDate: '',
isCurrentPosition: true,
description: 'Leading development of enterprise web applications using modern technologies.',
achievements: [
'Improved application performance by 40%',
'Led team of 5 developers',
'Implemented CI/CD pipeline reducing deployment time by 60%'
],
technologies: ['Angular', 'Node.js', 'MongoDB', 'AWS']
}
],
education: [
{
id: '1',
degree: 'Master of Computer Science',
institution: 'Technical University of Munich',
location: 'Munich, Germany',
startDate: '2018-09',
endDate: '2020-07',
grade: '1.2',
description: 'Specialized in Software Engineering and Web Technologies'
}
],
skills: [
{ id: '1', name: 'Angular', category: 'technical', proficiency: 'expert', proficiencyLevel: 9 },
{ id: '2', name: 'TypeScript', category: 'technical', proficiency: 'expert', proficiencyLevel: 9 },
{ id: '3', name: 'Node.js', category: 'technical', proficiency: 'advanced', proficiencyLevel: 8 },
{ id: '4', name: 'Leadership', category: 'soft', proficiency: 'advanced', proficiencyLevel: 8 }
],
languages: [
{ id: '1', name: 'German', proficiency: 'native', level: 'C2' },
{ id: '2', name: 'English', proficiency: 'fluent', level: 'C1' },
{ id: '3', name: 'Spanish', proficiency: 'conversational', level: 'B2' }
],
certifications: [],
projects: [],
awards: [],
references: [],
createdAt: new Date(),
updatedAt: new Date(),
version: '1.0.0'
};
}
/**
* Load resume data from localStorage
*/
private loadFromStorage(): void {
try {
const stored = localStorage.getItem(this.STORAGE_KEY);
if (stored) {
const resumeData: ResumeData = JSON.parse(stored);
this.validateResumeData(resumeData);
this.resumeDataSubject.next(resumeData);
}
} catch (error) {
console.error('Error loading resume data from storage:', error);
// If data is corrupted, start with empty data
this.resumeDataSubject.next(this.getEmptyResumeData());
}
}
/**
* Save resume data to localStorage
* @param resumeData Resume data to save
*/
private saveToStorage(resumeData: ResumeData): void {
try {
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(resumeData));
} catch (error) {
console.error('Error saving resume data to storage:', error);
}
}
/**
* Validate resume data structure
* @param resumeData Resume data to validate
*/
private validateResumeData(resumeData: ResumeData): void {
if (!resumeData.personalInfo) {
throw new Error('Invalid resume data: missing personal info');
}
if (!Array.isArray(resumeData.experience)) {
resumeData.experience = [];
}
if (!Array.isArray(resumeData.education)) {
resumeData.education = [];
}
if (!Array.isArray(resumeData.skills)) {
resumeData.skills = [];
}
if (!Array.isArray(resumeData.languages)) {
resumeData.languages = [];
}
if (!Array.isArray(resumeData.certifications)) {
resumeData.certifications = [];
}
if (!Array.isArray(resumeData.projects)) {
resumeData.projects = [];
}
if (!Array.isArray(resumeData.awards)) {
resumeData.awards = [];
}
if (!Array.isArray(resumeData.references)) {
resumeData.references = [];
}
}
/**
* Get empty resume data structure
* @returns Empty resume data object
*/
private getEmptyResumeData(): ResumeData {
return {
personalInfo: {
firstName: '',
lastName: '',
jobTitle: '',
email: '',
phone: '',
city: '',
country: '',
location: '',
summary: ''
},
experience: [],
education: [],
skills: [],
languages: [],
certifications: [],
projects: [],
awards: [],
references: [],
createdAt: new Date(),
updatedAt: new Date(),
version: '1.0.0'
};
}
}

View File

@@ -0,0 +1,347 @@
/**
* Professional Theme Service
* Author: David Valera Melendez
* Email: david@valera-melendez.de
* Created: August 8, 2025
*
* Manages application theming, color schemes, and CSS custom properties
*/
import { Injectable, Inject } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { BehaviorSubject, Observable } from 'rxjs';
export interface ColorScheme {
name: string;
displayName: string;
primary: string;
primaryLight: string;
primaryDark: string;
accent: string;
success: string;
warning: string;
error: string;
info: string;
}
export interface Theme {
name: string;
displayName: string;
isDark: boolean;
colors: ColorScheme;
}
@Injectable({
providedIn: 'root'
})
export class ThemeService {
private currentThemeSubject = new BehaviorSubject<Theme>(this.getDefaultTheme());
public currentTheme$ = this.currentThemeSubject.asObservable();
private currentColorSchemeSubject = new BehaviorSubject<ColorScheme>(this.getDefaultColorScheme());
public currentColorScheme$ = this.currentColorSchemeSubject.asObservable();
// Predefined color schemes
private colorSchemes: ColorScheme[] = [
{
name: 'blue',
displayName: 'Professional Blue',
primary: '#1976d2',
primaryLight: '#42a5f5',
primaryDark: '#1565c0',
accent: '#ff9800',
success: '#4caf50',
warning: '#ff9800',
error: '#f44336',
info: '#2196f3'
},
{
name: 'green',
displayName: 'Success Green',
primary: '#4caf50',
primaryLight: '#81c784',
primaryDark: '#388e3c',
accent: '#ff5722',
success: '#4caf50',
warning: '#ff9800',
error: '#f44336',
info: '#2196f3'
},
{
name: 'purple',
displayName: 'Creative Purple',
primary: '#9c27b0',
primaryLight: '#ba68c8',
primaryDark: '#7b1fa2',
accent: '#ff9800',
success: '#4caf50',
warning: '#ff9800',
error: '#f44336',
info: '#2196f3'
},
{
name: 'indigo',
displayName: 'Deep Indigo',
primary: '#3f51b5',
primaryLight: '#7986cb',
primaryDark: '#303f9f',
accent: '#ff4081',
success: '#4caf50',
warning: '#ff9800',
error: '#f44336',
info: '#2196f3'
},
{
name: 'teal',
displayName: 'Modern Teal',
primary: '#009688',
primaryLight: '#4db6ac',
primaryDark: '#00796b',
accent: '#ff5722',
success: '#4caf50',
warning: '#ff9800',
error: '#f44336',
info: '#2196f3'
}
];
constructor(@Inject(DOCUMENT) private document: Document) {
this.initializeTheme();
}
/**
* Initialize theme from localStorage or default
*/
private initializeTheme(): void {
const savedTheme = this.getSavedTheme();
const savedColorScheme = this.getSavedColorScheme();
if (savedColorScheme) {
this.setColorScheme(savedColorScheme, false);
}
if (savedTheme) {
this.setTheme(savedTheme, false);
} else {
// Apply default theme
this.applyTheme(this.currentThemeSubject.value);
}
}
/**
* Get available color schemes
*/
getColorSchemes(): ColorScheme[] {
return [...this.colorSchemes];
}
/**
* Get current color scheme
*/
getCurrentColorScheme(): ColorScheme {
return this.currentColorSchemeSubject.value;
}
/**
* Set color scheme by name
*/
setColorScheme(schemeName: string, save: boolean = true): void {
const scheme = this.colorSchemes.find(s => s.name === schemeName);
if (!scheme) {
console.warn(`Color scheme '${schemeName}' not found`);
return;
}
this.currentColorSchemeSubject.next(scheme);
this.applyColorScheme(scheme);
if (save) {
this.saveColorScheme(schemeName);
}
}
/**
* Set theme (light/dark)
*/
setTheme(themeName: 'light' | 'dark', save: boolean = true): void {
const currentColors = this.currentColorSchemeSubject.value;
const theme: Theme = {
name: themeName,
displayName: themeName === 'dark' ? 'Dark Theme' : 'Light Theme',
isDark: themeName === 'dark',
colors: currentColors
};
this.currentThemeSubject.next(theme);
this.applyTheme(theme);
if (save) {
this.saveTheme(themeName);
}
}
/**
* Toggle between light and dark themes
*/
toggleTheme(): void {
const currentTheme = this.currentThemeSubject.value;
const newTheme = currentTheme.isDark ? 'light' : 'dark';
this.setTheme(newTheme);
}
/**
* Apply color scheme to CSS custom properties
*/
private applyColorScheme(scheme: ColorScheme): void {
const root = this.document.documentElement;
// Apply primary colors
root.style.setProperty('--color-primary', scheme.primary);
root.style.setProperty('--color-primary-light', scheme.primaryLight);
root.style.setProperty('--color-primary-dark', scheme.primaryDark);
root.style.setProperty('--color-primary-500', scheme.primary);
root.style.setProperty('--color-primary-600', scheme.primary);
root.style.setProperty('--color-primary-700', scheme.primaryDark);
// Apply accent color
root.style.setProperty('--color-accent', scheme.accent);
root.style.setProperty('--color-accent-light', this.lightenColor(scheme.accent));
root.style.setProperty('--color-accent-dark', this.darkenColor(scheme.accent));
// Apply semantic colors
root.style.setProperty('--color-success', scheme.success);
root.style.setProperty('--color-warning', scheme.warning);
root.style.setProperty('--color-error', scheme.error);
root.style.setProperty('--color-info', scheme.info);
// Update gradients
root.style.setProperty('--gradient-primary',
`linear-gradient(135deg, ${scheme.primary} 0%, ${scheme.primaryLight} 100%)`);
root.style.setProperty('--gradient-accent',
`linear-gradient(135deg, ${scheme.accent} 0%, ${this.lightenColor(scheme.accent)} 100%)`);
}
/**
* Apply theme (light/dark mode)
*/
private applyTheme(theme: Theme): void {
const root = this.document.documentElement;
// Set theme attribute for CSS selectors
root.setAttribute('data-theme', theme.name);
// Apply color scheme
this.applyColorScheme(theme.colors);
// Additional theme-specific properties can be set here
if (theme.isDark) {
root.classList.add('dark-theme');
root.classList.remove('light-theme');
} else {
root.classList.add('light-theme');
root.classList.remove('dark-theme');
}
}
/**
* Create custom color scheme
*/
createCustomColorScheme(name: string, colors: Partial<ColorScheme>): void {
const customScheme: ColorScheme = {
name: name,
displayName: colors.displayName || name,
primary: colors.primary || '#1976d2',
primaryLight: colors.primaryLight || this.lightenColor(colors.primary || '#1976d2'),
primaryDark: colors.primaryDark || this.darkenColor(colors.primary || '#1976d2'),
accent: colors.accent || '#ff9800',
success: colors.success || '#4caf50',
warning: colors.warning || '#ff9800',
error: colors.error || '#f44336',
info: colors.info || '#2196f3'
};
// Add to available schemes
const existingIndex = this.colorSchemes.findIndex(s => s.name === name);
if (existingIndex >= 0) {
this.colorSchemes[existingIndex] = customScheme;
} else {
this.colorSchemes.push(customScheme);
}
}
/**
* Reset to default theme and colors
*/
resetToDefault(): void {
this.setColorScheme('blue');
this.setTheme('light');
}
/**
* Utility: Lighten a hex color
*/
private lightenColor(hex: string, percent: number = 20): string {
const num = parseInt(hex.replace('#', ''), 16);
const amt = Math.round(2.55 * percent);
const R = (num >> 16) + amt;
const G = (num >> 8 & 0x00FF) + amt;
const B = (num & 0x0000FF) + amt;
return '#' + (0x1000000 + (R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 +
(G < 255 ? G < 1 ? 0 : G : 255) * 0x100 +
(B < 255 ? B < 1 ? 0 : B : 255)).toString(16).slice(1);
}
/**
* Utility: Darken a hex color
*/
private darkenColor(hex: string, percent: number = 20): string {
const num = parseInt(hex.replace('#', ''), 16);
const amt = Math.round(2.55 * percent);
const R = (num >> 16) - amt;
const G = (num >> 8 & 0x00FF) - amt;
const B = (num & 0x0000FF) - amt;
return '#' + (0x1000000 + (R > 255 ? 255 : R < 0 ? 0 : R) * 0x10000 +
(G > 255 ? 255 : G < 0 ? 0 : G) * 0x100 +
(B > 255 ? 255 : B < 0 ? 0 : B)).toString(16).slice(1);
}
// Storage methods
private getSavedTheme(): 'light' | 'dark' | null {
if (typeof localStorage !== 'undefined') {
return localStorage.getItem('app-theme') as 'light' | 'dark' | null;
}
return null;
}
private saveTheme(theme: 'light' | 'dark'): void {
if (typeof localStorage !== 'undefined') {
localStorage.setItem('app-theme', theme);
}
}
private getSavedColorScheme(): string | null {
if (typeof localStorage !== 'undefined') {
return localStorage.getItem('app-color-scheme');
}
return null;
}
private saveColorScheme(scheme: string): void {
if (typeof localStorage !== 'undefined') {
localStorage.setItem('app-color-scheme', scheme);
}
}
private getDefaultTheme(): Theme {
return {
name: 'light',
displayName: 'Light Theme',
isDark: false,
colors: this.getDefaultColorScheme()
};
}
private getDefaultColorScheme(): ColorScheme {
return this.colorSchemes[0]; // Professional Blue
}
}

View File

@@ -0,0 +1,234 @@
/**
* API Endpoints Configuration
* Professional Angular Resume Builder - Enterprise API Management
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
import { environment } from '../../environments/environment';
/**
* Centralized API endpoint management for professional organization
*/
export class ApiEndpoints {
// Base URLs
private static readonly API_BASE = environment.apiUrl;
private static readonly AUTH_BASE = environment.authApiUrl;
// Authentication Endpoints
static readonly AUTH = {
LOGIN: `${ApiEndpoints.AUTH_BASE}/login`,
REGISTER: `${ApiEndpoints.AUTH_BASE}/register`,
LOGOUT: `${ApiEndpoints.AUTH_BASE}/logout`,
REFRESH_TOKEN: `${ApiEndpoints.AUTH_BASE}/refresh`,
PROFILE: `${ApiEndpoints.AUTH_BASE}/profile`,
// Password Management
PASSWORD_RESET: {
REQUEST: `${ApiEndpoints.AUTH_BASE}/password-reset`,
CONFIRM: `${ApiEndpoints.AUTH_BASE}/password-reset/confirm`
},
// Two-Factor Authentication
TWO_FACTOR: {
SETUP: `${ApiEndpoints.AUTH_BASE}/2fa/setup`,
VERIFY: `${ApiEndpoints.AUTH_BASE}/2fa/verify`,
DISABLE: `${ApiEndpoints.AUTH_BASE}/2fa/disable`,
BACKUP_CODES: `${ApiEndpoints.AUTH_BASE}/2fa/backup-codes`
},
// Session Management
SESSION: {
INFO: `${ApiEndpoints.AUTH_BASE}/session/info`,
REFRESH: `${ApiEndpoints.AUTH_BASE}/session/refresh`,
TERMINATE_ALL: `${ApiEndpoints.AUTH_BASE}/session/terminate-all`
}
};
// Resume Builder API Endpoints
static readonly RESUME = {
// Resume CRUD Operations
LIST: `${ApiEndpoints.API_BASE}/resumes`,
CREATE: `${ApiEndpoints.API_BASE}/resumes`,
GET: (id: string) => `${ApiEndpoints.API_BASE}/resumes/${id}`,
UPDATE: (id: string) => `${ApiEndpoints.API_BASE}/resumes/${id}`,
DELETE: (id: string) => `${ApiEndpoints.API_BASE}/resumes/${id}`,
DUPLICATE: (id: string) => `${ApiEndpoints.API_BASE}/resumes/${id}/duplicate`,
// Resume Export/Import
EXPORT: {
PDF: (id: string) => `${ApiEndpoints.API_BASE}/resumes/${id}/export/pdf`,
DOCX: (id: string) => `${ApiEndpoints.API_BASE}/resumes/${id}/export/docx`,
JSON: (id: string) => `${ApiEndpoints.API_BASE}/resumes/${id}/export/json`,
HTML: (id: string) => `${ApiEndpoints.API_BASE}/resumes/${id}/export/html`
},
// Templates
TEMPLATES: {
LIST: `${ApiEndpoints.API_BASE}/resume-templates`,
GET: (id: string) => `${ApiEndpoints.API_BASE}/resume-templates/${id}`,
PREVIEW: (id: string) => `${ApiEndpoints.API_BASE}/resume-templates/${id}/preview`
},
// Sharing & Collaboration
SHARING: {
GET_LINK: (id: string) => `${ApiEndpoints.API_BASE}/resumes/${id}/share`,
UPDATE_PERMISSIONS: (id: string) => `${ApiEndpoints.API_BASE}/resumes/${id}/share/permissions`,
REVOKE_ACCESS: (id: string) => `${ApiEndpoints.API_BASE}/resumes/${id}/share/revoke`
}
};
// User Management Endpoints
static readonly USER = {
PROFILE: `${ApiEndpoints.API_BASE}/users/profile`,
UPDATE_PROFILE: `${ApiEndpoints.API_BASE}/users/profile`,
CHANGE_PASSWORD: `${ApiEndpoints.API_BASE}/users/change-password`,
DELETE_ACCOUNT: `${ApiEndpoints.API_BASE}/users/account`,
// User Preferences
PREFERENCES: {
GET: `${ApiEndpoints.API_BASE}/users/preferences`,
UPDATE: `${ApiEndpoints.API_BASE}/users/preferences`,
RESET: `${ApiEndpoints.API_BASE}/users/preferences/reset`
},
// User Activity
ACTIVITY: {
LIST: `${ApiEndpoints.API_BASE}/users/activity`,
CLEAR: `${ApiEndpoints.API_BASE}/users/activity/clear`
}
};
// File Management Endpoints
static readonly FILES = {
UPLOAD: `${ApiEndpoints.API_BASE}/files/upload`,
DOWNLOAD: (id: string) => `${ApiEndpoints.API_BASE}/files/${id}/download`,
DELETE: (id: string) => `${ApiEndpoints.API_BASE}/files/${id}`,
// Image Processing
IMAGES: {
UPLOAD: `${ApiEndpoints.API_BASE}/files/images/upload`,
RESIZE: (id: string) => `${ApiEndpoints.API_BASE}/files/images/${id}/resize`,
OPTIMIZE: (id: string) => `${ApiEndpoints.API_BASE}/files/images/${id}/optimize`
}
};
// Analytics & Reporting Endpoints
static readonly ANALYTICS = {
DASHBOARD: `${ApiEndpoints.API_BASE}/analytics/dashboard`,
RESUME_STATS: (id: string) => `${ApiEndpoints.API_BASE}/analytics/resumes/${id}/stats`,
USER_STATS: `${ApiEndpoints.API_BASE}/analytics/user/stats`,
EXPORT_DATA: `${ApiEndpoints.API_BASE}/analytics/export`
};
// Admin Endpoints (Role-based access)
static readonly ADMIN = {
USERS: {
LIST: `${ApiEndpoints.API_BASE}/admin/users`,
GET: (id: string) => `${ApiEndpoints.API_BASE}/admin/users/${id}`,
UPDATE: (id: string) => `${ApiEndpoints.API_BASE}/admin/users/${id}`,
DELETE: (id: string) => `${ApiEndpoints.API_BASE}/admin/users/${id}`,
SUSPEND: (id: string) => `${ApiEndpoints.API_BASE}/admin/users/${id}/suspend`,
ACTIVATE: (id: string) => `${ApiEndpoints.API_BASE}/admin/users/${id}/activate`
},
SYSTEM: {
HEALTH: `${ApiEndpoints.API_BASE}/admin/system/health`,
LOGS: `${ApiEndpoints.API_BASE}/admin/system/logs`,
METRICS: `${ApiEndpoints.API_BASE}/admin/system/metrics`,
BACKUP: `${ApiEndpoints.API_BASE}/admin/system/backup`
}
};
// Utility Methods for Dynamic Endpoints
/**
* Build query string from parameters
*/
static buildQuery(params: Record<string, any>): string {
const searchParams = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value !== null && value !== undefined && value !== '') {
searchParams.append(key, String(value));
}
});
const queryString = searchParams.toString();
return queryString ? `?${queryString}` : '';
}
/**
* Build endpoint with query parameters
*/
static withQuery(endpoint: string, params: Record<string, any>): string {
return endpoint + ApiEndpoints.buildQuery(params);
}
/**
* Validate endpoint URL format
*/
static isValidEndpoint(endpoint: string): boolean {
try {
new URL(endpoint);
return true;
} catch {
return false;
}
}
/**
* Get environment-specific base URL
*/
static getBaseUrl(type: 'api' | 'auth' = 'api'): string {
return type === 'auth' ? ApiEndpoints.AUTH_BASE : ApiEndpoints.API_BASE;
}
}
/**
* Type-safe endpoint builder for enhanced development experience
*/
export type EndpointBuilder = {
[K in keyof typeof ApiEndpoints]: typeof ApiEndpoints[K];
};
/**
* HTTP Method types for API requests
*/
export enum HttpMethod {
GET = 'GET',
POST = 'POST',
PUT = 'PUT',
PATCH = 'PATCH',
DELETE = 'DELETE'
}
/**
* API Request configuration interface
*/
export interface ApiRequestConfig {
endpoint: string;
method: HttpMethod;
body?: any;
params?: Record<string, any>;
headers?: Record<string, string>;
timeout?: number;
retries?: number;
}
/**
* Professional API endpoint documentation
*/
export const API_DOCUMENTATION = {
version: '1.0.0',
description: 'Professional Resume Builder API Endpoints',
baseUrl: environment.apiUrl,
authUrl: environment.authApiUrl,
supportedFormats: ['JSON', 'PDF', 'DOCX', 'HTML'],
authentication: 'JWT Bearer Token',
rateLimit: '1000 requests per hour',
pagination: 'Cursor-based pagination',
errorFormat: 'RFC 7807 Problem Details'
} as const;

25
src/app/utils/index.ts Normal file
View File

@@ -0,0 +1,25 @@
/**
* Utils Module Exports
* Professional Angular Resume Builder - Utility Functions
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
// API Endpoints Utility
export {
ApiEndpoints,
HttpMethod,
ApiRequestConfig,
API_DOCUMENTATION,
EndpointBuilder
} from './api-endpoints';
// HTTP Service
export { HttpService } from '../services/http.service';
// Future utility exports can be added here
// export { ValidationUtils } from './validation';
// export { FormatUtils } from './format';
// export { CacheUtils } from './cache';

View File

@@ -0,0 +1,73 @@
/**
* Production Environment Configuration
* Professional Angular Resume Builder - Enterprise Auth System
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
export const environment = {
production: true,
apiUrl: 'https://api.valera-melendez.de',
authApiUrl: 'https://api.valera-melendez.de/auth',
appName: 'Resume Builder',
appVersion: '1.0.0',
// Authentication Configuration
auth: {
tokenRefreshThreshold: 5 * 60 * 1000, // 5 minutes in milliseconds
sessionTimeout: 8 * 60 * 60 * 1000, // 8 hours in milliseconds (shorter for production)
maxLoginAttempts: 3, // More restrictive in production
lockoutDuration: 30 * 60 * 1000, // 30 minutes in milliseconds
enableRememberMe: true,
enableTwoFactor: true
},
// Security Configuration
security: {
enableHttps: true,
enableCsrf: true,
contentSecurityPolicy: true,
strictTransportSecurity: true,
// Encryption Configuration - CHANGE THESE IN PRODUCTION!
encryptionKey: 'DavidValeraMelendez2025SecurePasswordTransmissionProdKey32Plus!',
encryptionAlgorithm: 'AES-256-CBC',
keySize: 256,
iterations: 10000
},
// Feature Flags
features: {
socialLogin: true, // Enable in production
exportToPdf: true,
multiLanguage: true, // Enable in production
darkMode: true,
advancedTemplates: true
},
// Demo Configuration (Disabled in Production)
demo: {
enabled: false,
credentials: {
email: '',
password: ''
}
},
// Logging Configuration
logging: {
level: 'error', // Only log errors in production
enableConsoleLogging: false,
enableRemoteLogging: true,
logApiErrors: true
},
// Performance Configuration
performance: {
enableServiceWorker: true, // Enable PWA features in production
enableLazyLoading: true,
enableCompression: true,
cacheTimeout: 60 * 60 * 1000 // 1 hour
}
};

View File

@@ -0,0 +1,73 @@
/**
* Development Environment Configuration
* Professional Angular Resume Builder - Enterprise Auth System
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
export const environment = {
production: false,
apiUrl: 'http://localhost:3001',
authApiUrl: 'http://localhost:3001/auth',
appName: 'Resume Builder',
appVersion: '1.0.0',
// Authentication Configuration
auth: {
tokenRefreshThreshold: 5 * 60 * 1000, // 5 minutes in milliseconds
sessionTimeout: 24 * 60 * 60 * 1000, // 24 hours in milliseconds
maxLoginAttempts: 5,
lockoutDuration: 15 * 60 * 1000, // 15 minutes in milliseconds
enableRememberMe: true,
enableTwoFactor: true
},
// Security Configuration
security: {
enableHttps: true,
enableCsrf: true,
contentSecurityPolicy: true,
strictTransportSecurity: true,
// Encryption Configuration
encryptionKey: 'DavidValeraMelendez2025SecurePasswordTransmissionDevKey32Chars!',
encryptionAlgorithm: 'AES-256-CBC',
keySize: 256,
iterations: 10000
},
// Feature Flags
features: {
socialLogin: false,
exportToPdf: true,
multiLanguage: false,
darkMode: true,
advancedTemplates: true
},
// Demo Configuration (Development Only)
demo: {
enabled: true,
credentials: {
email: 'demo@example.com',
password: 'DemoPassword123!'
}
},
// Logging Configuration
logging: {
level: 'warn', // Reduced logging for production readiness
enableConsoleLogging: true,
enableRemoteLogging: false,
logApiErrors: true
},
// Performance Configuration
performance: {
enableServiceWorker: false,
enableLazyLoading: true,
enableCompression: false,
cacheTimeout: 30 * 60 * 1000 // 30 minutes
}
};

19
src/index.html Normal file
View File

@@ -0,0 +1,19 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Professional Resume Builder - David Valera Melendez</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="Professional Resume Builder created by David Valera Melendez - Made in Germany">
<meta name="author" content="David Valera Melendez">
<meta name="keywords" content="resume, cv, curriculum vitae, professional, builder, David Valera Melendez, Germany, Angular, Material UI">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body class="mat-typography">
<app-root></app-root>
</body>
</html>

65
src/main.ts Normal file
View File

@@ -0,0 +1,65 @@
/**
* Main Application Entry Point
* Professional Angular Resume Builder
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { provideAnimations } from '@angular/platform-browser/animations';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { importProvidersFrom } from '@angular/core';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatStepperModule } from '@angular/material/stepper';
import { MatIconModule } from '@angular/material/icon';
import { MatSelectModule } from '@angular/material/select';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatNativeDateModule } from '@angular/material/core';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatDividerModule } from '@angular/material/divider';
import { MatMenuModule } from '@angular/material/menu';
import { ReactiveFormsModule } from '@angular/forms';
import { routes } from './app/app.routes';
import { AppComponent } from './app/app.component';
import { jwtInterceptor } from './app/interceptors/jwt-interceptor.function';
/**
* Bootstrap the Angular application with providers
* Configures routing, animations, HTTP client, and Material UI modules
*/
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes),
provideAnimations(),
provideHttpClient(withInterceptors([jwtInterceptor])),
importProvidersFrom(
// Angular Material Modules
MatToolbarModule,
MatButtonModule,
MatCardModule,
MatInputModule,
MatFormFieldModule,
MatStepperModule,
MatIconModule,
MatSelectModule,
MatDatepickerModule,
MatNativeDateModule,
MatCheckboxModule,
MatProgressSpinnerModule,
MatSnackBarModule,
MatDividerModule,
MatMenuModule,
ReactiveFormsModule
)
]
}).catch(err => console.error('Application failed to start:', err));

394
src/styles.scss Normal file
View File

@@ -0,0 +1,394 @@
/**
* Global Styles
* Professional Angular Resume Builder
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
/* Import Material Theming */
@use '@angular/material' as mat;
/* Import Professional Color System */
@import 'styles/variables';
@import 'styles/colors.scss';
/* Define the custom theme */
@include mat.core();
/* Define custom color palettes using SCSS variables */
$david-valera-primary: mat.define-palette(mat.$blue-palette, 500, 100, 900);
$david-valera-accent: mat.define-palette(mat.$orange-palette, 500, 200, 800);
$david-valera-warn: mat.define-palette(mat.$red-palette);
/* Create the theme object */
$david-valera-theme: mat.define-light-theme((
color: (
primary: $david-valera-primary,
accent: $david-valera-accent,
warn: $david-valera-warn,
)
));
/* Apply the theme */
@include mat.all-component-themes($david-valera-theme);
/* Global Styles */
* {
box-sizing: border-box;
}
html, body {
height: 100%;
margin: 0;
padding: 0;
font-family: 'Roboto', 'Helvetica Neue', sans-serif;
line-height: 1.6;
color: var(--color-text-primary);
background-color: var(--color-background);
}
/* Professional Typography */
h1, h2, h3, h4, h5, h6 {
margin: 0 0 1rem 0;
font-weight: 400;
line-height: 1.3;
}
h1 {
font-size: 2.5rem;
font-weight: 300;
}
h2 {
font-size: 2rem;
font-weight: 300;
}
h3 {
font-size: 1.5rem;
font-weight: 400;
}
h4 {
font-size: 1.25rem;
font-weight: 500;
}
p {
margin: 0 0 1rem 0;
line-height: 1.6;
}
/* Links */
a {
color: var(--color-primary);
text-decoration: none;
transition: var(--transition-normal);
}
a:hover {
color: var(--color-primary-dark);
text-decoration: underline;
}
/* Utility Classes */
.text-center {
text-align: center;
}
.text-left {
text-align: left;
}
.text-right {
text-align: right;
}
.mt-0 { margin-top: 0; }
.mt-1 { margin-top: 0.25rem; }
.mt-2 { margin-top: 0.5rem; }
.mt-3 { margin-top: 1rem; }
.mt-4 { margin-top: 1.5rem; }
.mt-5 { margin-top: 3rem; }
.mb-0 { margin-bottom: 0; }
.mb-1 { margin-bottom: 0.25rem; }
.mb-2 { margin-bottom: 0.5rem; }
.mb-3 { margin-bottom: 1rem; }
.mb-4 { margin-bottom: 1.5rem; }
.mb-5 { margin-bottom: 3rem; }
.pt-0 { padding-top: 0; }
.pt-1 { padding-top: 0.25rem; }
.pt-2 { padding-top: 0.5rem; }
.pt-3 { padding-top: 1rem; }
.pt-4 { padding-top: 1.5rem; }
.pt-5 { padding-top: 3rem; }
.pb-0 { padding-bottom: 0; }
.pb-1 { padding-bottom: 0.25rem; }
.pb-2 { padding-bottom: 0.5rem; }
.pb-3 { padding-bottom: 1rem; }
.pb-4 { padding-bottom: 1.5rem; }
.pb-5 { padding-bottom: 3rem; }
/* Professional Container */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
}
.container-sm {
max-width: 900px;
margin: 0 auto;
padding: 0 1rem;
}
/* Custom Material UI Overrides */
.mat-toolbar {
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.mat-card {
border-radius: var(--border-radius-lg) !important;
box-shadow: var(--color-shadow-light) 0px 2px 8px !important;
}
.mat-card-header .mat-card-title {
font-size: 1.25rem !important;
font-weight: 500 !important;
}
.mat-form-field {
width: 100%;
}
.mat-form-field.mat-form-field-appearance-outline .mat-form-field-outline-thick {
color: var(--color-primary);
}
.mat-button, .mat-raised-button, .mat-stroked-button {
border-radius: var(--border-radius-md) !important;
font-weight: 500 !important;
text-transform: none !important;
}
.mat-step-header .mat-step-icon {
background-color: var(--color-primary) !important;
color: white !important;
}
.mat-step-header .mat-step-icon-selected {
background-color: var(--color-primary) !important;
}
.mat-progress-bar-fill::after {
background-color: var(--color-primary);
}
/* Custom Snackbar Styles */
.success-snackbar {
background: var(--color-success) !important;
color: white !important;
}
.error-snackbar {
background: var(--color-error) !important;
color: white !important;
}
.warning-snackbar {
background: var(--color-warning) !important;
color: white !important;
}
/* Professional Color Scheme */
.primary-color { color: #1976d2; }
.secondary-color { color: #666; }
.accent-color { color: #ff9800; }
.success-color { color: #4caf50; }
.warning-color { color: #ff9800; }
.error-color { color: #f44336; }
.primary-bg { background-color: #1976d2; color: white; }
.secondary-bg { background-color: #f5f5f5; }
.accent-bg { background-color: #ff9800; color: white; }
/* Professional Gradients */
.gradient-primary {
background: linear-gradient(135deg, #1976d2 0%, #1565c0 100%);
color: white;
}
.gradient-secondary {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
}
.gradient-accent {
background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%);
color: white;
}
/* David Valera Professional Branding */
.david-valera-signature {
color: #1976d2;
font-weight: 500;
font-style: italic;
}
.made-in-germany {
color: #666;
font-size: 0.875rem;
}
/* Professional Card Styles */
.professional-card {
background: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
padding: 24px;
margin-bottom: 24px;
transition: box-shadow 0.3s ease;
}
.professional-card:hover {
box-shadow: 0 6px 20px rgba(0,0,0,0.15);
}
.professional-card h3 {
color: #1976d2;
border-bottom: 2px solid #e3f2fd;
padding-bottom: 8px;
margin-bottom: 16px;
}
/* Responsive Design */
@media (max-width: 768px) {
.container {
padding: 0 0.5rem;
}
h1 {
font-size: 2rem;
}
h2 {
font-size: 1.75rem;
}
.professional-card {
padding: 16px;
margin-bottom: 16px;
}
}
@media (max-width: 480px) {
h1 {
font-size: 1.75rem;
}
h2 {
font-size: 1.5rem;
}
.professional-card {
padding: 12px;
}
}
/* Print Styles */
@media print {
body {
background: white;
color: black;
font-size: 12pt;
line-height: 1.4;
}
.mat-toolbar,
.action-bar,
.settings-header,
button {
display: none !important;
}
.professional-card,
.mat-card {
box-shadow: none !important;
border: 1px solid #ddd;
break-inside: avoid;
}
h1, h2, h3, h4, h5, h6 {
color: black !important;
}
.primary-color,
.accent-color {
color: black !important;
}
}
/* Focus Styles for Accessibility */
button:focus,
a:focus,
input:focus,
select:focus,
textarea:focus {
outline: 2px solid #1976d2;
outline-offset: 2px;
}
/* High Contrast Mode Support */
@media (prefers-contrast: high) {
body {
background: white;
color: black;
}
.mat-card {
border: 2px solid #333;
}
.primary-color {
color: #000080;
}
}
/* Animation Classes */
.fade-in {
animation: fadeIn 0.5s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.slide-in-left {
animation: slideInLeft 0.5s ease-out;
}
@keyframes slideInLeft {
from { opacity: 0; transform: translateX(-20px); }
to { opacity: 1; transform: translateX(0); }
}
/* Professional Loading Spinner */
.professional-spinner {
border: 3px solid #e0e0e0;
border-top: 3px solid #1976d2;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 20px auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

308
src/styles/_variables.scss Normal file
View File

@@ -0,0 +1,308 @@
/**
* SCSS Color Variables
* Author: David Valera Melendez
* Email: david@valera-melendez.de
* Created: August 8, 2025
*
* SCSS variables for use in component stylesheets and build-time processing
* These work alongside CSS custom properties for maximum flexibility
*/
/* ========================================
PRIMARY BRAND COLORS
======================================== */
$primary: #1976d2;
$primary-light: #42a5f5;
$primary-dark: #1565c0;
$primary-50: #e3f2fd;
$primary-100: #bbdefb;
$primary-200: #90caf9;
$primary-300: #64b5f6;
$primary-400: #42a5f5;
$primary-500: #1976d2;
$primary-600: #1976d2;
$primary-700: #1565c0;
$primary-800: #0d47a1;
$primary-900: #0d47a1;
/* ========================================
ACCENT COLORS
======================================== */
$accent: #ff9800;
$accent-light: #ffb74d;
$accent-dark: #f57f17;
/* ========================================
SEMANTIC COLORS
======================================== */
$success: #4caf50;
$success-light: #81c784;
$success-dark: #388e3c;
$warning: #ff9800;
$warning-light: #ffb74d;
$warning-dark: #f57f17;
$error: #f44336;
$error-light: #ef5350;
$error-dark: #c62828;
$info: #2196f3;
$info-light: #64b5f6;
$info-dark: #1976d2;
/* ========================================
NEUTRAL COLORS
======================================== */
$text-primary: #212121;
$text-secondary: #757575;
$text-disabled: #bdbdbd;
$text-hint: #9e9e9e;
$text-muted: #555555;
$text-dark: #444444;
$text-light: #999999;
/* ========================================
BACKGROUND COLORS
======================================== */
$background: #fafafa;
$background-paper: #ffffff;
$background-default: #f5f5f5;
$background-dialog: #ffffff;
/* ========================================
SURFACE COLORS
======================================== */
$surface: #ffffff;
$surface-variant: #f5f5f5;
/* ========================================
BORDER COLORS
======================================== */
$border: #e0e0e0;
$border-light: #f5f5f5;
$border-dark: #bdbdbd;
/* ========================================
SHADOW COLORS
======================================== */
$shadow-light: rgba(0, 0, 0, 0.1);
$shadow-medium: rgba(0, 0, 0, 0.15);
$shadow-dark: rgba(0, 0, 0, 0.25);
/* ========================================
GRADIENT DEFINITIONS
======================================== */
$gradient-primary: linear-gradient(135deg, #{$primary} 0%, #{$primary-light} 100%);
$gradient-accent: linear-gradient(135deg, #{$accent} 0%, #{$accent-light} 100%);
$gradient-success: linear-gradient(135deg, #{$success} 0%, #{$success-light} 100%);
/* ========================================
SPACING SYSTEM
======================================== */
$spacing-xs: 0.25rem; // 4px
$spacing-sm: 0.5rem; // 8px
$spacing-md: 1rem; // 16px
$spacing-lg: 1.5rem; // 24px
$spacing-xl: 2rem; // 32px
$spacing-xxl: 3rem; // 48px
/* ========================================
BORDER RADIUS SYSTEM
======================================== */
$border-radius-sm: 4px;
$border-radius-md: 8px;
$border-radius-lg: 12px;
$border-radius-xl: 16px;
$border-radius-round: 50%;
/* ========================================
BREAKPOINTS
======================================== */
$breakpoint-xs: 576px;
$breakpoint-sm: 768px;
$breakpoint-md: 992px;
$breakpoint-lg: 1200px;
$breakpoint-xl: 1440px;
/* ========================================
Z-INDEX LAYERS
======================================== */
$z-dropdown: 1000;
$z-sticky: 1010;
$z-fixed: 1020;
$z-modal-backdrop: 1030;
$z-modal: 1040;
$z-popover: 1050;
$z-tooltip: 1060;
$z-toast: 1070;
/* ========================================
TRANSITIONS
======================================== */
$transition-fast: 0.15s ease;
$transition-normal: 0.2s ease;
$transition-slow: 0.3s ease;
/* ========================================
TYPOGRAPHY SCALE
======================================== */
$font-size-xs: 0.75rem; // 12px
$font-size-sm: 0.875rem; // 14px
$font-size-base: 1rem; // 16px
$font-size-lg: 1.125rem; // 18px
$font-size-xl: 1.25rem; // 20px
$font-size-xxl: 1.5rem; // 24px
$font-size-xxxl: 2rem; // 32px
$font-weight-light: 300;
$font-weight-normal: 400;
$font-weight-medium: 500;
$font-weight-semibold: 600;
$font-weight-bold: 700;
/* ========================================
PROFESSIONAL SHADOW SYSTEM
======================================== */
$shadow-xs: 0 1px 2px 0 $shadow-light;
$shadow-sm: 0 1px 3px 0 $shadow-light, 0 1px 2px 0 $shadow-light;
$shadow-md: 0 4px 6px -1px $shadow-light, 0 2px 4px -1px $shadow-light;
$shadow-lg: 0 10px 15px -3px $shadow-light, 0 4px 6px -2px $shadow-light;
$shadow-xl: 0 20px 25px -5px $shadow-light, 0 10px 10px -5px $shadow-light;
$shadow-2xl: 0 25px 50px -12px $shadow-dark;
/* ========================================
DARK THEME COLORS (SCSS Variables)
======================================== */
// Dark theme primary colors
$dark-primary: #42a5f5;
$dark-primary-light: #64b5f6;
$dark-primary-dark: #1976d2;
// Dark theme text colors
$dark-text-primary: #ffffff;
$dark-text-secondary: #b0bec5;
$dark-text-disabled: #616161;
$dark-text-hint: #757575;
// Dark theme backgrounds
$dark-background: #121212;
$dark-background-paper: #1e1e1e;
$dark-background-default: #202020;
$dark-background-dialog: #2d2d2d;
// Dark theme surfaces
$dark-surface: #1e1e1e;
$dark-surface-variant: #2d2d2d;
// Dark theme borders
$dark-border: #404040;
$dark-border-light: #2d2d2d;
$dark-border-dark: #555555;
/* ========================================
PROFESSIONAL COLOR FUNCTIONS
======================================== */
/// Lighten a color by a percentage
/// @param {Color} $color - The color to lighten
/// @param {Number} $percentage - The percentage to lighten by
/// @return {Color} - The lightened color
@function lighten-color($color, $percentage) {
@return lighten($color, $percentage);
}
/// Darken a color by a percentage
/// @param {Color} $color - The color to darken
/// @param {Number} $percentage - The percentage to darken by
/// @return {Color} - The darkened color
@function darken-color($color, $percentage) {
@return darken($color, $percentage);
}
/// Create a color with alpha transparency
/// @param {Color} $color - The base color
/// @param {Number} $alpha - The alpha value (0-1)
/// @return {Color} - The color with alpha
@function alpha-color($color, $alpha) {
@return rgba($color, $alpha);
}
/* ========================================
PROFESSIONAL MIXINS
======================================== */
/// Apply primary color scheme
@mixin primary-colors {
color: var(--color-primary);
background-color: var(--color-background-paper);
border-color: var(--color-primary);
}
/// Apply accent color scheme
@mixin accent-colors {
color: var(--color-accent);
background-color: var(--color-background-paper);
border-color: var(--color-accent);
}
/// Apply success color scheme
@mixin success-colors {
color: var(--color-success);
background-color: var(--color-background-paper);
border-color: var(--color-success);
}
/// Apply professional card styling
@mixin card-style {
background-color: var(--color-surface);
border-radius: var(--border-radius-lg);
box-shadow: var(--color-shadow-light) 0px 2px 8px;
border: 1px solid var(--color-border);
}
/// Apply professional button styling
@mixin button-style($color: $primary) {
background-color: $color;
color: white;
border: none;
border-radius: var(--border-radius-md);
padding: var(--spacing-sm) var(--spacing-md);
font-weight: $font-weight-medium;
transition: var(--transition-normal);
cursor: pointer;
&:hover {
background-color: darken-color($color, 10%);
transform: translateY(-1px);
box-shadow: var(--color-shadow-medium) 0px 4px 12px;
}
&:active {
transform: translateY(0);
}
&:disabled {
background-color: var(--color-text-disabled);
cursor: not-allowed;
transform: none;
box-shadow: none;
}
}

294
src/styles/colors.scss Normal file
View File

@@ -0,0 +1,294 @@
/**
* Professional Color Configuration System
* Author: David Valera Melendez
* Email: david@valera-melendez.de
* Created: August 8, 2025
*
* Centralized color management with CSS custom properties and SCSS variables
* Supports light/dark themes and easy color customization
*/
/* ========================================
GLOBAL COLOR VARIABLES (CSS Custom Properties)
======================================== */
:root {
/* ===== PRIMARY BRAND COLORS ===== */
--color-primary: #1976d2; /* Main brand blue */
--color-primary-light: #42a5f5; /* Lighter blue */
--color-primary-dark: #1565c0; /* Darker blue */
--color-primary-50: #e3f2fd; /* Very light blue */
--color-primary-100: #bbdefb; /* Light blue */
--color-primary-200: #90caf9; /* Medium light blue */
--color-primary-300: #64b5f6; /* Medium blue */
--color-primary-400: #42a5f5; /* Medium dark blue */
--color-primary-500: #1976d2; /* Base primary */
--color-primary-600: #1976d2; /* Primary 600 */
--color-primary-700: #1565c0; /* Primary 700 */
--color-primary-800: #0d47a1; /* Primary 800 */
--color-primary-900: #0d47a1; /* Darkest primary */
/* ===== ACCENT COLORS ===== */
--color-accent: #ff9800; /* Orange accent */
--color-accent-light: #ffb74d; /* Light orange */
--color-accent-dark: #f57f17; /* Dark orange */
/* ===== SEMANTIC COLORS ===== */
--color-success: #4caf50; /* Success green */
--color-success-light: #81c784; /* Light success */
--color-success-dark: #388e3c; /* Dark success */
--color-warning: #ff9800; /* Warning orange */
--color-warning-light: #ffb74d; /* Light warning */
--color-warning-dark: #f57f17; /* Dark warning */
--color-error: #f44336; /* Error red */
--color-error-light: #ef5350; /* Light error */
--color-error-dark: #c62828; /* Dark error */
--color-error-bg: rgba(244, 67, 54, 0.05); /* Error background */
--color-info: #2196f3; /* Info blue */
--color-info-light: #64b5f6; /* Light info */
--color-info-dark: #1976d2; /* Dark info */
/* ===== SPECIAL COLORS ===== */
--color-indigo: #4f46e5; /* Indigo */
--color-purple: #667eea; /* Purple */
--color-blue-600: #0066cc; /* Blue 600 */
--color-blue-700: #004499; /* Blue 700 */
--color-blue-400: #42a5f5; /* Blue 400 */
--color-blue-300: #64b5f6; /* Blue 300 */
--color-blue-200: #90caf9; /* Blue 200 */
--color-blue-100: #bbdefb; /* Blue 100 */
--color-blue-glow: rgba(33, 150, 243, 0.5); /* Blue glow */
--color-blue-200-glow: rgba(144, 202, 249, 0.5); /* Blue 200 glow */
--color-purple-glow-light: rgba(102, 126, 234, 0.2); /* Light purple glow */
--color-purple-glow-medium: rgba(102, 126, 234, 0.3); /* Medium purple glow */
--color-purple-glow-strong: rgba(102, 126, 234, 0.4); /* Strong purple glow */
/* ===== SOCIAL/BRAND COLORS ===== */
--color-google: #ea4335; /* Google red */
--color-google-bg: rgba(234, 67, 53, 0.04); /* Google background */
--color-microsoft: #0078d4; /* Microsoft blue */
--color-microsoft-bg: rgba(0, 120, 212, 0.04); /* Microsoft background */
--color-slate-300: #e2e8f0; /* Slate 300 */
--color-slate-50: #f8fafc; /* Slate 50 */
--color-purple-brand: #667eea; /* Purple brand */
/* ===== ERROR STATE COLORS ===== */
--color-error-bg-light: #fef2f2; /* Light error background */
--color-error-bg-medium: #fee2e2; /* Medium error background */
--color-error-border-light: #fecaca; /* Light error border */
--color-error-text-dark: #991b1b; /* Dark error text */
--gradient-error-light: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);
/* ===== WARNING STATE COLORS ===== */
--color-warning-bg-light: #fffbeb; /* Light warning background */
--color-warning-bg-medium: #fef3c7; /* Medium warning background */
--color-warning-border-light: #fde68a; /* Light warning border */
--color-warning-text: #d97706; /* Warning text */
--color-warning-text-dark: #92400e; /* Dark warning text */
--gradient-warning-light: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%);
/* ===== NEUTRAL COLORS ===== */
--color-text-primary: #212121; /* Primary text */
--color-text-secondary: #757575; /* Secondary text */
--color-text-disabled: #bdbdbd; /* Disabled text */
--color-text-hint: #9e9e9e; /* Hint text */
--color-text-muted: #555555; /* Muted text */
--color-text-dark: #444444; /* Dark text */
--color-text-light: #999999; /* Light text */
--color-text-white: #ffffff; /* White text */
--color-text-white-muted: rgba(255, 255, 255, 0.9); /* Muted white text */
--color-text-white-light: rgba(255, 255, 255, 0.8); /* Light white text */
--color-text-white-subtle: rgba(255, 255, 255, 0.7); /* Subtle white text */
--color-text-slate: #64748b; /* Slate text */
--color-text-slate-dark: #475569; /* Dark slate text - UPDATED */
--color-text-slate-light: #94a3b8; /* Light slate text */
--color-text-gray-900: #1a202c; /* Very dark gray */
--color-text-slate-700: #334155; /* Slate 700 */
--color-text-slate-600: #475569; /* Slate 600 */
--color-overlay-dark: rgba(30, 41, 59, 0.95); /* Dark overlay */
--color-black: #000000; /* Pure black */
/* ===== BACKGROUND COLORS ===== */
--color-background: #fafafa; /* Main background */
--color-background-paper: #ffffff; /* Card/paper background */
--color-background-default: #f5f5f5; /* Default background */
--color-background-dialog: #ffffff; /* Dialog background */
--color-background-light: #f8fafc; /* Light background */
--color-background-slate: #e2e8f0; /* Slate background */
--color-background-white-overlay: rgba(255, 255, 255, 0.95); /* White overlay */
--color-background-white-subtle: rgba(255, 255, 255, 0.15); /* Subtle white overlay */
--color-background-dark-subtle: rgba(0, 0, 0, 0.05); /* Subtle dark overlay */
/* ===== SURFACE COLORS ===== */
--color-surface: #ffffff; /* Surface color */
--color-surface-variant: #f5f5f5; /* Surface variant */
/* ===== BORDER COLORS ===== */
--color-border: #e0e0e0; /* Default border */
--color-border-light: #f5f5f5; /* Light border */
--color-border-dark: #bdbdbd; /* Dark border */
--color-border-white-subtle: rgba(255, 255, 255, 0.3); /* Subtle white border */
--color-border-dark-subtle: rgba(0, 0, 0, 0.06); /* Subtle dark border */
/* ===== SHADOW COLORS ===== */
--color-shadow-light: rgba(0, 0, 0, 0.1);
--color-shadow-medium: rgba(0, 0, 0, 0.15);
--color-shadow-dark: rgba(0, 0, 0, 0.25);
--color-shadow-subtle: rgba(0, 0, 0, 0.05);
--color-shadow-heavy: rgba(0, 0, 0, 0.3);
--color-shadow-card: rgba(0, 0, 0, 0.08);
--color-shadow-deep: rgba(0, 0, 0, 0.12);
--color-text-shadow: rgba(0, 0, 0, 0.3);
--color-disabled-overlay: rgba(0, 0, 0, 0.12);
/* ===== OVERLAY COLORS ===== */
--color-overlay: rgba(0, 0, 0, 0.7);
--color-overlay-light: rgba(0, 0, 0, 0.5);
--color-surface-overlay: rgba(255, 255, 255, 0.1);
--color-surface-overlay-light: rgba(255, 255, 255, 0.2);
--color-surface-overlay-medium: rgba(255, 255, 255, 0.4);
/* ===== GRADIENT COLORS ===== */
--gradient-primary: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-light) 100%);
--gradient-accent: linear-gradient(135deg, var(--color-accent) 0%, var(--color-accent-light) 100%);
--gradient-success: linear-gradient(135deg, var(--color-success) 0%, var(--color-success-light) 100%);
--gradient-background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
--gradient-progress: linear-gradient(90deg, #0066cc, #004499, #0066cc);
--gradient-progress-glow: linear-gradient(90deg, #42a5f5, #64b5f6, #90caf9, #bbdefb, #90caf9, #64b5f6);
/* ===== PROFESSIONAL SPACING ===== */
--spacing-xs: 0.25rem; /* 4px */
--spacing-sm: 0.5rem; /* 8px */
--spacing-md: 1rem; /* 16px */
--spacing-lg: 1.5rem; /* 24px */
--spacing-xl: 2rem; /* 32px */
--spacing-xxl: 3rem; /* 48px */
/* ===== BORDER RADIUS ===== */
--border-radius-sm: 4px;
--border-radius-md: 8px;
--border-radius-lg: 12px;
--border-radius-xl: 16px;
--border-radius-round: 50%;
/* ===== TRANSITIONS ===== */
--transition-fast: 0.15s ease;
--transition-normal: 0.2s ease;
--transition-slow: 0.3s ease;
/* ===== Z-INDEX LAYERS ===== */
--z-index-dropdown: 1000;
--z-index-sticky: 1010;
--z-index-fixed: 1020;
--z-index-modal-backdrop: 1030;
--z-index-modal: 1040;
--z-index-popover: 1050;
--z-index-tooltip: 1060;
--z-index-toast: 1070;
}
/* ========================================
DARK THEME COLORS
======================================== */
[data-theme="dark"] {
/* Primary colors remain the same */
--color-primary: #42a5f5; /* Lighter blue for dark theme */
--color-primary-light: #64b5f6;
--color-primary-dark: #1976d2;
/* Accent colors */
--color-accent: #ffb74d; /* Lighter orange for dark theme */
/* Text colors for dark theme */
--color-text-primary: #ffffff;
--color-text-secondary: #b0bec5;
--color-text-disabled: #616161;
--color-text-hint: #757575;
/* Background colors for dark theme */
--color-background: #121212;
--color-background-paper: #1e1e1e;
--color-background-default: #202020;
--color-background-dialog: #2d2d2d;
/* Surface colors for dark theme */
--color-surface: #1e1e1e;
--color-surface-variant: #2d2d2d;
/* Border colors for dark theme */
--color-border: #404040;
--color-border-light: #2d2d2d;
--color-border-dark: #555555;
}
/* ========================================
UTILITY CLASSES FOR COLORS
======================================== */
/* Text Colors */
.text-primary { color: var(--color-primary) !important; }
.text-accent { color: var(--color-accent) !important; }
.text-success { color: var(--color-success) !important; }
.text-warning { color: var(--color-warning) !important; }
.text-error { color: var(--color-error) !important; }
.text-info { color: var(--color-info) !important; }
/* Background Colors */
.bg-primary { background-color: var(--color-primary) !important; }
.bg-accent { background-color: var(--color-accent) !important; }
.bg-success { background-color: var(--color-success) !important; }
.bg-warning { background-color: var(--color-warning) !important; }
.bg-error { background-color: var(--color-error) !important; }
.bg-info { background-color: var(--color-info) !important; }
/* Gradient Backgrounds */
.bg-gradient-primary { background: var(--gradient-primary) !important; }
.bg-gradient-accent { background: var(--gradient-accent) !important; }
.bg-gradient-success { background: var(--gradient-success) !important; }
/* Border Colors */
.border-primary { border-color: var(--color-primary) !important; }
.border-accent { border-color: var(--color-accent) !important; }
.border-success { border-color: var(--color-success) !important; }
.border-warning { border-color: var(--color-warning) !important; }
.border-error { border-color: var(--color-error) !important; }
/* ========================================
RESPONSIVE COLOR ADJUSTMENTS
======================================== */
@media (max-width: 768px) {
:root {
/* Slightly adjust colors for mobile if needed */
--color-shadow-light: rgba(0, 0, 0, 0.08);
--color-shadow-medium: rgba(0, 0, 0, 0.12);
}
}
/* ========================================
HIGH CONTRAST MODE
======================================== */
@media (prefers-contrast: high) {
:root {
--color-primary: #0d47a1; /* Darker primary for high contrast */
--color-text-secondary: #424242; /* Darker secondary text */
--color-border: #424242; /* Darker borders */
}
}
/* ========================================
REDUCED MOTION PREFERENCES
======================================== */
@media (prefers-reduced-motion: reduce) {
:root {
--transition-fast: 0s;
--transition-normal: 0s;
--transition-slow: 0s;
}
}

13
tsconfig.app.json Normal file
View File

@@ -0,0 +1,13 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": [
"src/main.ts"
],
"include": [
"src/**/*.d.ts"
]
}

40
tsconfig.json Normal file
View File

@@ -0,0 +1,40 @@
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"moduleResolution": "node",
"importHelpers": true,
"target": "ES2022",
"module": "ES2022",
"useDefineForClassFields": false,
"lib": [
"ES2022",
"dom"
],
"paths": {
"@/*": ["src/*"],
"@/components/*": ["src/app/components/*"],
"@/services/*": ["src/app/services/*"],
"@/models/*": ["src/app/models/*"],
"@/utils/*": ["src/app/utils/*"],
"@/shared/*": ["src/app/shared/*"]
}
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}

13
tsconfig.spec.json Normal file
View File

@@ -0,0 +1,13 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine"
]
},
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}