init commit

This commit is contained in:
David Melendez
2026-01-14 22:44:08 +01:00
parent 1d98d30629
commit c3868062e6
72 changed files with 21172 additions and 1 deletions

171
README.md
View File

@@ -1,2 +1,171 @@
# Next.js
# Professional Resume Builder
**Created by David Valera Melendez** | david@valera-melendez.de | Made in Germany 🇩🇪
A modern, professional resume builder application built with Next.js, TypeScript, and Tailwind CSS. Designed specifically for creating impressive resumes for job applications.
## 🚀 Features
- **Professional Design**: Clean, modern interface optimized for professional use
- **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 (coming soon)
- **TypeScript**: Fully typed for better development experience
- **Tailwind CSS**: Modern styling with custom professional theme
## 🛠️ Tech Stack
- **Framework**: Next.js 14+ with App Router
- **Language**: TypeScript
- **Styling**: Tailwind CSS
- **UI Components**: Custom React components
- **Icons**: Emoji-based icons for better compatibility
- **Export**: HTML to PDF conversion (planned)
## 📦 Installation
1. **Prerequisites**: Node.js 18+ and npm 9+
2. **Install dependencies**:
```bash
npm install
```
3. **Run development server**:
```bash
npm run dev
```
4. **Open in browser**:
Navigate to [http://localhost:3000](http://localhost:3000)
## 🚀 Available Scripts
- `npm run dev` - Start development server
- `npm run build` - Build for production
- `npm run start` - Start production server
- `npm run lint` - Run ESLint
- `npm run type-check` - Run TypeScript compiler check
- `npm run format` - Format code with Prettier
- `npm run format:check` - Check code formatting
## 📁 Project Structure
```
src/
├── app/ # Next.js App Router
│ ├── globals.css # Global styles
│ ├── layout.tsx # Root layout
│ └── page.tsx # Home page
├── components/ # React components
│ ├── layout/ # Layout components
│ │ └── Header.tsx # App header
│ └── resume/ # Resume-specific components
│ ├── ResumeBuilder.tsx # Main builder interface
│ ├── ResumePreview.tsx # Live preview
│ └── forms/ # Form components
│ ├── PersonalInfoForm.tsx
│ ├── ExperienceForm.tsx
│ ├── EducationForm.tsx
│ └── SkillsForm.tsx
├── types/ # TypeScript type definitions
│ └── resume.ts # Resume data types
└── utils/ # Utility functions (future)
```
## 🎨 Design System
### Colors
- **Primary**: Blue theme (#0ea5e9 variations) - Professional and trustworthy
- **Secondary**: Gray theme - Clean and readable text
- **Accent**: Purple theme - Highlights and interactive elements
- **Semantic**: Success, warning, and error colors
### Typography
- **Primary Font**: Inter (Google Fonts)
- **Responsive Sizes**: Configured for optimal readability
- **Professional Styling**: Clean, modern typography
### Components
- **Cards**: Elevated surfaces for content sections
- **Buttons**: Multiple variants (primary, secondary, outline, ghost)
- **Forms**: Consistent input styling with validation states
- **Navigation**: Clean, intuitive section navigation
## 🎯 Usage
1. **Personal Information**: Start by filling in your basic contact details and professional summary
2. **Work Experience**: Add your employment history with achievements and responsibilities
3. **Education**: Include your academic background and qualifications
4. **Skills**: Categorize your technical and soft skills with proficiency levels
5. **Preview**: Use the live preview to see how your resume looks
6. **Export**: Download your finished resume as PDF (feature in development)
## 🔧 Development Guidelines
### Code Standards
- All components use TypeScript with strict typing
- Follow the established component structure
- Use Tailwind CSS classes for styling
- Include proper error handling
- Maintain accessibility standards
### Component Development
- Use 'use client' for interactive components
- Follow PascalCase naming convention
- Include comprehensive prop interfaces
- Add meaningful comments and documentation
### Professional Quality
- Code is production-ready and employer-review friendly
- Comprehensive TypeScript coverage
- Responsive design implementation
- Clean, maintainable architecture
## 📱 Browser Support
- Chrome (recommended)
- Firefox
- Safari
- Edge
## 🚀 Deployment
The application is configured for static export and can be deployed on:
- Vercel (recommended for Next.js)
- Netlify
- GitHub Pages
- Any static hosting service
Build command: `npm run build`
Output directory: `out/`
## 👨‍💻 Author
**David Valera Melendez**
- Email: david@valera-melendez.de
- Location: Germany 🇩🇪
- Created: August 7, 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 Resume Builder** - Helping professionals create outstanding resumes | Made in Germany 🇩🇪

5
next-env.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

52
next.config.js Normal file
View File

@@ -0,0 +1,52 @@
/**
* Next.js Configuration
* Professional Resume Builder
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
/** @type {import('next').NextConfig} */
const nextConfig = {
/* config options here */
experimental: {
typedRoutes: true,
},
images: {
domains: ['localhost'],
remotePatterns: [
{
protocol: 'https',
hostname: '**',
},
],
},
// Optimization settings
swcMinify: true,
// Custom headers for security
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'X-Frame-Options',
value: 'DENY',
},
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin',
},
],
},
];
},
};
module.exports = nextConfig;

7153
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

75
package.json Normal file
View File

@@ -0,0 +1,75 @@
{
"name": "david-valera-resume-builder",
"version": "1.0.0",
"description": "Professional Resume Builder - Created by David Valera Melendez",
"author": {
"name": "David Valera Melendez",
"email": "david@valera-melendez.de",
"url": "https://valera-melendez.de"
},
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"type-check": "tsc --noEmit",
"format": "prettier --write .",
"format:check": "prettier --check ."
},
"dependencies": {
"@headlessui/react": "^2.1.2",
"@heroicons/react": "^2.2.0",
"@hookform/resolvers": "^5.2.1",
"@types/js-cookie": "^3.0.6",
"autoprefixer": "^10.4.21",
"axios": "^1.11.0",
"clsx": "^2.1.1",
"framer-motion": "^11.3.8",
"html2canvas": "^1.4.1",
"jose": "^6.0.12",
"js-cookie": "^3.0.5",
"jspdf": "^2.5.1",
"lucide-react": "^0.416.0",
"next": "14.2.5",
"react": "^18",
"react-dom": "^18",
"react-hook-form": "^7.62.0",
"tailwind-merge": "^2.4.0",
"zod": "^4.0.15",
"zustand": "^5.0.7"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"eslint": "^8",
"eslint-config-next": "14.2.5",
"postcss": "^8",
"prettier": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.5",
"tailwindcss": "^3.4.1",
"typescript": "^5"
},
"keywords": [
"resume",
"cv",
"builder",
"nextjs",
"typescript",
"tailwind",
"professional",
"david-valera-melendez"
],
"repository": {
"type": "git",
"url": "https://github.com/davidvalera/resume-builder.git"
},
"license": "MIT",
"engines": {
"node": ">=18.17.0",
"npm": ">=9.6.0"
}
}

15
postcss.config.js Normal file
View File

@@ -0,0 +1,15 @@
/**
* PostCSS Configuration
* Professional Resume Builder
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

0
public/favicon.svg Normal file
View File

View File

@@ -0,0 +1,155 @@
/**
* Access Denied Page Component
* Professional Next.js Resume Builder - Enterprise Auth System
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
'use client';
import React from 'react';
import { useRouter } from 'next/navigation';
import { Shield, ArrowLeft, Home, Lock } from 'lucide-react';
import { useAuthStore } from '@/lib/auth-store';
/**
* Access Denied Component
*
* Displays when a user attempts to access a resource they don't have permission for.
* Provides navigation options and clear messaging about the access restriction.
*/
export default function AccessDeniedPage() {
const router = useRouter();
const { user, logout } = useAuthStore();
/**
* Handle logout
*/
const handleLogout = async () => {
try {
await logout();
router.push('/auth/login');
} catch (error) {
console.error('Logout error:', error);
// Redirect anyway
router.push('/auth/login');
}
};
/**
* Navigate back to dashboard
*/
const handleBackToDashboard = () => {
router.push('/dashboard');
};
/**
* Navigate to previous page
*/
const handleGoBack = () => {
if (window.history.length > 1) {
router.back();
} else {
router.push('/dashboard');
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-red-50 to-orange-50 flex items-center justify-center px-4">
<div className="max-w-md w-full space-y-8">
{/* Header */}
<div className="text-center">
<div className="w-16 h-16 bg-red-600 rounded-full flex items-center justify-center mx-auto mb-6">
<Lock className="h-8 w-8 text-white" />
</div>
<h1 className="text-3xl font-bold text-gray-900">
Zugriff verweigert
</h1>
<p className="mt-2 text-gray-600">
Sie haben keine Berechtigung für diese Seite
</p>
</div>
{/* Access Information */}
<div className="bg-white rounded-xl shadow-lg p-8">
<div className="space-y-6">
{/* Error Message */}
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<div className="flex items-start">
<Shield className="h-5 w-5 text-red-600 mt-0.5 mr-3 flex-shrink-0" />
<div>
<h3 className="text-sm font-medium text-red-800">
Unzureichende Berechtigungen
</h3>
<p className="text-sm text-red-700 mt-1">
Sie benötigen höhere Berechtigungen, um auf diese Ressource
zuzugreifen. Wenden Sie sich an Ihren Administrator, wenn Sie
glauben, dass Sie Zugriff haben sollten.
</p>
</div>
</div>
</div>
{/* User Information */}
{user && (
<div className="border border-gray-200 rounded-lg p-4">
<h4 className="text-sm font-medium text-gray-900 mb-2">
Aktuelle Sitzung
</h4>
<div className="space-y-1 text-sm text-gray-600">
<p>
<span className="font-medium">Benutzer:</span> {user.firstName} {user.lastName}
</p>
<p>
<span className="font-medium">E-Mail:</span> {user.email}
</p>
{user.roles && user.roles.length > 0 && (
<p>
<span className="font-medium">Rollen:</span> {user.roles.join(', ')}
</p>
)}
</div>
</div>
)}
{/* Action Buttons */}
<div className="space-y-3">
<button
onClick={handleGoBack}
className="w-full flex items-center justify-center px-4 py-3 border border-gray-300 rounded-lg shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
<ArrowLeft className="h-4 w-4 mr-2" />
Zurück zur vorherigen Seite
</button>
<button
onClick={handleBackToDashboard}
className="w-full flex items-center justify-center px-4 py-3 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
<Home className="h-4 w-4 mr-2" />
Zum Dashboard
</button>
<button
onClick={handleLogout}
className="w-full flex items-center justify-center px-4 py-3 border border-red-300 rounded-lg shadow-sm text-sm font-medium text-red-700 bg-white hover:bg-red-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
>
Mit anderem Konto anmelden
</button>
</div>
</div>
</div>
{/* Help Information */}
<div className="text-center">
<p className="text-xs text-gray-500">
Benötigen Sie Hilfe? Kontaktieren Sie Ihren Systemadministrator oder
wenden Sie sich an den Support.
</p>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,18 @@
/**
* Login Demo Page - Showcase Beautiful Login Design
* Professional Resume Builder - Login Interface Demo
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
import LoginPage from '../login/page';
/**
* Demo page to showcase the beautiful login form
* This can be accessed at /login-demo or similar route
*/
export default function LoginDemoPage() {
return <LoginPage />;
}

View File

@@ -0,0 +1,236 @@
/**
* Device Verification Page Component
* Professional Next.js Resume Builder - Enterprise Auth System
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
'use client';
import React, { useState, useEffect } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { Shield, Smartphone, Mail, AlertTriangle, CheckCircle } from 'lucide-react';
import { useAuthStore } from '@/lib/auth-store';
/**
* Device Verification Component
*
* Handles device verification when a user logs in from an unrecognized device.
* Includes verification code input and resend functionality.
*/
export default function DeviceVerificationPage() {
const router = useRouter();
const searchParams = useSearchParams();
const { verifyDevice, resendDeviceVerification, isLoading, error } = useAuthStore();
const [verificationCode, setVerificationCode] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [countdown, setCountdown] = useState(0);
const [showSuccess, setShowSuccess] = useState(false);
const sessionId = searchParams.get('session') || '';
const email = searchParams.get('email') || '';
// Countdown timer for resend
useEffect(() => {
if (countdown > 0) {
const timer = setTimeout(() => setCountdown(countdown - 1), 1000);
return () => clearTimeout(timer);
}
}, [countdown]);
// Redirect if no session ID
useEffect(() => {
if (!sessionId) {
router.push('/auth/login');
}
}, [sessionId, router]);
/**
* Handle verification code submission
*/
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!verificationCode.trim() || verificationCode.length < 6) {
return;
}
setIsSubmitting(true);
try {
await verifyDevice({
sessionToken: sessionId,
verificationCode: verificationCode.trim(),
});
// If we reach here, verification was successful
setShowSuccess(true);
setTimeout(() => {
router.push('/dashboard');
}, 2000);
} catch (error) {
console.error('Device verification error:', error);
} finally {
setIsSubmitting(false);
}
};
/**
* Handle resend verification code
*/
const handleResendCode = async () => {
if (countdown > 0) return;
try {
await resendDeviceVerification(sessionId);
setCountdown(60); // 60 seconds countdown
setVerificationCode(''); // Clear current code
} catch (error) {
console.error('Resend verification error:', error);
}
};
/**
* Handle input change
*/
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value.replace(/\D/g, '').slice(0, 6);
setVerificationCode(value);
};
if (showSuccess) {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center px-4">
<div className="max-w-md w-full bg-white rounded-xl shadow-lg p-8 text-center">
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-6">
<CheckCircle className="h-8 w-8 text-green-600" />
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">
Gerät verifiziert!
</h2>
<p className="text-gray-600">
Ihr Gerät wurde erfolgreich verifiziert. Sie werden weitergeleitet...
</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center px-4">
<div className="max-w-md w-full space-y-8">
{/* Header */}
<div className="text-center">
<div className="w-16 h-16 bg-blue-600 rounded-full flex items-center justify-center mx-auto mb-6">
<Shield className="h-8 w-8 text-white" />
</div>
<h2 className="text-3xl font-bold text-gray-900">
Gerät verifizieren
</h2>
<p className="mt-2 text-gray-600">
Aus Sicherheitsgründen müssen wir Ihr Gerät verifizieren
</p>
</div>
{/* Security Notice */}
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4">
<div className="flex items-start">
<AlertTriangle className="h-5 w-5 text-amber-600 mt-0.5 mr-3 flex-shrink-0" />
<div>
<h3 className="text-sm font-medium text-amber-800">
Unbekanntes Gerät erkannt
</h3>
<p className="text-sm text-amber-700 mt-1">
Wir haben eine Anmeldung von einem neuen Gerät festgestellt.
Bitte verifizieren Sie Ihre Identität mit dem Code, den wir an
Ihre E-Mail-Adresse gesendet haben.
</p>
</div>
</div>
</div>
{/* Verification Form */}
<div className="bg-white rounded-xl shadow-lg p-8">
<form onSubmit={handleSubmit} className="space-y-6">
{/* Email Display */}
<div className="flex items-center justify-center space-x-2 text-sm text-gray-600">
<Mail className="h-4 w-4" />
<span>Code gesendet an: {email}</span>
</div>
{/* Verification Code Input */}
<div>
<label htmlFor="verificationCode" className="block text-sm font-medium text-gray-700 mb-2">
Verifizierungscode
</label>
<input
id="verificationCode"
type="text"
value={verificationCode}
onChange={handleInputChange}
placeholder="6-stelliger Code"
className="w-full px-4 py-3 border border-gray-300 rounded-lg text-center text-2xl font-mono tracking-widest focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
maxLength={6}
autoComplete="one-time-code"
autoFocus
/>
</div>
{/* Error Message */}
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-3">
<p className="text-sm text-red-600">{error.message}</p>
</div>
)}
{/* Submit Button */}
<button
type="submit"
disabled={isSubmitting || verificationCode.length < 6}
className="w-full flex items-center justify-center px-4 py-3 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSubmitting ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Verifiziere...
</>
) : (
<>
<Smartphone className="h-4 w-4 mr-2" />
Gerät verifizieren
</>
)}
</button>
{/* Resend Code */}
<div className="text-center">
<button
type="button"
onClick={handleResendCode}
disabled={countdown > 0}
className="text-sm text-blue-600 hover:text-blue-500 disabled:text-gray-400 disabled:cursor-not-allowed"
>
{countdown > 0 ? (
`Code erneut senden in ${countdown}s`
) : (
'Code erneut senden'
)}
</button>
</div>
</form>
</div>
{/* Help Text */}
<div className="text-center">
<p className="text-xs text-gray-500">
Haben Sie keinen Code erhalten? Überprüfen Sie Ihren Spam-Ordner oder
fordern Sie einen neuen Code an.
</p>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,219 @@
/**
* Forgot Password Page Component
* Professional Next.js Resume Builder - Enterprise Auth System
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
'use client';
import React, { useState } from 'react';
import { useRouter } from 'next/navigation';
import { Mail, ArrowLeft, Send, CheckCircle } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
/**
* Forgot password form validation schema
*/
const forgotPasswordSchema = z.object({
email: z
.string()
.min(1, 'E-Mail-Adresse ist erforderlich')
.email('Ungültige E-Mail-Adresse'),
});
type ForgotPasswordForm = z.infer<typeof forgotPasswordSchema>;
/**
* Forgot Password Component
*
* Allows users to request a password reset by entering their email address.
* Provides feedback and navigation back to login.
*/
export default function ForgotPasswordPage() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [submittedEmail, setSubmittedEmail] = useState('');
const {
register,
handleSubmit,
formState: { errors },
} = useForm<ForgotPasswordForm>({
resolver: zodResolver(forgotPasswordSchema),
});
/**
* Handle form submission
*/
const onSubmit = async (data: ForgotPasswordForm) => {
setIsSubmitting(true);
try {
// TODO: Implement actual password reset API call
// const response = await passwordResetRequest(data.email);
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 2000));
setSubmittedEmail(data.email);
setIsSuccess(true);
} catch (error) {
console.error('Password reset error:', error);
// Handle error appropriately
} finally {
setIsSubmitting(false);
}
};
/**
* Navigate back to login
*/
const handleBackToLogin = () => {
router.push('/auth/login');
};
if (isSuccess) {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center px-4">
<div className="max-w-md w-full space-y-8">
{/* Success Header */}
<div className="text-center">
<div className="w-16 h-16 bg-green-600 rounded-full flex items-center justify-center mx-auto mb-6">
<CheckCircle className="h-8 w-8 text-white" />
</div>
<h2 className="text-3xl font-bold text-gray-900">
E-Mail gesendet
</h2>
<p className="mt-2 text-gray-600">
Überprüfen Sie Ihr E-Mail-Postfach
</p>
</div>
{/* Success Message */}
<div className="bg-white rounded-xl shadow-lg p-8">
<div className="space-y-6">
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<div className="flex items-start">
<Mail className="h-5 w-5 text-green-600 mt-0.5 mr-3 flex-shrink-0" />
<div>
<h3 className="text-sm font-medium text-green-800">
Passwort-Reset angefordert
</h3>
<p className="text-sm text-green-700 mt-1">
Wir haben eine E-Mail mit Anweisungen zum Zurücksetzen
Ihres Passworts an <strong>{submittedEmail}</strong> gesendet.
</p>
</div>
</div>
</div>
<div className="text-sm text-gray-600 space-y-2">
<p>
<strong>Nächste Schritte:</strong>
</p>
<ul className="list-disc list-inside space-y-1 ml-4">
<li>Überprüfen Sie Ihr E-Mail-Postfach</li>
<li>Klicken Sie auf den Link in der E-Mail</li>
<li>Erstellen Sie ein neues Passwort</li>
<li>Melden Sie sich mit dem neuen Passwort an</li>
</ul>
</div>
<button
onClick={handleBackToLogin}
className="w-full flex items-center justify-center px-4 py-3 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
Zur Anmeldung zurückkehren
</button>
</div>
</div>
{/* Help Text */}
<div className="text-center">
<p className="text-xs text-gray-500">
Haben Sie keine E-Mail erhalten? Überprüfen Sie Ihren Spam-Ordner
oder versuchen Sie es in wenigen Minuten erneut.
</p>
</div>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center px-4">
<div className="max-w-md w-full space-y-8">
{/* Header */}
<div className="text-center">
<div className="w-16 h-16 bg-blue-600 rounded-full flex items-center justify-center mx-auto mb-6">
<Mail className="h-8 w-8 text-white" />
</div>
<h2 className="text-3xl font-bold text-gray-900">
Passwort vergessen?
</h2>
<p className="mt-2 text-gray-600">
Geben Sie Ihre E-Mail-Adresse ein, um ein neues Passwort zu erhalten
</p>
</div>
{/* Reset Form */}
<div className="bg-white rounded-xl shadow-lg p-8">
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
{/* Email Field */}
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-2">
E-Mail-Adresse
</label>
<input
{...register('email')}
type="email"
autoComplete="email"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="ihre.email@beispiel.de"
/>
{errors.email && (
<p className="mt-1 text-sm text-red-600">{errors.email.message}</p>
)}
</div>
{/* Submit Button */}
<button
type="submit"
disabled={isSubmitting}
className="w-full flex items-center justify-center px-4 py-3 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSubmitting ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Sende E-Mail...
</>
) : (
<>
<Send className="h-4 w-4 mr-2" />
Passwort zurücksetzen
</>
)}
</button>
</form>
</div>
{/* Back to Login */}
<div className="text-center">
<button
onClick={handleBackToLogin}
className="inline-flex items-center text-sm text-blue-600 hover:text-blue-500"
>
<ArrowLeft className="h-4 w-4 mr-2" />
Zurück zur Anmeldung
</button>
</div>
</div>
</div>
);
}

276
src/app/auth/login/page.tsx Normal file
View File

@@ -0,0 +1,276 @@
/**
* Login Page Component
* Professional Next.js Resume Builder - Enterprise Auth System
*
<h2 className="text-3xl font-bold text-gray-900">
Willkommen zurück
</h2>
<p className="mt-2 text-gray-600">
Melden Sie sich in Ihrem Konto an, um fortzufahren
</p>hor David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
'use client';
import React, { useState, useEffect } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import Link from 'next/link';
import { Eye, EyeOff, Lock, Mail, AlertCircle, CheckCircle } from 'lucide-react';
import { useAuthStore } from '@/lib/auth-store';
import { useGuestGuard } from '@/hooks/auth';
import { loginSchema } from '@/lib/validation';
import { LoginFormData } from '@/types/auth';
/**
* Professional Login Page Component
*/
export default function LoginPage() {
const router = useRouter();
const searchParams = useSearchParams();
const { login, isLoading, error, clearError } = useAuthStore();
const [showPassword, setShowPassword] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
// Protect route - redirect if already authenticated
useGuestGuard();
const {
register,
handleSubmit,
formState: { errors, isValid },
watch,
} = useForm<LoginFormData>({
resolver: zodResolver(loginSchema),
mode: 'onChange',
defaultValues: {
email: '',
password: '',
rememberMe: false,
},
});
// Clear errors when component mounts or form changes
useEffect(() => {
if (error) {
const timer = setTimeout(() => clearError(), 5000);
return () => clearTimeout(timer);
}
}, [error, clearError]);
const onSubmit = async (data: LoginFormData) => {
setIsSubmitting(true);
clearError();
try {
await login({
email: data.email,
password: data.password,
rememberMe: data.rememberMe,
});
// If login successful and no 2FA required, redirect
const returnUrl = searchParams.get('returnUrl');
if (returnUrl && returnUrl.startsWith('/') && !returnUrl.startsWith('//')) {
router.push(returnUrl as any);
} else {
router.push('/dashboard');
}
} catch (err) {
// Error is handled by the store
console.error('Login error:', err);
} finally {
setIsSubmitting(false);
}
};
const formEmail = watch('email');
const formPassword = watch('password');
const canSubmit = isValid && formEmail && formPassword && !isSubmitting && !isLoading;
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-purple-50 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
{/* Header */}
<div className="text-center">
<div className="mx-auto h-12 w-12 bg-blue-600 rounded-lg flex items-center justify-center">
<Lock className="h-6 w-6 text-white" />
</div>
<h2 className="mt-6 text-3xl font-bold text-gray-900">
Willkommen zurück
</h2>
<p className="mt-2 text-sm text-gray-600">
Melden Sie sich in Ihrem Konto an, um fortzufahren
</p>
</div>
{/* Login Form */}
<div className="bg-white shadow-xl rounded-2xl px-8 py-8 space-y-6">
{/* Error Alert */}
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<div className="flex items-center">
<AlertCircle className="h-5 w-5 text-red-500 mr-3" />
<div>
<h3 className="text-sm font-medium text-red-800">
Anmeldung fehlgeschlagen
</h3>
<p className="text-sm text-red-700 mt-1">
{error.message}
</p>
</div>
</div>
</div>
)}
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
{/* Email Field */}
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-2">
E-Mail-Adresse
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Mail className="h-5 w-5 text-gray-400" />
</div>
<input
{...register('email')}
type="email"
id="email"
className={`
block w-full pl-10 pr-3 py-3 border rounded-lg shadow-sm
placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
${errors.email ? 'border-red-300 bg-red-50' : 'border-gray-300'}
`}
placeholder="Geben Sie Ihre E-Mail ein"
autoComplete="email"
/>
</div>
{errors.email && (
<p className="mt-2 text-sm text-red-600">{errors.email.message}</p>
)}
</div>
{/* Password Field */}
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-2">
Passwort
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Lock className="h-5 w-5 text-gray-400" />
</div>
<input
{...register('password')}
type={showPassword ? 'text' : 'password'}
id="password"
className={`
block w-full pl-10 pr-10 py-3 border rounded-lg shadow-sm
placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
${errors.password ? 'border-red-300 bg-red-50' : 'border-gray-300'}
`}
placeholder="Geben Sie Ihr Passwort ein"
autoComplete="current-password"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute inset-y-0 right-0 pr-3 flex items-center"
>
{showPassword ? (
<EyeOff className="h-5 w-5 text-gray-400 hover:text-gray-600" />
) : (
<Eye className="h-5 w-5 text-gray-400 hover:text-gray-600" />
)}
</button>
</div>
{errors.password && (
<p className="mt-2 text-sm text-red-600">{errors.password.message}</p>
)}
</div>
{/* Remember Me and Forgot Password */}
<div className="flex items-center justify-between">
<div className="flex items-center">
<input
{...register('rememberMe')}
id="rememberMe"
type="checkbox"
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label htmlFor="rememberMe" className="ml-2 block text-sm text-gray-700">
Angemeldet bleiben
</label>
</div>
<Link
href="/auth/forgot-password"
className="text-sm text-blue-600 hover:text-blue-500 font-medium"
>
Passwort vergessen?
</Link>
</div>
{/* Submit Button */}
<button
type="submit"
disabled={!canSubmit}
className={`
w-full flex justify-center items-center py-3 px-4 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500
${canSubmit
? 'bg-blue-600 hover:bg-blue-700 transform hover:scale-[1.02] transition-all duration-200'
: 'bg-gray-400 cursor-not-allowed'
}
`}
>
{(isSubmitting || isLoading) ? (
<>
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
Anmelden...
</>
) : (
<>
<CheckCircle className="h-5 w-5 mr-2" />
Anmelden
</>
)}
</button>
</form>
{/* Divider */}
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300" />
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-white text-gray-500">Neu auf unserer Plattform?</span>
</div>
</div>
{/* Register Link */}
<div className="text-center">
<Link
href="/auth/register"
className="text-blue-600 hover:text-blue-500 font-medium text-sm"
>
Neues Konto erstellen
</Link>
</div>
</div>
{/* Footer */}
<div className="text-center text-xs text-gray-500">
<p>Professioneller Lebenslauf-Builder</p>
<p className="mt-1">Created by David Valera Melendez</p>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,435 @@
/**
* Register Page Component
* Professional Next.js Resume Builder - Enterprise Auth System
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
'use client';
import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import Link from 'next/link';
import {
Eye, EyeOff, Lock, Mail, User, AlertCircle, CheckCircle,
Check, X, Shield
} from 'lucide-react';
import { useAuthStore } from '@/lib/auth-store';
import { useGuestGuard } from '@/hooks/auth';
import { registerSchema, validatePasswordStrength } from '@/lib/validation';
import { RegisterFormData } from '@/types/auth';
/**
* Professional Register Page Component
*/
export default function RegisterPage() {
const router = useRouter();
const { register: registerUser, isLoading, error, clearError } = useAuthStore();
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [passwordStrength, setPasswordStrength] = useState<{
score: number;
feedback: string[];
isValid: boolean;
}>({ score: 0, feedback: [], isValid: false });
// Protect route - redirect if already authenticated
useGuestGuard();
const {
register,
handleSubmit,
formState: { errors, isValid },
watch,
} = useForm<RegisterFormData>({
resolver: zodResolver(registerSchema),
mode: 'onChange',
defaultValues: {
firstName: '',
lastName: '',
email: '',
password: '',
confirmPassword: '',
acceptTerms: false,
},
});
const watchedPassword = watch('password');
// Update password strength when password changes
useEffect(() => {
if (watchedPassword) {
const strength = validatePasswordStrength(watchedPassword);
setPasswordStrength(strength);
}
}, [watchedPassword]);
// Clear errors when component mounts or form changes
useEffect(() => {
if (error) {
const timer = setTimeout(() => clearError(), 5000);
return () => clearTimeout(timer);
}
}, [error, clearError]);
const onSubmit = async (data: RegisterFormData) => {
setIsSubmitting(true);
clearError();
try {
await registerUser({
firstName: data.firstName,
lastName: data.lastName,
email: data.email,
password: data.password,
confirmPassword: data.confirmPassword,
acceptTerms: data.acceptTerms,
});
// If registration successful, redirect to dashboard
router.push('/dashboard');
} catch (err) {
// Error is handled by the store
console.error('Registration error:', err);
} finally {
setIsSubmitting(false);
}
};
const getPasswordStrengthColor = (score: number): string => {
if (score >= 6) return 'text-green-600';
if (score >= 4) return 'text-yellow-600';
return 'text-red-600';
};
const getPasswordStrengthText = (score: number): string => {
if (score >= 6) return 'Stark';
if (score >= 4) return 'Mittel';
if (score >= 2) return 'Schwach';
return 'Sehr schwach';
};
const formFirstName = watch('firstName');
const formLastName = watch('lastName');
const formEmail = watch('email');
const formPassword = watch('password');
const formConfirmPassword = watch('confirmPassword');
const formAcceptTerms = watch('acceptTerms');
const canSubmit = isValid &&
formFirstName && formLastName && formEmail &&
formPassword && formConfirmPassword && formAcceptTerms &&
!isSubmitting && !isLoading;
return (
<div className="min-h-screen bg-gradient-to-br from-green-50 via-white to-blue-50 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
{/* Header */}
<div className="text-center">
<div className="mx-auto h-12 w-12 bg-green-600 rounded-lg flex items-center justify-center">
<User className="h-6 w-6 text-white" />
</div>
<h2 className="mt-6 text-3xl font-bold text-gray-900">
Konto erstellen
</h2>
<p className="mt-2 text-sm text-gray-600">
Treten Sie uns bei, um professionelle Lebensläufe zu erstellen
</p>
</div>
{/* Registration Form */}
<div className="bg-white shadow-xl rounded-2xl px-8 py-8 space-y-6">
{/* Error Alert */}
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<div className="flex items-center">
<AlertCircle className="h-5 w-5 text-red-500 mr-3" />
<div>
<h3 className="text-sm font-medium text-red-800">
Registrierung fehlgeschlagen
</h3>
<p className="text-sm text-red-700 mt-1">
{error.message}
</p>
</div>
</div>
</div>
)}
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
{/* Name Fields */}
<div className="grid grid-cols-2 gap-4">
{/* First Name */}
<div>
<label htmlFor="firstName" className="block text-sm font-medium text-gray-700 mb-2">
Vorname
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<User className="h-5 w-5 text-gray-400" />
</div>
<input
{...register('firstName')}
type="text"
id="firstName"
className={`
block w-full pl-10 pr-3 py-3 border rounded-lg shadow-sm
placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent
${errors.firstName ? 'border-red-300 bg-red-50' : 'border-gray-300'}
`}
placeholder="Vorname"
autoComplete="given-name"
/>
</div>
{errors.firstName && (
<p className="mt-2 text-sm text-red-600">{errors.firstName.message}</p>
)}
</div>
{/* Last Name */}
<div>
<label htmlFor="lastName" className="block text-sm font-medium text-gray-700 mb-2">
Nachname
</label>
<input
{...register('lastName')}
type="text"
id="lastName"
className={`
block w-full px-3 py-3 border rounded-lg shadow-sm
placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent
${errors.lastName ? 'border-red-300 bg-red-50' : 'border-gray-300'}
`}
placeholder="Nachname"
autoComplete="family-name"
/>
{errors.lastName && (
<p className="mt-2 text-sm text-red-600">{errors.lastName.message}</p>
)}
</div>
</div>
{/* Email Field */}
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-2">
E-Mail-Adresse
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Mail className="h-5 w-5 text-gray-400" />
</div>
<input
{...register('email')}
type="email"
id="email"
className={`
block w-full pl-10 pr-3 py-3 border rounded-lg shadow-sm
placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent
${errors.email ? 'border-red-300 bg-red-50' : 'border-gray-300'}
`}
placeholder="Geben Sie Ihre E-Mail ein"
autoComplete="email"
/>
</div>
{errors.email && (
<p className="mt-2 text-sm text-red-600">{errors.email.message}</p>
)}
</div>
{/* Password Field */}
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-2">
Passwort
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Lock className="h-5 w-5 text-gray-400" />
</div>
<input
{...register('password')}
type={showPassword ? 'text' : 'password'}
id="password"
className={`
block w-full pl-10 pr-10 py-3 border rounded-lg shadow-sm
placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent
${errors.password ? 'border-red-300 bg-red-50' : 'border-gray-300'}
`}
placeholder="Erstellen Sie ein starkes Passwort"
autoComplete="new-password"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute inset-y-0 right-0 pr-3 flex items-center"
>
{showPassword ? (
<EyeOff className="h-5 w-5 text-gray-400 hover:text-gray-600" />
) : (
<Eye className="h-5 w-5 text-gray-400 hover:text-gray-600" />
)}
</button>
</div>
{/* Password Strength Indicator */}
{watchedPassword && (
<div className="mt-3">
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-gray-600">Passwortstärke:</span>
<span className={`text-sm font-medium ${getPasswordStrengthColor(passwordStrength.score)}`}>
{getPasswordStrengthText(passwordStrength.score)}
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className={`h-2 rounded-full transition-all duration-300 ${
passwordStrength.score >= 6 ? 'bg-green-500' :
passwordStrength.score >= 4 ? 'bg-yellow-500' :
passwordStrength.score >= 2 ? 'bg-orange-500' : 'bg-red-500'
}`}
style={{ width: `${(passwordStrength.score / 8) * 100}%` }}
/>
</div>
{passwordStrength.feedback.length > 0 && (
<div className="mt-2 text-xs text-gray-600">
{passwordStrength.feedback.map((item, index) => (
<div key={index} className="flex items-center">
<X className="h-3 w-3 text-red-500 mr-1" />
{item}
</div>
))}
</div>
)}
</div>
)}
{errors.password && (
<p className="mt-2 text-sm text-red-600">{errors.password.message}</p>
)}
</div>
{/* Confirm Password Field */}
<div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700 mb-2">
Passwort bestätigen
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Shield className="h-5 w-5 text-gray-400" />
</div>
<input
{...register('confirmPassword')}
type={showConfirmPassword ? 'text' : 'password'}
id="confirmPassword"
className={`
block w-full pl-10 pr-10 py-3 border rounded-lg shadow-sm
placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent
${errors.confirmPassword ? 'border-red-300 bg-red-50' : 'border-gray-300'}
`}
placeholder="Bestätigen Sie Ihr Passwort"
autoComplete="new-password"
/>
<button
type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="absolute inset-y-0 right-0 pr-3 flex items-center"
>
{showConfirmPassword ? (
<EyeOff className="h-5 w-5 text-gray-400 hover:text-gray-600" />
) : (
<Eye className="h-5 w-5 text-gray-400 hover:text-gray-600" />
)}
</button>
</div>
{errors.confirmPassword && (
<p className="mt-2 text-sm text-red-600">{errors.confirmPassword.message}</p>
)}
</div>
{/* Terms Acceptance */}
<div className="flex items-start">
<input
{...register('acceptTerms')}
id="acceptTerms"
type="checkbox"
className="h-4 w-4 text-green-600 focus:ring-green-500 border-gray-300 rounded mt-0.5"
/>
<label htmlFor="acceptTerms" className="ml-3 block text-sm text-gray-700">
Ich stimme den{' '}
<Link href={"/terms" as any} className="text-green-600 hover:text-green-500 font-medium">
Nutzungsbedingungen
</Link>{' '}
und der{' '}
<Link href={"/privacy" as any} className="text-green-600 hover:text-green-500 font-medium">
Datenschutzerklärung
</Link> zu
</label>
</div>
{errors.acceptTerms && (
<p className="text-sm text-red-600">{errors.acceptTerms.message}</p>
)}
{/* Submit Button */}
<button
type="submit"
disabled={!canSubmit}
className={`
w-full flex justify-center items-center py-3 px-4 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500
${canSubmit
? 'bg-green-600 hover:bg-green-700 transform hover:scale-[1.02] transition-all duration-200'
: 'bg-gray-400 cursor-not-allowed'
}
`}
>
{(isSubmitting || isLoading) ? (
<>
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
Konto wird erstellt...
</>
) : (
<>
<CheckCircle className="h-5 w-5 mr-2" />
Konto erstellen
</>
)}
</button>
</form>
{/* Divider */}
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300" />
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-white text-gray-500">Haben Sie bereits ein Konto?</span>
</div>
</div>
{/* Login Link */}
<div className="text-center">
<Link
href="/auth/login"
className="text-green-600 hover:text-green-500 font-medium text-sm"
>
In Ihr Konto anmelden
</Link>
</div>
</div>
{/* Footer */}
<div className="text-center text-xs text-gray-500">
<p>Professioneller Lebenslauf-Builder</p>
<p className="mt-1">Erstellt von David Valera Melendez</p>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,359 @@
/**
* Two-Factor Authentication Component
* Professional Next.js Resume Builder - Enterprise Auth System
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
'use client';
import React, { useState, useEffect, useRef } from 'react';
import { useRouter } from 'next/navigation';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Shield, AlertCircle, Clock, RefreshCw, CheckCircle, ArrowLeft } from 'lucide-react';
import { useAuthStore } from '@/lib/auth-store';
import { twoFactorSchema } from '@/lib/validation';
import { TwoFactorFormData } from '@/types/auth';
/**
* Professional Two-Factor Authentication Component
*/
export default function TwoFactorAuthPage() {
const router = useRouter();
const {
verifyTwoFactor,
twoFactorData,
isLoading,
error,
clearError,
clearTwoFactor,
} = useAuthStore();
const [countdown, setCountdown] = useState(0);
const [isSubmitting, setIsSubmitting] = useState(false);
const [canResend, setCanResend] = useState(false);
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
const {
register,
handleSubmit,
formState: { errors },
setValue,
watch,
reset,
} = useForm<TwoFactorFormData>({
resolver: zodResolver(twoFactorSchema),
mode: 'onChange',
});
// Redirect if no 2FA data
useEffect(() => {
if (!twoFactorData) {
router.push('/auth/login');
}
}, [twoFactorData, router]);
// Initialize countdown timer
useEffect(() => {
const timer = setInterval(() => {
setCountdown(prev => {
if (prev <= 1) {
setCanResend(true);
return 0;
}
return prev - 1;
});
}, 1000);
// Set initial countdown (5 minutes)
setCountdown(300);
return () => clearInterval(timer);
}, []);
// Clear errors on form change
useEffect(() => {
if (error) {
const timer = setTimeout(() => clearError(), 5000);
return () => clearTimeout(timer);
}
}, [error, clearError]);
// Handle individual input changes for the 6-digit code
const handleCodeInputChange = (index: number, value: string) => {
const newValue = value.replace(/\D/g, ''); // Only digits
if (newValue.length <= 1) {
// Update the specific position
const currentCode = watch('verificationCode') || '';
const codeArray = currentCode.split('');
codeArray[index] = newValue;
const newCode = codeArray.join('').slice(0, 6);
setValue('verificationCode', newCode);
// Auto-focus next input
if (newValue && index < 5) {
inputRefs.current[index + 1]?.focus();
}
}
};
// Handle backspace
const handleKeyDown = (index: number, e: React.KeyboardEvent) => {
if (e.key === 'Backspace') {
const currentCode = watch('verificationCode') || '';
const codeArray = currentCode.split('');
if (!codeArray[index] && index > 0) {
// If current is empty, go to previous and clear it
inputRefs.current[index - 1]?.focus();
codeArray[index - 1] = '';
} else {
// Clear current
codeArray[index] = '';
}
const newCode = codeArray.join('');
setValue('verificationCode', newCode);
}
};
// Handle paste
const handlePaste = (e: React.ClipboardEvent) => {
e.preventDefault();
const pastedData = e.clipboardData.getData('text').replace(/\D/g, '').slice(0, 6);
setValue('verificationCode', pastedData);
// Focus appropriate input
const focusIndex = Math.min(pastedData.length, 5);
inputRefs.current[focusIndex]?.focus();
};
const onSubmit = async (data: TwoFactorFormData) => {
if (!twoFactorData) return;
setIsSubmitting(true);
clearError();
try {
await verifyTwoFactor({
code: data.verificationCode,
sessionToken: twoFactorData.sessionToken || '',
email: twoFactorData.userEmail,
});
// Success - redirect to dashboard
router.push('/dashboard');
} catch (err) {
console.error('2FA verification error:', err);
reset(); // Clear the form on error
} finally {
setIsSubmitting(false);
}
};
const handleResendCode = async () => {
// TODO: Implement resend logic
setCanResend(false);
setCountdown(300); // Reset countdown
clearError();
};
const handleGoBack = () => {
clearTwoFactor();
router.push('/auth/login');
};
const formatTime = (seconds: number): string => {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
};
const verificationCode = watch('verificationCode') || '';
const canSubmit = verificationCode.length === 6 && !isSubmitting && !isLoading;
if (!twoFactorData) {
return null; // Will redirect
}
return (
<div className="min-h-screen bg-gradient-to-br from-purple-50 via-white to-blue-50 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
{/* Header */}
<div className="text-center">
<div className="mx-auto h-12 w-12 bg-purple-600 rounded-lg flex items-center justify-center">
<Shield className="h-6 w-6 text-white" />
</div>
<h2 className="mt-6 text-3xl font-bold text-gray-900">
Zwei-Faktor-Authentifizierung
</h2>
<p className="mt-2 text-sm text-gray-600">
Wir haben einen Bestätigungscode an Ihr Gerät gesendet
</p>
</div>
{/* User Info */}
<div className="bg-blue-50 rounded-lg p-4">
<div className="text-center">
<p className="text-sm font-medium text-gray-700">
Zugriff verifizieren für
</p>
<p className="text-lg font-semibold text-blue-900">
{twoFactorData.userName}
</p>
<p className="text-sm text-gray-600">
{twoFactorData.userEmail}
</p>
</div>
</div>
{/* 2FA Form */}
<div className="bg-white shadow-xl rounded-2xl px-8 py-8 space-y-6">
{/* Error Alert */}
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<div className="flex items-center">
<AlertCircle className="h-5 w-5 text-red-500 mr-3" />
<div>
<h3 className="text-sm font-medium text-red-800">
Verifizierung fehlgeschlagen
</h3>
<p className="text-sm text-red-700 mt-1">
{error.message}
</p>
</div>
</div>
</div>
)}
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
{/* Verification Code Input */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-4">
Geben Sie den 6-stelligen Bestätigungscode ein
</label>
{/* Individual digit inputs */}
<div className="flex justify-center space-x-3 mb-4">
{Array.from({ length: 6 }).map((_, index) => (
<input
key={index}
ref={(el) => {
inputRefs.current[index] = el;
}}
type="text"
inputMode="numeric"
maxLength={1}
value={verificationCode[index] || ''}
onChange={(e) => handleCodeInputChange(index, e.target.value)}
onKeyDown={(e) => handleKeyDown(index, e)}
onPaste={handlePaste}
className={`
w-12 h-12 text-center text-lg font-semibold border-2 rounded-lg
focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent
${errors.verificationCode ? 'border-red-300 bg-red-50' : 'border-gray-300'}
${verificationCode[index] ? 'border-purple-500 bg-purple-50' : ''}
`}
/>
))}
</div>
{/* Hidden input for form validation */}
<input
{...register('verificationCode')}
type="hidden"
value={verificationCode}
/>
{errors.verificationCode && (
<p className="text-sm text-red-600 text-center">
{errors.verificationCode.message}
</p>
)}
</div>
{/* Timer and Resend */}
<div className="text-center">
{countdown > 0 ? (
<div className="flex items-center justify-center text-sm text-gray-600">
<Clock className="h-4 w-4 mr-2" />
Code läuft ab in {formatTime(countdown)}
</div>
) : (
<button
type="button"
onClick={handleResendCode}
disabled={!canResend}
className="flex items-center justify-center text-sm text-purple-600 hover:text-purple-500 font-medium mx-auto"
>
<RefreshCw className="h-4 w-4 mr-2" />
Code erneut senden
</button>
)}
</div>
{/* Submit Button */}
<button
type="submit"
disabled={!canSubmit}
className={`
w-full flex justify-center items-center py-3 px-4 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500
${canSubmit
? 'bg-purple-600 hover:bg-purple-700 transform hover:scale-[1.02] transition-all duration-200'
: 'bg-gray-400 cursor-not-allowed'
}
`}
>
{(isSubmitting || isLoading) ? (
<>
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
Verifiziere...
</>
) : (
<>
<CheckCircle className="h-5 w-5 mr-2" />
Code verifizieren
</>
)}
</button>
</form>
{/* Back to Login */}
<div className="text-center border-t pt-6">
<button
onClick={handleGoBack}
className="flex items-center justify-center text-sm text-gray-600 hover:text-gray-500 mx-auto"
>
<ArrowLeft className="h-4 w-4 mr-2" />
Back to login
</button>
</div>
</div>
{/* Security Notice */}
<div className="bg-gray-50 rounded-lg p-4">
<div className="text-center">
<p className="text-xs text-gray-600">
🔒 Dieser zusätzliche Sicherheitsschritt hilft, Ihr Konto zu schützen
</p>
</div>
</div>
{/* Footer */}
<div className="text-center text-xs text-gray-500">
<p>Professioneller Lebenslauf-Builder</p>
<p className="mt-1">Erstellt von David Valera Melendez</p>
</div>
</div>
</div>
);
}

318
src/app/dashboard/page.tsx Normal file
View File

@@ -0,0 +1,318 @@
/**
* Dashboard Page - Resume Management Dashboard with Enterprise Authentication
* Resume Builder Application
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @updated 2025-08-08 - Added enterprise authentication
* @location Made in Germany 🇩🇪
*/
'use client';
import React, { useState } from 'react';
import { ModernCard } from '@/components/ui/ModernCard';
import ResumePreview from '@/components/resume/ResumePreview';
import { sampleResumeData } from '@/data/sampleResumeData';
import { UserDropdown } from '@/components/ui/UserDropdown';
import { useAuthGuard } from '@/hooks/auth';
import { useAuthStore } from '@/lib/auth-store';
import {
Plus,
FileText,
Edit,
Download,
Copy,
Trash2,
Calendar,
Clock,
Shield,
User,
LogOut,
} from 'lucide-react';
/**
* Dashboard Component - Main Resume Management Interface with Authentication
*
* Professional dashboard for managing resumes with modern UI patterns and enterprise-grade authentication.
* Features resume overview, creation, management capabilities, and secure authentication.
*
* Key Features:
* - Enterprise authentication with route protection
* - User account management
* - Resume creation and management
* - Small resume previews with action buttons
* - Responsive design
* - German localization
* - Security status display
*
* @returns Dashboard page with resume management and authentication
*/
export default function Dashboard() {
const { user, logout } = useAuthStore();
// Protect this route - require authentication
const { isAuthenticated } = useAuthGuard();
// Loading state while authentication is being verified
if (!isAuthenticated || !user) {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-white flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Lade Dashboard...</p>
</div>
</div>
);
}
/**
* Handles logout
*/
const handleLogout = async () => {
try {
await logout();
} catch (error) {
console.error('Logout error:', error);
}
};
/**
* Sample resumes data - in real app, this would come from API/database
*/
const resumes = [
{
id: '1',
title: 'Senior Frontend Developer',
lastModified: '2025-01-07',
createdAt: '2025-01-01',
data: sampleResumeData,
},
// Add more sample resumes if needed
];
/**
* Handles creating a new resume
*/
const handleCreateResume = () => {
// Navigate to personal info form to start creating
window.location.href = '/personal-info';
};
/**
* Handles resume actions
*/
const handleEdit = (resumeId: string) => {
window.location.href = '/personal-info';
};
const handleDownload = (resumeId: string) => {
console.log('Download resume:', resumeId);
};
const handleDuplicate = (resumeId: string) => {
console.log('Duplicate resume:', resumeId);
};
const handleDelete = (resumeId: string) => {
console.log('Delete resume:', resumeId);
};
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-white">
{/* Header */}
<header className="border-b border-gray-200 bg-white shadow-sm">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="flex h-16 items-center justify-between">
{/* Logo and User Info */}
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-primary-600 to-primary-700 shadow-sm">
<FileText className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-xl font-bold text-primary-700">
{user.firstName} {user.lastName}
</h1>
<p className="text-xs text-gray-500">Resume Builder</p>
</div>
</div>
{/* Authentication Status and User Dropdown */}
<div className="flex items-center space-x-4">
{/* Authentication Status */}
<div className="hidden md:flex items-center space-x-2 px-3 py-1 bg-green-50 border border-green-200 rounded-lg">
<Shield className="h-4 w-4 text-green-600" />
<span className="text-xs text-green-700 font-medium">Authenticated</span>
</div>
{/* User Info */}
<div className="flex items-center space-x-3">
<div className="w-8 h-8 bg-primary-600 rounded-full flex items-center justify-center">
<User className="h-5 w-5 text-white" />
</div>
<div className="hidden lg:block">
<p className="text-sm font-medium text-gray-900">
{user.firstName} {user.lastName}
</p>
<p className="text-xs text-gray-500">{user.email}</p>
</div>
</div>
{/* Logout Button */}
<button
onClick={handleLogout}
className="flex items-center px-3 py-2 rounded-md text-sm font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50 transition-colors"
>
<LogOut className="h-4 w-4 mr-2" />
<span className="hidden sm:inline">Abmelden</span>
</button>
</div>
</div>
</div>
</header>{' '}
{/* Main Content */}
<main className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
{/* Welcome Section with Create Button */}
<div className="mb-8 flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">
Willkommen zurück, {user.firstName}!
</h1>
<p className="mt-2 text-gray-600">
Verwalten Sie Ihre Lebensläufe und erstellen Sie neue
professionelle Bewerbungsunterlagen
</p>
</div>
<button
onClick={handleCreateResume}
className="inline-flex items-center gap-2 rounded-lg bg-primary-600 px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
>
<Plus className="h-5 w-5" />
Neuen Lebenslauf erstellen
</button>
</div>
{/* Security Status Card */}
<div className="mb-6 bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="flex items-center">
<Shield className="h-5 w-5 text-blue-600 mr-3" />
<div>
<h4 className="text-sm font-medium text-blue-900">
Sicherheitsstatus
</h4>
<p className="text-sm text-blue-700 mt-1">
Sie sind sicher mit Enterprise-Sicherheit authentifiziert.
{user.roles && user.roles.length > 0 && (
<span className="ml-2">
Rollen: {user.roles.join(', ')}
</span>
)}
</p>
</div>
</div>
</div>
{/* Resumes Grid */}
<div className="grid gap-6 sm:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3">
{resumes.map(resume => (
<ModernCard key={resume.id} variant="elevated" padding="md">
<div className="flex gap-4">
{/* Small Resume Preview - Left Side */}
<div className="flex-shrink-0">
<div className="flex h-52 w-28 items-center justify-center overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm">
<div className="origin-center scale-[0.18] transform">
<div style={{ width: '650px', height: '920px' }}>
<ResumePreview resumeData={resume.data} />
</div>
</div>
</div>
</div>
{/* Resume Info and Actions - Right Side */}
<div className="flex flex-1 flex-col justify-between">
{/* Title and Info */}
<div>
<h3 className="mb-2 text-lg font-semibold text-gray-900">
{resume.title}
</h3>
<div className="mb-4 space-y-1 text-xs text-gray-500">
<div className="flex items-center gap-1">
<Clock className="h-3 w-3" />
Bearbeitet:{' '}
{new Date(resume.lastModified).toLocaleDateString(
'de-DE'
)}
</div>
<div className="flex items-center gap-1">
<Calendar className="h-3 w-3" />
Erstellt:{' '}
{new Date(resume.createdAt).toLocaleDateString('de-DE')}
</div>
</div>
</div>
{/* Action Buttons */}
<div className="space-y-2">
<div className="flex gap-2">
<button
onClick={() => handleEdit(resume.id)}
className="flex flex-1 items-center justify-center gap-1 rounded-lg border border-gray-300 bg-white px-3 py-2 text-xs font-medium text-gray-700 transition-colors hover:bg-gray-50"
>
<Edit className="h-3 w-3" />
Bearbeiten
</button>
<button
onClick={() => handleDownload(resume.id)}
className="flex flex-1 items-center justify-center gap-1 rounded-lg border border-gray-300 bg-white px-3 py-2 text-xs font-medium text-gray-700 transition-colors hover:bg-gray-50"
>
<Download className="h-3 w-3" />
Download
</button>
</div>
<div className="flex gap-2">
<button
onClick={() => handleDuplicate(resume.id)}
className="flex flex-1 items-center justify-center gap-1 rounded-lg border border-gray-300 bg-white px-3 py-2 text-xs font-medium text-gray-700 transition-colors hover:bg-gray-50"
>
<Copy className="h-3 w-3" />
Duplizieren
</button>
<button
onClick={() => handleDelete(resume.id)}
className="flex flex-1 items-center justify-center gap-1 rounded-lg border border-red-300 bg-white px-3 py-2 text-xs font-medium text-red-600 transition-colors hover:bg-red-50"
>
<Trash2 className="h-3 w-3" />
Löschen
</button>
</div>
</div>
</div>
</div>
</ModernCard>
))}
</div>
{/* Empty State (if no resumes) */}
{resumes.length === 0 && (
<div className="mt-12 text-center">
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-gray-100">
<FileText className="h-8 w-8 text-gray-400" />
</div>
<h3 className="mt-4 text-lg font-medium text-gray-900">
Keine Lebensläufe vorhanden
</h3>
<p className="mt-2 text-gray-500">
Erstellen Sie Ihren ersten professionellen Lebenslauf
</p>
<button
onClick={handleCreateResume}
className="mt-6 inline-flex items-center gap-2 rounded-lg bg-primary-600 px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-primary-700"
>
<Plus className="h-5 w-5" />
Ersten Lebenslauf erstellen
</button>
</div>
)}
</main>
</div>
);
}

192
src/app/globals.css Normal file
View File

@@ -0,0 +1,192 @@
/**
* Global Styles
* Professional Resume Builder
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
html {
scroll-behavior: smooth;
}
body {
@apply bg-secondary-50 text-secondary-900 antialiased;
font-feature-settings: 'cv03', 'cv04', 'cv11';
}
* {
@apply border-secondary-200;
}
h1, h2, h3, h4, h5, h6 {
@apply font-semibold tracking-tight;
}
h1 {
@apply text-4xl lg:text-5xl;
}
h2 {
@apply text-3xl lg:text-4xl;
}
h3 {
@apply text-2xl lg:text-3xl;
}
h4 {
@apply text-xl lg:text-2xl;
}
h5 {
@apply text-lg lg:text-xl;
}
h6 {
@apply text-base lg:text-lg;
}
}
@layer components {
/* David Valera Melendez - Professional Button Styles */
.btn {
@apply inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-medium transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50;
}
.btn-primary {
@apply btn bg-primary-600 text-white hover:bg-primary-700 focus:ring-primary-500;
}
.btn-secondary {
@apply btn bg-secondary-600 text-white hover:bg-secondary-700 focus:ring-secondary-500;
}
.btn-outline {
@apply btn border border-secondary-300 bg-white text-secondary-900 hover:bg-secondary-50 focus:ring-secondary-500;
}
.btn-ghost {
@apply btn text-secondary-700 hover:bg-secondary-100 focus:ring-secondary-500;
}
/* Professional Card Styles */
.card {
@apply rounded-xl bg-white p-6 shadow-soft;
}
.card-elevated {
@apply card shadow-medium;
}
/* Form Input Styles */
.input {
@apply w-full rounded-lg border border-secondary-300 px-3 py-2 text-sm placeholder:text-secondary-400 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/20;
}
.textarea {
@apply input resize-none;
}
.label {
@apply block text-sm font-medium text-secondary-700;
}
/* Professional Layout Components */
.section {
@apply py-12 lg:py-16;
}
.container-custom {
@apply mx-auto max-w-7xl px-4 sm:px-6 lg:px-8;
}
/* David Valera Melendez - Professional Resume Styles */
.resume-section {
@apply space-y-6;
}
.resume-header {
@apply border-b border-secondary-200 pb-6;
}
.resume-content {
@apply space-y-8;
}
.resume-item {
@apply relative;
}
.resume-item::before {
@apply absolute -left-4 top-2 h-2 w-2 rounded-full bg-primary-600 content-[''];
}
.resume-timeline {
@apply relative border-l border-secondary-200 pl-6;
}
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
.animation-delay-200 {
animation-delay: 200ms;
}
.animation-delay-400 {
animation-delay: 400ms;
}
.animation-delay-600 {
animation-delay: 600ms;
}
/* Professional Gradient Backgrounds */
.gradient-primary {
background: linear-gradient(135deg, theme('colors.primary.600'), theme('colors.primary.700'));
}
.gradient-secondary {
background: linear-gradient(135deg, theme('colors.secondary.600'), theme('colors.secondary.700'));
}
.gradient-accent {
background: linear-gradient(135deg, theme('colors.accent.600'), theme('colors.accent.700'));
}
}
/* Print Styles for Resume Export */
@media print {
.no-print {
display: none !important;
}
.print-break-before {
page-break-before: always;
}
.print-break-after {
page-break-after: always;
}
.print-break-inside-avoid {
page-break-inside: avoid;
}
body {
@apply bg-white text-black;
}
.card {
@apply shadow-none;
}
}

87
src/app/layout.tsx Normal file
View File

@@ -0,0 +1,87 @@
/**
* Root Layout Component
* Professional Resume Builder
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: 'David Valera Melendez - Professional Resume Builder',
description:
'Professional Resume Builder created by David Valera Melendez - Made in Germany',
keywords: [
'resume',
'cv',
'curriculum vitae',
'professional',
'builder',
'David Valera Melendez',
'Germany',
'job application',
],
authors: [
{
name: 'David Valera Melendez',
url: 'https://valera-melendez.de',
},
],
creator: 'David Valera Melendez',
publisher: 'David Valera Melendez',
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
},
},
openGraph: {
type: 'website',
locale: 'en_US',
url: 'https://resume.valera-melendez.de',
title: 'David Valera Melendez - Professional Resume Builder',
description:
'Professional Resume Builder created by David Valera Melendez - Made in Germany',
siteName: 'David Valera Resume Builder',
},
twitter: {
card: 'summary_large_image',
title: 'David Valera Melendez - Professional Resume Builder',
description:
'Professional Resume Builder created by David Valera Melendez - Made in Germany',
},
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" className="scroll-smooth">
<head>
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#0ea5e9" />
</head>
<body className={`${inter.className} antialiased`}>
<div className="from-grayscale-50 via-primary-25 to-grayscale-100 relative min-h-screen bg-gradient-to-br">
{/* Professional background pattern */}
<div className="absolute inset-0 opacity-30">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_1px_1px,rgba(14,165,233,0.15)_1px,transparent_0)] bg-[length:24px_24px]" />
</div>
{/* Content overlay */}
<div className="relative z-10">{children}</div>
</div>
</body>
</html>
);
}

111
src/app/login/page.tsx Normal file
View File

@@ -0,0 +1,111 @@
/**
* Login Page - Professional Authentication
* Professional Resume Builder - Enhanced with modern authentication standards
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
'use client';
import React from 'react';
import { useRouter } from 'next/navigation';
import LoginForm from '@/components/auth/LoginForm';
/**
* Login credentials interface
*/
interface LoginCredentials {
email: string;
password: string;
rememberMe: boolean;
}
/**
* LoginPage Component - Professional Authentication Page
*
* Complete login page with David Valera Melendez branding and German localization.
* Integrates with the professional resume builder application with modern
* authentication patterns and user experience.
*
* Key Features:
* - Professional German branding
* - David Valera Melendez logo and identity
* - Modern authentication flow
* - Responsive design
* - Integration with Next.js routing
* - Professional color scheme
*
* @returns Complete login page with authentication functionality
*/
export default function LoginPage() {
const router = useRouter();
/**
* Handles successful login
*
* @param credentials - User login credentials
*/
const handleLogin = async (credentials: LoginCredentials) => {
try {
// Here you would typically make an API call to authenticate
console.log('Login attempted with:', credentials);
// For now, simulate successful login and redirect
// In a real application, you would:
// 1. Send credentials to your authentication API
// 2. Store the authentication token
// 3. Update user context/state
// 4. Redirect to the main application
// Simulate API delay
await new Promise(resolve => setTimeout(resolve, 1000));
// Redirect to main application
router.push('/');
} catch (error) {
console.error('Login failed:', error);
// Handle login error (show error message, etc.)
}
};
/**
* Handles forgot password action
*/
const handleForgotPassword = () => {
// Here you would typically navigate to a forgot password page
// or show a forgot password modal
console.log('Forgot password clicked');
// For now, you could redirect to a forgot password page
// router.push('/auth/forgot-password');
// Or show an alert (temporary solution)
alert('Funktion "Passwort vergessen" wird in Kürze verfügbar sein.');
};
/**
* Handles sign up action
*/
const handleSignUp = () => {
// Here you would typically navigate to a registration page
console.log('Sign up clicked');
// For now, you could redirect to a sign up page
// router.push('/auth/signup');
// Or show an alert (temporary solution)
alert('Registrierung wird in Kürze verfügbar sein.');
};
return (
<div>
<LoginForm
onLogin={handleLogin}
onForgotPassword={handleForgotPassword}
onSignUp={handleSignUp}
/>
</div>
);
}

352
src/app/page.tsx Normal file
View File

@@ -0,0 +1,352 @@
/**
* Home Page Component
* Professional Resume Builder
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
'use client';
import { useState, useEffect } from 'react';
import HorizontalStepper, {
Step,
} from '@/components/navigation/HorizontalStepper';
import StepContentRenderer from '@/components/resume/StepContentRenderer';
import ResumePreview from '@/components/resume/ResumePreview';
import { ResumeData } from '@/types/resume';
// Import organized data from centralized data folder
import { emptyResumeTemplate } from '@/data/resumeTemplates';
import { stepDescriptions } from '@/data/uiConstants';
/**
* Initial resume data using centralized empty template
*/
const initialResumeData: ResumeData = emptyResumeTemplate;
/**
* Step configuration for the resume builder process
* Defines the sequence and metadata for each step in the resume creation workflow
*/
const resumeSteps: Step[] = [
{
id: 'contact',
title: 'Contact',
description: stepDescriptions.contact,
icon: '👤',
completed: false,
},
{
id: 'experience',
title: 'Experience',
description: stepDescriptions.experience,
icon: '💼',
completed: false,
},
{
id: 'education',
title: 'Education',
description: stepDescriptions.education,
icon: '🎓',
completed: false,
},
{
id: 'skills',
title: 'Skills',
description: stepDescriptions.skills,
icon: '⚡',
completed: false,
},
{
id: 'about',
title: 'About',
description: stepDescriptions.about,
icon: '📝',
completed: false,
},
{
id: 'finish',
title: 'Finish',
description: '',
icon: '✨',
completed: false,
},
];
/**
* HomePage Component - Main Resume Builder Application
*
* Professional resume builder with split-screen layout featuring a step-by-step form
* interface and real-time preview. Implements modern CV format with comprehensive
* data validation and progress tracking.
*
* Features:
* - Multi-step form wizard with progress tracking
* - Real-time resume preview with professional styling
* - Modern CV format compliance
* - Auto-completion validation
* - Responsive design with mobile support
* - PDF export capability
*
* @returns Complete resume builder application interface
*/
export default function HomePage() {
const [resumeData, setResumeData] = useState<ResumeData>(initialResumeData);
const [currentStep, setCurrentStep] = useState('contact');
const [steps, setSteps] = useState(resumeSteps);
/**
* Updates resume data with partial data changes
* Merges new data with existing resume data while maintaining immutability
*
* @param updatedData - Partial resume data to merge with current data
*/
const handleDataUpdate = (updatedData: Partial<ResumeData>) => {
setResumeData(prev => ({ ...prev, ...updatedData }));
};
/**
* Handles step navigation when user clicks on a specific step
*
* @param stepId - The ID of the step to navigate to
*/
const handleStepChange = (stepId: string) => {
setCurrentStep(stepId);
};
/**
* Navigates to the next step in the resume builder process
* Marks current step as completed and advances to next step
*/
const handleNext = () => {
const currentIndex = steps.findIndex(step => step.id === currentStep);
if (currentIndex < steps.length - 1) {
// Mark current step as completed
setSteps(prev =>
prev.map(step =>
step.id === currentStep ? { ...step, completed: true } : step
)
);
setCurrentStep(steps[currentIndex + 1].id);
}
};
/**
* Navigates to the previous step in the resume builder process
*/
const handlePrevious = () => {
const currentIndex = steps.findIndex(step => step.id === currentStep);
if (currentIndex > 0) {
setCurrentStep(steps[currentIndex - 1].id);
}
};
/**
* Determines if the current step is the first step
*/
const isFirstStep = steps.findIndex(step => step.id === currentStep) === 0;
/**
* Determines if the current step is the last step
*/
const isLastStep =
steps.findIndex(step => step.id === currentStep) === steps.length - 1;
/**
* Auto-completion effect - Updates step completion status based on resume data
* Monitors resume data changes and automatically marks steps as completed
* when required data is present
*/
useEffect(() => {
setSteps(prev =>
prev.map(step => {
switch (step.id) {
case 'contact':
return {
...step,
completed: !!(
resumeData.personalInfo.firstName &&
resumeData.personalInfo.lastName &&
resumeData.personalInfo.email
),
};
case 'experience':
return { ...step, completed: resumeData.experience.length > 0 };
case 'education':
return { ...step, completed: resumeData.education.length > 0 };
case 'skills':
return { ...step, completed: resumeData.skills.length > 0 };
case 'about':
return {
...step,
completed: !!(
resumeData.personalInfo.summary &&
resumeData.personalInfo.summary.length > 10
),
};
default:
return step;
}
})
);
}, [resumeData]);
return (
<main className="min-h-screen bg-gradient-to-br from-grayscale-100 via-primary-50 to-grayscale-50">
{/* Main Content Area - Enhanced Professional Layout */}
<div className="flex h-screen">
{/* Left Panel - Entire Panel Scrolls Together */}
<div className="relative z-10 w-1/2 overflow-y-auto border-r border-grayscale-200 bg-gradient-to-b from-white to-grayscale-25 shadow-2xl">
{/* Horizontal Stepper Header - Scrolls with content */}
<div className="border-b border-grayscale-100 bg-gradient-to-r from-white via-grayscale-25 to-white shadow-soft">
<HorizontalStepper
steps={steps}
currentStep={currentStep}
onStepChange={handleStepChange}
/>
</div>
{/* Step Content - No separate scrolling */}
<div className="bg-gradient-to-b from-transparent to-grayscale-25/30">
<StepContentRenderer
currentStep={currentStep}
resumeData={resumeData}
onDataUpdate={handleDataUpdate}
onNext={handleNext}
onPrevious={handlePrevious}
isFirstStep={isFirstStep}
isLastStep={isLastStep}
/>
</div>
</div>
{/* Right Panel - Independent Scrolling Resume Preview */}
<div className="relative flex w-1/2 flex-col bg-gradient-to-br from-grayscale-200 via-grayscale-100 to-primary-50">
{/* Background Pattern for Professional Look */}
<div className="absolute inset-0 opacity-20">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_2px_2px,rgba(100,116,139,0.15)_1px,transparent_0)] bg-[length:32px_32px]" />
</div>
{/* Right Panel - Independent Scrollable Content */}
<div className="relative z-10 flex-1 overflow-y-auto">
<div className="p-4">
{/* Preview Container - More Compact */}
<div className="relative flex min-h-screen items-start justify-center p-4">
<div className="" style={{ minHeight: '800px' }}>
<ResumePreview resumeData={resumeData} />
</div>
{/* Floating Action Buttons - Smaller */}
<div className="absolute right-2 top-2 flex items-center gap-1">
<button className="group flex h-7 w-7 items-center justify-center rounded-full border border-border-primary bg-background-primary text-text-tertiary shadow-medium transition-all duration-200 hover:text-primary-600 hover:shadow-large">
<svg
className="h-3.5 w-3.5 transition-transform duration-200 group-hover:scale-110"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
</button>
<button className="group flex h-10 w-10 items-center justify-center rounded-full border border-border-primary bg-background-primary text-text-tertiary shadow-medium transition-all duration-200 hover:text-primary-600 hover:shadow-large">
<svg
className="h-5 w-5 transition-transform duration-200 group-hover:scale-110"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"
/>
</svg>
</button>
</div>
</div>
{/* Download Section - Enhanced */}
<div className="mt-6 space-y-4 text-center">
<button className="group inline-flex min-w-40 transform items-center justify-center rounded-xl bg-gradient-to-r from-primary-500 to-primary-600 px-6 py-2.5 text-sm font-semibold text-white shadow-large transition-all duration-300 hover:-translate-y-0.5 hover:from-primary-600 hover:to-primary-700 hover:shadow-xl">
<svg
className="mr-2 h-4 w-4 transition-transform duration-200 group-hover:scale-110"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
Download PDF
</button>
<div className="flex items-center justify-center gap-4 text-sm text-text-tertiary">
<span className="flex items-center gap-1">
<svg
className="h-4 w-4 text-success-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
ATS-Friendly
</span>
<span className="flex items-center gap-1">
<svg
className="h-4 w-4 text-success-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
Professional Format
</span>
<span className="flex items-center gap-1">
<svg
className="h-4 w-4 text-success-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
Made in Germany 🇩🇪
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
);
}

170
src/app/preview/page.tsx Normal file
View File

@@ -0,0 +1,170 @@
/**
* Preview Page - Resume Preview
* Resume Builder Application
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
'use client';
import React from 'react';
import Layout from '@/components/layout/Layout';
import ResumePreview from '@/components/resume/ResumePreview';
import { ModernCard } from '@/components/ui/ModernCard';
import { Eye, Download, Edit, Share } from 'lucide-react';
import { sampleResumeData } from '@/data/sampleResumeData';
/**
* Preview Component - Resume Preview Page
*
* Page for viewing the complete resume preview with export options.
*
* Key Features:
* - Full resume preview
* - Export to PDF functionality
* - Edit shortcuts
* - Responsive design
*
* @returns Preview page with resume display
*/
export default function Preview() {
/**
* Handles PDF export
*/
const handleExport = () => {
// Implement PDF export logic
console.log('Export to PDF');
};
/**
* Handles edit action
*/
const handleEdit = () => {
// Navigate to edit page
window.location.href = '/personal-info';
};
return (
<Layout>
<div className="space-y-6">
{/* Page Header */}
<div className="border-b border-gray-200 pb-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-gradient-to-br from-primary-100 to-primary-200 shadow-sm">
<Eye className="h-6 w-6 text-primary-600" />
</div>
<div>
<h1 className="text-2xl font-bold text-gray-900">
Lebenslauf Vorschau
</h1>
<p className="text-gray-600">
Vorschau Ihres professionellen Lebenslaufs
</p>
</div>
</div>
<div className="flex items-center gap-3">
<button
onClick={handleEdit}
className="inline-flex items-center gap-2 rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50"
>
<Edit className="h-4 w-4" />
<span>Bearbeiten</span>
</button>
<button
onClick={handleExport}
className="inline-flex items-center gap-2 rounded-lg bg-primary-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-primary-700"
>
<Download className="h-4 w-4" />
<span>PDF Export</span>
</button>
</div>
</div>
</div>
{/* Preview Content */}
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
{/* Resume Preview */}
<div className="lg:col-span-2">
<ModernCard variant="elevated" padding="lg">
<div className="flex justify-center">
<ResumePreview resumeData={sampleResumeData} />
</div>
</ModernCard>
</div>
{/* Actions Sidebar */}
<div className="space-y-6">
{/* Export Options */}
<ModernCard variant="elevated" padding="md">
<h3 className="mb-4 text-lg font-semibold text-gray-900">
Export-Optionen
</h3>
<div className="space-y-3">
<button
onClick={handleExport}
className="flex w-full items-center gap-3 rounded-lg border border-gray-200 p-3 text-left transition-colors hover:bg-gray-50"
>
<Download className="h-5 w-5 text-primary-600" />
<div>
<p className="font-medium text-gray-900">
PDF herunterladen
</p>
<p className="text-sm text-gray-500">
Hochauflösender PDF-Export
</p>
</div>
</button>
<button className="flex w-full items-center gap-3 rounded-lg border border-gray-200 p-3 text-left transition-colors hover:bg-gray-50">
<Share className="h-5 w-5 text-primary-600" />
<div>
<p className="font-medium text-gray-900">Link teilen</p>
<p className="text-sm text-gray-500">
Öffentlichen Link erstellen
</p>
</div>
</button>
</div>
</ModernCard>
{/* Tips */}
<ModernCard variant="elevated" padding="md">
<h3 className="mb-4 text-lg font-semibold text-gray-900">
Tipps für bessere Ergebnisse
</h3>
<div className="space-y-3 text-sm text-gray-600">
<div className="flex items-start gap-2">
<div className="mt-2 h-2 w-2 flex-shrink-0 rounded-full bg-primary-600"></div>
<p>
Verwenden Sie konkrete Zahlen und Erfolge in Ihren
Beschreibungen
</p>
</div>
<div className="flex items-start gap-2">
<div className="mt-2 h-2 w-2 flex-shrink-0 rounded-full bg-primary-600"></div>
<p>
Halten Sie Beschreibungen präzise und relevant für die
Zielposition
</p>
</div>
<div className="flex items-start gap-2">
<div className="mt-2 h-2 w-2 flex-shrink-0 rounded-full bg-primary-600"></div>
<p>Überprüfen Sie Rechtschreibung und Grammatik sorgfältig</p>
</div>
<div className="flex items-start gap-2">
<div className="mt-2 h-2 w-2 flex-shrink-0 rounded-full bg-primary-600"></div>
<p>
Aktualisieren Sie regelmäßig Ihre Fähigkeiten und
Erfahrungen
</p>
</div>
</div>
</ModernCard>
</div>
</div>
</div>
</Layout>
);
}

234
src/app/settings/page.tsx Normal file
View File

@@ -0,0 +1,234 @@
/**
* Settings Page Component
* User Account Settings and Profile Management
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
'use client';
import { useState } from 'react';
import { Save, ArrowLeft, User } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { ModernCard } from '@/components/ui/ModernCard';
import { ModernInput } from '@/components/ui/ModernInput';
import { UserDropdown } from '@/components/ui/UserDropdown';
/**
* Settings Component - User Profile Management
*
* Professional settings page for managing user account information
* with German address format and comprehensive form validation.
*
* Features:
* - Personal information management
* - German address format
* - Form validation
* - Professional styling
* - Responsive design
*
* @returns Settings page with user profile management
*/
export default function Settings() {
const router = useRouter();
const [isSaving, setIsSaving] = useState(false);
// User profile state
const [profile, setProfile] = useState({
firstName: 'David',
lastName: 'Valera Melendez',
email: 'david@valera-melendez.de',
phone: '+49 123 456 789',
street: 'Musterstraße 123',
zipCode: '10115',
city: 'Berlin',
country: 'Deutschland',
});
/**
* Handles form input changes
*/
const handleInputChange = (
field: string,
event: React.ChangeEvent<HTMLInputElement>
) => {
setProfile(prev => ({
...prev,
[field]: event.target.value,
}));
};
/**
* Handles form submission
*/
const handleSave = async () => {
setIsSaving(true);
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
// Here you would typically save to your backend
console.log('Saving profile:', profile);
setIsSaving(false);
// Show success message (you can implement a toast notification)
alert('Profil erfolgreich gespeichert!');
};
/**
* Handles navigation back to dashboard
*/
const handleBackToDashboard = () => {
router.push('/dashboard');
};
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-white">
{/* Header */}
<header className="border-b border-gray-200 bg-white shadow-sm">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="flex h-16 items-center justify-between">
{/* Back Button - Left Side */}
<div className="flex items-center gap-4">
<button
onClick={handleBackToDashboard}
className="flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-100"
>
<ArrowLeft className="h-4 w-4" />
Zurück zum Dashboard
</button>
</div>
{/* User Info and Logo - Right Side */}
<UserDropdown currentPage="settings" showLogo={true} />
</div>
</div>
</header>
{/* Main Content */}
<main className="mx-auto max-w-4xl px-4 py-8 sm:px-6 lg:px-8">
{/* Page Title */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">
Konto-Einstellungen
</h1>
<p className="mt-2 text-gray-600">
Verwalten Sie Ihre persönlichen Informationen und Kontoeinstellungen
</p>
</div>
{/* Settings Form */}
<div className="grid gap-8 lg:grid-cols-1">
<ModernCard variant="elevated" padding="lg">
<div className="mb-6">
<div className="mb-4 flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-primary-100 to-primary-200">
<User className="h-5 w-5 text-primary-600" />
</div>
<div>
<h2 className="text-xl font-semibold text-gray-900">
Persönliche Informationen
</h2>
<p className="text-sm text-gray-500">
Aktualisieren Sie Ihre persönlichen Daten
</p>
</div>
</div>
</div>
<div className="space-y-6">
{/* Name Fields */}
<div className="grid gap-4 sm:grid-cols-2">
<ModernInput
label="Vorname"
value={profile.firstName}
onChange={e => handleInputChange('firstName', e)}
placeholder="Ihr Vorname"
required
/>
<ModernInput
label="Nachname"
value={profile.lastName}
onChange={e => handleInputChange('lastName', e)}
placeholder="Ihr Nachname"
required
/>
</div>
{/* Contact Information */}
<div className="grid gap-4 sm:grid-cols-2">
<ModernInput
label="E-Mail-Adresse"
type="email"
value={profile.email}
onChange={e => handleInputChange('email', e)}
placeholder="ihre.email@domain.de"
required
/>
<ModernInput
label="Telefonnummer"
type="tel"
value={profile.phone}
onChange={e => handleInputChange('phone', e)}
placeholder="+49 123 456 789"
/>
</div>
{/* Address Fields */}
<div className="space-y-4">
<h3 className="text-lg font-medium text-gray-900">Adresse</h3>
<ModernInput
label="Straße und Hausnummer"
value={profile.street}
onChange={e => handleInputChange('street', e)}
placeholder="Musterstraße 123"
/>
<div className="grid gap-4 sm:grid-cols-3">
<ModernInput
label="Postleitzahl"
value={profile.zipCode}
onChange={e => handleInputChange('zipCode', e)}
placeholder="12345"
maxLength={5}
/>
<div className="sm:col-span-2">
<ModernInput
label="Stadt"
value={profile.city}
onChange={e => handleInputChange('city', e)}
placeholder="Berlin"
/>
</div>
</div>
<ModernInput
label="Land"
value={profile.country}
onChange={e => handleInputChange('country', e)}
placeholder="Deutschland"
/>
</div>
{/* Save Button */}
<div className="flex justify-end pt-6">
<button
onClick={handleSave}
disabled={isSaving}
className="inline-flex items-center gap-2 rounded-lg bg-primary-600 px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
>
<Save className="h-4 w-4" />
{isSaving ? 'Wird gespeichert...' : 'Änderungen speichern'}
</button>
</div>
</div>
</ModernCard>
</div>
</main>
</div>
);
}

View File

@@ -0,0 +1,317 @@
/**
* Login Form Component - Professional Authentication
* Professional Resume Builder - Enhanced with modern authentication standards
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
'use client';
import React, { useState } from 'react';
import { ModernCard } from '@/components/ui/ModernCard';
import { ModernInput } from '@/components/ui/ModernInput';
import {
Mail,
Lock,
Eye,
EyeOff,
User,
ArrowRight,
Shield,
Check,
} from 'lucide-react';
/**
* Props interface for LoginForm component
*/
interface LoginFormProps {
onLogin?: (credentials: LoginCredentials) => void;
onForgotPassword?: () => void;
onSignUp?: () => void;
}
/**
* Login credentials interface
*/
interface LoginCredentials {
email: string;
password: string;
rememberMe: boolean;
}
/**
* LoginForm Component - Professional Authentication Interface
*
* Beautiful and modern login form component with German localization
* and professional branding. Features secure authentication with
* modern UX patterns and accessibility standards.
*
* Key Features:
* - German localization for professional German market
* - David Valera Melendez branding and logo
* - Modern authentication UI with validation
* - Password visibility toggle
* - Remember me functionality
* - Professional color scheme matching CV design
* - Responsive design for all devices
*
* @param props - Component properties
* @param props.onLogin - Callback function for login submission
* @param props.onForgotPassword - Callback for forgotten password
* @param props.onSignUp - Callback for new user registration
* @returns Professional login form with German localization
*/
export default function LoginForm({
onLogin,
onForgotPassword,
onSignUp,
}: LoginFormProps) {
/**
* Form state management
*/
const [credentials, setCredentials] = useState<LoginCredentials>({
email: '',
password: '',
rememberMe: false,
});
const [showPassword, setShowPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [errors, setErrors] = useState<Partial<LoginCredentials>>({});
/**
* Handles input field changes and updates the credentials state
*
* @param field - The field name to update
* @param value - The new value for the field
*/
const handleChange = (
field: keyof LoginCredentials,
value: string | boolean
) => {
setCredentials(prev => ({ ...prev, [field]: value }));
// Clear error when user starts typing
if (errors[field as keyof typeof errors]) {
setErrors(prev => ({ ...prev, [field]: undefined }));
}
};
/**
* Validates form inputs
*
* @returns Boolean indicating if form is valid
*/
const validateForm = (): boolean => {
const newErrors: Partial<LoginCredentials> = {};
if (!credentials.email) {
newErrors.email = 'E-Mail-Adresse ist erforderlich';
} else if (!/\S+@\S+\.\S+/.test(credentials.email)) {
newErrors.email = 'Bitte geben Sie eine gültige E-Mail-Adresse ein';
}
if (!credentials.password) {
newErrors.password = 'Passwort ist erforderlich';
} else if (credentials.password.length < 6) {
newErrors.password = 'Passwort muss mindestens 6 Zeichen lang sein';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
/**
* Handles form submission
*
* @param event - Form submission event
*/
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
if (!validateForm()) return;
setIsLoading(true);
try {
// Simulate API call delay
await new Promise(resolve => setTimeout(resolve, 1000));
if (onLogin) {
onLogin(credentials);
}
} catch (error) {
console.error('Login failed:', error);
} finally {
setIsLoading(false);
}
};
/**
* Toggles password visibility
*/
const togglePasswordVisibility = () => {
setShowPassword(prev => !prev);
};
return (
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-primary-50 via-white to-primary-100 px-4 py-8">
<div className="w-full max-w-md">
{/* Brand Header */}
<div className="mb-8 text-center">
<div className="mb-4 flex justify-center"></div>
<h1 className="mb-2 text-2xl font-bold tracking-wide text-primary-700">
David Valera Melendez
</h1>
<p className="text-sm font-medium text-gray-600">
Professional Resume Builder
</p>
</div>
{/* Login Form */}
<ModernCard variant="elevated" padding="lg">
<div className="mb-6">
<div className="mb-4 flex items-center justify-center">
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-gradient-to-br from-primary-100 to-primary-200 shadow-sm">
<Lock className="h-6 w-6 text-primary-600" />
</div>
</div>
<h2 className="mb-2 text-center text-xl font-semibold text-text-primary">
Willkommen zurück
</h2>
<p className="text-center text-sm text-text-secondary">
Melden Sie sich in Ihrem Konto an, um fortzufahren
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
{/* Email Input */}
<div>
<ModernInput
label="E-Mail-Adresse"
type="email"
value={credentials.email}
onChange={e => handleChange('email', e.target.value)}
placeholder="ihre.email@beispiel.de"
leftIcon={<Mail className="h-4 w-4" />}
variant="filled"
required
error={errors.email}
/>
</div>
{/* Password Input */}
<div>
<div className="relative">
<ModernInput
label="Passwort"
type={showPassword ? 'text' : 'password'}
value={credentials.password}
onChange={e => handleChange('password', e.target.value)}
placeholder="Ihr sicheres Passwort"
leftIcon={<Lock className="h-4 w-4" />}
variant="filled"
required
error={errors.password}
/>
<button
type="button"
onClick={togglePasswordVisibility}
className="absolute right-3 top-8 rounded p-1 text-gray-400 hover:text-gray-600 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
>
{showPassword ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</button>
</div>
</div>
{/* Remember Me & Forgot Password */}
<div className="flex items-center justify-between">
<label className="flex cursor-pointer items-center gap-2">
<input
type="checkbox"
checked={credentials.rememberMe}
onChange={e => handleChange('rememberMe', e.target.checked)}
className="h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 focus:ring-offset-2"
/>
<span className="text-sm text-text-secondary">
Angemeldet bleiben
</span>
</label>
<button
type="button"
onClick={onForgotPassword}
className="text-sm font-medium text-primary-600 hover:text-primary-700 focus:underline focus:outline-none"
>
Passwort vergessen?
</button>
</div>
{/* Submit Button */}
<button
type="submit"
disabled={isLoading}
className={`group relative flex w-full items-center justify-center gap-2 rounded-lg px-4 py-3 text-sm font-semibold text-white shadow-sm transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 ${
isLoading
? 'cursor-not-allowed bg-gray-400'
: 'bg-primary-600 hover:bg-primary-700 hover:shadow-md active:scale-[0.98]'
} `}
>
{isLoading ? (
<>
<div className="h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" />
<span>Anmeldung läuft...</span>
</>
) : (
<>
<span>Anmelden</span>
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-1" />
</>
)}
</button>
</form>
{/* Sign Up Link */}
<div className="mt-6 text-center">
<p className="text-sm text-text-secondary">
Noch kein Konto?{' '}
<button
onClick={onSignUp}
className="font-medium text-primary-600 hover:text-primary-700 focus:underline focus:outline-none"
>
Jetzt registrieren
</button>
</p>
</div>
{/* Security Features */}
<div className="mt-6 border-t border-gray-200 pt-4">
<div className="flex items-center justify-center gap-4 text-xs text-gray-500">
<div className="flex items-center gap-1">
<Shield className="h-3 w-3" />
<span>SSL-verschlüsselt</span>
</div>
<div className="flex items-center gap-1">
<Check className="h-3 w-3" />
<span>DSGVO-konform</span>
</div>
</div>
</div>
</ModernCard>
{/* Footer */}
<div className="mt-8 text-center text-xs text-gray-500">
<p>© 2025 David Valera Melendez. Alle Rechte vorbehalten.</p>
<p className="mt-1">
Professional Resume Builder - Made in Germany 🇩🇪
</p>
</div>
</div>
</div>
);
}

29
src/components/index.ts Normal file
View File

@@ -0,0 +1,29 @@
/**
* Components Barrel Export
* Professional Resume Builder - Component Library Entry Point
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
// UI Components (Reusable Design System)
export * from './ui';
// Layout Components
export { default as Header } from './layout/Header';
// Navigation Components
export { default as HorizontalStepper } from './navigation/HorizontalStepper';
export { default as StepperNavigation } from './navigation/StepperNavigation';
// Resume Components
export { default as ResumeBuilder } from './resume/ResumeBuilder';
export { default as ResumePreview } from './resume/ResumePreview';
export { default as StepContentRenderer } from './resume/StepContentRenderer';
// Form Components
export { default as PersonalInfoForm } from './resume/forms/PersonalInfoForm';
export { default as ExperienceForm } from './resume/forms/ExperienceForm';
export { default as EducationForm } from './resume/forms/EducationForm';
export { default as SkillsForm } from './resume/forms/SkillsForm';

View File

@@ -0,0 +1,111 @@
/**
* Header Component
* Professional Resume Builder
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
'use client';
import { ResumeData } from '@/types/resume';
interface HeaderProps {
activeView: 'builder' | 'preview';
onViewChange: (view: 'builder' | 'preview') => void;
resumeData: ResumeData;
}
/**
* Header Component - Professional navigation and branding
*
* Provides the main application header with logo, view toggle controls,
* and PDF export functionality. Responsive design with mobile-optimized UI.
*
* @param props - Component properties
* @param props.activeView - Currently active view mode ('builder' or 'preview')
* @param props.onViewChange - Callback to handle view mode changes
* @param props.resumeData - Complete resume data for export functionality
* @returns Professional header component with navigation controls
*/
export default function Header({
activeView,
onViewChange,
resumeData,
}: HeaderProps) {
/**
* Handles PDF export functionality
* Initiates the PDF generation process with current resume data
*/
const handleExportPDF = () => {
// PDF export implementation will be added here
const exportData = {
timestamp: new Date().toISOString(),
resumeData,
format: 'PDF',
version: '1.0',
};
// Future: Implement PDF export service integration
};
return (
<header className="border-b border-secondary-200 bg-white shadow-soft">
<div className="container-custom">
<div className="flex items-center justify-between py-4">
{/* Logo & Branding */}
<div className="flex items-center space-x-3">
<div className="bg-gradient-primary flex h-10 w-10 items-center justify-center rounded-lg">
<span className="text-lg font-bold text-white">DV</span>
</div>
<div>
<h1 className="text-xl font-bold text-secondary-900">
Resume Builder
</h1>
<p className="text-xs text-secondary-600">
by David Valera Melendez
</p>
</div>
</div>
{/* View Toggle (Mobile) */}
<div className="flex items-center space-x-2 lg:hidden">
<button
onClick={() => onViewChange('builder')}
className={`rounded-md px-3 py-1 text-sm font-medium transition-colors ${
activeView === 'builder'
? 'bg-primary-600 text-white'
: 'bg-secondary-100 text-secondary-700 hover:bg-secondary-200'
}`}
>
Builder
</button>
<button
onClick={() => onViewChange('preview')}
className={`rounded-md px-3 py-1 text-sm font-medium transition-colors ${
activeView === 'preview'
? 'bg-primary-600 text-white'
: 'bg-secondary-100 text-secondary-700 hover:bg-secondary-200'
}`}
>
Preview
</button>
</div>
{/* Actions */}
<div className="hidden items-center space-x-4 lg:flex">
<button onClick={handleExportPDF} className="btn-primary">
Export PDF
</button>
<div className="text-right">
<p className="text-sm text-secondary-600">Made in Germany 🇩🇪</p>
<p className="text-xs text-secondary-500">
david@valera-melendez.de
</p>
</div>
</div>
</div>
</div>
</header>
);
}

View File

@@ -0,0 +1,56 @@
/**
* Main Layout Component - Application Layout
* Resume Builder Application
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
'use client';
import React from 'react';
import Navigation from './Navigation';
/**
* Layout props interface
*/
interface LayoutProps {
children: React.ReactNode;
}
/**
* Layout Component - Main Application Layout
*
* Standard layout component that provides consistent structure across the application.
* Includes navigation and main content area with responsive design.
*
* Key Features:
* - Responsive design for all screen sizes
* - Integrated navigation system
* - Clean content area
* - Standard layout patterns
*
* @param props - Component properties
* @param props.children - Page content to be rendered
* @returns Main application layout with navigation
*/
export default function Layout({ children }: LayoutProps) {
return (
<div className="min-h-screen bg-gray-50">
{/* Navigation */}
<Navigation />
{/* Main Content */}
<div className="lg:pl-72">
<div className="min-h-screen">
<main className="py-6">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
{children}
</div>
</main>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,376 @@
/**
* Navigation Component - Main App Navigation
* Resume Builder Application
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
'use client';
import React, { useState } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import {
User,
FileText,
Settings,
LogOut,
Menu,
X,
Home,
Briefcase,
GraduationCap,
Award,
Languages,
Eye,
Download,
Shield,
} from 'lucide-react';
/**
* Navigation item interface
*/
interface NavItem {
href: string;
label: string;
icon: React.ReactNode;
description?: string;
badge?: string;
}
/**
* Navigation Component - Main Application Navigation
*
* Standard navigation component with German localization and responsive design.
* Features clean routing structure and user-friendly interface.
*
* Key Features:
* - German localization for European market
* - Responsive mobile navigation
* - Active route highlighting
* - Clean user interface
* - Standard navigation patterns
*
* @returns Main navigation component with routing
*/
export default function Navigation() {
const pathname = usePathname();
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
/**
* Main navigation items
*/
const navItems: NavItem[] = [
{
href: '/',
label: 'Übersicht',
icon: <Home className="h-5 w-5" />,
description: 'Übersicht und Start',
},
{
href: '/personal-info',
label: 'Persönliche Daten',
icon: <User className="h-5 w-5" />,
description: 'Name, Kontakt, Adresse',
},
{
href: '/experience',
label: 'Berufserfahrung',
icon: <Briefcase className="h-5 w-5" />,
description: 'Arbeitsstellen und Erfolge',
},
{
href: '/education',
label: 'Bildung',
icon: <GraduationCap className="h-5 w-5" />,
description: 'Ausbildung und Studium',
},
{
href: '/skills',
label: 'Fähigkeiten',
icon: <Award className="h-5 w-5" />,
description: 'Technische und Soft Skills',
},
{
href: '/languages',
label: 'Sprachen',
icon: <Languages className="h-5 w-5" />,
description: 'Sprachkenntnisse',
},
{
href: '/preview',
label: 'Vorschau',
icon: <Eye className="h-5 w-5" />,
description: 'Lebenslauf anzeigen',
badge: 'Live',
},
];
/**
* Secondary navigation items
*/
const secondaryItems: NavItem[] = [
{
href: '/export',
label: 'Export PDF',
icon: <Download className="h-5 w-5" />,
description: 'Lebenslauf herunterladen',
},
{
href: '/settings',
label: 'Einstellungen',
icon: <Settings className="h-5 w-5" />,
description: 'App-Konfiguration',
},
];
/**
* Checks if a route is currently active
*/
const isActive = (href: string): boolean => {
if (href === '/') {
return pathname === '/';
}
return pathname.startsWith(href);
};
/**
* Handles mobile menu toggle
*/
const toggleMobileMenu = () => {
setIsMobileMenuOpen(!isMobileMenuOpen);
};
/**
* Handles logout action
*/
const handleLogout = () => {
// Implement logout logic
console.log('Logout clicked');
window.location.href = '/login';
};
return (
<>
{/* Desktop Navigation Sidebar */}
<div className="hidden lg:fixed lg:inset-y-0 lg:flex lg:w-72 lg:flex-col">
<div className="flex flex-grow flex-col border-r border-gray-200 bg-white shadow-xl">
{/* Brand Header */}
<div className="flex items-center border-b border-gray-200 px-6 py-6">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-primary-600 to-primary-700 shadow-sm">
<FileText className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-lg font-bold text-primary-700">
David Valera Melendez
</h1>
<p className="text-xs text-gray-500">Resume Builder</p>
</div>
</div>
</div>
{/* Navigation Menu */}
<nav className="flex-1 space-y-1 overflow-y-auto px-4 py-6">
{navItems.map(item => {
const active = isActive(item.href);
return (
<Link
key={item.href}
href={item.href as any}
className={`group flex items-center rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-200 ${
active
? 'border border-primary-200 bg-primary-50 text-primary-700'
: 'text-gray-700 hover:bg-gray-50 hover:text-primary-600'
} `}
>
<div
className={`mr-3 flex h-6 w-6 items-center justify-center transition-colors ${active ? 'text-primary-600' : 'text-gray-400 group-hover:text-primary-500'} `}
>
{item.icon}
</div>
<div className="flex-1">
<div className="flex items-center justify-between">
<span>{item.label}</span>
{item.badge && (
<span className="ml-2 rounded-full bg-primary-100 px-2 py-0.5 text-xs font-semibold text-primary-700">
{item.badge}
</span>
)}
</div>
{item.description && (
<p className="mt-0.5 text-xs text-gray-500">
{item.description}
</p>
)}
</div>
</Link>
);
})}
{/* Divider */}
<div className="my-4 border-t border-gray-200"></div>
{/* Secondary Items */}
{secondaryItems.map(item => {
const active = isActive(item.href);
return (
<Link
key={item.href}
href={item.href as any}
className={`group flex items-center rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-200 ${
active
? 'border border-primary-200 bg-primary-50 text-primary-700'
: 'text-gray-700 hover:bg-gray-50 hover:text-primary-600'
} `}
>
<div
className={`mr-3 flex h-6 w-6 items-center justify-center transition-colors ${active ? 'text-primary-600' : 'text-gray-400 group-hover:text-primary-500'} `}
>
{item.icon}
</div>
<div className="flex-1">
<span>{item.label}</span>
{item.description && (
<p className="mt-0.5 text-xs text-gray-500">
{item.description}
</p>
)}
</div>
</Link>
);
})}
</nav>
{/* User Profile & Logout */}
<div className="flex-shrink-0 border-t border-gray-200 p-4">
<div className="mb-3 flex items-center gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-gradient-to-br from-primary-100 to-primary-200">
<User className="h-4 w-4 text-primary-600" />
</div>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium text-gray-900">
David Valera
</p>
<p className="text-xs text-gray-500">
david@valera-melendez.de
</p>
</div>
</div>
<button
onClick={handleLogout}
className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 hover:text-red-600"
>
<LogOut className="h-4 w-4" />
<span>Abmelden</span>
</button>
</div>
{/* Footer */}
<div className="flex-shrink-0 border-t border-gray-200 px-4 py-3">
<div className="flex items-center justify-center gap-2 text-xs text-gray-500">
<Shield className="h-3 w-3" />
<span>Made in Germany 🇩🇪</span>
</div>
</div>
</div>
</div>
{/* Mobile Navigation */}
<div className="lg:hidden">
{/* Mobile Header */}
<div className="border-b border-gray-200 bg-white px-4 py-3 shadow-sm">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-primary-600 to-primary-700 shadow-sm">
<FileText className="h-4 w-4 text-white" />
</div>
<div>
<h1 className="text-sm font-bold text-primary-700">
David Valera Melendez
</h1>
</div>
</div>
<button
onClick={toggleMobileMenu}
className="rounded-lg p-2 transition-colors hover:bg-gray-100"
>
{isMobileMenuOpen ? (
<X className="h-6 w-6 text-gray-600" />
) : (
<Menu className="h-6 w-6 text-gray-600" />
)}
</button>
</div>
</div>
{/* Mobile Menu Overlay */}
{isMobileMenuOpen && (
<div
className="fixed inset-0 z-50 bg-black bg-opacity-50"
onClick={toggleMobileMenu}
>
<div
className="fixed inset-y-0 left-0 w-72 bg-white shadow-xl"
onClick={e => e.stopPropagation()}
>
<div className="p-4">
<div className="mb-6 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-primary-600 to-primary-700 shadow-sm">
<FileText className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-lg font-bold text-primary-700">
David Valera Melendez
</h1>
<p className="text-xs text-gray-500">Resume Builder</p>
</div>
</div>
<button
onClick={toggleMobileMenu}
className="rounded-lg p-2 transition-colors hover:bg-gray-100"
>
<X className="h-6 w-6 text-gray-600" />
</button>
</div>
<nav className="space-y-1">
{navItems.map(item => {
const active = isActive(item.href);
return (
<Link
key={item.href}
href={item.href as any}
onClick={toggleMobileMenu}
className={`group flex items-center rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-200 ${
active
? 'border border-primary-200 bg-primary-50 text-primary-700'
: 'text-gray-700 hover:bg-gray-50 hover:text-primary-600'
} `}
>
<div
className={`mr-3 flex h-6 w-6 items-center justify-center transition-colors ${active ? 'text-primary-600' : 'text-gray-400 group-hover:text-primary-500'} `}
>
{item.icon}
</div>
<span>{item.label}</span>
{item.badge && (
<span className="ml-auto rounded-full bg-primary-100 px-2 py-0.5 text-xs font-semibold text-primary-700">
{item.badge}
</span>
)}
</Link>
);
})}
</nav>
</div>
</div>
</div>
)}
</div>
</>
);
}

View File

@@ -0,0 +1,171 @@
/**
* Horizontal Stepper Header Component
* Professional Resume Builder
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
'use client';
/**
* Step configuration interface for stepper component
*/
export interface Step {
id: string;
title: string;
description: string;
icon: string;
completed: boolean;
}
/**
* Props interface for HorizontalStepper component
*/
interface HorizontalStepperProps {
steps: Step[];
currentStep: string;
onStepChange: (stepId: string) => void;
}
/**
* HorizontalStepper Component - Professional multi-step navigation
*
* Displays a horizontal progress stepper with visual indicators for completion status.
* Features responsive design with mobile-optimized layout and accessibility support.
*
* @param props - Component properties
* @param props.steps - Array of step configurations with completion status
* @param props.currentStep - ID of the currently active step
* @param props.onStepChange - Callback function when user clicks on a step
* @returns Horizontal stepper navigation component with progress tracking
*/
export default function HorizontalStepper({
steps,
currentStep,
onStepChange,
}: HorizontalStepperProps) {
/**
* Calculates the current step index for progress display
* @returns The zero-based index of the current step
*/
const currentStepIndex = steps.findIndex(step => step.id === currentStep);
return (
<header className="bg-white">
<div className="px-4 py-4">
{/* Header Title - Compact */}
<div className="mb-4 flex items-center justify-between">
<div>
<h1 className="text-lg font-bold text-secondary-900">
Resume Builder
</h1>
<p className="text-xs text-secondary-600">Made in Germany 🇩🇪</p>
</div>
<div className="rounded bg-secondary-50 px-2 py-1 text-xs text-secondary-500">
Step {currentStepIndex + 1} of {steps.length}
</div>
</div>
{/* Horizontal Progress Stepper */}
<div className="relative">
{/* Progress Line */}
<div className="absolute left-0 right-0 top-4 h-0.5 bg-secondary-200">
<div
className="h-full bg-primary-500 transition-all duration-500"
style={{
width: `${(currentStepIndex / (steps.length - 1)) * 100}%`,
}}
/>
</div>
{/* Steps */}
<div className="relative flex justify-between">
{steps.map((step, index) => {
const isActive = step.id === currentStep;
const isCompleted = step.completed;
const isPast = index < currentStepIndex;
const isFuture = index > currentStepIndex && !isCompleted;
return (
<button
key={step.id}
onClick={() => onStepChange(step.id)}
disabled={isFuture}
className={`flex min-w-0 flex-col items-center transition-all duration-300 ${
isFuture
? 'cursor-not-allowed'
: 'hover:scale-105 hover:transform'
}`}
>
{/* Step Circle - Ultra Compact */}
<div
className={`mb-1 flex h-8 w-8 items-center justify-center rounded-full text-xs font-semibold transition-all duration-300 ${
isCompleted
? 'bg-primary-500 text-white shadow-md'
: isActive
? 'border-2 border-primary-500 bg-primary-100 text-primary-700 shadow-sm'
: isFuture
? 'border border-secondary-200 bg-secondary-100 text-secondary-400'
: 'border border-primary-300 bg-primary-200 text-primary-600'
}`}
>
{isCompleted ? (
<span className="text-xs font-bold text-white"></span>
) : (
<span className="text-sm">{step.icon}</span>
)}
</div>
{/* Step Label - Title Only */}
<div className="text-center">
<div
className={`text-xs font-medium transition-colors ${
isActive
? 'text-primary-700'
: isCompleted
? 'text-secondary-900'
: isFuture
? 'text-secondary-400'
: 'text-secondary-600'
}`}
>
{step.title}
</div>
</div>
{/* Active Pulse Effect */}
{isActive && (
<div className="absolute -left-1 -top-1 h-14 w-14 animate-ping rounded-full bg-primary-200 opacity-20" />
)}
</button>
);
})}
</div>
</div>
{/* Overall Progress Bar */}
<div className="mt-6">
<div className="mb-2 flex items-center justify-between">
<span className="text-sm text-secondary-600">Overall Progress</span>
<span className="text-sm font-semibold text-secondary-900">
{Math.round(
(steps.filter(s => s.completed).length / steps.length) * 100
)}
%
</span>
</div>
<div className="h-2 w-full rounded-full bg-secondary-200">
<div
className="h-2 rounded-full bg-gradient-to-r from-primary-500 to-primary-600 transition-all duration-500"
style={{
width: `${(steps.filter(s => s.completed).length / steps.length) * 100}%`,
}}
/>
</div>
</div>
</div>
</header>
);
}

View File

@@ -0,0 +1,377 @@
/**
* Navigation Component - Main App Navigation
* Resume Builder Application
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
'use client';
import React, { useState } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import {
User,
FileText,
Settings,
LogOut,
Menu,
X,
Home,
Briefcase,
GraduationCap,
Award,
Languages,
Eye,
Download,
Shield,
} from 'lucide-react';
/**
* Navigation item interface
*/
interface NavItem {
href: string;
label: string;
icon: React.ReactNode;
description?: string;
badge?: string;
}
/**
* Navigation Component - Main Application Navigation
*
* Standard navigation component with German localization and responsive design.
* Features clean routing structure and user-friendly interface.
*
* Key Features:
* - German localization for European market
* - Responsive mobile navigation
* - Active route highlighting
* - Clean user interface
* - Standard navigation patterns
*
* @returns Main navigation component with routing
*/
export default function Navigation() {
const pathname = usePathname();
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
/**
* Main navigation items
*/
const navItems: NavItem[] = [
{
href: '/',
label: 'Dashboard',
icon: <Home className="h-5 w-5" />,
description: 'Übersicht und Start',
},
{
href: '/personal-info',
label: 'Persönliche Daten',
icon: <User className="h-5 w-5" />,
description: 'Name, Kontakt, Adresse',
},
{
href: '/experience',
label: 'Berufserfahrung',
icon: <Briefcase className="h-5 w-5" />,
description: 'Arbeitsstellen und Erfolge',
},
{
href: '/education',
label: 'Bildung',
icon: <GraduationCap className="h-5 w-5" />,
description: 'Ausbildung und Studium',
},
{
href: '/skills',
label: 'Fähigkeiten',
icon: <Award className="h-5 w-5" />,
description: 'Technische und Soft Skills',
},
{
href: '/languages',
label: 'Sprachen',
icon: <Languages className="h-5 w-5" />,
description: 'Sprachkenntnisse',
},
{
href: '/preview',
label: 'Vorschau',
icon: <Eye className="h-5 w-5" />,
description: 'Lebenslauf anzeigen',
badge: 'Live',
},
];
/**
* Secondary navigation items
*/
const secondaryItems: NavItem[] = [
{
href: '/export',
label: 'Export PDF',
icon: <Download className="h-5 w-5" />,
description: 'Lebenslauf herunterladen',
},
{
href: '/settings',
label: 'Einstellungen',
icon: <Settings className="h-5 w-5" />,
description: 'App-Konfiguration',
},
];
/**
* Checks if a route is currently active
*/
const isActive = (href: string): boolean => {
if (href === '/') {
return pathname === '/';
}
return pathname.startsWith(href);
};
/**
* Handles mobile menu toggle
*/
const toggleMobileMenu = () => {
setIsMobileMenuOpen(!isMobileMenuOpen);
};
/**
* Handles logout action
*/
const handleLogout = () => {
// Here you would implement logout logic
console.log('Logout clicked');
// For now, redirect to login
window.location.href = '/login';
};
return (
<>
{/* Desktop Navigation Sidebar */}
<div className="hidden lg:fixed lg:inset-y-0 lg:flex lg:w-72 lg:flex-col">
<div className="flex flex-grow flex-col border-r border-gray-200 bg-white shadow-xl">
{/* Brand Header */}
<div className="flex items-center border-b border-gray-200 px-6 py-6">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-primary-600 to-primary-700 shadow-sm">
<FileText className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-lg font-bold text-primary-700">
David Valera Melendez
</h1>
<p className="text-xs text-gray-500">Resume Builder</p>
</div>
</div>
</div>
{/* Navigation Menu */}
<nav className="flex-1 space-y-1 overflow-y-auto px-4 py-6">
{navItems.map(item => {
const active = isActive(item.href);
return (
<Link
key={item.href}
href={item.href as any}
className={`group flex items-center rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-200 ${
active
? 'border border-primary-200 bg-primary-50 text-primary-700'
: 'text-gray-700 hover:bg-gray-50 hover:text-primary-600'
} `}
>
<div
className={`mr-3 flex h-6 w-6 items-center justify-center transition-colors ${active ? 'text-primary-600' : 'text-gray-400 group-hover:text-primary-500'} `}
>
{item.icon}
</div>
<div className="flex-1">
<div className="flex items-center justify-between">
<span>{item.label}</span>
{item.badge && (
<span className="ml-2 rounded-full bg-primary-100 px-2 py-0.5 text-xs font-semibold text-primary-700">
{item.badge}
</span>
)}
</div>
{item.description && (
<p className="mt-0.5 text-xs text-gray-500">
{item.description}
</p>
)}
</div>
</Link>
);
})}
{/* Divider */}
<div className="my-4 border-t border-gray-200"></div>
{/* Secondary Items */}
{secondaryItems.map(item => {
const active = isActive(item.href);
return (
<Link
key={item.href}
href={item.href as any}
className={`group flex items-center rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-200 ${
active
? 'border border-primary-200 bg-primary-50 text-primary-700'
: 'text-gray-700 hover:bg-gray-50 hover:text-primary-600'
} `}
>
<div
className={`mr-3 flex h-6 w-6 items-center justify-center transition-colors ${active ? 'text-primary-600' : 'text-gray-400 group-hover:text-primary-500'} `}
>
{item.icon}
</div>
<div className="flex-1">
<span>{item.label}</span>
{item.description && (
<p className="mt-0.5 text-xs text-gray-500">
{item.description}
</p>
)}
</div>
</Link>
);
})}
</nav>
{/* User Profile & Logout */}
<div className="flex-shrink-0 border-t border-gray-200 p-4">
<div className="mb-3 flex items-center gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-gradient-to-br from-primary-100 to-primary-200">
<User className="h-4 w-4 text-primary-600" />
</div>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium text-gray-900">
David Valera
</p>
<p className="text-xs text-gray-500">
david@valera-melendez.de
</p>
</div>
</div>
<button
onClick={handleLogout}
className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 hover:text-red-600"
>
<LogOut className="h-4 w-4" />
<span>Abmelden</span>
</button>
</div>
{/* Footer */}
<div className="flex-shrink-0 border-t border-gray-200 px-4 py-3">
<div className="flex items-center justify-center gap-2 text-xs text-gray-500">
<Shield className="h-3 w-3" />
<span>Made in Germany 🇩🇪</span>
</div>
</div>
</div>
</div>
{/* Mobile Navigation */}
<div className="lg:hidden">
{/* Mobile Header */}
<div className="border-b border-gray-200 bg-white px-4 py-3 shadow-sm">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-primary-600 to-primary-700 shadow-sm">
<FileText className="h-4 w-4 text-white" />
</div>
<div>
<h1 className="text-sm font-bold text-primary-700">
David Valera Melendez
</h1>
</div>
</div>
<button
onClick={toggleMobileMenu}
className="rounded-lg p-2 transition-colors hover:bg-gray-100"
>
{isMobileMenuOpen ? (
<X className="h-6 w-6 text-gray-600" />
) : (
<Menu className="h-6 w-6 text-gray-600" />
)}
</button>
</div>
</div>
{/* Mobile Menu Overlay */}
{isMobileMenuOpen && (
<div
className="fixed inset-0 z-50 bg-black bg-opacity-50"
onClick={toggleMobileMenu}
>
<div
className="fixed inset-y-0 left-0 w-72 bg-white shadow-xl"
onClick={e => e.stopPropagation()}
>
<div className="p-4">
<div className="mb-6 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-primary-600 to-primary-700 shadow-sm">
<FileText className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-lg font-bold text-primary-700">
David Valera Melendez
</h1>
<p className="text-xs text-gray-500">Resume Builder</p>
</div>
</div>
<button
onClick={toggleMobileMenu}
className="rounded-lg p-2 transition-colors hover:bg-gray-100"
>
<X className="h-6 w-6 text-gray-600" />
</button>
</div>
<nav className="space-y-1">
{navItems.map(item => {
const active = isActive(item.href);
return (
<Link
key={item.href}
href={item.href as any}
onClick={toggleMobileMenu}
className={`group flex items-center rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-200 ${
active
? 'border border-primary-200 bg-primary-50 text-primary-700'
: 'text-gray-700 hover:bg-gray-50 hover:text-primary-600'
} `}
>
<div
className={`mr-3 flex h-6 w-6 items-center justify-center transition-colors ${active ? 'text-primary-600' : 'text-gray-400 group-hover:text-primary-500'} `}
>
{item.icon}
</div>
<span>{item.label}</span>
{item.badge && (
<span className="ml-auto rounded-full bg-primary-100 px-2 py-0.5 text-xs font-semibold text-primary-700">
{item.badge}
</span>
)}
</Link>
);
})}
</nav>
</div>
</div>
</div>
)}
</div>
</>
);
}

View File

@@ -0,0 +1,147 @@
/**
* Step Navigation Component
* Professional Resume Builder - Enhanced with resume-example.com design patterns
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
'use client';
import React from 'react';
import { ModernButton } from '@/components/ui/ModernButton';
interface StepNavigationProps {
onPrevious?: () => void;
onNext?: () => void;
onSave?: () => void;
isFirstStep?: boolean;
isLastStep?: boolean;
canProceed?: boolean;
isLoading?: boolean;
className?: string;
}
export const StepNavigation: React.FC<StepNavigationProps> = ({
onPrevious,
onNext,
onSave,
isFirstStep = false,
isLastStep = false,
canProceed = true,
isLoading = false,
className = '',
}) => {
return (
<div
className={`border-border-primary flex items-center justify-between border-t pt-6 ${className}`}
>
<div>
{!isFirstStep && onPrevious && (
<ModernButton
variant="outline"
size="lg"
onClick={onPrevious}
disabled={isLoading}
className="min-w-32"
>
<svg
className="mr-2 h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
Previous
</ModernButton>
)}
</div>
<div className="flex items-center gap-3">
{onSave && (
<ModernButton
variant="secondary"
size="lg"
onClick={onSave}
disabled={isLoading}
className="min-w-24"
>
<svg
className="mr-2 h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3-3m0 0l-3 3m3-3v12"
/>
</svg>
Save
</ModernButton>
)}
{isLastStep
? onNext && (
<ModernButton
variant="primary"
size="lg"
onClick={onNext}
disabled={!canProceed || isLoading}
loading={isLoading}
className="min-w-32"
>
<svg
className="mr-2 h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
Complete
</ModernButton>
)
: onNext && (
<ModernButton
variant="primary"
size="lg"
onClick={onNext}
disabled={!canProceed || isLoading}
loading={isLoading}
className="min-w-32"
>
Continue
<svg
className="ml-2 h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
</ModernButton>
)}
</div>
</div>
);
};

View File

@@ -0,0 +1,168 @@
/**
* Stepper Navigation Component
* Professional Resume Builder
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
'use client';
// Using Unicode checkmark instead of external icon
export interface Step {
id: string;
title: string;
description: string;
icon: string;
completed: boolean;
}
interface StepperNavigationProps {
steps: Step[];
currentStep: string;
onStepChange: (stepId: string) => void;
}
export default function StepperNavigation({
steps,
currentStep,
onStepChange,
}: StepperNavigationProps) {
const currentStepIndex = steps.findIndex(step => step.id === currentStep);
return (
<div className="mx-auto w-full max-w-md p-6">
<div className="mb-8">
<h1 className="mb-2 text-3xl font-bold text-secondary-900">
Lebenslauf-Builder
</h1>
<p className="text-secondary-600">
Erstellen Sie Ihren professionellen Lebenslauf Schritt für Schritt
</p>
<div className="mt-4 text-sm text-secondary-500">
Schritt {currentStepIndex + 1} von {steps.length}
</div>
</div>
<div className="space-y-6">
{steps.map((step, index) => {
const isActive = step.id === currentStep;
const isCompleted = step.completed;
const isPast = index < currentStepIndex;
const isFuture = index > currentStepIndex;
return (
<div key={step.id} className="relative">
{/* Connection Line */}
{index < steps.length - 1 && (
<div
className={`absolute left-6 top-12 h-16 w-0.5 ${
isPast || isCompleted
? 'bg-primary-500'
: 'bg-secondary-200'
}`}
/>
)}
{/* Step Item */}
<button
onClick={() => onStepChange(step.id)}
className={`flex w-full items-start space-x-4 rounded-lg p-3 text-left transition-all duration-300 hover:bg-secondary-50 ${
isActive ? 'border border-primary-200 bg-primary-50' : ''
}`}
disabled={isFuture && !isCompleted}
>
{/* Step Circle */}
<div
className={`flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full text-sm font-semibold transition-all duration-300 ${
isCompleted
? 'bg-primary-500 text-white'
: isActive
? 'border-2 border-primary-500 bg-primary-100 text-primary-700'
: isFuture
? 'border border-secondary-200 bg-secondary-100 text-secondary-400'
: 'bg-secondary-200 text-secondary-600'
}`}
>
{isCompleted ? (
<span className="text-white"></span>
) : (
<span className="text-xl">{step.icon}</span>
)}
</div>
{/* Step Content */}
<div className="min-w-0 flex-1">
<h3
className={`mb-1 text-base font-semibold transition-colors ${
isActive
? 'text-primary-700'
: isCompleted
? 'text-secondary-900'
: isFuture
? 'text-secondary-400'
: 'text-secondary-700'
}`}
>
{step.title}
</h3>
<p
className={`text-sm leading-relaxed ${
isActive
? 'text-primary-600'
: isCompleted
? 'text-secondary-600'
: isFuture
? 'text-secondary-400'
: 'text-secondary-500'
}`}
>
{step.description}
</p>
</div>
{/* Active Indicator */}
{isActive && (
<div className="h-2 w-2 flex-shrink-0 animate-pulse rounded-full bg-primary-500" />
)}
</button>
</div>
);
})}
</div>
{/* Progress Bar */}
<div className="mt-8">
<div className="mb-2 flex items-center justify-between">
<span className="text-sm text-secondary-600">Overall Progress</span>
<span className="text-sm font-semibold text-secondary-900">
{Math.round(
(steps.filter(s => s.completed).length / steps.length) * 100
)}
%
</span>
</div>
<div className="h-2 w-full rounded-full bg-secondary-200">
<div
className="h-2 rounded-full bg-gradient-to-r from-primary-500 to-primary-600 transition-all duration-500"
style={{
width: `${(steps.filter(s => s.completed).length / steps.length) * 100}%`,
}}
/>
</div>
</div>
{/* Footer Info */}
<div className="mt-8 border-t border-secondary-200 pt-6">
<div className="text-center">
<p className="mb-1 text-xs text-secondary-500">
Created by David Valera Melendez
</p>
<p className="text-xs text-secondary-400">Made in Germany 🇩🇪</p>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,119 @@
/**
* Resume Builder Component
* Professional Resume Builder
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
'use client';
import { useState } from 'react';
import { ResumeData } from '@/types/resume';
import PersonalInfoForm from './forms/PersonalInfoForm';
import ExperienceForm from './forms/ExperienceForm';
import EducationForm from './forms/EducationForm';
import SkillsForm from './forms/SkillsForm';
interface ResumeBuilderProps {
resumeData: ResumeData;
onDataUpdate: (data: Partial<ResumeData>) => void;
}
type Section = 'personal' | 'experience' | 'education' | 'skills' | 'languages' | 'certifications' | 'projects';
export default function ResumeBuilder({ resumeData, onDataUpdate }: ResumeBuilderProps) {
const [activeSection, setActiveSection] = useState<Section>('personal');
const sections = [
{ id: 'personal' as Section, name: 'Persönliche Daten', icon: '👤' },
{ id: 'experience' as Section, name: 'Berufserfahrung', icon: '💼' },
{ id: 'education' as Section, name: 'Ausbildung', icon: '🎓' },
{ id: 'skills' as Section, name: 'Fähigkeiten', icon: '⚡' },
{ id: 'languages' as Section, name: 'Sprachen', icon: '🌍' },
{ id: 'certifications' as Section, name: 'Zertifikate', icon: '🏆' },
{ id: 'projects' as Section, name: 'Projekte', icon: '🚀' },
];
const renderSectionContent = () => {
switch (activeSection) {
case 'personal':
return (
<PersonalInfoForm
data={resumeData.personalInfo}
onChange={(personalInfo) => onDataUpdate({ personalInfo })}
/>
);
case 'experience':
return (
<ExperienceForm
data={resumeData.experience}
onChange={(experience) => onDataUpdate({ experience })}
/>
);
case 'education':
return (
<EducationForm
data={resumeData.education}
onChange={(education) => onDataUpdate({ education })}
/>
);
case 'skills':
return (
<SkillsForm
data={resumeData.skills}
onChange={(skills) => onDataUpdate({ skills })}
/>
);
default:
return (
<div className="text-center py-12">
<div className="text-4xl mb-4">🚧</div>
<h3 className="text-lg font-semibold text-secondary-700 mb-2">
Coming Soon
</h3>
<p className="text-secondary-500">
This section is under development by David Valera Melendez
</p>
</div>
);
}
};
return (
<div className="space-y-6">
{/* Section Navigation */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-2">
{sections.map((section) => (
<button
key={section.id}
onClick={() => setActiveSection(section.id)}
className={`p-3 rounded-lg text-left transition-all duration-200 ${
activeSection === section.id
? 'bg-primary-100 border-2 border-primary-300 text-primary-800'
: 'bg-secondary-50 border border-secondary-200 text-secondary-700 hover:bg-secondary-100'
}`}
>
<div className="text-lg mb-1">{section.icon}</div>
<div className="text-sm font-medium">{section.name}</div>
</button>
))}
</div>
{/* Active Section Content */}
<div className="bg-secondary-50 rounded-lg p-6">
<div className="mb-4">
<h3 className="text-xl font-bold text-secondary-900 mb-1">
{sections.find(s => s.id === activeSection)?.name}
</h3>
<p className="text-sm text-secondary-600">
Fill in your information below
</p>
</div>
{renderSectionContent()}
</div>
</div>
);
}

View File

@@ -0,0 +1,263 @@
/**
* Resume Preview Component
* Professional Resume Builder
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
'use client';
import { ResumeData } from '@/types/resume';
/**
* Props interface for ResumePreview component
*/
interface ResumePreviewProps {
resumeData: ResumeData;
}
/**
* ResumePreview Component - Professional CV Preview with Print Support
*
* Renders a complete, professional resume preview with modern European format.
* Features optimized typography, layout, and print-friendly styling for PDF export.
*
* Key Features:
* - Professional typography with proper hierarchy
* - Modern European CV layout
* - Print-optimized styling
* - Responsive design for various screen sizes
* - ATS-friendly formatting
* - Dynamic content rendering based on available data
*
* @param props - Component properties
* @param props.resumeData - Complete resume data object to render
* @returns Professional resume preview component ready for display and print
*/
export default function ResumePreview({ resumeData }: ResumePreviewProps) {
/**
* Destructures resume data for cleaner component code
*/
const {
personalInfo,
experience,
education,
skills,
languages,
certifications,
projects,
} = resumeData;
return (
<div className="origin-center scale-90 transform">
{/* Two Column Layout with Fixed Width but Flexible Height */}
<div className="flex min-h-[600px] w-[650px] bg-white shadow-lg">
{/* Left Column - Dark Sidebar (1/3 width) */}
<div className="flex w-1/3 flex-col bg-gray-900 p-6 text-white">
{/* Profile Image Placeholder */}
<div className="mb-6 flex justify-center">
<div className="flex h-32 w-32 items-center justify-center rounded-lg border-2 border-gray-600 bg-gray-800 text-xs text-gray-400">
Profile Image
</div>
</div>
{/* Technical Skills */}
{skills.length > 0 && (
<div className="mb-8">
<h2 className="mb-4 border-b border-gray-600 pb-2 text-sm font-bold uppercase tracking-wide text-white">
Technische Fähigkeiten
</h2>
<div className="space-y-4">
{Array.from(new Set(skills.map(skill => skill.category))).map(
category => (
<div key={category}>
<h3 className="mb-2 break-words text-xs font-semibold text-gray-300">
{category}
</h3>
<p className="break-words text-xs leading-relaxed text-gray-400">
{skills
.filter(skill => skill.category === category)
.map(skill => skill.name)
.join(', ')}
</p>
</div>
)
)}
</div>
</div>
)}
{/* Languages */}
{languages && languages.length > 0 && (
<div className="mb-8">
<h2 className="mb-4 border-b border-gray-600 pb-2 text-sm font-bold uppercase tracking-wide text-white">
Sprachen
</h2>
<div className="space-y-3">
{languages.map((lang, index) => (
<div
key={index}
className="flex items-center justify-between"
>
<span className="mr-2 flex-1 break-words text-xs font-medium text-white">
{lang.name}
</span>
<span className="flex-shrink-0 rounded bg-gray-800 px-2 py-1 text-xs text-blue-400">
{lang.proficiency}
</span>
</div>
))}
</div>
</div>
)}
{/* Personal Information */}
<div className="mb-8">
<h2 className="mb-4 border-b border-gray-600 pb-2 text-sm font-bold uppercase tracking-wide text-white">
Persönliche Daten
</h2>
<div className="space-y-3">
{personalInfo.dateOfBirth && (
<div className="text-xs">
<div className="mb-1 text-gray-300">Geburtsdatum</div>
<div className="font-medium text-white">
{personalInfo.dateOfBirth}
</div>
</div>
)}
{personalInfo.nationality && (
<div className="text-xs">
<div className="mb-1 text-gray-300">Nationalität</div>
<div className="font-medium text-white">
{personalInfo.nationality}
</div>
</div>
)}
{personalInfo.maritalStatus && (
<div className="text-xs">
<div className="mb-1 text-gray-300">Familienstand</div>
<div className="font-medium text-white">
{personalInfo.maritalStatus}
</div>
</div>
)}
</div>
</div>
</div>
{/* Right Column - Main Content (2/3 width) */}
<div className="flex w-2/3 flex-col bg-white p-6">
{/* Header Section */}
<div className="mb-6 border-b-2 border-primary-600 pb-4">
<h1 className="mb-2 break-words text-xl font-bold uppercase leading-tight tracking-wide text-primary-600">
{personalInfo.firstName} {personalInfo.lastName}
</h1>
{personalInfo.jobTitle && (
<p className="mb-4 break-words text-sm font-medium text-gray-600">
{personalInfo.jobTitle}
</p>
)}
<div className="space-y-1 text-xs text-gray-500">
{personalInfo.email && (
<div className="flex items-center gap-1 break-words">
<span className="flex-shrink-0">📧</span>{' '}
<span className="break-all">{personalInfo.email}</span>
</div>
)}
{personalInfo.phone && (
<div className="flex items-center gap-1 break-words">
<span className="flex-shrink-0">📱</span>{' '}
<span>{personalInfo.phone}</span>
</div>
)}
{personalInfo.location && (
<div className="flex items-center gap-1 break-words">
<span className="flex-shrink-0">🏠</span>{' '}
<span>{personalInfo.location}</span>
</div>
)}
</div>
</div>
{/* Experience Section */}
<div className="mb-8">
<h2 className="mb-4 border-b border-gray-300 pb-2 text-sm font-bold uppercase tracking-wide text-primary-600">
Berufserfahrung
</h2>
{experience && experience.length > 0 && (
<div className="space-y-6">
{experience.map((exp, index) => (
<div key={index} className="resume-item">
<div className="mb-2 flex items-start justify-between">
<div className="mr-4 flex-1">
<h3 className="break-words text-xs font-semibold text-gray-900">
{exp.position}
</h3>
<p className="break-words text-xs font-medium text-primary-600">
{exp.company}
</p>
</div>
<div className="flex-shrink-0 text-right text-xs text-gray-500">
{exp.startDate} - {exp.endDate || 'Heute'}
</div>
</div>
{exp.description && (
<p className="mb-2 break-words text-xs text-gray-600">
{exp.description}
</p>
)}
{exp.achievements && exp.achievements.length > 0 && (
<ul className="ml-4 list-outside list-disc space-y-1 text-xs text-gray-500">
{exp.achievements.map((achievement, i) => (
<li key={i} className="break-words">
{achievement}
</li>
))}
</ul>
)}
</div>
))}
</div>
)}
</div>
{/* Education Section */}
<div className="mb-8">
<h2 className="mb-4 border-b border-gray-300 pb-2 text-sm font-bold uppercase tracking-wide text-primary-600">
Bildung
</h2>
{education && education.length > 0 && (
<div className="space-y-4">
{education.map((edu, index) => (
<div key={index} className="resume-item">
<div className="mb-2">
<h3 className="break-words text-xs font-semibold text-gray-900">
{edu.degree}
</h3>
<p className="break-words text-xs font-medium text-primary-600">
{edu.institution}
</p>
{(edu.startDate || edu.endDate) && (
<p className="text-xs text-gray-500">
{edu.startDate} - {edu.endDate}
</p>
)}
{edu.description && (
<p className="mt-1 break-words text-xs text-gray-500">
{edu.description}
</p>
)}
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,279 @@
/**
* Step Content Renderer Component
* Professional Resume Builder
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
'use client';
import PersonalInfoForm from '@/components/resume/forms/PersonalInfoForm';
import ExperienceForm from '@/components/resume/forms/ExperienceForm';
import EducationForm from '@/components/resume/forms/EducationForm';
import SkillsForm from '@/components/resume/forms/SkillsForm';
import { ResumeData } from '@/types/resume';
/**
* Props interface for StepContentRenderer component
*/
interface StepContentRendererProps {
currentStep: string;
resumeData: ResumeData;
onDataUpdate: (data: Partial<ResumeData>) => void;
onNext: () => void;
onPrevious: () => void;
isFirstStep: boolean;
isLastStep: boolean;
}
/**
* StepContentRenderer Component - Dynamic form renderer for resume steps
*
* Renders the appropriate form component based on the current step in the resume builder.
* Handles data flow between step forms and provides navigation controls with validation.
*
* @param props - Component properties
* @param props.currentStep - ID of the currently active step
* @param props.resumeData - Complete resume data object
* @param props.onDataUpdate - Callback to update resume data
* @param props.onNext - Callback to navigate to next step
* @param props.onPrevious - Callback to navigate to previous step
* @param props.isFirstStep - Boolean indicating if current step is the first
* @param props.isLastStep - Boolean indicating if current step is the last
* @returns Dynamic form component with navigation controls
*/
export default function StepContentRenderer({
currentStep,
resumeData,
onDataUpdate,
onNext,
onPrevious,
isFirstStep,
isLastStep,
}: StepContentRendererProps) {
/**
* Renders the appropriate form component based on current step
* @returns JSX element for the current step's form
*/
const renderStepContent = () => {
switch (currentStep) {
case 'contact':
return (
<PersonalInfoForm
data={resumeData.personalInfo}
onChange={personalInfo => onDataUpdate({ personalInfo })}
/>
);
case 'experience':
return (
<ExperienceForm
data={resumeData.experience}
onChange={experience => onDataUpdate({ experience })}
/>
);
case 'education':
return (
<EducationForm
data={resumeData.education}
onChange={education => onDataUpdate({ education })}
/>
);
case 'skills':
return (
<SkillsForm
data={resumeData.skills}
onChange={skills => onDataUpdate({ skills })}
/>
);
case 'about':
return (
<div className="space-y-6">
<div className="card p-6">
<h2 className="mb-4 text-xl font-semibold text-secondary-900">
Professional Summary
</h2>
<textarea
className="input min-h-[120px]"
placeholder="Write a brief professional summary about yourself..."
value={resumeData.personalInfo.summary || ''}
onChange={e =>
onDataUpdate({
personalInfo: {
...resumeData.personalInfo,
summary: e.target.value,
},
})
}
/>
</div>
<div className="card p-6">
<h3 className="mb-4 text-lg font-semibold text-secondary-900">
Additional Information
</h3>
<div className="space-y-4">
<div>
<label className="label">Languages</label>
<input
type="text"
className="input"
placeholder="English (Native), Spanish (C1), German (B2)"
value={resumeData.languages
.map(lang => `${lang.name} (${lang.proficiency})`)
.join(', ')}
onChange={e => {
const languageStrings = e.target.value
.split(',')
.map(s => s.trim());
const languages = languageStrings
.filter(s => s)
.map((langStr, index) => {
const match = langStr.match(/^(.+)\s*\((.+)\)$/);
return {
id: Date.now().toString() + index,
name: match ? match[1].trim() : langStr,
proficiency: (match ? match[2].trim() : 'B2') as
| 'A1'
| 'A2'
| 'B1'
| 'B2'
| 'C1'
| 'C2'
| 'Native',
};
});
onDataUpdate({ languages });
}}
/>
</div>
<div>
<label className="label">Certifications</label>
<textarea
className="input min-h-[80px]"
placeholder="List your certifications, one per line"
value={resumeData.certifications
.map(cert => cert.name)
.join('\n')}
onChange={e => {
const certLines = e.target.value
.split('\n')
.filter(line => line.trim());
const certifications = certLines.map((name, index) => ({
id: Date.now().toString() + index,
name: name.trim(),
issuer: '',
issueDate: new Date().toISOString().split('T')[0],
}));
onDataUpdate({ certifications });
}}
/>
</div>
</div>
</div>
</div>
);
case 'finish':
return (
<div className="space-y-6">
<div className="card p-6 text-center">
<div className="mb-4 text-6xl">🎉</div>
<h2 className="mb-2 text-2xl font-bold text-secondary-900">
Congratulations!
</h2>
<p className="mb-6 text-secondary-600">
Your professional resume is ready! Review it on the right panel
and download when you're satisfied.
</p>
<div className="mb-6 rounded-lg border border-primary-200 bg-primary-50 p-4">
<h3 className="mb-2 font-semibold text-primary-900">
Resume Summary
</h3>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-secondary-600">
Experience entries:
</span>
<span className="ml-2 font-semibold">
{resumeData.experience.length}
</span>
</div>
<div>
<span className="text-secondary-600">
Education entries:
</span>
<span className="ml-2 font-semibold">
{resumeData.education.length}
</span>
</div>
<div>
<span className="text-secondary-600">Skills:</span>
<span className="ml-2 font-semibold">
{resumeData.skills.length}
</span>
</div>
<div>
<span className="text-secondary-600">Languages:</span>
<span className="ml-2 font-semibold">
{resumeData.languages.length}
</span>
</div>
</div>
</div>
<button className="btn-primary px-8 py-3 text-lg">
Download PDF Resume
</button>
</div>
</div>
);
default:
return <div>Step not found</div>;
}
};
return (
<div className="flex min-h-0 flex-col">
{/* Step Content - No separate scrolling */}
<div className="px-4 py-2">{renderStepContent()}</div>
{/* Navigation Buttons - Compact Footer */}
<div className="mt-auto border-t border-secondary-200 bg-white px-4 py-2">
<div className="flex items-center justify-between">
<button
onClick={onPrevious}
disabled={isFirstStep}
className={`rounded-lg px-3 py-1.5 text-sm font-medium transition-colors ${
isFirstStep
? 'cursor-not-allowed bg-secondary-100 text-secondary-400'
: 'bg-secondary-200 text-secondary-700 hover:bg-secondary-300'
}`}
>
Previous
</button>
<div className="text-center">
<p className="text-xs text-secondary-500">
Press Enter to continue
</p>
</div>
<button
onClick={onNext}
disabled={isLastStep}
className={`rounded-lg px-3 py-1.5 text-sm font-medium transition-colors ${
isLastStep
? 'cursor-not-allowed bg-secondary-100 text-secondary-400'
: 'bg-primary-500 text-white hover:bg-primary-600'
}`}
>
{isLastStep ? 'Finished' : 'Next'}
</button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,224 @@
/**
* Education Form Component
* Professional Resume Builder
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
'use client';
import { useState } from 'react';
import { Education } from '@/types/resume';
/**
* Props interface for EducationForm component
*/
interface EducationFormProps {
data: Education[];
onChange: (data: Education[]) => void;
}
/**
* EducationForm Component - Professional Education History Management
*
* Comprehensive form component for managing educational background entries in modern CV format.
* Features academic degree tracking, institution details, and professional educational formatting.
*
* Key Features:
* - Add, edit, and delete education entries
* - Academic system compliance
* - Degree, institution, and location tracking
* - Academic achievement and GPA recording
* - Date validation and chronological ordering
* - Professional education formatting
*
* @param props - Component properties
* @param props.data - Array of education entries
* @param props.onChange - Callback function to handle education data changes
* @returns Professional education history management form
*/
export default function EducationForm({ data, onChange }: EducationFormProps) {
const [editingId, setEditingId] = useState<string | null>(null);
/**
* Creates a new education entry and sets it for editing
*/
const handleAdd = () => {
const newEducation: Education = {
id: Date.now().toString(),
degree: '',
institution: '',
location: '',
startDate: '',
endDate: '',
gpa: '',
description: '',
};
onChange([...data, newEducation]);
setEditingId(newEducation.id);
};
const handleUpdate = (id: string, updates: Partial<Education>) => {
onChange(data.map(edu => (edu.id === id ? { ...edu, ...updates } : edu)));
};
const handleDelete = (id: string) => {
onChange(data.filter(edu => edu.id !== id));
if (editingId === id) setEditingId(null);
};
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-secondary-900">Ausbildung</h3>
<button onClick={handleAdd} className="btn-primary text-sm">
+ Ausbildung hinzufügen
</button>
</div>
{data.length === 0 ? (
<div className="py-8 text-center text-secondary-500">
<div className="mb-2 text-4xl">🎓</div>
<p>Noch keine Ausbildung hinzugefügt</p>
<p className="text-sm">Klicken Sie auf "Ausbildung hinzufügen", um zu beginnen</p>
</div>
) : (
<div className="space-y-4">
{data.map(education => (
<div key={education.id} className="card p-4">
<div className="mb-4 flex items-start justify-between">
<h4 className="font-semibold text-secondary-900">
{education.degree || 'Neuer Abschluss'}
{education.institution && ` an ${education.institution}`}
</h4>
<div className="flex space-x-2">
<button
onClick={() =>
setEditingId(
editingId === education.id ? null : education.id
)
}
className="text-sm text-primary-600 hover:text-primary-700"
>
{editingId === education.id ? 'Fertig' : 'Bearbeiten'}
</button>
<button
onClick={() => handleDelete(education.id)}
className="text-sm text-red-600 hover:text-red-700"
>
Löschen
</button>
</div>
</div>
{editingId === education.id && (
<div className="space-y-4 border-t pt-4">
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<label className="label">Abschluss/Studiengang *</label>
<input
type="text"
className="input"
value={education.degree}
onChange={e =>
handleUpdate(education.id, { degree: e.target.value })
}
placeholder="Bachelor of Science in Informatik"
/>
</div>
<div>
<label className="label">Institution *</label>
<input
type="text"
className="input"
value={education.institution}
onChange={e =>
handleUpdate(education.id, {
institution: e.target.value,
})
}
placeholder="Universitätsname"
/>
</div>
</div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-4">
<div>
<label className="label">Standort</label>
<input
type="text"
className="input"
value={education.location}
onChange={e =>
handleUpdate(education.id, {
location: e.target.value,
})
}
placeholder="Stadt, Land"
/>
</div>
<div>
<label className="label">Startdatum</label>
<input
type="month"
className="input"
value={education.startDate}
onChange={e =>
handleUpdate(education.id, {
startDate: e.target.value,
})
}
/>
</div>
<div>
<label className="label">Enddatum</label>
<input
type="month"
className="input"
value={education.endDate || ''}
onChange={e =>
handleUpdate(education.id, {
endDate: e.target.value,
})
}
/>
</div>
<div>
<label className="label">Notendurchschnitt (Optional)</label>
<input
type="text"
className="input"
value={education.gpa || ''}
onChange={e =>
handleUpdate(education.id, { gpa: e.target.value })
}
placeholder="1,5 oder 3.8/4.0"
/>
</div>
</div>
<div>
<label className="label">Beschreibung (Optional)</label>
<textarea
className="textarea"
rows={3}
value={education.description || ''}
onChange={e =>
handleUpdate(education.id, {
description: e.target.value,
})
}
placeholder="Relevante Kurse, Auszeichnungen, Erfolge..."
/>
</div>
</div>
)}
</div>
))}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,325 @@
/**
* Experience Form Component
* Professional Resume Builder
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
'use client';
import { useState } from 'react';
import { Experience } from '@/types/resume';
/**
* Props interface for ExperienceForm component
*/
interface ExperienceFormProps {
data: Experience[];
onChange: (data: Experience[]) => void;
}
/**
* ExperienceForm Component - Professional Work Experience Management
*
* Comprehensive form component for managing work experience entries in modern CV format.
* Features CRUD operations, validation, and professional formatting with achievements tracking.
*
* Key Features:
* - Add, edit, and delete work experience entries
* - Professional CV format compliance
* - Achievement and responsibility tracking
* - Date validation and formatting
* - Real-time updates and state management
* - Responsive form layout
*
* @param props - Component properties
* @param props.data - Array of work experience entries
* @param props.onChange - Callback function to handle experience data changes
* @returns Professional work experience management form
*/
export default function ExperienceForm({
data,
onChange,
}: ExperienceFormProps) {
const [editingId, setEditingId] = useState<string | null>(null);
/**
* Creates a new work experience entry and sets it for editing
*/
const handleAdd = () => {
const newExperience: Experience = {
id: Date.now().toString(),
position: '',
company: '',
location: '',
startDate: '',
endDate: '',
isCurrentPosition: false,
description: '',
achievements: [],
};
onChange([...data, newExperience]);
setEditingId(newExperience.id);
};
/**
* Updates an existing experience entry with partial data
* @param id - The ID of the experience entry to update
* @param updates - Partial experience object with updates
*/
const handleUpdate = (id: string, updates: Partial<Experience>) => {
onChange(data.map(exp => (exp.id === id ? { ...exp, ...updates } : exp)));
};
/**
* Removes an experience entry from the data array
* @param id - The ID of the experience entry to delete
*/
const handleDelete = (id: string) => {
onChange(data.filter(exp => exp.id !== id));
if (editingId === id) setEditingId(null);
};
/**
* Adds a new empty achievement to an experience entry
* @param id - The ID of the experience entry to add an achievement to
*/
const handleAddAchievement = (id: string) => {
const experience = data.find(exp => exp.id === id);
if (experience) {
handleUpdate(id, {
achievements: [...experience.achievements, ''],
});
}
};
const handleUpdateAchievement = (
id: string,
index: number,
value: string
) => {
const experience = data.find(exp => exp.id === id);
if (experience) {
const achievements = [...experience.achievements];
achievements[index] = value;
handleUpdate(id, { achievements });
}
};
const handleRemoveAchievement = (id: string, index: number) => {
const experience = data.find(exp => exp.id === id);
if (experience) {
const achievements = experience.achievements.filter(
(_, i) => i !== index
);
handleUpdate(id, { achievements });
}
};
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-secondary-900">
Berufserfahrung
</h3>
<button onClick={handleAdd} className="btn-primary text-sm">
+ Erfahrung hinzufügen
</button>
</div>
{data.length === 0 ? (
<div className="py-8 text-center text-secondary-500">
<div className="mb-2 text-4xl">💼</div>
<p>Noch keine Berufserfahrung hinzugefügt</p>
<p className="text-sm">Klicken Sie auf "Erfahrung hinzufügen", um zu beginnen</p>
</div>
) : (
<div className="space-y-4">
{data.map(experience => (
<div key={experience.id} className="card p-4">
<div className="mb-4 flex items-start justify-between">
<h4 className="font-semibold text-secondary-900">
{experience.position || 'Neue Position'}
{experience.company && ` bei ${experience.company}`}
</h4>
<div className="flex space-x-2">
<button
onClick={() =>
setEditingId(
editingId === experience.id ? null : experience.id
)
}
className="text-sm text-primary-600 hover:text-primary-700"
>
{editingId === experience.id ? 'Fertig' : 'Bearbeiten'}
</button>
<button
onClick={() => handleDelete(experience.id)}
className="text-sm text-red-600 hover:text-red-700"
>
Löschen
</button>
</div>
</div>
{editingId === experience.id && (
<div className="space-y-4 border-t pt-4">
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<label className="label">Position *</label>
<input
type="text"
className="input"
value={experience.position}
onChange={e =>
handleUpdate(experience.id, {
position: e.target.value,
})
}
placeholder="Software-Entwickler"
/>
</div>
<div>
<label className="label">Unternehmen *</label>
<input
type="text"
className="input"
value={experience.company}
onChange={e =>
handleUpdate(experience.id, {
company: e.target.value,
})
}
placeholder="Firmenname"
/>
</div>
</div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
<div>
<label className="label">Standort</label>
<input
type="text"
className="input"
value={experience.location}
onChange={e =>
handleUpdate(experience.id, {
location: e.target.value,
})
}
placeholder="Stadt, Land"
/>
</div>
<div>
<label className="label">Startdatum</label>
<input
type="month"
className="input"
value={experience.startDate}
onChange={e =>
handleUpdate(experience.id, {
startDate: e.target.value,
})
}
/>
</div>
<div>
<label className="label">Enddatum</label>
<div className="space-y-2">
<input
type="month"
className="input"
value={experience.endDate || ''}
onChange={e =>
handleUpdate(experience.id, {
endDate: e.target.value,
})
}
disabled={experience.isCurrentPosition}
/>
<label className="flex items-center text-sm">
<input
type="checkbox"
className="mr-2"
checked={experience.isCurrentPosition}
onChange={e =>
handleUpdate(experience.id, {
isCurrentPosition: e.target.checked,
endDate: e.target.checked
? null
: experience.endDate,
})
}
/>
Aktuelle Position
</label>
</div>
</div>
</div>
<div>
<label className="label">Tätigkeitsbeschreibung</label>
<textarea
className="textarea"
rows={3}
value={experience.description}
onChange={e =>
handleUpdate(experience.id, {
description: e.target.value,
})
}
placeholder="Kurze Beschreibung Ihrer Rolle und Verantwortlichkeiten..."
/>
</div>
<div>
<div className="mb-2 flex items-center justify-between">
<label className="label">Wichtige Erfolge</label>
<button
type="button"
onClick={() => handleAddAchievement(experience.id)}
className="text-sm text-primary-600 hover:text-primary-700"
>
+ Erfolg hinzufügen
</button>
</div>
<div className="space-y-2">
{experience.achievements.map((achievement, index) => (
<div key={index} className="flex space-x-2">
<input
type="text"
className="input flex-1"
value={achievement}
onChange={e =>
handleUpdateAchievement(
experience.id,
index,
e.target.value
)
}
placeholder="Beschreiben Sie einen wichtigen Erfolg..."
/>
<button
type="button"
onClick={() =>
handleRemoveAchievement(experience.id, index)
}
className="px-2 text-red-600 hover:text-red-700"
>
×
</button>
</div>
))}
</div>
</div>
</div>
)}
</div>
))}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,463 @@
/**
* Personal Info Form Component - Professional CV Format
* Professional Resume Builder - Enhanced with modern CV standards
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
'use client';
import React from 'react';
import { PersonalInfo } from '@/types/resume';
import { ModernCard } from '@/components/ui/ModernCard';
import { ModernInput } from '@/components/ui/ModernInput';
import {
User,
Mail,
Phone,
MapPin,
Globe,
Linkedin,
Github,
Upload,
Calendar,
Flag,
Heart,
Shield,
Briefcase,
Camera,
} from 'lucide-react';
// Import organized data from centralized data folder
import {
nationalityOptions,
maritalStatusOptions,
visaStatusOptions,
} from '@/data/formOptions';
import { placeholders } from '@/data/uiConstants';
/**
* Props interface for PersonalInfoForm component
*/
interface PersonalInfoFormProps {
data: PersonalInfo;
onChange: (data: PersonalInfo) => void;
}
/**
* PersonalInfoForm Component - Professional Personal Information Form
*
* Comprehensive form component for collecting personal information according to
* modern CV standards. Features profile image upload, European address format,
* and professional personal details with validation.
*
* Key Features:
* - Profile image upload with drag-and-drop support
* - European address format (Street, ZIP, City, Country)
* - Personal details (nationality, birth date, marital status, visa status)
* - Professional links (website, LinkedIn, GitHub)
* - Real-time validation and error handling
*
* @param props - Component properties
* @param props.data - Current personal information data
* @param props.onChange - Callback function to handle data changes
* @returns Professional personal information form with modern CV format
*/
export default function PersonalInfoForm({
data,
onChange,
}: PersonalInfoFormProps) {
/**
* Handles input field changes and updates the data state
*
* @param field - The field name to update
* @param value - The new value for the field
*/
const handleChange = (field: keyof PersonalInfo, value: string) => {
onChange({ ...data, [field]: value });
};
/**
* Handles profile image file selection
* Converts selected file to base64 string for storage
*
* @param event - File input change event
*/
const handleImageChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = e => {
const result = e.target?.result as string;
handleChange('profileImage', result);
};
reader.readAsDataURL(file);
}
};
return (
<div className="space-y-3">
{/* Job Title and Profile Image Section */}
<ModernCard variant="elevated" padding="sm">
<div className="mb-3 flex items-center gap-3 border-b border-grayscale-200 pb-2">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-primary-100 to-primary-200 shadow-sm">
<Briefcase className="h-4 w-4 text-primary-600" />
</div>
<div>
<h2 className="text-sm font-semibold text-text-primary">
Berufliche Informationen
</h2>
<p className="text-xs text-text-secondary">
Berufsbezeichnung und Profilbild
</p>
</div>
</div>
<div className="grid gap-3 md:grid-cols-2">
{/* Job Title */}
<div className="md:col-span-2">
<ModernInput
label="Berufsbezeichnung"
type="text"
value={data.jobTitle || ''}
onChange={e => handleChange('jobTitle', e.target.value)}
placeholder={placeholders.jobTitle}
leftIcon={<Briefcase className="h-3 w-3" />}
variant="filled"
required
/>
</div>
{/* Profile Image Upload */}
<div className="md:col-span-2">
<label className="mb-1 block text-xs font-medium text-text-primary">
Profilbild
</label>
<div className="flex items-center gap-2">
{data.profileImage && (
<div className="h-10 w-10 overflow-hidden rounded-full bg-grayscale-100">
<img
src={data.profileImage}
alt="Profile"
className="h-full w-full object-cover"
/>
</div>
)}
<div className="flex-1">
<label className="hover:bg-primary-25 flex w-full cursor-pointer items-center justify-center rounded-lg border-2 border-dashed border-grayscale-300 px-2 py-2 transition-colors hover:border-primary-500">
<div className="flex items-center gap-1 text-xs text-grayscale-600">
<Upload className="h-3 w-3" />
<span>Profilbild hochladen</span>
</div>
<input
type="file"
accept="image/*"
onChange={handleImageChange}
className="hidden"
/>
</label>
</div>
</div>
</div>
</div>
</ModernCard>
{/* Personal Information Section */}
<ModernCard variant="elevated" padding="sm">
<div className="mb-3 flex items-center gap-3 border-b border-grayscale-200 pb-2">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-primary-100 to-primary-200 shadow-sm">
<User className="h-4 w-4 text-primary-600" />
</div>
<div>
<h2 className="text-sm font-semibold text-text-primary">
Persönliche Informationen
</h2>
<p className="text-xs text-text-secondary">
Ihr Name und grundlegende Details
</p>
</div>
</div>
<div className="grid gap-2 md:grid-cols-2">
{/* Basic Information */}
<ModernInput
label="Vorname"
type="text"
value={data.firstName}
onChange={e => handleChange('firstName', e.target.value)}
placeholder={placeholders.firstName}
leftIcon={<User className="h-3 w-3" />}
variant="filled"
required
/>
<ModernInput
label="Nachname"
type="text"
value={data.lastName}
onChange={e => handleChange('lastName', e.target.value)}
placeholder={placeholders.lastName}
leftIcon={<User className="h-3 w-3" />}
variant="filled"
required
/>
{/* Contact Information */}
<ModernInput
label="E-Mail"
type="email"
value={data.email}
onChange={e => handleChange('email', e.target.value)}
placeholder={placeholders.email}
leftIcon={<Mail className="h-3 w-3" />}
variant="filled"
required
/>
<ModernInput
label="Telefon"
type="tel"
value={data.phone}
onChange={e => handleChange('phone', e.target.value)}
placeholder={placeholders.phone}
leftIcon={<Phone className="h-3 w-3" />}
variant="filled"
/>
{/* Address Section */}
<div className="mt-3 border-t border-grayscale-200 pt-3 md:col-span-2">
<div className="mb-3 flex items-center gap-3 border-b border-grayscale-200 pb-2">
<div className="flex h-6 w-6 items-center justify-center rounded-lg bg-gradient-to-br from-primary-100 to-primary-200 shadow-sm">
<MapPin className="h-3 w-3 text-primary-600" />
</div>
<div>
<h3 className="text-sm font-semibold text-text-primary">
Adresse
</h3>
<p className="text-xs text-text-secondary">
Ihre Standortdaten
</p>
</div>
</div>
<div className="grid gap-2">
{/* Street and House Number on same line */}
<div className="grid grid-cols-4 gap-2">
<div className="col-span-3">
<ModernInput
label="Straße"
type="text"
value={data.street || ''}
onChange={e => handleChange('street', e.target.value)}
placeholder="Musterstraße"
leftIcon={<MapPin className="h-3 w-3" />}
variant="filled"
/>
</div>
<div className="col-span-1">
<ModernInput
label="Hausnummer"
type="text"
value={data.houseNumber || ''}
onChange={e => handleChange('houseNumber', e.target.value)}
placeholder="123"
variant="filled"
/>
</div>
</div>
{/* ZIP Code and City on same line */}
<div className="grid grid-cols-2 gap-2">
<ModernInput
label="PLZ"
type="text"
value={data.zipCode || ''}
onChange={e => handleChange('zipCode', e.target.value)}
placeholder="12345"
leftIcon={<MapPin className="h-3 w-3" />}
variant="filled"
/>
<ModernInput
label="Stadt"
type="text"
value={data.city}
onChange={e => handleChange('city', e.target.value)}
placeholder="Berlin"
leftIcon={<MapPin className="h-3 w-3" />}
variant="filled"
required
/>
</div>
{/* Country on separate line */}
<ModernInput
label="Land"
type="text"
value={data.country || ''}
onChange={e => handleChange('country', e.target.value)}
placeholder="Deutschland"
leftIcon={<Flag className="h-3 w-3" />}
variant="filled"
/>
</div>
</div>
{/* Online Presence Section */}
<div className="mt-3 border-t border-grayscale-200 pt-3 md:col-span-2">
<div className="mb-3 flex items-center gap-3 border-b border-grayscale-200 pb-2">
<div className="flex h-6 w-6 items-center justify-center rounded-lg bg-gradient-to-br from-primary-100 to-primary-200 shadow-sm">
<Globe className="h-3 w-3 text-primary-600" />
</div>
<div>
<h3 className="text-sm font-semibold text-text-primary">
Online-Präsenz
</h3>
<p className="text-xs text-text-secondary">
Ihre professionellen Links
</p>
</div>
</div>
<div className="grid gap-2">
<ModernInput
label="Website"
type="url"
value={data.website || ''}
onChange={e => handleChange('website', e.target.value)}
placeholder="https://ihre-website.com"
leftIcon={<Globe className="h-3 w-3" />}
variant="filled"
/>
<ModernInput
label="LinkedIn"
type="text"
value={data.linkedin || ''}
onChange={e => handleChange('linkedin', e.target.value)}
placeholder="linkedin.com/in/ihr-profil"
leftIcon={<Linkedin className="h-3 w-3" />}
variant="filled"
/>
<ModernInput
label="GitHub"
type="text"
value={data.github || ''}
onChange={e => handleChange('github', e.target.value)}
placeholder="github.com/yourusername"
leftIcon={<Github className="h-3 w-3" />}
variant="filled"
/>
</div>
</div>
{/* Personal Details */}
<div className="mt-3 border-t border-grayscale-200 pt-3 md:col-span-2">
<div className="mb-3 flex items-center gap-3 border-b border-grayscale-200 pb-2">
<div className="flex h-6 w-6 items-center justify-center rounded-lg bg-gradient-to-br from-primary-100 to-primary-200 shadow-sm">
<Shield className="h-3 w-3 text-primary-600" />
</div>
<div>
<h3 className="text-sm font-semibold text-text-primary">
Personal Details
</h3>
<p className="text-xs text-text-secondary">
Additional personal information
</p>
</div>
</div>
<div className="grid gap-2 md:grid-cols-2">
{/* Nationality */}
<div>
<label className="block text-sm font-semibold tracking-wide text-text-primary">
Nationality
</label>
<div className="mt-2">
<select
value={data.nationality || ''}
onChange={e => handleChange('nationality', e.target.value)}
className="w-full rounded-lg border border-grayscale-300 px-2 py-2 text-xs transition-colors focus:border-primary-500 focus:ring-1 focus:ring-primary-100"
>
<option value="">Select nationality</option>
{nationalityOptions.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
</div>
{/* Date of Birth */}
<ModernInput
label="Date of Birth"
type="date"
value={data.dateOfBirth || ''}
onChange={e => handleChange('dateOfBirth', e.target.value)}
leftIcon={<Calendar className="h-3 w-3" />}
variant="filled"
/>
{/* Place of Birth */}
<ModernInput
label="Place of Birth"
type="text"
value={data.placeOfBirth || ''}
onChange={e => handleChange('placeOfBirth', e.target.value)}
placeholder="Berlin, Germany"
leftIcon={<MapPin className="h-3 w-3" />}
variant="filled"
/>
{/* Marital Status */}
<div>
<label className="block text-sm font-semibold tracking-wide text-text-primary">
Marital Status
</label>
<div className="mt-2">
<select
value={data.maritalStatus || ''}
onChange={e =>
handleChange('maritalStatus', e.target.value)
}
className="w-full rounded-lg border border-grayscale-300 px-2 py-2 text-xs transition-colors focus:border-primary-500 focus:ring-1 focus:ring-primary-100"
>
<option value="">Select marital status</option>
{maritalStatusOptions.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
</div>
{/* Visa Status */}
<div className="md:col-span-2">
<label className="block text-sm font-semibold tracking-wide text-text-primary">
Visa Status
</label>
<div className="mt-2">
<select
value={data.visaStatus || ''}
onChange={e => handleChange('visaStatus', e.target.value)}
className="w-full rounded-lg border border-grayscale-300 px-2 py-2 text-xs transition-colors focus:border-primary-500 focus:ring-1 focus:ring-primary-100"
>
<option value="">Select visa status</option>
{visaStatusOptions.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
</div>
</div>
</div>
</div>
</ModernCard>
</div>
);
}

View File

@@ -0,0 +1,250 @@
/**
* Skills Form Component
* Professional Resume Builder
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
'use client';
import { useState } from 'react';
import { Skill } from '@/types/resume';
// Import organized data from centralized data folder
import { skillCategories, proficiencyLevels } from '@/data/formOptions';
import {
placeholders,
emptyStateMessages,
buttonTexts,
} from '@/data/uiConstants';
/**
* Props interface for SkillsForm component
*/
interface SkillsFormProps {
data: Skill[];
onChange: (data: Skill[]) => void;
}
/**
* SkillsForm Component - Professional Skills Management
*
* Comprehensive form component for managing technical and soft skills with proficiency levels.
* Features skill categorization, level assessment, and professional organization for modern CVs.
*
* Key Features:
* - Add, edit, and delete skills with proficiency levels
* - Skill categorization (Technical, Soft Skills, Languages, etc.)
* - Visual proficiency indicators
* - Professional CV format compliance
* - Real-time skill management
* - Category-based organization
*
* @param props - Component properties
* @param props.data - Array of skill entries
* @param props.onChange - Callback function to handle skills data changes
* @returns Professional skills management form with categorization
*/
export default function SkillsForm({ data, onChange }: SkillsFormProps) {
/**
* State for managing new skill input form
*/
const [newSkill, setNewSkill] = useState<{
name: string;
category: string;
level: Skill['level'];
}>({
name: '',
category: '',
level: 'Intermediate',
});
// Use proficiency levels from centralized data (map to Skill interface format)
const levels: Skill['level'][] = [
'Beginner',
'Intermediate',
'Advanced',
'Expert',
];
const handleAdd = () => {
if (newSkill.name.trim() && newSkill.category.trim()) {
const skill: Skill = {
id: Date.now().toString(),
name: newSkill.name.trim(),
category: newSkill.category.trim(),
level: newSkill.level,
};
onChange([...data, skill]);
setNewSkill({ name: '', category: '', level: 'Intermediate' });
}
};
const handleDelete = (id: string) => {
onChange(data.filter(skill => skill.id !== id));
};
const handleUpdate = (id: string, updates: Partial<Skill>) => {
onChange(
data.map(skill => (skill.id === id ? { ...skill, ...updates } : skill))
);
};
const groupedSkills = data.reduce(
(acc, skill) => {
if (!acc[skill.category]) {
acc[skill.category] = [];
}
acc[skill.category].push(skill);
return acc;
},
{} as Record<string, Skill[]>
);
return (
<div className="space-y-6">
{/* Add New Skill */}
<div className="card border border-primary-200 bg-primary-50 p-4">
<h3 className="mb-4 text-lg font-semibold text-secondary-900">
Neue Fähigkeit hinzufügen
</h3>
<div className="grid grid-cols-1 gap-4 md:grid-cols-4">
<div>
<label className="label">Fähigkeitsname *</label>
<input
type="text"
className="input"
value={newSkill.name}
onChange={e => setNewSkill({ ...newSkill, name: e.target.value })}
placeholder={placeholders.skillName}
onKeyPress={e => e.key === 'Enter' && handleAdd()}
/>
</div>
<div>
<label className="label">Kategorie *</label>
<select
className="input"
value={newSkill.category}
onChange={e =>
setNewSkill({ ...newSkill, category: e.target.value })
}
>
<option value="">Kategorie auswählen</option>
{skillCategories.map(category => (
<option key={category} value={category}>
{category}
</option>
))}
</select>
</div>
<div>
<label className="label">Kenntnisstand</label>
<select
className="input"
value={newSkill.level}
onChange={e =>
setNewSkill({
...newSkill,
level: e.target.value as Skill['level'],
})
}
>
{levels.map(level => (
<option key={level} value={level}>
{level}
</option>
))}
</select>
</div>
<div className="flex items-end">
<button
onClick={handleAdd}
className="btn-primary w-full"
disabled={!newSkill.name.trim() || !newSkill.category.trim()}
>
Fähigkeit hinzufügen
</button>
</div>
</div>
</div>
{/* Skills Display */}
{Object.keys(groupedSkills).length === 0 ? (
<div className="py-8 text-center text-secondary-500">
<div className="mb-2 text-4xl"></div>
<p>Noch keine Fähigkeiten hinzugefügt</p>
<p className="text-sm">Fügen Sie Ihre erste Fähigkeit oben hinzu</p>
</div>
) : (
<div className="space-y-6">
{Object.entries(groupedSkills).map(([category, skills]) => (
<div key={category} className="card p-4">
<h4 className="mb-3 text-lg font-semibold text-secondary-900">
{category}
</h4>
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3">
{skills.map(skill => (
<div
key={skill.id}
className="flex items-center justify-between rounded-lg bg-secondary-50 p-3"
>
<div className="flex-1">
<div className="font-medium text-secondary-900">
{skill.name}
</div>
<div className="text-sm text-secondary-600">
{skill.level}
</div>
</div>
<div className="flex space-x-2">
<select
className="rounded border border-secondary-300 px-2 py-1 text-xs"
value={skill.level}
onChange={e =>
handleUpdate(skill.id, {
level: e.target.value as Skill['level'],
})
}
>
{levels.map(level => (
<option key={level} value={level}>
{level}
</option>
))}
</select>
<button
onClick={() => handleDelete(skill.id)}
className="p-1 text-sm text-red-600 hover:text-red-700"
title="Fähigkeit löschen"
>
×
</button>
</div>
</div>
))}
</div>
</div>
))}
</div>
)}
<div className="rounded-lg border border-secondary-200 bg-secondary-50 p-4">
<div className="flex items-start space-x-3">
<div className="text-secondary-600">💡</div>
<div>
<h4 className="mb-1 text-sm font-semibold text-secondary-900">
Empfehlung für Fähigkeiten
</h4>
<p className="text-sm text-secondary-700">
Konzentrieren Sie sich auf Fähigkeiten, die für Ihre Zielposition relevant sind.
Fügen Sie sowohl technische als auch soziale Kompetenzen hinzu. Seien Sie ehrlich
bezüglich Ihrer Kenntnisse - Arbeitgeber schätzen Genauigkeit.
</p>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,120 @@
/**
* Modern Avatar Component
* Professional Resume Builder - Reusable UI Component
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
'use client';
import React, { useState } from 'react';
import { User } from 'lucide-react';
import { cn } from '@/utils';
export interface ModernAvatarProps {
src?: string;
alt?: string;
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';
fallback?: string;
className?: string;
onClick?: () => void;
status?: 'online' | 'offline' | 'away' | 'busy';
showStatus?: boolean;
}
export const ModernAvatar: React.FC<ModernAvatarProps> = ({
src,
alt = 'Avatar',
size = 'md',
fallback,
className = '',
onClick,
status,
showStatus = false,
}) => {
const [imageError, setImageError] = useState(false);
const sizeClasses = {
xs: 'w-6 h-6',
sm: 'w-8 h-8',
md: 'w-10 h-10',
lg: 'w-12 h-12',
xl: 'w-16 h-16',
'2xl': 'w-20 h-20',
};
const iconSizeClasses = {
xs: 'w-3 h-3',
sm: 'w-4 h-4',
md: 'w-5 h-5',
lg: 'w-6 h-6',
xl: 'w-8 h-8',
'2xl': 'w-10 h-10',
};
const statusClasses = {
online: 'bg-success-500 border-white',
offline: 'bg-grayscale-400 border-white',
away: 'bg-warning-500 border-white',
busy: 'bg-error-500 border-white',
};
const statusSizeClasses = {
xs: 'w-2 h-2 border',
sm: 'w-2.5 h-2.5 border',
md: 'w-3 h-3 border-2',
lg: 'w-3.5 h-3.5 border-2',
xl: 'w-4 h-4 border-2',
'2xl': 'w-5 h-5 border-2',
};
const baseClasses = cn(
'relative inline-flex items-center justify-center rounded-full bg-gradient-to-br from-grayscale-100 to-grayscale-200 overflow-hidden transition-all duration-200',
sizeClasses[size],
onClick && 'cursor-pointer hover:shadow-medium hover:scale-105',
className
);
const renderContent = () => {
if (src && !imageError) {
return (
<img
src={src}
alt={alt}
className="h-full w-full object-cover"
onError={() => setImageError(true)}
/>
);
}
if (fallback) {
return (
<span className="text-grayscale-600 text-sm font-medium">
{fallback}
</span>
);
}
return <User className={cn('text-grayscale-500', iconSizeClasses[size])} />;
};
return (
<div className={baseClasses} onClick={onClick}>
{renderContent()}
{/* Status indicator */}
{showStatus && status && (
<span
className={cn(
'absolute bottom-0 right-0 rounded-full',
statusClasses[status],
statusSizeClasses[size]
)}
aria-label={`Status: ${status}`}
/>
)}
</div>
);
};

View File

@@ -0,0 +1,71 @@
/**
* Modern Badge Component
* Professional Resume Builder - Reusable UI Component
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
'use client';
import React from 'react';
import { cn } from '@/utils';
export interface ModernBadgeProps {
children: React.ReactNode;
variant?: 'default' | 'primary' | 'success' | 'warning' | 'error' | 'info';
size?: 'sm' | 'md' | 'lg';
rounded?: 'sm' | 'md' | 'lg' | 'full';
className?: string;
icon?: React.ReactNode;
}
export const ModernBadge: React.FC<ModernBadgeProps> = ({
children,
variant = 'default',
size = 'md',
rounded = 'md',
className = '',
icon,
}) => {
const baseClasses =
'inline-flex items-center gap-1.5 font-medium transition-all duration-200';
const variantClasses = {
default: 'bg-grayscale-100 text-grayscale-800 border border-grayscale-200',
primary: 'bg-primary-100 text-primary-800 border border-primary-200',
success: 'bg-success-100 text-success-800 border border-success-200',
warning: 'bg-warning-100 text-warning-800 border border-warning-200',
error: 'bg-error-100 text-error-800 border border-error-200',
info: 'bg-blue-100 text-blue-800 border border-blue-200',
};
const sizeClasses = {
sm: 'px-2 py-1 text-xs',
md: 'px-3 py-1.5 text-sm',
lg: 'px-4 py-2 text-base',
};
const roundedClasses = {
sm: 'rounded-sm',
md: 'rounded-md',
lg: 'rounded-lg',
full: 'rounded-full',
};
return (
<span
className={cn(
baseClasses,
variantClasses[variant],
sizeClasses[size],
roundedClasses[rounded],
className
)}
>
{icon && <span className="flex-shrink-0">{icon}</span>}
{children}
</span>
);
};

View File

@@ -0,0 +1,69 @@
/**
* Modern Button Component
* Professional Resume Builder - Enhanced with resume-example.com design patterns
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
'use client';
import React from 'react';
export interface ModernButtonProps {
children: React.ReactNode;
onClick?: () => void;
variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
loading?: boolean;
className?: string;
type?: 'button' | 'submit' | 'reset';
}
export const ModernButton: React.FC<ModernButtonProps> = ({
children,
onClick,
variant = 'primary',
size = 'md',
disabled = false,
loading = false,
className = '',
type = 'button',
}) => {
const baseClasses =
'inline-flex items-center justify-center font-semibold transition-all duration-200 ease-out focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed relative overflow-hidden';
const variantClasses = {
primary:
'bg-gradient-to-r from-primary-500 to-primary-600 text-white hover:from-primary-600 hover:to-primary-700 focus:ring-primary-500 shadow-medium hover:shadow-large active:shadow-soft',
secondary:
'bg-grayscale-100 text-text-primary hover:bg-grayscale-200 focus:ring-grayscale-500 border border-border-primary shadow-soft hover:shadow-medium',
outline:
'border-2 border-primary-500 text-primary-600 hover:bg-primary-50 focus:ring-primary-500 shadow-soft hover:shadow-medium',
ghost: 'text-primary-600 hover:bg-primary-50 focus:ring-primary-500',
};
const sizeClasses = {
sm: 'px-3 py-1.5 text-sm rounded-lg',
md: 'px-4 py-2.5 text-sm rounded-xl',
lg: 'px-6 py-3 text-base rounded-xl',
};
return (
<button
type={type}
onClick={onClick}
disabled={disabled || loading}
className={` ${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${className} `}
>
{loading && (
<div className="absolute inset-0 flex items-center justify-center">
<div className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
</div>
)}
<span className={loading ? 'opacity-0' : 'opacity-100'}>{children}</span>
</button>
);
};

View File

@@ -0,0 +1,68 @@
/**
* Modern Card Component
* Professional Resume Builder - Enhanced with resume-example.com design patterns
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
'use client';
import React from 'react';
export interface ModernCardProps {
children: React.ReactNode;
variant?: 'default' | 'elevated' | 'outlined' | 'filled';
padding?: 'none' | 'sm' | 'md' | 'lg' | 'xl';
className?: string;
onClick?: () => void;
hoverable?: boolean;
}
export const ModernCard: React.FC<ModernCardProps> = ({
children,
variant = 'default',
padding = 'md',
className = '',
onClick,
hoverable = false,
}) => {
const baseClasses = 'rounded-2xl transition-all duration-300 ease-out';
const variantClasses = {
default:
'bg-gradient-to-br from-white to-grayscale-50 border border-grayscale-200 shadow-medium backdrop-blur-sm',
elevated:
'bg-gradient-to-br from-white via-white to-grayscale-25 shadow-xl border border-grayscale-100',
outlined:
'bg-gradient-to-br from-grayscale-25 to-white border-2 border-primary-200 shadow-soft',
filled:
'bg-gradient-to-br from-grayscale-100 to-grayscale-50 border border-grayscale-200 shadow-soft',
};
const paddingClasses = {
none: '',
sm: 'p-4',
md: 'p-6',
lg: 'p-8',
xl: 'p-10',
};
const hoverClasses = hoverable
? 'hover:shadow-2xl hover:scale-[1.02] hover:bg-gradient-to-br hover:from-white hover:to-primary-25 cursor-pointer transform-gpu'
: '';
const Component = onClick ? 'button' : 'div';
return (
<Component
onClick={onClick}
className={`${baseClasses} ${variantClasses[variant]} ${paddingClasses[padding]} ${hoverClasses} ${className} relative overflow-hidden`}
>
{/* Subtle overlay for depth */}
<div className="pointer-events-none absolute inset-0 bg-gradient-to-br from-white/20 to-transparent" />
<div className="relative z-10">{children}</div>
</Component>
);
};

View File

@@ -0,0 +1,353 @@
/**
* Modern Form Builder Component (Reusable)
* Professional UI Component Library
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
'use client';
import React from 'react';
import { ModernCard } from './ModernCard';
import { ModernInput } from './ModernInput';
import { ModernButton } from './ModernButton';
import { cn } from '@/utils';
import { Plus, Trash2, Edit3 } from 'lucide-react';
export interface FormFieldConfig {
key: string;
label: string;
type?: 'text' | 'email' | 'tel' | 'url' | 'date' | 'textarea' | 'select';
placeholder?: string;
required?: boolean;
options?: { value: string; label: string }[]; // For select fields
icon?: React.ReactNode;
validation?: (value: string) => string | undefined;
width?: 'full' | 'half' | 'third';
}
export interface ListItemData {
id: string;
[key: string]: any;
}
export interface ModernFormListProps<T extends ListItemData> {
title: string;
description?: string;
icon?: React.ReactNode;
data: T[];
fields: FormFieldConfig[];
onAdd: () => void;
onEdit: (id: string) => void;
onDelete: (id: string) => void;
onSave: (item: T) => void;
onCancel: () => void;
editingId?: string | null;
editingItem?: T | null;
emptyStateText?: string;
addButtonText?: string;
className?: string;
renderCustomField?: (
field: FormFieldConfig,
value: any,
onChange: (value: any) => void
) => React.ReactNode;
renderListItem?: (
item: T,
onEdit: () => void,
onDelete: () => void
) => React.ReactNode;
}
export function ModernFormList<T extends ListItemData>({
title,
description,
icon,
data,
fields,
onAdd,
onEdit,
onDelete,
onSave,
onCancel,
editingId = null,
editingItem = null,
emptyStateText = 'Noch keine Einträge hinzugefügt',
addButtonText = 'Element hinzufügen',
className = '',
renderCustomField,
renderListItem,
}: ModernFormListProps<T>) {
const [formData, setFormData] = React.useState<Partial<T>>(editingItem || {});
const [errors, setErrors] = React.useState<Record<string, string>>({});
React.useEffect(() => {
if (editingItem) {
setFormData(editingItem);
} else {
setFormData({});
}
setErrors({});
}, [editingItem, editingId]);
const handleFieldChange = (key: string, value: any) => {
setFormData(prev => ({ ...prev, [key]: value }));
// Clear error when user starts typing
if (errors[key]) {
setErrors(prev => ({ ...prev, [key]: '' }));
}
};
const validateForm = (): boolean => {
const newErrors: Record<string, string> = {};
fields.forEach(field => {
const value = formData[field.key];
if (field.required && (!value || String(value).trim() === '')) {
newErrors[field.key] = `${field.label} is required`;
} else if (value && field.validation) {
const validationError = field.validation(String(value));
if (validationError) {
newErrors[field.key] = validationError;
}
}
});
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSave = () => {
if (validateForm()) {
onSave(formData as T);
}
};
const getFieldWidth = (width?: string) => {
switch (width) {
case 'half':
return 'md:col-span-6';
case 'third':
return 'md:col-span-4';
default:
return 'md:col-span-12';
}
};
const renderField = (field: FormFieldConfig) => {
const value = formData[field.key] || '';
if (renderCustomField) {
const customField = renderCustomField(field, value, newValue =>
handleFieldChange(field.key, newValue)
);
if (customField) return customField;
}
switch (field.type) {
case 'textarea':
return (
<div
key={field.key}
className={cn('col-span-12', getFieldWidth(field.width))}
>
<label className="text-text-primary mb-2 block text-sm font-medium">
{field.label}
{field.required && <span className="text-error-500 ml-1">*</span>}
</label>
<textarea
value={value}
onChange={e => handleFieldChange(field.key, e.target.value)}
placeholder={field.placeholder}
rows={3}
className={cn(
'w-full rounded-lg border px-4 py-3 transition-colors focus:border-primary-500 focus:ring-2 focus:ring-primary-100',
errors[field.key] ? 'border-error-500' : 'border-grayscale-300'
)}
/>
{errors[field.key] && (
<p className="text-error-600 mt-1 text-sm">{errors[field.key]}</p>
)}
</div>
);
case 'select':
return (
<div
key={field.key}
className={cn('col-span-12', getFieldWidth(field.width))}
>
<label className="text-text-primary mb-2 block text-sm font-medium">
{field.label}
{field.required && <span className="text-error-500 ml-1">*</span>}
</label>
<select
value={value}
onChange={e => handleFieldChange(field.key, e.target.value)}
className={cn(
'w-full rounded-lg border px-4 py-3 transition-colors focus:border-primary-500 focus:ring-2 focus:ring-primary-100',
errors[field.key] ? 'border-error-500' : 'border-grayscale-300'
)}
>
<option value="">Select {field.label}</option>
{field.options?.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
{errors[field.key] && (
<p className="text-error-600 mt-1 text-sm">{errors[field.key]}</p>
)}
</div>
);
default:
return (
<div
key={field.key}
className={cn('col-span-12', getFieldWidth(field.width))}
>
<ModernInput
label={field.label}
type={field.type || 'text'}
value={value}
onChange={e => handleFieldChange(field.key, e.target.value)}
placeholder={field.placeholder}
leftIcon={field.icon}
required={field.required}
error={errors[field.key]}
variant="filled"
/>
</div>
);
}
};
const defaultListItemRenderer = (
item: T,
onEditItem: () => void,
onDeleteItem: () => void
) => (
<div className="bg-grayscale-50 flex items-start justify-between rounded-lg p-4">
<div className="flex-1">
<h4 className="text-text-primary font-medium">
{item[fields[0]?.key] || 'Untitled'}
</h4>
<div className="text-text-secondary mt-1 space-y-1 text-sm">
{fields.slice(1, 3).map(
field =>
item[field.key] && (
<p key={field.key}>
<span className="font-medium">{field.label}:</span>{' '}
{item[field.key]}
</p>
)
)}
</div>
</div>
<div className="ml-4 flex items-center gap-2">
<button
onClick={onEditItem}
className="text-grayscale-600 rounded-lg p-2 transition-colors hover:bg-primary-50 hover:text-primary-600"
>
<Edit3 className="h-4 w-4" />
</button>
<button
onClick={onDeleteItem}
className="text-grayscale-600 hover:text-error-600 hover:bg-error-50 rounded-lg p-2 transition-colors"
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</div>
);
return (
<div className={cn('space-y-4', className)}>
{/* Header */}
<ModernCard variant="elevated" padding="md">
<div className="border-grayscale-200 mb-4 flex items-center justify-between border-b pb-3">
<div className="flex items-center gap-3">
{icon && (
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-gradient-to-br from-primary-100 to-primary-200 shadow-sm">
{icon}
</div>
)}
<div>
<h2 className="text-text-primary text-lg font-semibold">
{title}
</h2>
{description && (
<p className="text-text-secondary mt-0.5 text-sm">
{description}
</p>
)}
</div>
</div>
<ModernButton
variant="outline"
size="sm"
onClick={onAdd}
disabled={!!editingId}
>
<Plus className="mr-2 h-4 w-4" />
{addButtonText}
</ModernButton>
</div>
{/* Form for editing/adding */}
{editingId && (
<div className="bg-primary-25 mb-6 rounded-lg border border-primary-200 p-4">
<h3 className="text-text-primary mb-4 text-lg font-medium">
{editingItem ? 'Element bearbeiten' : 'Neues Element hinzufügen'}
</h3>
<div className="mb-4 grid grid-cols-12 gap-4">
{fields.map(renderField)}
</div>
<div className="flex items-center gap-3">
<ModernButton variant="primary" size="sm" onClick={handleSave}>
Speichern
</ModernButton>
<ModernButton variant="ghost" size="sm" onClick={onCancel}>
Abbrechen
</ModernButton>
</div>
</div>
)}
{/* List of items */}
<div className="space-y-3">
{data.length === 0 ? (
<div className="text-text-tertiary py-8 text-center">
<div className="bg-grayscale-100 mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full">
{icon || <Plus className="h-6 w-6" />}
</div>
<p>{emptyStateText}</p>
</div>
) : (
data.map(item => (
<div key={item.id}>
{renderListItem
? renderListItem(
item,
() => onEdit(item.id),
() => onDelete(item.id)
)
: defaultListItemRenderer(
item,
() => onEdit(item.id),
() => onDelete(item.id)
)}
</div>
))
)}
</div>
</ModernCard>
</div>
);
}

View File

@@ -0,0 +1,120 @@
/**
* Modern Form Input Component
* Professional Resume Builder - Enhanced with resume-example.com design patterns
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
'use client';
import React, { forwardRef } from 'react';
export interface ModernInputProps
extends React.InputHTMLAttributes<HTMLInputElement> {
label?: string;
error?: string;
helperText?: string;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
variant?: 'default' | 'filled' | 'outlined';
}
export const ModernInput = forwardRef<HTMLInputElement, ModernInputProps>(
(
{
label,
error,
helperText,
leftIcon,
rightIcon,
variant = 'default',
className = '',
...props
},
ref
) => {
const baseInputClasses = `
w-full px-2.5 py-3 text-xs transition-all duration-200 ease-out
placeholder-text-quaternary focus:outline-none disabled:opacity-50
disabled:cursor-not-allowed
`;
const variantClasses = {
default: `
border border-grayscale-300 rounded-lg bg-gradient-to-br from-white to-grayscale-25
focus:border-primary-500 focus:ring-1 focus:ring-primary-100 focus:shadow-focus
hover:border-grayscale-400 hover:shadow-soft
${error ? 'border-error-500 focus:border-error-500 focus:ring-error-100 focus:shadow-focus-error' : ''}
`,
filled: `
border-0 rounded-lg bg-gradient-to-br from-grayscale-100 to-grayscale-50 shadow-inner
focus:bg-gradient-to-br focus:from-white focus:to-primary-25 focus:ring-1 focus:ring-primary-100 focus:shadow-focus
hover:bg-gradient-to-br hover:from-grayscale-50 hover:to-white hover:shadow-soft
${error ? 'bg-gradient-to-br from-error-50 to-error-25 focus:from-error-50 focus:to-error-25 focus:ring-error-100 focus:shadow-focus-error' : ''}
`,
outlined: `
border-2 border-grayscale-300 rounded-lg bg-transparent backdrop-blur-sm
focus:border-primary-500 focus:bg-gradient-to-br focus:from-white/50 focus:to-primary-25/50 focus:shadow-focus
hover:border-grayscale-400 hover:bg-gradient-to-br hover:from-white/30 hover:to-grayscale-25/30
${error ? 'border-error-500 focus:border-error-500 focus:from-error-50/50 focus:to-error-25/50 focus:shadow-focus-error' : ''}
`,
};
return (
<div className="space-y-2">
{label && (
<label className="block text-sm font-semibold tracking-wide text-text-primary">
{label}
</label>
)}
<div className="relative">
{leftIcon && (
<div className="absolute left-2 top-1/2 -translate-y-1/2 transform text-text-tertiary">
{leftIcon}
</div>
)}
<input
ref={ref}
className={` ${baseInputClasses} ${variantClasses[variant]} ${leftIcon ? 'pl-7' : ''} ${rightIcon ? 'pr-7' : ''} ${className} `}
{...props}
/>
{rightIcon && (
<div className="absolute right-2 top-1/2 -translate-y-1/2 transform text-text-tertiary">
{rightIcon}
</div>
)}
</div>
{error && (
<p className="flex animate-slide-up items-center gap-1 text-sm font-medium text-error-500">
<svg
className="h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
{error}
</p>
)}
{helperText && !error && (
<p className="text-sm text-text-tertiary">{helperText}</p>
)}
</div>
);
}
);
ModernInput.displayName = 'ModernInput';

View File

@@ -0,0 +1,299 @@
/**
* Modern Navigation Component (Reusable)
* Professional UI Component Library
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
'use client';
import React from 'react';
import { cn } from '@/utils';
import { ChevronRight, ChevronDown } from 'lucide-react';
export interface NavigationItem {
id: string;
label: string;
href?: string;
icon?: React.ReactNode;
badge?: string | number;
isActive?: boolean;
isCompleted?: boolean;
isDisabled?: boolean;
children?: NavigationItem[];
onClick?: () => void;
}
export interface ModernNavigationProps {
items: NavigationItem[];
variant?: 'vertical' | 'horizontal' | 'breadcrumb';
size?: 'sm' | 'md' | 'lg';
showIcons?: boolean;
showBadges?: boolean;
allowCollapse?: boolean;
onItemClick?: (item: NavigationItem) => void;
className?: string;
}
export function ModernNavigation({
items,
variant = 'vertical',
size = 'md',
showIcons = true,
showBadges = true,
allowCollapse = false,
onItemClick,
className = '',
}: ModernNavigationProps) {
const [collapsedItems, setCollapsedItems] = React.useState<Set<string>>(
new Set()
);
const toggleCollapse = (itemId: string) => {
if (!allowCollapse) return;
setCollapsedItems(prev => {
const newSet = new Set(prev);
if (newSet.has(itemId)) {
newSet.delete(itemId);
} else {
newSet.add(itemId);
}
return newSet;
});
};
const handleItemClick = (item: NavigationItem, e?: React.MouseEvent) => {
if (item.isDisabled) return;
if (item.children && allowCollapse) {
e?.preventDefault();
toggleCollapse(item.id);
}
if (item.onClick) {
item.onClick();
}
if (onItemClick) {
onItemClick(item);
}
};
const getSizeClasses = () => {
switch (size) {
case 'sm':
return 'text-sm px-2 py-1.5';
case 'lg':
return 'text-lg px-4 py-3';
default:
return 'text-base px-3 py-2';
}
};
const renderBadge = (badge: string | number) => (
<span className="inline-flex h-5 w-5 items-center justify-center rounded-full bg-primary-500 text-xs font-medium text-white">
{badge}
</span>
);
const renderIcon = (
icon: React.ReactNode,
isCompleted?: boolean,
isActive?: boolean
) => {
if (!showIcons && !isCompleted) return null;
return (
<div
className={cn(
'flex h-5 w-5 items-center justify-center',
isCompleted && 'text-success-600',
isActive && !isCompleted && 'text-primary-600'
)}
>
{icon}
</div>
);
};
const renderVerticalItem = (item: NavigationItem, level = 0) => {
const isCollapsed = collapsedItems.has(item.id);
const hasChildren = item.children && item.children.length > 0;
return (
<div key={item.id}>
<div
className={cn(
'flex cursor-pointer items-center justify-between rounded-lg transition-all duration-200',
getSizeClasses(),
item.isActive &&
'bg-primary-50 font-medium text-primary-700 shadow-sm',
item.isCompleted && !item.isActive && 'text-success-600',
item.isDisabled && 'cursor-not-allowed opacity-50',
!item.isActive && !item.isDisabled && 'hover:bg-grayscale-50',
level > 0 && 'border-grayscale-200 ml-6 border-l-2 pl-4'
)}
onClick={e => handleItemClick(item, e)}
style={{ paddingLeft: level > 0 ? `${level * 1.5}rem` : undefined }}
>
<div className="flex items-center gap-3">
{renderIcon(item.icon, item.isCompleted, item.isActive)}
<span className="truncate">{item.label}</span>
{showBadges && item.badge && renderBadge(item.badge)}
</div>
{hasChildren && allowCollapse && (
<div className="flex items-center">
{isCollapsed ? (
<ChevronRight className="text-grayscale-500 h-4 w-4" />
) : (
<ChevronDown className="text-grayscale-500 h-4 w-4" />
)}
</div>
)}
</div>
{hasChildren && (!allowCollapse || !isCollapsed) && (
<div className="mt-1 space-y-1">
{item.children!.map(child => renderVerticalItem(child, level + 1))}
</div>
)}
</div>
);
};
const renderHorizontalItem = (item: NavigationItem, isLast: boolean) => (
<div key={item.id} className="flex items-center">
<div
className={cn(
'flex cursor-pointer items-center gap-2 rounded-lg transition-all duration-200',
getSizeClasses(),
item.isActive && 'bg-primary-50 font-medium text-primary-700',
item.isCompleted && !item.isActive && 'text-success-600',
item.isDisabled && 'cursor-not-allowed opacity-50',
!item.isActive && !item.isDisabled && 'hover:bg-grayscale-50'
)}
onClick={() => handleItemClick(item)}
>
{renderIcon(item.icon, item.isCompleted, item.isActive)}
<span className="truncate">{item.label}</span>
{showBadges && item.badge && renderBadge(item.badge)}
</div>
{!isLast && (
<ChevronRight className="text-grayscale-400 mx-2 h-4 w-4 flex-shrink-0" />
)}
</div>
);
const renderBreadcrumbItem = (item: NavigationItem, isLast: boolean) => (
<div key={item.id} className="flex items-center">
<div
className={cn(
'flex items-center gap-2 transition-colors duration-200',
!isLast && 'cursor-pointer hover:text-primary-600',
isLast && 'text-text-primary font-medium',
!isLast && 'text-text-secondary',
item.isDisabled && 'cursor-not-allowed opacity-50'
)}
onClick={() => !isLast && handleItemClick(item)}
>
{renderIcon(item.icon, item.isCompleted, item.isActive)}
<span className="truncate">{item.label}</span>
</div>
{!isLast && (
<ChevronRight className="text-grayscale-400 mx-2 h-4 w-4 flex-shrink-0" />
)}
</div>
);
const containerClasses = cn(
'modern-navigation',
variant === 'vertical' && 'space-y-1',
variant === 'horizontal' && 'flex items-center flex-wrap',
variant === 'breadcrumb' && 'flex items-center flex-wrap text-sm',
className
);
return (
<nav className={containerClasses} role="navigation">
{variant === 'vertical' && items.map(item => renderVerticalItem(item))}
{variant === 'horizontal' &&
items.map((item, index) =>
renderHorizontalItem(item, index === items.length - 1)
)}
{variant === 'breadcrumb' &&
items.map((item, index) =>
renderBreadcrumbItem(item, index === items.length - 1)
)}
</nav>
);
}
// Export additional components for specific use cases
export function ModernSidebar({
items,
title,
className = '',
...props
}: Omit<ModernNavigationProps, 'variant'> & { title?: string }) {
return (
<aside
className={cn(
'bg-background-primary border-grayscale-200 w-64 border-r p-4',
className
)}
>
{title && (
<h2 className="text-text-primary mb-4 text-lg font-semibold">
{title}
</h2>
)}
<ModernNavigation
{...props}
items={items}
variant="vertical"
allowCollapse={true}
/>
</aside>
);
}
export function ModernBreadcrumbs({
items,
className = '',
...props
}: Omit<ModernNavigationProps, 'variant'>) {
return (
<ModernNavigation
{...props}
items={items}
variant="breadcrumb"
showBadges={false}
className={className}
/>
);
}
export function ModernTabNavigation({
items,
className = '',
...props
}: Omit<ModernNavigationProps, 'variant'>) {
return (
<div className={cn('border-grayscale-200 border-b', className)}>
<ModernNavigation
{...props}
items={items}
variant="horizontal"
size="md"
/>
</div>
);
}

View File

@@ -0,0 +1,175 @@
/**
* Modern Page Header Component (Reusable)
* Professional UI Component Library
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
'use client';
import React from 'react';
import { cn } from '@/utils';
export interface HeaderAction {
id: string;
label: string;
icon?: React.ReactNode;
onClick: () => void;
variant?: 'primary' | 'secondary' | 'ghost';
disabled?: boolean;
}
export interface ModernPageHeaderProps {
title: string;
subtitle?: string;
badge?: {
text: string;
variant?: 'default' | 'primary' | 'success' | 'warning' | 'error';
};
logo?: {
src?: string;
alt?: string;
fallback?: React.ReactNode;
};
actions?: HeaderAction[];
breadcrumbs?: {
label: string;
href?: string;
}[];
variant?: 'default' | 'compact' | 'elevated';
className?: string;
children?: React.ReactNode;
}
export const ModernPageHeader: React.FC<ModernPageHeaderProps> = ({
title,
subtitle,
badge,
logo,
actions = [],
breadcrumbs = [],
variant = 'default',
className = '',
children,
}) => {
const variantClasses = {
default: 'bg-white border-b border-grayscale-200 shadow-soft',
compact: 'bg-white border-b border-grayscale-200',
elevated: 'bg-white border-b border-grayscale-200 shadow-medium',
};
const badgeVariants = {
default: 'bg-grayscale-100 text-grayscale-800 border-grayscale-200',
primary: 'bg-primary-100 text-primary-800 border-primary-200',
success: 'bg-success-100 text-success-800 border-success-200',
warning: 'bg-warning-100 text-warning-800 border-warning-200',
error: 'bg-error-100 text-error-800 border-error-200',
};
const buttonVariants = {
primary: 'bg-primary-500 text-white hover:bg-primary-600 shadow-medium',
secondary:
'bg-grayscale-100 text-grayscale-800 hover:bg-grayscale-200 border border-grayscale-300',
ghost: 'text-grayscale-600 hover:bg-grayscale-100 hover:text-grayscale-900',
};
return (
<header className={cn(variantClasses[variant], 'px-6 py-4', className)}>
<div className="flex items-center justify-between">
{/* Left section - Logo, Title, Subtitle */}
<div className="flex items-center gap-4">
{logo && (
<div className="flex-shrink-0">
{logo.src ? (
<img
src={logo.src}
alt={logo.alt || 'Logo'}
className="h-10 w-10 rounded-lg object-cover"
/>
) : logo.fallback ? (
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-primary-500 to-primary-600 font-bold text-white">
{logo.fallback}
</div>
) : null}
</div>
)}
<div>
{/* Breadcrumbs */}
{breadcrumbs.length > 0 && (
<nav className="text-grayscale-600 mb-1 flex items-center text-sm">
{breadcrumbs.map((crumb, index) => (
<React.Fragment key={index}>
{crumb.href ? (
<a
href={crumb.href}
className="transition-colors hover:text-primary-600"
>
{crumb.label}
</a>
) : (
<span className="text-grayscale-900 font-medium">
{crumb.label}
</span>
)}
{index < breadcrumbs.length - 1 && (
<span className="mx-2">/</span>
)}
</React.Fragment>
))}
</nav>
)}
{/* Title and Badge */}
<div className="flex items-center gap-3">
<h1 className="text-grayscale-900 text-xl font-bold">{title}</h1>
{badge && (
<span
className={cn(
'rounded-md border px-2 py-1 text-xs font-medium',
badgeVariants[badge.variant || 'default']
)}
>
{badge.text}
</span>
)}
</div>
{/* Subtitle */}
{subtitle && (
<p className="text-grayscale-600 mt-1 text-sm">{subtitle}</p>
)}
</div>
</div>
{/* Right section - Actions */}
<div className="flex items-center gap-3">
{actions.map(action => (
<button
key={action.id}
onClick={action.onClick}
disabled={action.disabled}
className={cn(
'flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-all duration-200',
buttonVariants[action.variant || 'secondary'],
action.disabled && 'cursor-not-allowed opacity-50'
)}
>
{action.icon}
{action.label}
</button>
))}
</div>
</div>
{/* Custom children content */}
{children && (
<div className="border-grayscale-200 mt-4 border-t pt-4">
{children}
</div>
)}
</header>
);
};

View File

@@ -0,0 +1,76 @@
/**
* Modern Loading Spinner Component
* Professional Resume Builder - Reusable UI Component
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
'use client';
import React from 'react';
import { cn } from '@/utils';
export interface ModernSpinnerProps {
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
variant?: 'primary' | 'secondary' | 'white';
className?: string;
label?: string;
}
export const ModernSpinner: React.FC<ModernSpinnerProps> = ({
size = 'md',
variant = 'primary',
className = '',
label = 'Loading...',
}) => {
const sizeClasses = {
xs: 'w-4 h-4',
sm: 'w-5 h-5',
md: 'w-6 h-6',
lg: 'w-8 h-8',
xl: 'w-10 h-10',
};
const variantClasses = {
primary: 'text-primary-600',
secondary: 'text-grayscale-600',
white: 'text-white',
};
return (
<div
className="inline-flex items-center justify-center"
role="status"
aria-label={label}
>
<svg
className={cn(
'animate-spin',
sizeClasses[size],
variantClasses[variant],
className
)}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="m4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
<span className="sr-only">{label}</span>
</div>
);
};

View File

@@ -0,0 +1,220 @@
/**
* Modern Stepper Component (Reusable)
* Professional UI Component Library
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
'use client';
import React from 'react';
import { cn } from '@/utils';
import { Check } from 'lucide-react';
export interface StepItem {
id: string;
title: string;
description?: string;
icon?: React.ReactNode;
completed: boolean;
disabled?: boolean;
}
export interface ModernStepperProps {
steps: StepItem[];
currentStep: string;
onStepChange?: (stepId: string) => void;
variant?: 'horizontal' | 'vertical';
size?: 'sm' | 'md' | 'lg';
showProgress?: boolean;
clickable?: boolean;
className?: string;
}
export const ModernStepper: React.FC<ModernStepperProps> = ({
steps,
currentStep,
onStepChange,
variant = 'horizontal',
size = 'md',
showProgress = true,
clickable = true,
className = '',
}) => {
const currentStepIndex = steps.findIndex(step => step.id === currentStep);
const completedSteps = steps.filter(step => step.completed).length;
const progressPercentage = (completedSteps / steps.length) * 100;
const sizeClasses = {
sm: {
step: 'w-6 h-6 text-xs',
title: 'text-sm',
description: 'text-xs',
gap: 'gap-2',
},
md: {
step: 'w-8 h-8 text-sm',
title: 'text-base',
description: 'text-sm',
gap: 'gap-3',
},
lg: {
step: 'w-10 h-10 text-base',
title: 'text-lg',
description: 'text-base',
gap: 'gap-4',
},
};
const handleStepClick = (stepId: string, disabled?: boolean) => {
if (clickable && !disabled && onStepChange) {
onStepChange(stepId);
}
};
const renderStepIndicator = (step: StepItem, index: number) => {
const isActive = step.id === currentStep;
const isCompleted = step.completed;
const isPast = index < currentStepIndex;
const isFuture = index > currentStepIndex;
const stepClasses = cn(
'flex items-center justify-center rounded-full border-2 transition-all duration-200 font-medium',
sizeClasses[size].step,
{
// Completed state
'bg-success-500 border-success-500 text-white': isCompleted,
// Active state
'bg-primary-500 border-primary-500 text-white':
isActive && !isCompleted,
// Past state
'bg-grayscale-100 border-grayscale-300 text-grayscale-600':
isPast && !isCompleted,
// Future state
'bg-white border-grayscale-300 text-grayscale-500': isFuture,
// Disabled
'opacity-50 cursor-not-allowed': step.disabled,
// Clickable
'cursor-pointer hover:scale-105': clickable && !step.disabled,
}
);
return (
<div
className={stepClasses}
onClick={() => handleStepClick(step.id, step.disabled)}
>
{isCompleted ? (
<Check className="h-4 w-4" />
) : step.icon ? (
step.icon
) : (
index + 1
)}
</div>
);
};
const renderHorizontalStepper = () => (
<div className={cn('w-full', className)}>
{showProgress && (
<div className="mb-4">
<div className="text-grayscale-600 mb-1 flex justify-between text-sm">
<span>Progress</span>
<span>{Math.round(progressPercentage)}%</span>
</div>
<div className="bg-grayscale-200 h-2 w-full rounded-full">
<div
className="h-2 rounded-full bg-gradient-to-r from-primary-500 to-primary-600 transition-all duration-500 ease-out"
style={{ width: `${progressPercentage}%` }}
/>
</div>
</div>
)}
<div className="flex items-center justify-between">
{steps.map((step, index) => (
<React.Fragment key={step.id}>
<div
className={cn(
'flex flex-col items-center',
sizeClasses[size].gap
)}
>
{renderStepIndicator(step, index)}
<div className="text-center">
<div
className={cn(
'text-grayscale-900 font-medium',
sizeClasses[size].title
)}
>
{step.title}
</div>
{step.description && (
<div
className={cn(
'text-grayscale-600 mt-1',
sizeClasses[size].description
)}
>
{step.description}
</div>
)}
</div>
</div>
{index < steps.length - 1 && (
<div className="mx-4 flex-1">
<div
className={cn(
'h-0.5 transition-colors duration-300',
index < currentStepIndex || steps[index].completed
? 'bg-primary-500'
: 'bg-grayscale-300'
)}
/>
</div>
)}
</React.Fragment>
))}
</div>
</div>
);
const renderVerticalStepper = () => (
<div className={cn('space-y-4', className)}>
{steps.map((step, index) => (
<div key={step.id} className="flex items-start gap-4">
{renderStepIndicator(step, index)}
<div className="flex-1">
<div
className={cn(
'text-grayscale-900 font-medium',
sizeClasses[size].title
)}
>
{step.title}
</div>
{step.description && (
<div
className={cn(
'text-grayscale-600 mt-1',
sizeClasses[size].description
)}
>
{step.description}
</div>
)}
</div>
</div>
))}
</div>
);
return variant === 'horizontal'
? renderHorizontalStepper()
: renderVerticalStepper();
};

252
src/components/ui/README.md Normal file
View File

@@ -0,0 +1,252 @@
# 🎨 UI Component Library Documentation
**Professional Resume Builder - Reusable Components**
**Author:** David Valera Melendez (david@valera-melendez.de)
**Created:** 2025-08-07
**Location:** Made in Germany 🇩🇪
---
## 📚 Component Library Overview
This is a professional, enterprise-ready component library built with **TypeScript**, **React**, and **Tailwind CSS**. Each component is designed for maximum reusability, type safety, and developer experience.
## 🧩 Available Components
### 1. **ModernCard**
Professional card component with multiple variants and padding options.
```tsx
import { ModernCard } from '@/components/ui';
<ModernCard variant="elevated" padding="lg">
<p>Your content here</p>
</ModernCard>;
```
**Props:**
- `variant`: 'default' | 'elevated' | 'outlined' | 'filled'
- `padding`: 'none' | 'sm' | 'md' | 'lg' | 'xl'
- `hoverable`: boolean
- `onClick`: () => void
### 2. **ModernInput**
Enhanced form input with icons, validation, and multiple variants.
```tsx
import { ModernInput } from '@/components/ui';
import { Mail } from 'lucide-react';
<ModernInput
label="Email Address"
type="email"
placeholder="your.email@example.com"
leftIcon={<Mail className="h-4 w-4" />}
variant="filled"
required
/>;
```
**Props:**
- `variant`: 'default' | 'filled' | 'outlined'
- `leftIcon`: React.ReactNode
- `rightIcon`: React.ReactNode
- `error`: string
- `helperText`: string
### 3. **ModernButton**
Professional button component with loading states and multiple variants.
```tsx
import { ModernButton } from '@/components/ui';
<ModernButton
variant="primary"
size="lg"
loading={isLoading}
onClick={handleSubmit}
>
Submit Application
</ModernButton>;
```
**Props:**
- `variant`: 'primary' | 'secondary' | 'outline' | 'ghost'
- `size`: 'sm' | 'md' | 'lg'
- `loading`: boolean
- `disabled`: boolean
### 4. **ModernBadge**
Versatile badge component for status indicators and tags.
```tsx
import { ModernBadge } from '@/components/ui';
import { CheckCircle } from 'lucide-react';
<ModernBadge variant="success" icon={<CheckCircle className="h-3 w-3" />}>
Verified
</ModernBadge>;
```
**Props:**
- `variant`: 'default' | 'primary' | 'success' | 'warning' | 'error' | 'info'
- `size`: 'sm' | 'md' | 'lg'
- `rounded`: 'sm' | 'md' | 'lg' | 'full'
- `icon`: React.ReactNode
### 5. **ModernAvatar**
Professional avatar component with fallbacks and status indicators.
```tsx
import { ModernAvatar } from '@/components/ui';
<ModernAvatar
src="/profile.jpg"
size="lg"
fallback="DV"
status="online"
showStatus={true}
/>;
```
**Props:**
- `size`: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'
- `status`: 'online' | 'offline' | 'away' | 'busy'
- `showStatus`: boolean
- `fallback`: string
### 6. **ModernSpinner**
Loading spinner with multiple sizes and variants.
```tsx
import { ModernSpinner } from '@/components/ui';
<ModernSpinner size="lg" variant="primary" />;
```
**Props:**
- `size`: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
- `variant`: 'primary' | 'secondary' | 'white'
- `label`: string (for accessibility)
## 🎨 Design System
### Design Tokens
Centralized design system with consistent spacing, colors, and styling.
```tsx
import { DESIGN_TOKENS, getToken } from '@/components/ui';
// Access design tokens
const spacing = getToken.spacing('lg'); // 24px
const shadow = getToken.shadow('medium');
const radius = getToken.radius('xl');
```
### Theme Colors
- **Primary:** Blue (#0ea5e9) - Main brand color
- **Success:** Green (#22c55e) - Success states
- **Warning:** Orange (#f59e0b) - Warning states
- **Error:** Red (#ef4444) - Error states
- **Accent:** Purple (#a855f7) - Accent elements
## 🏗️ Architecture Benefits
### ✅ **Enterprise-Ready Features**
- **TypeScript First:** Full type safety and IntelliSense
- **Consistent API:** All components follow the same patterns
- **Accessibility:** WCAG 2.1 compliant with ARIA labels
- **Performance:** Optimized with React best practices
- **Responsive:** Mobile-first design approach
### ✅ **Developer Experience**
- **Barrel Exports:** Clean imports from single entry point
- **Prop Forwarding:** Extends native HTML props
- **Design Tokens:** Centralized styling system
- **Documentation:** Comprehensive type definitions
### ✅ **Scalability**
- **Variant System:** Easy to extend with new styles
- **Composition:** Components work together seamlessly
- **Tree Shaking:** Only import what you use
- **Theme Support:** Easy to customize colors and spacing
## 📦 Usage Examples
### Import Components
```tsx
// Single component import
import { ModernCard } from '@/components/ui/ModernCard';
// Multiple components (recommended)
import { ModernCard, ModernButton, ModernInput } from '@/components/ui';
// Import with types
import { ModernCard, type ModernCardProps } from '@/components/ui';
```
### Form Example
```tsx
import { ModernCard, ModernInput, ModernButton } from '@/components/ui';
export function ContactForm() {
return (
<ModernCard variant="elevated" padding="lg">
<div className="space-y-4">
<ModernInput
label="Full Name"
placeholder="Enter your name"
variant="filled"
required
/>
<ModernInput
label="Email"
type="email"
placeholder="your.email@example.com"
variant="filled"
required
/>
<ModernButton variant="primary" size="lg" className="w-full">
Send Message
</ModernButton>
</div>
</ModernCard>
);
}
```
## 🚀 Professional Standards
This component library demonstrates:
- **Clean Code:** Well-structured, readable, and maintainable
- **Best Practices:** Following React and TypeScript conventions
- **Performance:** Optimized for production use
- **Scalability:** Easy to extend and customize
- **Documentation:** Clear examples and API references
- **Testing Ready:** Components designed for easy unit testing
---
**Built by David Valera Melendez - Professional Frontend Developer**
**Portfolio:** https://valera-melendez.de
**Email:** david@valera-melendez.de

View File

@@ -0,0 +1,152 @@
/**
* User Dropdown Component
* Reusable header dropdown with user information and navigation
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { User, FileText, ChevronDown, Settings, LogOut } from 'lucide-react';
interface UserDropdownProps {
/** Current page identifier to highlight in dropdown */
currentPage?: 'dashboard' | 'settings';
/** Whether to show the logo icon */
showLogo?: boolean;
/** Custom CSS classes */
className?: string;
}
/**
* UserDropdown Component - Reusable header dropdown
*
* Professional user dropdown component with user information display
* and navigation options. Can be customized for different pages.
*
* Features:
* - User information display (name + email)
* - Navigation menu with current page highlighting
* - Settings and logout functionality
* - Optional logo display
* - Responsive design
*
* @param currentPage - Current page to highlight in dropdown
* @param showLogo - Whether to display the logo icon
* @param className - Additional CSS classes
* @returns Reusable user dropdown component
*/
export function UserDropdown({
currentPage,
showLogo = false,
className = '',
}: UserDropdownProps) {
const router = useRouter();
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
/**
* Handles navigation to settings page
*/
const handleAccountSettings = () => {
setIsUserMenuOpen(false);
router.push('/settings');
};
/**
* Handles logout functionality
*/
const handleLogout = () => {
setIsUserMenuOpen(false);
// Here you would typically clear user session/tokens
console.log('Logging out...');
// Redirect to login or home page
router.push('/');
};
/**
* Closes dropdown when clicking outside
*/
const handleDropdownToggle = () => {
setIsUserMenuOpen(!isUserMenuOpen);
};
return (
<div className={`flex items-center gap-3 ${className}`}>
{/* Optional Logo */}
{showLogo && (
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-primary-600 to-primary-700 shadow-sm">
<FileText className="h-6 w-6 text-white" />
</div>
)}
{/* User Info Display */}
<div className="text-right">
<h1 className="text-lg font-bold text-primary-700">
David Valera Melendez
</h1>
<p className="text-xs text-gray-500">david@valera-melendez.de</p>
</div>
{/* User Dropdown */}
<div className="relative">
<button
onClick={handleDropdownToggle}
className="flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-100"
aria-label="User menu"
aria-expanded={isUserMenuOpen}
>
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-gradient-to-br from-primary-100 to-primary-200">
<User className="h-4 w-4 text-primary-600" />
</div>
<ChevronDown className="h-4 w-4" />
</button>
{/* Dropdown Menu */}
{isUserMenuOpen && (
<>
{/* Backdrop for mobile */}
<div
className="fixed inset-0 z-40 lg:hidden"
onClick={() => setIsUserMenuOpen(false)}
/>
<div className="absolute right-0 top-full z-50 mt-2 w-56 rounded-lg border border-gray-200 bg-white shadow-lg">
<div className="py-1">
{/* Settings Option */}
<button
onClick={handleAccountSettings}
className={`flex w-full items-center gap-2 px-4 py-2 text-left text-sm transition-colors ${
currentPage === 'settings'
? 'bg-primary-50 text-primary-600'
: 'text-gray-700 hover:bg-gray-50'
}`}
>
<Settings className="h-4 w-4" />
Konto-Einstellungen
</button>
{/* Divider */}
<div className="mx-1 my-1 border-t border-gray-100" />
{/* Logout Option */}
<button
onClick={handleLogout}
className="flex w-full items-center gap-2 px-4 py-2 text-left text-sm text-red-600 transition-colors hover:bg-red-50"
>
<LogOut className="h-4 w-4" />
Abmelden
</button>
</div>
</div>
</>
)}
</div>
</div>
);
}
export default UserDropdown;

View File

@@ -0,0 +1,129 @@
/**
* Design System Tokens
* Professional Resume Builder - Design System Constants
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
// Design Tokens for consistent styling across components
export const DESIGN_TOKENS = {
// Spacing Scale (Tailwind-based)
spacing: {
none: '0',
xs: '0.5rem', // 8px
sm: '0.75rem', // 12px
md: '1rem', // 16px
lg: '1.5rem', // 24px
xl: '2rem', // 32px
'2xl': '3rem', // 48px
'3xl': '4rem', // 64px
},
// Border Radius
radius: {
none: '0',
sm: '0.375rem', // 6px
md: '0.5rem', // 8px
lg: '0.75rem', // 12px
xl: '1rem', // 16px
'2xl': '1.5rem', // 24px
full: '9999px',
},
// Shadow Scale
shadows: {
none: 'none',
soft: '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
medium: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
large: '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)',
xl: '0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)',
},
// Animation Durations
animation: {
fast: '150ms',
normal: '300ms',
slow: '500ms',
},
// Breakpoints (for responsive design)
breakpoints: {
sm: '640px',
md: '768px',
lg: '1024px',
xl: '1280px',
'2xl': '1536px',
},
// Z-Index Scale
zIndex: {
hide: -1,
auto: 'auto',
base: 0,
docked: 10,
dropdown: 1000,
sticky: 1100,
banner: 1200,
overlay: 1300,
modal: 1400,
popover: 1500,
skipLink: 1600,
toast: 1700,
tooltip: 1800,
},
// Component Variants
variants: {
card: ['default', 'elevated', 'outlined', 'filled'],
button: ['primary', 'secondary', 'outline', 'ghost'],
input: ['default', 'filled', 'outlined'],
alert: ['info', 'success', 'warning', 'error'],
},
// Professional Color Palette Reference
colors: {
primary: 'rgb(14 165 233)', // Blue
success: 'rgb(34 197 94)', // Green
warning: 'rgb(245 158 11)', // Orange
error: 'rgb(239 68 68)', // Red
accent: 'rgb(168 85 247)', // Purple
},
} as const;
// Type definitions for design tokens
export type SpacingKey = keyof typeof DESIGN_TOKENS.spacing;
export type RadiusKey = keyof typeof DESIGN_TOKENS.radius;
export type ShadowKey = keyof typeof DESIGN_TOKENS.shadows;
export type BreakpointKey = keyof typeof DESIGN_TOKENS.breakpoints;
// Utility function to get design token values
export const getToken = {
spacing: (key: SpacingKey) => DESIGN_TOKENS.spacing[key],
radius: (key: RadiusKey) => DESIGN_TOKENS.radius[key],
shadow: (key: ShadowKey) => DESIGN_TOKENS.shadows[key],
breakpoint: (key: BreakpointKey) => DESIGN_TOKENS.breakpoints[key],
};
// Component size presets
export const SIZE_PRESETS = {
button: {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
},
input: {
sm: 'px-3 py-2 text-sm',
md: 'px-4 py-3 text-base',
lg: 'px-5 py-4 text-lg',
},
card: {
sm: 'p-4',
md: 'p-6',
lg: 'p-8',
xl: 'p-10',
},
} as const;
export default DESIGN_TOKENS;

View File

@@ -0,0 +1,52 @@
/**
* UI Components Barrel Export
* Professional Resume Builder - Reusable Component Library
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
// Core UI Components
export { ModernCard } from './ModernCard';
export { ModernInput } from './ModernInput';
export { ModernButton } from './ModernButton';
export { ModernBadge } from './ModernBadge';
export { ModernAvatar } from './ModernAvatar';
export { ModernSpinner } from './ModernSpinner';
export { ModernStepper } from './ModernStepper';
export { ModernPageHeader } from './ModernPageHeader';
export { ModernFormList } from './ModernFormList';
export {
ModernNavigation,
ModernSidebar,
ModernBreadcrumbs,
ModernTabNavigation,
} from './ModernNavigation';
// Design System
export { DESIGN_TOKENS, getToken, SIZE_PRESETS } from './design-tokens';
// Type exports for better developer experience
export type { ModernCardProps } from './ModernCard';
export type { ModernInputProps } from './ModernInput';
export type { ModernButtonProps } from './ModernButton';
export type { ModernBadgeProps } from './ModernBadge';
export type { ModernAvatarProps } from './ModernAvatar';
export type { ModernSpinnerProps } from './ModernSpinner';
export type { ModernStepperProps, StepItem } from './ModernStepper';
export type { ModernPageHeaderProps, HeaderAction } from './ModernPageHeader';
export type {
ModernFormListProps,
FormFieldConfig,
ListItemData,
} from './ModernFormList';
export type { ModernNavigationProps, NavigationItem } from './ModernNavigation';
// Design token types
export type {
SpacingKey,
RadiusKey,
ShadowKey,
BreakpointKey,
} from './design-tokens';

287
src/data/formFields.ts Normal file
View File

@@ -0,0 +1,287 @@
/**
* Form Field Configurations
* Professional Resume Builder
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
import { SelectOption } from './formOptions';
/**
* Form field configuration interface
*/
export interface FormField {
key: string;
label: string;
type: 'text' | 'email' | 'tel' | 'url' | 'date' | 'textarea' | 'select';
placeholder?: string;
required?: boolean;
width?: 'full' | 'half' | 'third' | 'quarter';
options?: SelectOption[];
icon?: React.ReactNode;
validation?: {
minLength?: number;
maxLength?: number;
pattern?: RegExp;
};
}
/**
* Experience form field configuration
* Professional work experience entry fields
*/
export const experienceFormFields: FormField[] = [
{
key: 'position',
label: 'Job Title',
type: 'text',
placeholder: 'e.g., Senior Frontend Developer',
required: true,
width: 'full',
},
{
key: 'company',
label: 'Company Name',
type: 'text',
placeholder: 'e.g., Tech Innovation GmbH',
required: true,
width: 'half',
},
{
key: 'location',
label: 'Location',
type: 'text',
placeholder: 'e.g., Berlin, Germany',
required: true,
width: 'half',
},
{
key: 'startDate',
label: 'Start Date',
type: 'date',
required: true,
width: 'half',
},
{
key: 'endDate',
label: 'End Date',
type: 'date',
required: false,
width: 'half',
},
{
key: 'description',
label: 'Job Description',
type: 'textarea',
placeholder: 'Describe your main responsibilities and role...',
required: false,
width: 'full',
},
];
/**
* Education form field configuration
* Academic history entry fields
*/
export const educationFormFields: FormField[] = [
{
key: 'degree',
label: 'Degree/Program',
type: 'text',
placeholder: 'e.g., Bachelor of Science in Computer Science',
required: true,
width: 'full',
},
{
key: 'institution',
label: 'Institution',
type: 'text',
placeholder: 'e.g., Technical University of Berlin',
required: true,
width: 'half',
},
{
key: 'location',
label: 'Location',
type: 'text',
placeholder: 'e.g., Berlin, Germany',
required: true,
width: 'half',
},
{
key: 'startDate',
label: 'Start Date',
type: 'date',
required: true,
width: 'half',
},
{
key: 'endDate',
label: 'End Date',
type: 'date',
required: false,
width: 'half',
},
{
key: 'gpa',
label: 'Grade/GPA',
type: 'text',
placeholder: 'e.g., 1.5 (German system) or 3.8/4.0',
required: false,
width: 'half',
},
{
key: 'description',
label: 'Description',
type: 'textarea',
placeholder: 'Relevant coursework, achievements, thesis topic...',
required: false,
width: 'full',
},
];
/**
* Skills form field configuration
* Technical and soft skills entry
*/
export const skillsFormFields: FormField[] = [
{
key: 'name',
label: 'Skill Name',
type: 'text',
placeholder: 'e.g., React.js, Leadership, German',
required: true,
width: 'half',
},
{
key: 'category',
label: 'Category',
type: 'select',
required: true,
width: 'quarter',
},
{
key: 'level',
label: 'Proficiency Level',
type: 'select',
required: true,
width: 'quarter',
},
];
/**
* Project form field configuration
* Personal and professional projects
*/
export const projectFormFields: FormField[] = [
{
key: 'name',
label: 'Project Name',
type: 'text',
placeholder: 'e.g., E-commerce Platform',
required: true,
width: 'full',
},
{
key: 'role',
label: 'Your Role',
type: 'text',
placeholder: 'e.g., Lead Developer, Team Member',
required: true,
width: 'half',
},
{
key: 'technologies',
label: 'Technologies Used',
type: 'text',
placeholder: 'e.g., React, Node.js, PostgreSQL',
required: true,
width: 'half',
},
{
key: 'startDate',
label: 'Start Date',
type: 'date',
required: false,
width: 'half',
},
{
key: 'endDate',
label: 'End Date',
type: 'date',
required: false,
width: 'half',
},
{
key: 'url',
label: 'Project URL',
type: 'url',
placeholder: 'https://github.com/username/project',
required: false,
width: 'full',
},
{
key: 'description',
label: 'Project Description',
type: 'textarea',
placeholder:
'Describe the project goals, your contributions, and key achievements...',
required: false,
width: 'full',
},
];
/**
* Certification form field configuration
* Professional certifications and licenses
*/
export const certificationFormFields: FormField[] = [
{
key: 'name',
label: 'Certification Name',
type: 'text',
placeholder: 'e.g., AWS Certified Solutions Architect',
required: true,
width: 'full',
},
{
key: 'issuer',
label: 'Issuing Organization',
type: 'text',
placeholder: 'e.g., Amazon Web Services',
required: true,
width: 'half',
},
{
key: 'issueDate',
label: 'Issue Date',
type: 'date',
required: true,
width: 'half',
},
{
key: 'expiryDate',
label: 'Expiry Date',
type: 'date',
required: false,
width: 'half',
},
{
key: 'credentialUrl',
label: 'Credential URL',
type: 'url',
placeholder: 'https://credentials.example.com/cert-id',
required: false,
width: 'half',
},
{
key: 'description',
label: 'Description',
type: 'textarea',
placeholder: 'Brief description of the certification and skills gained...',
required: false,
width: 'full',
},
];

168
src/data/formOptions.ts Normal file
View File

@@ -0,0 +1,168 @@
/**
* Form Options and Configuration Data
* Professional Resume Builder
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
/**
* Common option interface for select fields
*/
export interface SelectOption {
value: string;
label: string;
}
/**
* Nationality options for European CV format
* Ordered by relevance for German job market
*/
export const nationalityOptions: SelectOption[] = [
{ value: 'German', label: 'German' },
{ value: 'EU Citizen', label: 'EU Citizen' },
{ value: 'Austrian', label: 'Austrian' },
{ value: 'Swiss', label: 'Swiss' },
{ value: 'French', label: 'French' },
{ value: 'Italian', label: 'Italian' },
{ value: 'Spanish', label: 'Spanish' },
{ value: 'Dutch', label: 'Dutch' },
{ value: 'Belgian', label: 'Belgian' },
{ value: 'Polish', label: 'Polish' },
{ value: 'Czech', label: 'Czech' },
{ value: 'Hungarian', label: 'Hungarian' },
{ value: 'Romanian', label: 'Romanian' },
{ value: 'Bulgarian', label: 'Bulgarian' },
{ value: 'Croatian', label: 'Croatian' },
{ value: 'Non-EU', label: 'Non-EU' },
{ value: 'American', label: 'American' },
{ value: 'Canadian', label: 'Canadian' },
{ value: 'British', label: 'British' },
{ value: 'Australian', label: 'Australian' },
{ value: 'Other', label: 'Other' },
];
/**
* Marital status options according to European CV standards
*/
export const maritalStatusOptions: SelectOption[] = [
{ value: 'Single', label: 'Single' },
{ value: 'Married', label: 'Married' },
{ value: 'Divorced', label: 'Divorced' },
{ value: 'Widowed', label: 'Widowed' },
{ value: 'Civil Partnership', label: 'Civil Partnership' },
{ value: 'Other', label: 'Other' },
];
/**
* Visa status options for German/European employment
*/
export const visaStatusOptions: SelectOption[] = [
{ value: 'German Citizen', label: 'German Citizen' },
{ value: 'EU Citizen', label: 'EU Citizen' },
{ value: 'Blue Card', label: 'EU Blue Card' },
{ value: 'Work Permit', label: 'Work Permit' },
{ value: 'Student Visa', label: 'Student Visa (Working Rights)' },
{ value: 'Visa Required', label: 'Visa Required' },
{ value: 'Other', label: 'Other' },
];
/**
* Skill categories for professional development
* Organized by modern tech industry standards
*/
export const skillCategories: string[] = [
'Programming Languages',
'Frontend Development',
'Backend Development',
'Full Stack Development',
'Mobile Development',
'DevOps & Cloud',
'Database Technologies',
'Frameworks & Libraries',
'Tools & Technologies',
'Design & UI/UX',
'Data Science & Analytics',
'Machine Learning & AI',
'Quality Assurance',
'Project Management',
'Soft Skills',
'Communication',
'Languages',
'Certifications',
'Other',
];
/**
* Proficiency levels with European framework alignment
*/
export const proficiencyLevels: SelectOption[] = [
{ value: 'Beginner', label: 'Beginner (A1-A2)' },
{ value: 'Intermediate', label: 'Intermediate (B1-B2)' },
{ value: 'Advanced', label: 'Advanced (C1)' },
{ value: 'Expert', label: 'Expert/Native (C2)' },
];
/**
* Language proficiency levels following CEFR standards
*/
export const languageProficiencyLevels: SelectOption[] = [
{ value: 'A1', label: 'A1 - Beginner' },
{ value: 'A2', label: 'A2 - Elementary' },
{ value: 'B1', label: 'B1 - Intermediate' },
{ value: 'B2', label: 'B2 - Upper Intermediate' },
{ value: 'C1', label: 'C1 - Advanced' },
{ value: 'C2', label: 'C2 - Proficient' },
{ value: 'Native', label: 'Native Speaker' },
];
/**
* Common European countries for location/experience fields
*/
export const europeanCountries: SelectOption[] = [
{ value: 'Germany', label: 'Germany' },
{ value: 'Austria', label: 'Austria' },
{ value: 'Switzerland', label: 'Switzerland' },
{ value: 'Netherlands', label: 'Netherlands' },
{ value: 'Belgium', label: 'Belgium' },
{ value: 'France', label: 'France' },
{ value: 'Italy', label: 'Italy' },
{ value: 'Spain', label: 'Spain' },
{ value: 'Portugal', label: 'Portugal' },
{ value: 'United Kingdom', label: 'United Kingdom' },
{ value: 'Ireland', label: 'Ireland' },
{ value: 'Denmark', label: 'Denmark' },
{ value: 'Sweden', label: 'Sweden' },
{ value: 'Norway', label: 'Norway' },
{ value: 'Finland', label: 'Finland' },
{ value: 'Poland', label: 'Poland' },
{ value: 'Czech Republic', label: 'Czech Republic' },
{ value: 'Hungary', label: 'Hungary' },
{ value: 'Romania', label: 'Romania' },
{ value: 'Bulgaria', label: 'Bulgaria' },
{ value: 'Croatia', label: 'Croatia' },
{ value: 'Other', label: 'Other' },
];
/**
* German major cities for location fields
*/
export const germanCities: SelectOption[] = [
{ value: 'Berlin', label: 'Berlin' },
{ value: 'Munich', label: 'Munich (München)' },
{ value: 'Hamburg', label: 'Hamburg' },
{ value: 'Frankfurt am Main', label: 'Frankfurt am Main' },
{ value: 'Cologne', label: 'Cologne (Köln)' },
{ value: 'Stuttgart', label: 'Stuttgart' },
{ value: 'Düsseldorf', label: 'Düsseldorf' },
{ value: 'Leipzig', label: 'Leipzig' },
{ value: 'Dortmund', label: 'Dortmund' },
{ value: 'Essen', label: 'Essen' },
{ value: 'Dresden', label: 'Dresden' },
{ value: 'Bremen', label: 'Bremen' },
{ value: 'Hannover', label: 'Hannover' },
{ value: 'Nuremberg', label: 'Nuremberg (Nürnberg)' },
{ value: 'Duisburg', label: 'Duisburg' },
{ value: 'Other', label: 'Other' },
];

23
src/data/index.ts Normal file
View File

@@ -0,0 +1,23 @@
/**
* Data Module Exports
* Professional Resume Builder - Centralized Data Management
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
// Form Options and Configuration
export * from './formOptions';
// Form Field Configurations
export * from './formFields';
// UI Text Constants and Messages
export * from './uiConstants';
// Resume Data Templates
export * from './resumeTemplates';
// Legacy support - keep existing sample data available
export * from './sampleResumeData';

480
src/data/resumeTemplates.ts Normal file
View File

@@ -0,0 +1,480 @@
/**
* Resume Data Templates
* Professional Resume Builder
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
import { ResumeData } from '@/types/resume';
/**
* Empty resume data template with proper structure
* Provides a clean starting point for new resumes
*/
export const emptyResumeTemplate: ResumeData = {
personalInfo: {
// Basic Information
firstName: '',
lastName: '',
jobTitle: '',
// Contact Information
email: '',
phone: '',
street: '',
zipCode: '',
city: '',
country: '',
location: '', // Keep for backward compatibility
// Professional Links
website: '',
linkedin: '',
github: '',
// Personal Details (Modern CV style)
nationality: '',
dateOfBirth: '',
placeOfBirth: '',
maritalStatus: undefined,
visaStatus: undefined,
// Professional Summary
summary: '',
},
experience: [],
education: [],
skills: [],
languages: [],
certifications: [],
projects: [],
};
/**
* Demo resume template with realistic German tech worker data
* Perfect for demonstrations and testing
*/
export const demoResumeTemplate: ResumeData = {
personalInfo: {
firstName: 'Max',
lastName: 'Müller',
jobTitle: 'Senior Full Stack Developer',
email: 'max.mueller@example.de',
phone: '+49 30 12345678',
street: 'Alexanderstraße',
zipCode: '10178',
city: 'Berlin',
country: 'Germany',
location: 'Berlin, Germany',
website: 'https://maxmueller.dev',
linkedin: 'linkedin.com/in/maxmueller-dev',
github: 'github.com/maxmueller',
nationality: 'German',
dateOfBirth: '1990-03-15',
maritalStatus: 'Single',
visaStatus: 'German Citizen',
summary:
'Experienced Full Stack Developer with 6+ years in modern web technologies. Specialized in React, Node.js, and cloud architectures. Passionate about clean code, user experience, and team collaboration in agile environments.',
},
experience: [
{
id: '1',
position: 'Senior Full Stack Developer',
company: 'TechStart Berlin GmbH',
location: 'Berlin, Germany',
startDate: '2022-03',
endDate: '',
isCurrentPosition: true,
description:
'Lead development of scalable web applications using modern JavaScript stack. Mentor junior developers and collaborate with product teams on technical solutions.',
achievements: [
'Architected and built a microservices platform serving 100,000+ daily active users',
'Reduced application loading time by 60% through code optimization and caching strategies',
'Led migration from monolithic to microservices architecture',
'Mentored 3 junior developers and established code review processes',
'Implemented CI/CD pipelines reducing deployment time from hours to minutes',
],
},
{
id: '2',
position: 'Full Stack Developer',
company: 'Digital Solutions AG',
location: 'Munich, Germany',
startDate: '2019-08',
endDate: '2022-02',
isCurrentPosition: false,
description:
'Developed and maintained various client projects using React, Node.js, and PostgreSQL. Collaborated with design and product teams to deliver high-quality solutions.',
achievements: [
'Delivered 15+ successful client projects on time and within budget',
'Improved team productivity by implementing automated testing workflows',
'Built responsive web applications for automotive and fintech industries',
'Integrated third-party APIs and payment systems (Stripe, PayPal)',
],
},
{
id: '3',
position: 'Junior Frontend Developer',
company: 'WebCraft Studio',
location: 'Hamburg, Germany',
startDate: '2018-06',
endDate: '2019-07',
isCurrentPosition: false,
description:
'Started career focusing on frontend development with React and modern CSS. Worked on e-commerce and corporate websites.',
achievements: [
'Contributed to 10+ successful website launches',
'Learned modern frontend technologies and best practices',
'Collaborated with senior developers on complex user interfaces',
],
},
],
education: [
{
id: '1',
degree: 'Bachelor of Science in Computer Science',
institution: 'Technical University of Berlin',
location: 'Berlin, Germany',
startDate: '2014',
endDate: '2018',
gpa: '1.8',
description:
'Focus on software engineering, web technologies, and database systems. Thesis: "Performance Optimization in Single Page Applications"',
},
{
id: '2',
degree: 'Abitur (German High School Diploma)',
institution: 'Gymnasium Berlin-Mitte',
location: 'Berlin, Germany',
startDate: '2012',
endDate: '2014',
gpa: '1.5',
description: 'Mathematics and Computer Science focus',
},
],
skills: [
{
id: '1',
name: 'JavaScript',
category: 'Programming Languages',
level: 'Expert',
},
{
id: '2',
name: 'TypeScript',
category: 'Programming Languages',
level: 'Advanced',
},
{
id: '3',
name: 'Python',
category: 'Programming Languages',
level: 'Intermediate',
},
{
id: '4',
name: 'React.js',
category: 'Frontend Development',
level: 'Expert',
},
{
id: '5',
name: 'Next.js',
category: 'Frontend Development',
level: 'Advanced',
},
{
id: '6',
name: 'Vue.js',
category: 'Frontend Development',
level: 'Intermediate',
},
{
id: '7',
name: 'Node.js',
category: 'Backend Development',
level: 'Advanced',
},
{
id: '8',
name: 'Express.js',
category: 'Backend Development',
level: 'Advanced',
},
{
id: '9',
name: 'PostgreSQL',
category: 'Database Technologies',
level: 'Advanced',
},
{
id: '10',
name: 'MongoDB',
category: 'Database Technologies',
level: 'Intermediate',
},
{
id: '11',
name: 'AWS',
category: 'DevOps & Cloud',
level: 'Intermediate',
},
{
id: '12',
name: 'Docker',
category: 'DevOps & Cloud',
level: 'Intermediate',
},
{
id: '13',
name: 'Git',
category: 'Tools & Technologies',
level: 'Advanced',
},
{
id: '14',
name: 'Tailwind CSS',
category: 'Frontend Development',
level: 'Advanced',
},
{
id: '15',
name: 'Team Leadership',
category: 'Soft Skills',
level: 'Advanced',
},
{
id: '16',
name: 'Agile/Scrum',
category: 'Project Management',
level: 'Advanced',
},
],
languages: [
{ id: '1', name: 'German', proficiency: 'Native' },
{ id: '2', name: 'English', proficiency: 'C1' },
{ id: '3', name: 'Spanish', proficiency: 'B1' },
],
certifications: [
{
id: '1',
name: 'AWS Certified Solutions Architect - Associate',
issuer: 'Amazon Web Services',
issueDate: '2023-05-15',
expirationDate: '2026-05-15',
credentialId: 'AWS-SAA-XXXXX',
url: 'https://aws.amazon.com/verification/XXXXX',
},
{
id: '2',
name: 'Professional Scrum Master I (PSM I)',
issuer: 'Scrum.org',
issueDate: '2022-11-20',
credentialId: 'PSM-I-XXXXX',
url: 'https://scrum.org/certificates/XXXXX',
},
],
projects: [
{
id: '1',
name: 'E-Commerce Platform',
description:
'Full-stack e-commerce solution with inventory management, payment processing, and admin dashboard. Built with modern tech stack and deployed on AWS.',
technologies: ['React', 'Node.js', 'PostgreSQL', 'Stripe API'],
startDate: '2023-01',
endDate: '2023-08',
url: 'https://github.com/maxmueller/ecommerce-platform',
achievements: [],
},
{
id: '2',
name: 'Task Management App',
description:
'Collaborative task management application with real-time updates, team collaboration features, and responsive design.',
technologies: ['Next.js', 'TypeScript', 'Supabase'],
startDate: '2022-06',
endDate: '2022-10',
url: 'https://taskmanager.maxmueller.dev',
achievements: [],
},
{
id: '3',
name: 'Open Source Component Library',
description:
'Reusable React component library with comprehensive documentation and automated testing. Used by 500+ developers.',
technologies: ['React', 'TypeScript', 'Storybook', 'npm'],
startDate: '2021-09',
endDate: null,
url: 'https://github.com/maxmueller/ui-components',
achievements: [],
},
],
};
/**
* Creates a personalized resume template based on role
*/
export const createRoleBasedTemplate = (
role: 'frontend' | 'backend' | 'fullstack' | 'mobile' | 'devops'
): ResumeData => {
const baseTemplate = { ...emptyResumeTemplate };
const roleSkills = {
frontend: [
{
id: '1',
name: 'React.js',
category: 'Frontend Development',
level: 'Advanced',
},
{
id: '2',
name: 'TypeScript',
category: 'Programming Languages',
level: 'Advanced',
},
{
id: '3',
name: 'CSS/SASS',
category: 'Frontend Development',
level: 'Advanced',
},
{
id: '4',
name: 'JavaScript',
category: 'Programming Languages',
level: 'Expert',
},
{
id: '5',
name: 'HTML5',
category: 'Frontend Development',
level: 'Expert',
},
],
backend: [
{
id: '1',
name: 'Node.js',
category: 'Backend Development',
level: 'Advanced',
},
{
id: '2',
name: 'Python',
category: 'Programming Languages',
level: 'Advanced',
},
{
id: '3',
name: 'PostgreSQL',
category: 'Database Technologies',
level: 'Advanced',
},
{
id: '4',
name: 'REST APIs',
category: 'Backend Development',
level: 'Advanced',
},
{
id: '5',
name: 'Docker',
category: 'DevOps & Cloud',
level: 'Intermediate',
},
],
fullstack: [
{
id: '1',
name: 'JavaScript',
category: 'Programming Languages',
level: 'Expert',
},
{
id: '2',
name: 'React.js',
category: 'Frontend Development',
level: 'Advanced',
},
{
id: '3',
name: 'Node.js',
category: 'Backend Development',
level: 'Advanced',
},
{
id: '4',
name: 'PostgreSQL',
category: 'Database Technologies',
level: 'Advanced',
},
{
id: '5',
name: 'TypeScript',
category: 'Programming Languages',
level: 'Advanced',
},
],
mobile: [
{
id: '1',
name: 'React Native',
category: 'Mobile Development',
level: 'Advanced',
},
{
id: '2',
name: 'Swift',
category: 'Programming Languages',
level: 'Intermediate',
},
{
id: '3',
name: 'Kotlin',
category: 'Programming Languages',
level: 'Intermediate',
},
{
id: '4',
name: 'JavaScript',
category: 'Programming Languages',
level: 'Advanced',
},
{
id: '5',
name: 'Mobile UI/UX',
category: 'Design & UI/UX',
level: 'Advanced',
},
],
devops: [
{ id: '1', name: 'AWS', category: 'DevOps & Cloud', level: 'Advanced' },
{
id: '2',
name: 'Docker',
category: 'DevOps & Cloud',
level: 'Advanced',
},
{
id: '3',
name: 'Kubernetes',
category: 'DevOps & Cloud',
level: 'Intermediate',
},
{ id: '4', name: 'CI/CD', category: 'DevOps & Cloud', level: 'Advanced' },
{
id: '5',
name: 'Linux',
category: 'Tools & Technologies',
level: 'Advanced',
},
],
};
baseTemplate.skills = roleSkills[role] as any[];
return baseTemplate;
};

View File

@@ -0,0 +1,139 @@
/**
* Sample Resume Data Service
* Resume Builder Application
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
import { ResumeData } from '@/types/resume';
/**
* Sample resume data for preview and testing purposes
*
* This data serves as a template and demonstration of the resume structure.
* It can be used for:
* - Preview functionality
* - Testing components
* - Default template for new resumes
*/
export const sampleResumeData: ResumeData = {
personalInfo: {
firstName: 'David',
lastName: 'Valera Melendez',
jobTitle: 'Senior Frontend Developer',
email: 'david@valera-melendez.de',
phone: '+49 123 456 7890',
city: 'Berlin',
country: 'Deutschland',
location: 'Berlin, Deutschland',
dateOfBirth: '1990-01-01',
nationality: 'German',
maritalStatus: 'Single' as any,
summary:
'Experienced Frontend Developer specializing in React and TypeScript',
},
experience: [
{
id: '1',
position: 'Senior Frontend Developer',
company: 'Tech Company GmbH',
location: 'Berlin, Germany',
startDate: '2022-01',
endDate: '',
isCurrentPosition: true,
description:
'Entwicklung moderner Webanwendungen mit React und TypeScript',
achievements: [
'Leitung eines 5-köpfigen Entwicklerteams',
'Implementierung einer neuen Architektur mit 40% besserer Performance',
'Einführung von Best Practices und Code Reviews',
],
},
{
id: '2',
position: 'Frontend Developer',
company: 'Digital Agency Berlin',
location: 'Berlin, Germany',
startDate: '2020-06',
endDate: '2021-12',
isCurrentPosition: false,
description: 'Entwicklung responsiver Websites und Webanwendungen',
achievements: [
'Entwicklung von 15+ erfolgreichen Kundenprojekten',
'Optimierung der Ladezeiten um durchschnittlich 60%',
],
},
],
education: [
{
id: '1',
degree: 'Bachelor of Science in Computer Science',
institution: 'Technische Universität Berlin',
location: 'Berlin, Germany',
startDate: '2016',
endDate: '2020',
description: 'Schwerpunkt: Softwareentwicklung und Web-Technologies',
},
],
skills: [
{ id: '1', name: 'React', category: 'Frontend', level: 'Advanced' },
{ id: '2', name: 'TypeScript', category: 'Frontend', level: 'Advanced' },
{ id: '3', name: 'Next.js', category: 'Frontend', level: 'Advanced' },
{ id: '4', name: 'Tailwind CSS', category: 'Frontend', level: 'Advanced' },
{ id: '5', name: 'Node.js', category: 'Backend', level: 'Intermediate' },
{
id: '6',
name: 'PostgreSQL',
category: 'Database',
level: 'Intermediate',
},
],
languages: [
{ id: '1', name: 'Deutsch', proficiency: 'Native' },
{ id: '2', name: 'English', proficiency: 'C2' },
{ id: '3', name: 'Español', proficiency: 'A2' },
],
certifications: [],
projects: [],
};
/**
* Get sample resume data
*
* @returns Sample resume data object
*/
export const getSampleResumeData = (): ResumeData => {
return sampleResumeData;
};
/**
* Create empty resume data template
*
* @returns Empty resume data structure
*/
export const createEmptyResumeData = (): ResumeData => {
return {
personalInfo: {
firstName: '',
lastName: '',
jobTitle: '',
email: '',
phone: '',
city: '',
country: '',
location: '',
dateOfBirth: '',
nationality: '',
maritalStatus: 'Single' as any,
summary: '',
},
experience: [],
education: [],
skills: [],
languages: [],
certifications: [],
projects: [],
};
};

225
src/data/uiConstants.ts Normal file
View File

@@ -0,0 +1,225 @@
/**
* UI Text Constants and Messages
* Professional Resume Builder
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
/**
* Placeholder text constants for form fields
* Consistent placeholder text across the application
*/
export const placeholders = {
// Personal Information
firstName: 'Enter your first name',
lastName: 'Enter your last name',
jobTitle: 'e.g., Senior Frontend Developer',
email: 'your.email@example.com',
phone: '+49 123 456 7890',
// Address
street: 'Musterstraße',
houseNumber: '123',
zipCode: '12345',
city: 'Berlin',
country: 'Deutschland',
// Professional Links
website: 'https://your-website.com',
linkedin: 'linkedin.com/in/yourprofile',
github: 'github.com/yourusername',
// Location
location: 'Berlin, Germany',
// Professional Summary
summary: 'Write a brief professional summary about yourself...',
// Experience
company: 'Tech Innovation GmbH',
position: 'Senior Frontend Developer',
workLocation: 'Berlin, Germany',
jobDescription: 'Describe your main responsibilities and achievements...',
// Education
degree: 'Bachelor of Science in Computer Science',
institution: 'Technical University of Berlin',
educationLocation: 'Berlin, Germany',
gpa: '1.5 (German system) or 3.8/4.0',
academicDescription: 'Relevant coursework, achievements, thesis topic...',
// Skills
skillName: 'React.js',
// Languages
languageInput: 'English (Native), Spanish (C1), German (B2)',
// Certifications
certificationInput: 'List your certifications, one per line',
// Projects
projectName: 'E-commerce Platform',
projectRole: 'Lead Developer',
projectTechnologies: 'React, Node.js, PostgreSQL',
projectUrl: 'https://github.com/username/project',
projectDescription:
'Describe the project goals, your contributions, and key achievements...',
// Generic
description: 'Enter description...',
achievements: 'List your key achievements...',
} as const;
/**
* Empty state messages for different sections
*/
export const emptyStateMessages = {
experience: {
title: 'No work experience added yet',
description:
'Click "Add Experience" to get started with your professional history',
icon: '💼',
},
education: {
title: 'No education added yet',
description: 'Click "Add Education" to include your academic background',
icon: '🎓',
},
skills: {
title: 'No skills added yet',
description:
'Add your technical and soft skills to showcase your expertise',
icon: '⚡',
},
languages: {
title: 'No languages added yet',
description: 'Add your language skills with proficiency levels',
icon: '🌍',
},
certifications: {
title: 'No certifications added yet',
description: 'Add your professional certifications and licenses',
icon: '🏆',
},
projects: {
title: 'No projects added yet',
description: 'Showcase your personal and professional projects',
icon: '🚀',
},
achievements: {
title: 'No achievements added yet',
description: 'Add key accomplishments and achievements',
icon: '🎯',
},
} as const;
/**
* Form section titles and descriptions
*/
export const sectionTitles = {
personalInfo: {
title: 'Personal Information',
description: 'Your name and basic details',
},
professionalInfo: {
title: 'Professional Information',
description: 'Job title and profile image',
},
contactInfo: {
title: 'Contact Information',
description: 'Email, phone, and address',
},
addressInfo: {
title: 'Address',
description: 'Your current location',
},
onlinePresence: {
title: 'Online Presence',
description: 'Your professional links',
},
personalDetails: {
title: 'Personal Details',
description: 'Additional personal information',
},
experience: {
title: 'Work Experience',
description: 'Your professional work history',
},
education: {
title: 'Education',
description: 'Your academic background',
},
skills: {
title: 'Skills',
description: 'Technical and soft skills',
},
languages: {
title: 'Languages',
description: 'Language skills with proficiency levels',
},
certifications: {
title: 'Certifications',
description: 'Professional certifications and licenses',
},
projects: {
title: 'Projects',
description: 'Personal and professional projects',
},
summary: {
title: 'Professional Summary',
description: 'Brief overview of your career and goals',
},
} as const;
/**
* Button text constants
*/
export const buttonTexts = {
add: 'Add',
addExperience: 'Add Experience',
addEducation: 'Add Education',
addSkill: 'Add Skill',
addLanguage: 'Add Language',
addCertification: 'Add Certification',
addProject: 'Add Project',
addAchievement: 'Add Achievement',
edit: 'Edit',
save: 'Save',
cancel: 'Cancel',
delete: 'Delete',
done: 'Done',
upload: 'Upload',
uploadImage: 'Upload Profile Image',
remove: 'Remove',
preview: 'Preview',
export: 'Export PDF',
back: 'Back',
next: 'Next',
finish: 'Finish',
} as const;
/**
* Validation messages
*/
export const validationMessages = {
required: 'This field is required',
emailInvalid: 'Please enter a valid email address',
phoneInvalid: 'Please enter a valid phone number',
urlInvalid: 'Please enter a valid URL',
dateInvalid: 'Please enter a valid date',
minLength: (min: number) => `Must be at least ${min} characters`,
maxLength: (max: number) => `Must be no more than ${max} characters`,
} as const;
/**
* Step descriptions for the resume builder process
*/
export const stepDescriptions = {
contact: 'Add your personal and contact information',
experience: 'Document your professional work history',
education: 'Include your educational background',
skills: 'Showcase your technical and soft skills',
about: 'Write a professional summary and add additional details',
} as const;

314
src/hooks/auth.ts Normal file
View File

@@ -0,0 +1,314 @@
/**
* Route Protection Hooks
* Professional Next.js Resume Builder - Enterprise Auth System
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
'use client';
import React, { useEffect } from 'react';
import { useRouter, usePathname } from 'next/navigation';
import { useAuthStore } from '@/lib/auth-store';
import { UserRole } from '@/types/auth';
/**
* Hook to protect routes that require authentication
*/
export function useAuthGuard(options?: {
requiredRoles?: UserRole[];
redirectTo?: string;
checkOnMount?: boolean;
}) {
const router = useRouter();
const pathname = usePathname();
const { isAuthenticated, user, checkAuth } = useAuthStore();
const {
requiredRoles = [],
redirectTo = '/auth/login',
checkOnMount = true,
} = options || {};
useEffect(() => {
if (checkOnMount) {
checkAuth();
}
}, [checkAuth, checkOnMount]);
useEffect(() => {
// Skip check during initial load
if (checkOnMount && !isAuthenticated && !user) {
return;
}
// Check authentication
if (!isAuthenticated) {
const loginUrl = new URL(redirectTo, window.location.origin);
loginUrl.searchParams.set('returnUrl', pathname);
router.push(loginUrl.toString() as any);
return;
}
// Check role requirements
if (requiredRoles.length > 0 && user) {
const hasRequiredRole = requiredRoles.some(role =>
user.roles.includes(role)
);
if (!hasRequiredRole) {
router.push('/access-denied');
return;
}
}
}, [isAuthenticated, user, router, pathname, requiredRoles, redirectTo]);
return {
isAuthenticated,
user,
hasRequiredRole: (role: UserRole) => user?.roles.includes(role) || false,
hasAnyRole: (roles: UserRole[]) => roles.some(role => user?.roles.includes(role)) || false,
};
}
/**
* Hook to protect routes that should only be accessible to guests (non-authenticated users)
*/
export function useGuestGuard(redirectTo: string = '/dashboard') {
const router = useRouter();
const { isAuthenticated, checkAuth } = useAuthStore();
useEffect(() => {
checkAuth();
}, [checkAuth]);
useEffect(() => {
if (isAuthenticated) {
router.push(redirectTo as any);
}
}, [isAuthenticated, router, redirectTo]);
return { isAuthenticated };
}
/**
* Hook to check if user has specific permissions
*/
export function usePermissions() {
const { user } = useAuthStore();
const hasPermission = (permission: string): boolean => {
return user?.permissions?.includes(permission) || false;
};
const hasRole = (role: UserRole): boolean => {
return user?.roles?.includes(role) || false;
};
const hasAnyRole = (roles: UserRole[]): boolean => {
return roles.some(role => hasRole(role));
};
const hasAllRoles = (roles: UserRole[]): boolean => {
return roles.every(role => hasRole(role));
};
const isAdmin = (): boolean => {
return hasRole(UserRole.ADMIN);
};
const isModerator = (): boolean => {
return hasRole(UserRole.MODERATOR);
};
const canAccess = (resource: string, action: string): boolean => {
const permission = `${resource}:${action}`;
return hasPermission(permission) || isAdmin();
};
return {
user,
hasPermission,
hasRole,
hasAnyRole,
hasAllRoles,
isAdmin,
isModerator,
canAccess,
};
}
/**
* Hook for handling authentication redirects
*/
export function useAuthRedirect() {
const router = useRouter();
const pathname = usePathname();
const redirectToLogin = (returnUrl?: string) => {
const loginUrl = new URL('/auth/login', window.location.origin);
loginUrl.searchParams.set('returnUrl', returnUrl || pathname);
router.push(loginUrl.toString() as any);
};
const redirectToDashboard = () => {
router.push('/dashboard');
};
const redirectToReturnUrl = (defaultUrl: string = '/dashboard') => {
const urlParams = new URLSearchParams(window.location.search);
const returnUrl = urlParams.get('returnUrl');
if (returnUrl && returnUrl.startsWith('/') && !returnUrl.startsWith('//')) {
router.push(returnUrl as any);
} else {
router.push(defaultUrl as any);
}
};
const redirectToAccessDenied = () => {
router.push('/access-denied');
};
return {
redirectToLogin,
redirectToDashboard,
redirectToReturnUrl,
redirectToAccessDenied,
};
}
/**
* Hook to check authentication status and handle loading states
*/
export function useAuthStatus() {
const {
isAuthenticated,
isLoading,
user,
error,
checkAuth,
clearError
} = useAuthStore();
useEffect(() => {
if (!isAuthenticated && !isLoading) {
checkAuth();
}
}, [isAuthenticated, isLoading, checkAuth]);
return {
isAuthenticated,
isLoading,
user,
error,
clearError,
};
}
/**
* Hook for checking specific route permissions
*/
export function useRoutePermissions(routePath: string) {
const { user } = useAuthStore();
// Define route permissions
const routePermissions: Record<string, {
requiredRoles?: UserRole[];
requiredPermissions?: string[];
}> = {
'/admin': { requiredRoles: [UserRole.ADMIN] },
'/admin/users': { requiredRoles: [UserRole.ADMIN] },
'/admin/analytics': { requiredRoles: [UserRole.ADMIN] },
'/moderation': { requiredRoles: [UserRole.ADMIN, UserRole.MODERATOR] },
'/dashboard': { requiredRoles: [UserRole.USER, UserRole.ADMIN, UserRole.MODERATOR] },
'/profile': { requiredRoles: [UserRole.USER, UserRole.ADMIN, UserRole.MODERATOR] },
'/resume-builder': { requiredRoles: [UserRole.USER, UserRole.ADMIN, UserRole.MODERATOR] },
};
const permissions = routePermissions[routePath];
if (!permissions) {
return { canAccess: true, missingPermissions: [] };
}
const missingPermissions: string[] = [];
let canAccess = true;
// Check required roles
if (permissions.requiredRoles && user) {
const hasRequiredRole = permissions.requiredRoles.some(role =>
user.roles.includes(role)
);
if (!hasRequiredRole) {
canAccess = false;
missingPermissions.push(`Required roles: ${permissions.requiredRoles.join(', ')}`);
}
}
// Check required permissions
if (permissions.requiredPermissions && user) {
const missingPerms = permissions.requiredPermissions.filter(permission =>
!user.permissions?.includes(permission)
);
if (missingPerms.length > 0) {
canAccess = false;
missingPermissions.push(`Required permissions: ${missingPerms.join(', ')}`);
}
}
return {
canAccess,
missingPermissions,
requiredRoles: permissions.requiredRoles || [],
requiredPermissions: permissions.requiredPermissions || [],
};
}
/**
* Higher-order component for route protection
*/
export function withAuthGuard<T extends object>(
Component: React.ComponentType<T>,
options?: {
requiredRoles?: UserRole[];
redirectTo?: string;
loading?: React.ComponentType;
}
) {
return function AuthGuardedComponent(props: T) {
const { isAuthenticated, user } = useAuthGuard(options);
if (!isAuthenticated || !user) {
if (options?.loading) {
const LoadingComponent = options.loading;
return React.createElement(LoadingComponent);
}
return React.createElement('div', null, 'Loading...');
}
return React.createElement(Component, props);
};
}
/**
* Higher-order component for guest-only routes
*/
export function withGuestGuard<T extends object>(
Component: React.ComponentType<T>,
redirectTo?: string
) {
return function GuestGuardedComponent(props: T) {
const { isAuthenticated } = useGuestGuard(redirectTo);
if (isAuthenticated) {
return React.createElement('div', null, 'Redirecting...');
}
return React.createElement(Component, props);
};
}

110
src/lib/api-endpoints.ts Normal file
View File

@@ -0,0 +1,110 @@
/**
* API Endpoints Configuration
* Professional Next.js Resume Builder - Enterprise Auth System
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
/**
* Environment configuration
*/
const config = {
API_BASE_URL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080',
AUTH_BASE_URL: process.env.NEXT_PUBLIC_AUTH_API_URL || 'http://localhost:8080/api/auth',
WS_BASE_URL: process.env.NEXT_PUBLIC_WS_URL || 'ws://localhost:8080',
APP_NAME: 'Professional Resume Builder',
APP_VERSION: '1.0.0',
REQUEST_TIMEOUT: 30000,
MAX_RETRY_ATTEMPTS: 3,
TOKEN_REFRESH_THRESHOLD: 5 * 60 * 1000, // 5 minutes
};
/**
* Centralized API endpoint management for professional organization
*/
export class ApiEndpoints {
// Base URLs
private static readonly API_BASE = config.API_BASE_URL;
private static readonly AUTH_BASE = config.AUTH_BASE_URL;
// Authentication Endpoints - following exact Java backend structure
static readonly AUTH = {
// Core authentication
LOGIN: `${ApiEndpoints.AUTH_BASE}/login`,
REGISTER: `${ApiEndpoints.AUTH_BASE}/register`,
LOGOUT: `${ApiEndpoints.AUTH_BASE}/logout`,
LOGOUT_ALL: `${ApiEndpoints.AUTH_BASE}/logout-all`,
REFRESH_TOKEN: `${ApiEndpoints.AUTH_BASE}/refresh`,
// Device verification
VERIFY_DEVICE: `${ApiEndpoints.AUTH_BASE}/verify-device`,
RESEND_DEVICE_VERIFICATION: `${ApiEndpoints.AUTH_BASE}/resend-device-verification`,
// Two-Factor Authentication - matching Java backend endpoints
TWO_FACTOR: {
VERIFY: `${ApiEndpoints.AUTH_BASE}/verify-2fa`,
RESEND: `${ApiEndpoints.AUTH_BASE}/resend-2fa`,
},
// Password Management
PASSWORD_RESET: {
REQUEST: `${ApiEndpoints.AUTH_BASE}/password-reset`,
CONFIRM: `${ApiEndpoints.AUTH_BASE}/password-reset/confirm`
},
// Profile Management
PROFILE: `${ApiEndpoints.AUTH_BASE}/profile`,
UPDATE_PROFILE: `${ApiEndpoints.AUTH_BASE}/profile`,
// Session Management
SESSIONS: `${ApiEndpoints.AUTH_BASE}/sessions`,
REVOKE_SESSION: `${ApiEndpoints.AUTH_BASE}/sessions/revoke`,
};
// Application Endpoints
static readonly APP = {
HEALTH: `${ApiEndpoints.API_BASE}/health`,
VERSION: `${ApiEndpoints.API_BASE}/version`,
};
// User Management Endpoints
static readonly USERS = {
BASE: `${ApiEndpoints.API_BASE}/users`,
PROFILE: `${ApiEndpoints.API_BASE}/users/profile`,
PREFERENCES: `${ApiEndpoints.API_BASE}/users/preferences`,
};
// Resume Builder Endpoints (for future use)
static readonly RESUMES = {
BASE: `${ApiEndpoints.API_BASE}/resumes`,
TEMPLATES: `${ApiEndpoints.API_BASE}/templates`,
EXPORT: `${ApiEndpoints.API_BASE}/resumes/export`,
};
/**
* Build URL with query parameters
*/
static buildUrl(endpoint: string, params?: Record<string, any>): string {
if (!params) return endpoint;
const url = new URL(endpoint);
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
url.searchParams.append(key, String(value));
}
});
return url.toString();
}
/**
* Get configuration values
*/
static get config() {
return { ...config };
}
}
export default ApiEndpoints;

471
src/lib/auth-store.ts Normal file
View File

@@ -0,0 +1,471 @@
/**
* Authentication Store
* Professional Next.js Resume Builder - Enterprise Auth System
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { devtools } from 'zustand/middleware';
import {
AuthState,
AuthActions,
AuthUser,
LoginCredentials,
RegisterData,
TwoFactorVerificationRequest,
DeviceVerificationRequest,
AuthResponse,
AuthError,
TwoFactorPageData,
DeviceVerificationResponse,
} from '@/types/auth';
import { ApiEndpoints } from '@/lib/api-endpoints';
import { HttpClientService } from '@/lib/http-client';
import { EncryptionService } from '@/lib/encryption';
import { DeviceFingerprintService } from '@/lib/device-fingerprint';
/**
* Combined auth store state and actions
*/
interface AuthStore extends AuthState, AuthActions {
// Helper methods
handleSuccessfulAuth: (response: AuthResponse, rememberMe?: boolean) => void;
storeTokens: (tokens: { accessToken: string; refreshToken?: string }, rememberMe?: boolean) => void;
getAccessToken: () => string | null;
getRefreshToken: () => string | null;
clearAuthState: () => void;
isTokenValid: (token: string) => boolean;
transformError: (error: any) => AuthError;
resendDeviceVerification: (sessionId: string) => Promise<void>;
}
/**
* Enterprise-grade authentication store with Zustand
* Handles authentication state, token management, and security features
*/
export const useAuthStore = create<AuthStore>()(
devtools(
persist(
(set, get) => ({
// Initial state
user: null,
isAuthenticated: false,
isLoading: false,
requiresTwoFactor: false,
requiresDeviceVerification: false,
twoFactorData: null,
deviceVerificationData: null,
error: null,
// Actions
login: async (credentials: LoginCredentials) => {
const httpClient = HttpClientService.getInstance();
const encryptionService = EncryptionService.getInstance();
const deviceService = DeviceFingerprintService.getInstance();
set({ isLoading: true, error: null });
try {
// Generate device fingerprint
const deviceFingerprint = deviceService.generateFingerprint();
// Create secure encrypted payload
const securePayload = encryptionService.createSecurePayloadSync(credentials.password);
// Prepare login payload with encrypted password and device fingerprint
const loginPayload = {
email: credentials.email,
fingerprint: deviceFingerprint,
...securePayload
};
const response = await httpClient.post<AuthResponse>(
ApiEndpoints.AUTH.LOGIN,
loginPayload,
{ includeFingerprint: true }
);
const authResponse = response.data;
if (authResponse.requiresTwoFactor) {
// 2FA is required
set({
requiresTwoFactor: true,
twoFactorData: {
userId: authResponse.user.id,
userEmail: authResponse.user.email,
userName: `${authResponse.user.firstName} ${authResponse.user.lastName}`,
reason: authResponse.reason,
riskScore: authResponse.riskScore,
tempToken: authResponse.tempToken,
sessionToken: authResponse.sessionToken,
timestamp: Date.now(),
},
isLoading: false,
});
} else if (authResponse.requiresDeviceVerification) {
// Device verification is required
set({
requiresDeviceVerification: true,
deviceVerificationData: {
deviceId: '',
isKnownDevice: false,
isTrustedDevice: false,
requiresTwoFactor: false,
device: {
name: deviceService.generateDeviceName(),
platform: deviceFingerprint.platform,
registeredAt: new Date(),
},
user: {
firstName: authResponse.user.firstName,
lastName: authResponse.user.lastName,
email: authResponse.user.email,
fullName: `${authResponse.user.firstName} ${authResponse.user.lastName}`,
},
},
twoFactorData: {
userId: authResponse.user.id,
userEmail: authResponse.user.email,
userName: `${authResponse.user.firstName} ${authResponse.user.lastName}`,
sessionToken: authResponse.sessionToken,
timestamp: Date.now(),
},
isLoading: false,
});
} else {
// Login successful
get().handleSuccessfulAuth(authResponse, credentials.rememberMe);
}
} catch (error) {
set({
error: get().transformError(error),
isLoading: false,
});
throw error;
}
},
register: async (data: RegisterData) => {
const httpClient = HttpClientService.getInstance();
const encryptionService = EncryptionService.getInstance();
set({ isLoading: true, error: null });
try {
// Create secure encrypted payloads
const passwordPayload = encryptionService.createSecurePayloadSync(data.password);
const confirmPasswordPayload = encryptionService.createSecurePayloadSync(data.confirmPassword);
const registerRequest = {
firstName: data.firstName,
lastName: data.lastName,
email: data.email,
encryptedPassword: passwordPayload.encryptedPassword,
passwordEncryptionMeta: passwordPayload.encryptionMeta,
encryptedConfirmPassword: confirmPasswordPayload.encryptedPassword,
confirmPasswordEncryptionMeta: confirmPasswordPayload.encryptionMeta,
acceptTerms: data.acceptTerms,
};
const response = await httpClient.post<AuthResponse>(
ApiEndpoints.AUTH.REGISTER,
registerRequest
);
get().handleSuccessfulAuth(response.data, false);
} catch (error) {
set({
error: get().transformError(error),
isLoading: false,
});
throw error;
}
},
logout: async () => {
const httpClient = HttpClientService.getInstance();
try {
const refreshToken = get().getRefreshToken();
if (refreshToken) {
await httpClient.post(ApiEndpoints.AUTH.LOGOUT, { refreshToken });
}
} catch (error) {
console.warn('Logout request failed:', error);
} finally {
get().clearAuthState();
httpClient.removeAuthToken();
// Redirect to login page
if (typeof window !== 'undefined') {
window.location.href = '/auth/login';
}
}
},
verifyTwoFactor: async (data: TwoFactorVerificationRequest) => {
const httpClient = HttpClientService.getInstance();
set({ isLoading: true, error: null });
try {
const response = await httpClient.post<AuthResponse>(
ApiEndpoints.AUTH.TWO_FACTOR.VERIFY,
data
);
get().handleSuccessfulAuth(response.data, false);
set({
requiresTwoFactor: false,
twoFactorData: null,
});
} catch (error) {
set({
error: get().transformError(error),
isLoading: false,
});
throw error;
}
},
verifyDevice: async (data: DeviceVerificationRequest) => {
const httpClient = HttpClientService.getInstance();
set({ isLoading: true, error: null });
try {
const response = await httpClient.post<AuthResponse>(
ApiEndpoints.AUTH.VERIFY_DEVICE,
data
);
get().handleSuccessfulAuth(response.data, false);
set({
requiresDeviceVerification: false,
deviceVerificationData: null,
});
} catch (error) {
set({
error: get().transformError(error),
isLoading: false,
});
throw error;
}
},
refreshToken: async () => {
const httpClient = HttpClientService.getInstance();
const refreshToken = get().getRefreshToken();
if (!refreshToken) {
throw new Error('No refresh token available');
}
try {
const response = await httpClient.post(
ApiEndpoints.AUTH.REFRESH_TOKEN,
{ refreshToken },
{ skipAuth: true } as any
);
const { accessToken, refreshToken: newRefreshToken } = response.data;
get().storeTokens({ accessToken, refreshToken: newRefreshToken });
httpClient.setAuthToken(accessToken);
} catch (error) {
get().clearAuthState();
throw error;
}
},
checkAuth: async () => {
const accessToken = get().getAccessToken();
if (!accessToken || !get().isTokenValid(accessToken)) {
get().clearAuthState();
return;
}
const httpClient = HttpClientService.getInstance();
httpClient.setAuthToken(accessToken);
try {
// Verify token by fetching user profile
const response = await httpClient.get<AuthUser>(ApiEndpoints.AUTH.PROFILE);
set({
user: response.data,
isAuthenticated: true,
});
} catch (error) {
console.warn('Auth check failed:', error);
get().clearAuthState();
}
},
clearError: () => {
set({ error: null });
},
clearTwoFactor: () => {
set({
requiresTwoFactor: false,
twoFactorData: null,
});
},
clearDeviceVerification: () => {
set({
requiresDeviceVerification: false,
deviceVerificationData: null,
});
},
// Helper methods (not part of the public API)
handleSuccessfulAuth: (response: AuthResponse, rememberMe: boolean = false) => {
if (!response.accessToken) {
throw new Error('Invalid authentication response: missing access token');
}
const tokens = {
accessToken: response.accessToken,
refreshToken: response.user?.id ? 'dummy-refresh-token' : undefined, // Adapt based on your backend
};
get().storeTokens(tokens, rememberMe);
const httpClient = HttpClientService.getInstance();
httpClient.setAuthToken(tokens.accessToken);
set({
user: response.user,
isAuthenticated: true,
isLoading: false,
requiresTwoFactor: false,
requiresDeviceVerification: false,
twoFactorData: null,
deviceVerificationData: null,
error: null,
});
},
storeTokens: (tokens: { accessToken: string; refreshToken?: string }, rememberMe: boolean = false) => {
const storage = rememberMe ? localStorage : sessionStorage;
storage.setItem('auth_access_token', tokens.accessToken);
if (tokens.refreshToken) {
storage.setItem('auth_refresh_token', tokens.refreshToken);
}
if (rememberMe) {
localStorage.setItem('auth_remember_me', 'true');
}
},
getAccessToken: (): string | null => {
if (typeof window === 'undefined') return null;
return localStorage.getItem('auth_access_token') ||
sessionStorage.getItem('auth_access_token');
},
getRefreshToken: (): string | null => {
if (typeof window === 'undefined') return null;
return localStorage.getItem('auth_refresh_token') ||
sessionStorage.getItem('auth_refresh_token');
},
clearAuthState: () => {
// Clear from both storages
if (typeof window !== 'undefined') {
[localStorage, sessionStorage].forEach(storage => {
storage.removeItem('auth_access_token');
storage.removeItem('auth_refresh_token');
storage.removeItem('auth_user_data');
});
localStorage.removeItem('auth_remember_me');
}
set({
user: null,
isAuthenticated: false,
isLoading: false,
requiresTwoFactor: false,
requiresDeviceVerification: false,
twoFactorData: null,
deviceVerificationData: null,
error: null,
});
},
isTokenValid: (token: string): boolean => {
try {
const payload = JSON.parse(atob(token.split('.')[1]));
const currentTime = Math.floor(Date.now() / 1000);
return payload.exp > currentTime;
} catch {
return false;
}
},
transformError: (error: any): AuthError => {
if (error?.code && error?.message) {
return error as AuthError;
}
return {
code: error?.response?.status ? `HTTP_${error.response.status}` : 'CLIENT_ERROR',
message: error?.response?.data?.message || error?.message || 'An authentication error occurred',
timestamp: new Date(),
details: error?.response?.data,
};
},
resendDeviceVerification: async (sessionId: string): Promise<void> => {
set({ isLoading: true, error: null });
try {
const httpClient = HttpClientService.getInstance();
await httpClient.post(ApiEndpoints.AUTH.RESEND_DEVICE_VERIFICATION, {
sessionId,
});
} catch (error) {
const authError = get().transformError(error);
set({ error: authError });
throw authError;
} finally {
set({ isLoading: false });
}
},
}),
{
name: 'auth-storage',
storage: createJSONStorage(() =>
typeof window !== 'undefined' ? localStorage : {
getItem: () => null,
setItem: () => {},
removeItem: () => {},
}
),
partialize: (state) => ({
user: state.user,
isAuthenticated: state.isAuthenticated,
}),
}
),
{
name: 'auth-store',
}
)
);
// Initialize auth state on app load
if (typeof window !== 'undefined') {
useAuthStore.getState().checkAuth();
}

0
src/lib/constants.ts Normal file
View File

View File

@@ -0,0 +1,304 @@
/**
* Device Fingerprinting Service
* Professional Next.js Resume Builder - Enterprise Auth System
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
import { DeviceFingerprint } from '@/types/auth';
/**
* Device fingerprinting service for enhanced security
* Generates unique device fingerprints for device verification
*/
export class DeviceFingerprintService {
private static instance: DeviceFingerprintService;
private fingerprint: DeviceFingerprint | null = null;
public static getInstance(): DeviceFingerprintService {
if (!DeviceFingerprintService.instance) {
DeviceFingerprintService.instance = new DeviceFingerprintService();
}
return DeviceFingerprintService.instance;
}
/**
* Generate comprehensive device fingerprint
*/
public generateFingerprint(): DeviceFingerprint {
if (typeof window === 'undefined') {
// Server-side rendering fallback
return this.getServerSideFingerprint();
}
const fingerprint: DeviceFingerprint = {
userAgent: navigator.userAgent,
acceptLanguage: navigator.language || 'en-US',
screenResolution: `${screen.width}x${screen.height}`,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
platform: navigator.platform,
cookieEnabled: navigator.cookieEnabled,
doNotTrack: navigator.doNotTrack === '1',
colorDepth: screen.colorDepth || 24,
hardwareConcurrency: navigator.hardwareConcurrency || 1,
deviceMemory: (navigator as any).deviceMemory || 0,
touchSupport: 'ontouchstart' in window || navigator.maxTouchPoints > 0,
};
// Add advanced fingerprinting if available
fingerprint.fontsHash = this.generateFontsHash();
fingerprint.canvasFingerprint = this.generateCanvasFingerprint();
fingerprint.webglFingerprint = this.generateWebGLFingerprint();
this.fingerprint = fingerprint;
return fingerprint;
}
/**
* Get cached fingerprint or generate new one
*/
public getFingerprint(): DeviceFingerprint {
if (!this.fingerprint) {
return this.generateFingerprint();
}
return this.fingerprint;
}
/**
* Generate fonts fingerprint hash
*/
private generateFontsHash(): string {
if (typeof window === 'undefined') return '';
const testFonts = [
'Arial', 'Helvetica', 'Times New Roman', 'Courier New',
'Verdana', 'Georgia', 'Palatino', 'Garamond',
'Bookman', 'Comic Sans MS', 'Trebuchet MS', 'Arial Black',
'Impact', 'Lucida Sans Unicode', 'Tahoma', 'Lucida Console',
'Monaco', 'Bradley Hand ITC', 'Brush Script MT', 'Luminari',
'Chalkduster'
];
const availableFonts: string[] = [];
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
if (!context) return '';
canvas.width = 200;
canvas.height = 50;
testFonts.forEach(font => {
context.font = `12px ${font}, monospace`;
context.fillText('Test font detection', 10, 25);
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
const fontData = Array.from(imageData.data).join(',');
// Simple hash of the font rendering
let hash = 0;
for (let i = 0; i < fontData.length; i++) {
const char = fontData.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32-bit integer
}
if (hash !== 0) {
availableFonts.push(font);
}
context.clearRect(0, 0, canvas.width, canvas.height);
});
return this.simpleHash(availableFonts.join(','));
}
/**
* Generate canvas fingerprint
*/
private generateCanvasFingerprint(): string {
if (typeof window === 'undefined') return '';
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
if (!context) return '';
canvas.width = 280;
canvas.height = 60;
// Draw text with various styles
context.textBaseline = 'top';
context.font = '14px Arial';
context.fillStyle = '#f60';
context.fillRect(125, 1, 62, 20);
context.fillStyle = '#069';
context.fillText('Device fingerprint test 🔒', 2, 15);
context.fillStyle = 'rgba(102, 204, 0, 0.7)';
context.fillText('Professional security', 4, 45);
// Draw shapes
context.globalCompositeOperation = 'multiply';
context.fillStyle = 'rgb(255,0,255)';
context.beginPath();
context.arc(50, 50, 50, 0, Math.PI * 2, true);
context.closePath();
context.fill();
context.fillStyle = 'rgb(0,255,255)';
context.beginPath();
context.arc(100, 50, 50, 0, Math.PI * 2, true);
context.closePath();
context.fill();
context.fillStyle = 'rgb(255,255,0)';
context.beginPath();
context.arc(75, 100, 50, 0, Math.PI * 2, true);
context.closePath();
context.fill();
return canvas.toDataURL();
}
/**
* Generate WebGL fingerprint
*/
private generateWebGLFingerprint(): string {
if (typeof window === 'undefined') return '';
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');
const vendor = debugInfo ? gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) : '';
const renderer = debugInfo ? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) : '';
const webglInfo = {
vendor,
renderer,
version: gl.getParameter(gl.VERSION),
shadingLanguageVersion: gl.getParameter(gl.SHADING_LANGUAGE_VERSION),
maxVertexAttribs: gl.getParameter(gl.MAX_VERTEX_ATTRIBS),
maxVertexUniformVectors: gl.getParameter(gl.MAX_VERTEX_UNIFORM_VECTORS),
maxVaryingVectors: gl.getParameter(gl.MAX_VARYING_VECTORS),
maxFragmentUniformVectors: gl.getParameter(gl.MAX_FRAGMENT_UNIFORM_VECTORS),
extensions: gl.getSupportedExtensions()?.join(',') || ''
};
return this.simpleHash(JSON.stringify(webglInfo));
} catch (e) {
return '';
}
}
/**
* Simple hash function for fingerprint data
*/
private simpleHash(str: string): string {
let hash = 0;
if (str.length === 0) return hash.toString();
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);
}
/**
* Server-side rendering fallback fingerprint
*/
private getServerSideFingerprint(): DeviceFingerprint {
return {
userAgent: 'SSR-UserAgent',
acceptLanguage: 'en-US',
screenResolution: '1920x1080',
timezone: 'UTC',
platform: 'SSR-Platform',
cookieEnabled: true,
doNotTrack: false,
colorDepth: 24,
hardwareConcurrency: 1,
deviceMemory: 0,
touchSupport: false,
fontsHash: '',
canvasFingerprint: '',
webglFingerprint: ''
};
}
/**
* Generate device name based on fingerprint
*/
public generateDeviceName(fingerprint?: DeviceFingerprint): string {
const fp = fingerprint || this.getFingerprint();
// Extract browser name
const userAgent = fp.userAgent.toLowerCase();
let browser = 'Unknown Browser';
if (userAgent.includes('chrome')) browser = 'Chrome';
else if (userAgent.includes('firefox')) browser = 'Firefox';
else if (userAgent.includes('safari')) browser = 'Safari';
else if (userAgent.includes('edge')) browser = 'Edge';
else if (userAgent.includes('opera')) browser = 'Opera';
// Extract OS
let os = 'Unknown OS';
if (userAgent.includes('windows')) os = 'Windows';
else if (userAgent.includes('mac')) os = 'macOS';
else if (userAgent.includes('linux')) os = 'Linux';
else if (userAgent.includes('android')) os = 'Android';
else if (userAgent.includes('ios')) os = 'iOS';
// Extract device type
const isMobile = userAgent.includes('mobile') || fp.touchSupport;
const deviceType = isMobile ? 'Mobile' : 'Desktop';
return `${browser} on ${os} ${deviceType}`;
}
/**
* Check if fingerprint matches stored fingerprint
*/
public compareFingerprinst(fp1: DeviceFingerprint, fp2: DeviceFingerprint): number {
const weights = {
userAgent: 0.3,
screenResolution: 0.2,
timezone: 0.1,
platform: 0.15,
canvasFingerprint: 0.15,
webglFingerprint: 0.1
};
let score = 0;
let totalWeight = 0;
Object.entries(weights).forEach(([key, weight]) => {
const key1 = fp1[key as keyof DeviceFingerprint];
const key2 = fp2[key as keyof DeviceFingerprint];
if (key1 && key2) {
totalWeight += weight;
if (key1 === key2) {
score += weight;
}
}
});
return totalWeight > 0 ? score / totalWeight : 0;
}
/**
* Clear cached fingerprint
*/
public clearFingerprint(): void {
this.fingerprint = null;
}
}
export default DeviceFingerprintService;

281
src/lib/encryption.ts Normal file
View File

@@ -0,0 +1,281 @@
/**
* Encryption Service
* Professional Next.js Resume Builder - Enterprise Auth System
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
/**
* Encryption metadata interface
*/
interface EncryptionMeta {
algorithm: string;
keyLength: number;
timestamp: string;
nonce: string;
}
/**
* Secure payload interface
*/
interface SecurePayload {
encryptedPassword: string;
encryptionMeta: EncryptionMeta;
}
/**
* Professional encryption service for secure data transmission
* Implements client-side encryption for sensitive data like passwords
*/
export class EncryptionService {
private static instance: EncryptionService;
private readonly algorithm = 'AES-GCM';
private readonly keyLength = 256;
public static getInstance(): EncryptionService {
if (!EncryptionService.instance) {
EncryptionService.instance = new EncryptionService();
}
return EncryptionService.instance;
}
/**
* Create secure payload with encrypted password
* For enterprise-grade security in password transmission
*/
public async createSecurePayload(password: string): Promise<SecurePayload> {
try {
// Generate a random key for this encryption
const key = await this.generateKey();
// Generate a random nonce
const nonce = crypto.getRandomValues(new Uint8Array(12));
// Encrypt the password
const encryptedData = await this.encrypt(password, key, nonce);
// Create metadata
const encryptionMeta: EncryptionMeta = {
algorithm: this.algorithm,
keyLength: this.keyLength,
timestamp: new Date().toISOString(),
nonce: this.arrayBufferToBase64(nonce.buffer)
};
return {
encryptedPassword: this.arrayBufferToBase64(encryptedData),
encryptionMeta
};
} catch (error) {
console.error('Encryption failed:', error);
// Fallback: return base64 encoded password for basic obfuscation
return this.createFallbackPayload(password);
}
}
/**
* Create secure payload (synchronous version for compatibility)
*/
public createSecurePayloadSync(password: string): SecurePayload {
// For compatibility with existing code, use simple base64 encoding
// In production, this should be replaced with proper encryption
return this.createFallbackPayload(password);
}
/**
* Generate encryption key
*/
private async generateKey(): Promise<CryptoKey> {
return await crypto.subtle.generateKey(
{
name: this.algorithm,
length: this.keyLength,
},
true,
['encrypt', 'decrypt']
);
}
/**
* Encrypt data using AES-GCM
*/
private async encrypt(data: string, key: CryptoKey, nonce: Uint8Array): Promise<ArrayBuffer> {
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(data);
return await crypto.subtle.encrypt(
{
name: this.algorithm,
iv: nonce as BufferSource,
},
key,
dataBuffer
);
}
/**
* Convert ArrayBuffer to base64 string
*/
private arrayBufferToBase64(buffer: ArrayBuffer): string {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
/**
* Convert base64 string to ArrayBuffer
*/
private base64ToArrayBuffer(base64: string): ArrayBuffer {
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes.buffer;
}
/**
* Create fallback payload when encryption is not available
*/
private createFallbackPayload(password: string): SecurePayload {
// Simple base64 encoding as fallback
const encoded = btoa(password);
const encryptionMeta: EncryptionMeta = {
algorithm: 'BASE64',
keyLength: 0,
timestamp: new Date().toISOString(),
nonce: ''
};
return {
encryptedPassword: encoded,
encryptionMeta
};
}
/**
* Hash password using SHA-256 for client-side verification
*/
public async hashPassword(password: string, salt?: string): Promise<string> {
try {
const encoder = new TextEncoder();
const data = encoder.encode(password + (salt || ''));
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
} catch (error) {
console.error('Hashing failed:', error);
// Fallback to simple encoding
return btoa(password);
}
}
/**
* Generate secure random string
*/
public generateRandomString(length: number = 32): string {
if (typeof window === 'undefined') {
// Server-side fallback
return Math.random().toString(36).substring(2, length + 2);
}
try {
const array = new Uint8Array(length);
crypto.getRandomValues(array);
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
} catch (error) {
// Fallback to Math.random
return Math.random().toString(36).substring(2, length + 2);
}
}
/**
* Validate password strength
*/
public validatePasswordStrength(password: string): {
score: number;
feedback: string[];
isValid: boolean;
} {
const feedback: string[] = [];
let score = 0;
// Length check
if (password.length >= 8) score += 1;
else feedback.push('Password must be at least 8 characters long');
if (password.length >= 12) score += 1;
// Character variety checks
if (/[a-z]/.test(password)) score += 1;
else feedback.push('Add lowercase letters');
if (/[A-Z]/.test(password)) score += 1;
else feedback.push('Add uppercase letters');
if (/[0-9]/.test(password)) score += 1;
else feedback.push('Add numbers');
if (/[^a-zA-Z0-9]/.test(password)) score += 1;
else feedback.push('Add special characters');
// Common patterns
if (!/(.)\1{2,}/.test(password)) score += 1;
else feedback.push('Avoid repeating characters');
if (!/^.*(123|abc|qwe|password|admin).*$/i.test(password)) score += 1;
else feedback.push('Avoid common patterns');
const isValid = score >= 6 && password.length >= 8;
return {
score: Math.min(score, 8),
feedback,
isValid
};
}
/**
* Encrypt sensitive data for storage
*/
public encryptForStorage(data: string, key: string): string {
try {
// Simple XOR encryption for local storage
let result = '';
for (let i = 0; i < data.length; i++) {
result += String.fromCharCode(
data.charCodeAt(i) ^ key.charCodeAt(i % key.length)
);
}
return btoa(result);
} catch (error) {
return btoa(data);
}
}
/**
* Decrypt data from storage
*/
public decryptFromStorage(encryptedData: string, key: string): string {
try {
const data = atob(encryptedData);
let result = '';
for (let i = 0; i < data.length; i++) {
result += String.fromCharCode(
data.charCodeAt(i) ^ key.charCodeAt(i % key.length)
);
}
return result;
} catch (error) {
return '';
}
}
}
export default EncryptionService;

446
src/lib/http-client.ts Normal file
View File

@@ -0,0 +1,446 @@
/**
* HTTP Client Service
* Professional Next.js Resume Builder - Enterprise Auth System
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';
import { ApiEndpoints } from './api-endpoints';
import { DeviceFingerprintService } from './device-fingerprint';
/**
* HTTP response interface
*/
interface HttpResponse<T = any> {
data: T;
status: number;
statusText: string;
headers: Record<string, string>;
}
/**
* HTTP error interface
*/
interface HttpError {
code: string;
message: string;
status?: number;
details?: any;
}
/**
* Request configuration interface
*/
interface RequestConfig extends AxiosRequestConfig {
skipAuth?: boolean;
skipRetry?: boolean;
includeFingerprint?: boolean;
}
/**
* Professional HTTP client service with enterprise features
* Handles authentication, device fingerprinting, retry logic, and error handling
*/
export class HttpClientService {
private static instance: HttpClientService;
private axiosInstance: AxiosInstance;
private deviceFingerprintService: DeviceFingerprintService;
private isRefreshing = false;
private failedQueue: Array<{
resolve: (value?: any) => void;
reject: (error?: any) => void;
}> = [];
private constructor() {
this.deviceFingerprintService = DeviceFingerprintService.getInstance();
this.axiosInstance = this.createAxiosInstance();
this.setupInterceptors();
}
public static getInstance(): HttpClientService {
if (!HttpClientService.instance) {
HttpClientService.instance = new HttpClientService();
}
return HttpClientService.instance;
}
/**
* Create axios instance with default configuration
*/
private createAxiosInstance(): AxiosInstance {
const config = ApiEndpoints.config;
return axios.create({
baseURL: config.API_BASE_URL,
timeout: config.REQUEST_TIMEOUT,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-Client-Version': config.APP_VERSION,
'X-Client-Name': config.APP_NAME,
},
});
}
/**
* Setup request and response interceptors
*/
private setupInterceptors(): void {
// Request interceptor
this.axiosInstance.interceptors.request.use(
(config) => {
// Add authentication token
if (!(config as any).skipAuth) {
const token = this.getAccessToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
}
// Add device fingerprint headers
if ((config as any).includeFingerprint !== false) {
this.addDeviceFingerprintHeaders(config);
}
// Add request timestamp
config.headers['X-Request-Time'] = new Date().toISOString();
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Response interceptor
this.axiosInstance.interceptors.response.use(
(response) => {
return response;
},
async (error) => {
const originalRequest = error.config;
// Handle 401 errors with token refresh
if (error.response?.status === 401 && !originalRequest._retry && !(originalRequest as any).skipAuth) {
if (this.isRefreshing) {
// If already refreshing, queue the request
return new Promise((resolve, reject) => {
this.failedQueue.push({ resolve, reject });
}).then(() => {
return this.axiosInstance(originalRequest);
}).catch((err) => {
return Promise.reject(err);
});
}
originalRequest._retry = true;
this.isRefreshing = true;
try {
await this.refreshToken();
this.processQueue(null);
return this.axiosInstance(originalRequest);
} catch (refreshError) {
this.processQueue(refreshError);
this.handleAuthenticationFailure();
return Promise.reject(refreshError);
} finally {
this.isRefreshing = false;
}
}
return Promise.reject(this.transformError(error));
}
);
}
/**
* Add device fingerprint headers to request
*/
private addDeviceFingerprintHeaders(config: AxiosRequestConfig): void {
if (typeof window === 'undefined') return;
const fingerprint = this.deviceFingerprintService.getFingerprint();
config.headers = {
...config.headers,
'User-Agent': fingerprint.userAgent,
'Accept-Language': fingerprint.acceptLanguage,
'Timezone': fingerprint.timezone,
'Screen-Resolution': fingerprint.screenResolution,
'Color-Depth': fingerprint.colorDepth?.toString() || '24',
'Hardware-Concurrency': fingerprint.hardwareConcurrency?.toString() || '1',
'Device-Memory': fingerprint.deviceMemory?.toString() || '0',
'Platform': fingerprint.platform,
'Cookie-Enabled': fingerprint.cookieEnabled.toString(),
'DNT': fingerprint.doNotTrack ? '1' : '0',
'Webdriver': 'false',
'Device-Fingerprint': JSON.stringify(fingerprint),
};
}
/**
* Process queued requests after token refresh
*/
private processQueue(error: any): void {
this.failedQueue.forEach(({ resolve, reject }) => {
if (error) {
reject(error);
} else {
resolve();
}
});
this.failedQueue = [];
}
/**
* Handle authentication failure
*/
private handleAuthenticationFailure(): void {
// Clear tokens and redirect to login
this.clearTokens();
if (typeof window !== 'undefined') {
window.location.href = '/auth/login';
}
}
/**
* Transform axios error to our error format
*/
private transformError(error: AxiosError): HttpError {
if (error.response) {
// Server responded with error status
return {
code: (error.response.data as any)?.code || `HTTP_${error.response.status}`,
message: (error.response.data as any)?.message || error.message,
status: error.response.status,
details: error.response.data,
};
} else if (error.request) {
// Request was made but no response
return {
code: 'NETWORK_ERROR',
message: 'Network error - please check your connection',
details: error.request,
};
} else {
// Something else happened
return {
code: 'CLIENT_ERROR',
message: error.message,
};
}
}
/**
* GET request
*/
public async get<T = any>(url: string, config?: RequestConfig): Promise<HttpResponse<T>> {
const response = await this.axiosInstance.get<T>(url, config);
return this.transformResponse(response);
}
/**
* POST request
*/
public async post<T = any>(url: string, data?: any, config?: RequestConfig): Promise<HttpResponse<T>> {
const response = await this.axiosInstance.post<T>(url, data, config);
return this.transformResponse(response);
}
/**
* PUT request
*/
public async put<T = any>(url: string, data?: any, config?: RequestConfig): Promise<HttpResponse<T>> {
const response = await this.axiosInstance.put<T>(url, data, config);
return this.transformResponse(response);
}
/**
* PATCH request
*/
public async patch<T = any>(url: string, data?: any, config?: RequestConfig): Promise<HttpResponse<T>> {
const response = await this.axiosInstance.patch<T>(url, data, config);
return this.transformResponse(response);
}
/**
* DELETE request
*/
public async delete<T = any>(url: string, config?: RequestConfig): Promise<HttpResponse<T>> {
const response = await this.axiosInstance.delete<T>(url, config);
return this.transformResponse(response);
}
/**
* Transform axios response to our response format
*/
private transformResponse<T>(response: AxiosResponse<T>): HttpResponse<T> {
return {
data: response.data,
status: response.status,
statusText: response.statusText,
headers: response.headers as Record<string, string>,
};
}
/**
* Get access token from storage
*/
private getAccessToken(): string | null {
if (typeof window === 'undefined') return null;
return localStorage.getItem('auth_access_token') ||
sessionStorage.getItem('auth_access_token');
}
/**
* Get refresh token from storage
*/
private getRefreshToken(): string | null {
if (typeof window === 'undefined') return null;
return localStorage.getItem('auth_refresh_token') ||
sessionStorage.getItem('auth_refresh_token');
}
/**
* Clear tokens from storage
*/
private clearTokens(): void {
if (typeof window === 'undefined') return;
localStorage.removeItem('auth_access_token');
localStorage.removeItem('auth_refresh_token');
sessionStorage.removeItem('auth_access_token');
sessionStorage.removeItem('auth_refresh_token');
}
/**
* Refresh authentication token
*/
private async refreshToken(): Promise<void> {
const refreshToken = this.getRefreshToken();
if (!refreshToken) {
throw new Error('No refresh token available');
}
try {
const response = await this.axiosInstance.post(
ApiEndpoints.AUTH.REFRESH_TOKEN,
{ refreshToken },
{ ...{}, skipAuth: true } as any
);
const { accessToken, refreshToken: newRefreshToken } = response.data;
// Store new tokens
const storage = localStorage.getItem('auth_remember_me') ? localStorage : sessionStorage;
storage.setItem('auth_access_token', accessToken);
if (newRefreshToken) {
storage.setItem('auth_refresh_token', newRefreshToken);
}
} catch (error) {
this.clearTokens();
throw error;
}
}
/**
* Update authentication token
*/
public setAuthToken(token: string): void {
this.axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${token}`;
}
/**
* Remove authentication token
*/
public removeAuthToken(): void {
delete this.axiosInstance.defaults.headers.common['Authorization'];
}
/**
* Get axios instance for advanced usage
*/
public getAxiosInstance(): AxiosInstance {
return this.axiosInstance;
}
/**
* Check if request should be retried
*/
private shouldRetry(error: AxiosError, retryCount: number): boolean {
const maxRetries = 3;
if (retryCount >= maxRetries) return false;
if ((error.config as any)?.skipRetry) return false;
if (error.response?.status && error.response.status < 500) return false;
return true;
}
/**
* Download file with progress tracking
*/
public async downloadFile(
url: string,
filename?: string,
onProgress?: (progress: number) => void
): Promise<void> {
const response = await this.axiosInstance.get(url, {
responseType: 'blob',
onDownloadProgress: (progressEvent) => {
if (onProgress && progressEvent.total) {
const progress = (progressEvent.loaded / progressEvent.total) * 100;
onProgress(progress);
}
},
});
// Create download link
const blob = new Blob([response.data]);
const downloadUrl = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = downloadUrl;
link.download = filename || 'download';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(downloadUrl);
}
/**
* Upload file with progress tracking
*/
public async uploadFile(
url: string,
file: File,
onProgress?: (progress: number) => void
): Promise<HttpResponse> {
const formData = new FormData();
formData.append('file', file);
const response = await this.axiosInstance.post(url, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
onUploadProgress: (progressEvent) => {
if (onProgress && progressEvent.total) {
const progress = (progressEvent.loaded / progressEvent.total) * 100;
onProgress(progress);
}
},
});
return this.transformResponse(response);
}
}
export default HttpClientService;

0
src/lib/utils.ts Normal file
View File

282
src/lib/validation.ts Normal file
View File

@@ -0,0 +1,282 @@
/**
* Form Validation Schemas
* Professional Next.js Resume Builder - Enterprise Auth System
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
import { z } from 'zod';
/**
* Login form validation schema
*/
export const loginSchema = z.object({
email: z
.string()
.min(1, 'E-Mail ist erforderlich')
.email('Bitte geben Sie eine gültige E-Mail-Adresse ein')
.max(255, 'E-Mail muss weniger als 255 Zeichen haben'),
password: z
.string()
.min(1, 'Passwort ist erforderlich')
.min(6, 'Passwort muss mindestens 6 Zeichen lang sein'),
rememberMe: z.boolean().optional(),
});
/**
* Registration form validation schema
*/
export const registerSchema = z.object({
firstName: z
.string()
.min(1, 'Vorname ist erforderlich')
.min(2, 'Vorname muss mindestens 2 Zeichen haben')
.max(50, 'Vorname muss weniger als 50 Zeichen haben')
.regex(/^[a-zA-ZÀ-ÿ\s'-]+$/, 'Vorname darf nur Buchstaben, Leerzeichen, Bindestriche und Apostrophe enthalten'),
lastName: z
.string()
.min(1, 'Nachname ist erforderlich')
.min(2, 'Nachname muss mindestens 2 Zeichen haben')
.max(50, 'Nachname muss weniger als 50 Zeichen haben')
.regex(/^[a-zA-ZÀ-ÿ\s'-]+$/, 'Nachname darf nur Buchstaben, Leerzeichen, Bindestriche und Apostrophe enthalten'),
email: z
.string()
.min(1, 'E-Mail ist erforderlich')
.email('Bitte geben Sie eine gültige E-Mail-Adresse ein')
.max(255, 'E-Mail muss weniger als 255 Zeichen haben'),
password: z
.string()
.min(1, 'Passwort ist erforderlich')
.min(8, 'Passwort muss mindestens 8 Zeichen lang sein')
.max(128, 'Passwort muss weniger als 128 Zeichen haben')
.regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/,
'Passwort muss mindestens einen Großbuchstaben, einen Kleinbuchstaben, eine Zahl und ein Sonderzeichen enthalten'),
confirmPassword: z
.string()
.min(1, 'Bitte bestätigen Sie Ihr Passwort'),
acceptTerms: z
.boolean()
.refine(val => val === true, 'Sie müssen die Nutzungsbedingungen akzeptieren'),
}).refine((data) => data.password === data.confirmPassword, {
message: 'Passwörter stimmen nicht überein',
path: ['confirmPassword'],
});
/**
* Two-factor authentication form validation schema
*/
export const twoFactorSchema = z.object({
verificationCode: z
.string()
.min(1, 'Bestätigungscode ist erforderlich')
.length(6, 'Bestätigungscode muss genau 6 Ziffern haben')
.regex(/^\d{6}$/, 'Bestätigungscode darf nur Zahlen enthalten'),
});
/**
* Device verification form validation schema
*/
export const deviceVerificationSchema = z.object({
verificationCode: z
.string()
.min(1, 'Bestätigungscode ist erforderlich')
.length(6, 'Bestätigungscode muss genau 6 Ziffern haben')
.regex(/^\d{6}$/, 'Bestätigungscode darf nur Zahlen enthalten'),
deviceName: z
.string()
.max(100, 'Device name must be less than 100 characters')
.optional(),
});
/**
* Password reset request form validation schema
*/
export const passwordResetRequestSchema = z.object({
email: z
.string()
.min(1, 'Email is required')
.email('Please enter a valid email address')
.max(255, 'Email must be less than 255 characters'),
});
/**
* Password reset confirmation form validation schema
*/
export const passwordResetConfirmationSchema = z.object({
token: z
.string()
.min(1, 'Reset token is required'),
newPassword: z
.string()
.min(1, 'New password is required')
.min(8, 'Password must be at least 8 characters long')
.max(128, 'Password must be less than 128 characters')
.regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/,
'Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character'),
confirmPassword: z
.string()
.min(1, 'Please confirm your new password'),
}).refine((data) => data.newPassword === data.confirmPassword, {
message: 'Passwords do not match',
path: ['confirmPassword'],
});
/**
* Profile update form validation schema
*/
export const profileUpdateSchema = z.object({
firstName: z
.string()
.min(1, 'First name is required')
.min(2, 'First name must be at least 2 characters')
.max(50, 'First name must be less than 50 characters')
.regex(/^[a-zA-ZÀ-ÿ\s'-]+$/, 'First name can only contain letters, spaces, hyphens, and apostrophes'),
lastName: z
.string()
.min(1, 'Last name is required')
.min(2, 'Last name must be at least 2 characters')
.max(50, 'Last name must be less than 50 characters')
.regex(/^[a-zA-ZÀ-ÿ\s'-]+$/, 'Last name can only contain letters, spaces, hyphens, and apostrophes'),
email: z
.string()
.min(1, 'Email is required')
.email('Please enter a valid email address')
.max(255, 'Email must be less than 255 characters'),
profileImage: z
.string()
.url('Profile image must be a valid URL')
.optional()
.or(z.literal('')),
});
/**
* Change password form validation schema
*/
export const changePasswordSchema = z.object({
currentPassword: z
.string()
.min(1, 'Current password is required'),
newPassword: z
.string()
.min(1, 'New password is required')
.min(8, 'Password must be at least 8 characters long')
.max(128, 'Password must be less than 128 characters')
.regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/,
'Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character'),
confirmPassword: z
.string()
.min(1, 'Please confirm your new password'),
}).refine((data) => data.newPassword === data.confirmPassword, {
message: 'Passwords do not match',
path: ['confirmPassword'],
}).refine((data) => data.currentPassword !== data.newPassword, {
message: 'New password must be different from current password',
path: ['newPassword'],
});
/**
* Email validation helper
*/
export const validateEmail = (email: string): boolean => {
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return emailRegex.test(email);
};
/**
* Password strength validation helper
*/
export const validatePasswordStrength = (password: string): {
score: number;
feedback: string[];
isValid: boolean;
} => {
const feedback: string[] = [];
let score = 0;
// Length checks
if (password.length >= 8) score += 1;
else feedback.push('Password must be at least 8 characters long');
if (password.length >= 12) score += 1;
// Character variety checks
if (/[a-z]/.test(password)) score += 1;
else feedback.push('Add lowercase letters');
if (/[A-Z]/.test(password)) score += 1;
else feedback.push('Add uppercase letters');
if (/[0-9]/.test(password)) score += 1;
else feedback.push('Add numbers');
if (/[^a-zA-Z0-9]/.test(password)) score += 1;
else feedback.push('Add special characters');
// Pattern checks
if (!/(.)\1{2,}/.test(password)) score += 1;
else feedback.push('Avoid repeating characters');
if (!/^.*(123|abc|qwe|password|admin).*$/i.test(password)) score += 1;
else feedback.push('Avoid common patterns');
const isValid = score >= 6 && password.length >= 8;
return {
score: Math.min(score, 8),
feedback,
isValid
};
};
/**
* Sanitize string input
*/
export const sanitizeString = (input: string): string => {
return input
.trim()
.replace(/[<>\"']/g, '') // Remove potentially dangerous characters
.substring(0, 1000); // Limit length
};
/**
* Validation error formatter
*/
export const formatValidationErrors = (errors: z.ZodError): Record<string, string> => {
const formattedErrors: Record<string, string> = {};
errors.issues.forEach((error) => {
const path = error.path.join('.');
formattedErrors[path] = error.message;
});
return formattedErrors;
};
// Export all schemas as a group for convenience
export const authSchemas = {
login: loginSchema,
register: registerSchema,
twoFactor: twoFactorSchema,
deviceVerification: deviceVerificationSchema,
passwordResetRequest: passwordResetRequestSchema,
passwordResetConfirmation: passwordResetConfirmationSchema,
profileUpdate: profileUpdateSchema,
changePassword: changePasswordSchema,
};

292
src/middleware.ts Normal file
View File

@@ -0,0 +1,292 @@
/**
* Route Protection Middleware
* Professional Next.js Resume Builder - Enterprise Auth System
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
import { NextRequest, NextResponse } from 'next/server';
import { jwtVerify } from 'jose';
import { UserRole } from '@/types/auth';
/**
* Route configuration interface
*/
interface RouteConfig {
requiresAuth?: boolean;
allowedRoles?: UserRole[];
redirectTo?: string;
isPublic?: boolean;
}
/**
* JWT payload interface for middleware
*/
interface JWTPayload {
sub: string;
email: string;
roles?: string[];
iat: number;
exp: number;
}
/**
* Route protection configuration
*/
const routeConfigs: Record<string, RouteConfig> = {
// Public routes (no authentication required)
'/': { isPublic: true },
'/auth/login': { isPublic: true },
'/auth/register': { isPublic: true },
'/auth/forgot-password': { isPublic: true },
'/auth/reset-password': { isPublic: true },
'/auth/verify-email': { isPublic: true },
'/about': { isPublic: true },
'/pricing': { isPublic: true },
'/contact': { isPublic: true },
'/terms': { isPublic: true },
'/privacy': { isPublic: true },
// Authentication flow routes
'/auth/two-factor': { requiresAuth: false }, // Special case - temp auth state
'/auth/device-verification': { requiresAuth: false }, // Special case - temp auth state
// Protected routes (authentication required)
'/dashboard': { requiresAuth: true },
'/profile': { requiresAuth: true },
'/settings': { requiresAuth: true },
'/resume-builder': { requiresAuth: true },
'/templates': { requiresAuth: true },
'/resumes': { requiresAuth: true },
// Admin routes (admin role required)
'/admin': { requiresAuth: true, allowedRoles: [UserRole.ADMIN] },
'/admin/users': { requiresAuth: true, allowedRoles: [UserRole.ADMIN] },
'/admin/analytics': { requiresAuth: true, allowedRoles: [UserRole.ADMIN] },
// Moderator routes
'/moderation': { requiresAuth: true, allowedRoles: [UserRole.ADMIN, UserRole.MODERATOR] },
};
/**
* Get route configuration for a path
*/
function getRouteConfig(pathname: string): RouteConfig {
// Exact match first
if (routeConfigs[pathname]) {
return routeConfigs[pathname];
}
// Pattern matching for dynamic routes
for (const [pattern, config] of Object.entries(routeConfigs)) {
if (pattern.includes('[') || pattern.includes('*')) {
// Handle dynamic routes and wildcards
const regex = new RegExp(
'^' + pattern.replace(/\[.*?\]/g, '[^/]+').replace(/\*/g, '.*') + '$'
);
if (regex.test(pathname)) {
return config;
}
}
}
// Default: require authentication for all other routes
return { requiresAuth: true };
}
/**
* Verify JWT token
*/
async function verifyJWTToken(token: string): Promise<JWTPayload | null> {
try {
const secret = new TextEncoder().encode(
process.env.JWT_SECRET || 'your-secret-key'
);
const { payload } = await jwtVerify(token, secret);
// Validate required fields exist
if (payload.sub && payload.email) {
return payload as unknown as JWTPayload;
}
return null;
} catch (error) {
console.error('JWT verification failed:', error);
return null;
}
}
/**
* Check if user has required role
*/
function hasRequiredRole(userRoles: string[], allowedRoles: UserRole[]): boolean {
if (!allowedRoles || allowedRoles.length === 0) {
return true;
}
return allowedRoles.some(role => userRoles.includes(role));
}
/**
* Get authentication token from request
*/
function getAuthToken(request: NextRequest): string | null {
// Check Authorization header first
const authHeader = request.headers.get('authorization');
if (authHeader && authHeader.startsWith('Bearer ')) {
return authHeader.substring(7);
}
// Check cookies
const tokenCookie = request.cookies.get('auth_access_token');
if (tokenCookie) {
return tokenCookie.value;
}
return null;
}
/**
* Main middleware function
*/
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Skip middleware for static files and API routes
if (
pathname.startsWith('/_next/') ||
pathname.startsWith('/api/') ||
pathname.startsWith('/static/') ||
pathname.includes('.') // Files with extensions
) {
return NextResponse.next();
}
const config = getRouteConfig(pathname);
// Allow public routes
if (config.isPublic) {
return NextResponse.next();
}
// Get authentication token
const token = getAuthToken(request);
// If route requires authentication but no token present
if (config.requiresAuth && !token) {
const loginUrl = new URL('/auth/login', request.url);
loginUrl.searchParams.set('returnUrl', pathname);
return NextResponse.redirect(loginUrl);
}
// If token is present, verify it
if (token) {
const payload = await verifyJWTToken(token);
if (!payload) {
// Invalid token - redirect to login
const loginUrl = new URL('/auth/login', request.url);
loginUrl.searchParams.set('returnUrl', pathname);
// Clear invalid token
const response = NextResponse.redirect(loginUrl);
response.cookies.delete('auth_access_token');
return response;
}
// Check role-based access
if (config.allowedRoles && !hasRequiredRole(payload.roles || [], config.allowedRoles)) {
// User doesn't have required role
return NextResponse.redirect(new URL('/access-denied', request.url));
}
// If user is authenticated and trying to access auth pages, redirect to dashboard
if (pathname.startsWith('/auth/') && !pathname.includes('two-factor') && !pathname.includes('device-verification')) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
}
// Continue with the request
return NextResponse.next();
}
/**
* Middleware configuration
*/
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* - public files (images, etc.)
*/
'/((?!api|_next/static|_next/image|favicon.ico|public).*)',
],
};
/**
* Helper function to protect API routes
*/
export async function protectAPIRoute(request: NextRequest, allowedRoles?: UserRole[]) {
const token = getAuthToken(request);
if (!token) {
return NextResponse.json(
{ error: 'Authentication required' },
{ status: 401 }
);
}
const payload = await verifyJWTToken(token);
if (!payload) {
return NextResponse.json(
{ error: 'Invalid or expired token' },
{ status: 401 }
);
}
if (allowedRoles && !hasRequiredRole(payload.roles || [], allowedRoles)) {
return NextResponse.json(
{ error: 'Insufficient permissions' },
{ status: 403 }
);
}
return { user: payload };
}
/**
* Helper function to check if user is authenticated
*/
export async function isAuthenticated(request: NextRequest): Promise<boolean> {
const token = getAuthToken(request);
if (!token) {
return false;
}
const payload = await verifyJWTToken(token);
return !!payload;
}
/**
* Helper function to get current user from request
*/
export async function getCurrentUser(request: NextRequest): Promise<JWTPayload | null> {
const token = getAuthToken(request);
if (!token) {
return null;
}
return await verifyJWTToken(token);
}

342
src/types/auth.ts Normal file
View File

@@ -0,0 +1,342 @@
/**
* Authentication Types
* Professional Next.js 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;
}
/**
* JWT token response interface
*/
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;
};
requiresDeviceVerification?: boolean;
sessionToken?: string;
}
/**
* 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;
colorDepth?: number;
hardwareConcurrency?: number;
deviceMemory?: number;
touchSupport?: boolean;
}
/**
* Device verification request interface
*/
export interface DeviceVerificationRequest {
sessionToken: string;
verificationCode: string;
deviceName?: string;
}
/**
* 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';
}
/**
* 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;
sessionToken: string;
email: string;
}
/**
* Two-factor authentication page data interface
*/
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;
sessionToken?: string;
timestamp?: number;
}
/**
* Auth store state interface
*/
export interface AuthState {
user: AuthUser | null;
isAuthenticated: boolean;
isLoading: boolean;
requiresTwoFactor: boolean;
requiresDeviceVerification: boolean;
twoFactorData: TwoFactorPageData | null;
deviceVerificationData: DeviceVerificationResponse | null;
error: AuthError | null;
}
/**
* Auth store actions interface
*/
export interface AuthActions {
login: (credentials: LoginCredentials) => Promise<void>;
register: (data: RegisterData) => Promise<void>;
logout: () => Promise<void>;
verifyTwoFactor: (data: TwoFactorVerificationRequest) => Promise<void>;
verifyDevice: (data: DeviceVerificationRequest) => Promise<void>;
refreshToken: () => Promise<void>;
checkAuth: () => Promise<void>;
clearError: () => void;
clearTwoFactor: () => void;
clearDeviceVerification: () => void;
}
/**
* Route protection types
*/
export interface RouteGuardConfig {
requiresAuth?: boolean;
requiredRoles?: UserRole[];
requiredPermissions?: string[];
redirectTo?: string;
}
/**
* Form validation schemas
*/
export interface LoginFormData {
email: string;
password: string;
rememberMe?: boolean;
}
export interface RegisterFormData {
firstName: string;
lastName: string;
email: string;
password: string;
confirmPassword: string;
acceptTerms: boolean;
}
export interface TwoFactorFormData {
verificationCode: string;
}
export interface DeviceVerificationFormData {
verificationCode: string;
deviceName?: string;
}

145
src/types/resume.ts Normal file
View File

@@ -0,0 +1,145 @@
/**
* Resume Data Types
* Professional Resume Builder
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
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?: 'Single' | 'Married' | 'Divorced' | 'Widowed' | 'Other';
visaStatus?:
| 'German Citizen'
| 'EU Citizen'
| 'Work Permit'
| 'Visa Required'
| 'Other';
// Online Presence
website?: string;
linkedin?: string;
github?: string;
// Professional Summary
summary: string;
}
export interface Experience {
id: string;
position: string;
company: string;
location: string;
startDate: string;
endDate: string | null;
isCurrentPosition: boolean;
description: string;
achievements: string[];
}
export interface Education {
id: string;
degree: string;
institution: string;
location: string;
startDate: string;
endDate: string | null;
gpa?: string;
description?: string;
}
export interface Skill {
id: string;
name: string;
category: string;
level: 'Beginner' | 'Intermediate' | 'Advanced' | 'Expert';
}
export interface Language {
id: string;
name: string;
proficiency: 'A1' | 'A2' | 'B1' | 'B2' | 'C1' | 'C2' | 'Native';
}
export interface Certification {
id: string;
name: string;
issuer: string;
issueDate: string;
expirationDate?: string | null;
credentialId?: string;
url?: string;
}
export interface Project {
id: string;
name: string;
description: string;
technologies: string[];
startDate: string;
endDate: string | null;
url?: string;
github?: string;
achievements: string[];
}
export interface ResumeData {
personalInfo: PersonalInfo;
experience: Experience[];
education: Education[];
skills: Skill[];
languages: Language[];
certifications: Certification[];
projects: Project[];
}
// David Valera Melendez - Professional Theme Types
export interface ResumeTheme {
id: string;
name: string;
colors: {
primary: string;
secondary: string;
accent: string;
text: string;
background: string;
};
fonts: {
primary: string;
secondary: string;
};
}
export interface ExportOptions {
format: 'pdf' | 'docx' | 'txt' | 'json';
theme: string;
includeSections: {
personalInfo: boolean;
experience: boolean;
education: boolean;
skills: boolean;
languages: boolean;
certifications: boolean;
projects: boolean;
};
}

84
src/utils/index.ts Normal file
View File

@@ -0,0 +1,84 @@
/**
* Utility Functions
* Professional Resume Builder
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
/**
* Utility function to merge Tailwind CSS classes
* Created by David Valera Melendez
*/
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
/**
* Format date for display in resume
* Created by David Valera Melendez
*/
export function formatDate(dateString: string | null): string {
if (!dateString) return 'Present';
try {
const date = new Date(dateString + '-01'); // Add day for month input
return date.toLocaleDateString('en-US', {
month: 'long',
year: 'numeric',
});
} catch {
return dateString;
}
}
/**
* Generate a unique ID for form elements
* Created by David Valera Melendez
*/
export function generateId(): string {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
/**
* Validate email address
* Created by David Valera Melendez
*/
export function isValidEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
/**
* Validate URL
* Created by David Valera Melendez
*/
export function isValidUrl(url: string): boolean {
try {
new URL(url);
return true;
} catch {
return false;
}
}
/**
* Calculate years of experience from date range
* Created by David Valera Melendez
*/
export function calculateYearsOfExperience(
startDate: string,
endDate: string | null
): number {
const start = new Date(startDate + '-01');
const end = endDate ? new Date(endDate + '-01') : new Date();
const diffTime = Math.abs(end.getTime() - start.getTime());
const diffYears = diffTime / (1000 * 60 * 60 * 24 * 365.25);
return Math.round(diffYears * 10) / 10; // Round to 1 decimal place
}

283
tailwind.config.ts Normal file
View File

@@ -0,0 +1,283 @@
/**
* Tailwind CSS Configuration
* Professional Resume Builder
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-07
* @location Made in Germany 🇩🇪
*/
import type { Config } from 'tailwindcss';
const config: Config = {
content: [
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
colors: {
// David Valera Melendez Professional Brand Colors
// Enhanced with resume-example.com design patterns
primary: {
50: '#f0f9ff',
100: '#e0f2fe',
200: '#bae6fd',
300: '#7dd3fc',
400: '#38bdf8',
500: '#0ea5e9',
600: '#0284c7',
700: '#0369a1',
800: '#075985',
900: '#0c4a6e',
950: '#082f49',
},
secondary: {
50: '#f8fafc',
100: '#f1f5f9',
200: '#e2e8f0',
300: '#cbd5e1',
400: '#94a3b8',
500: '#64748b',
600: '#475569',
700: '#334155',
800: '#1e293b',
900: '#0f172a',
950: '#020617',
},
// Enhanced grayscale system inspired by resume-example.com
grayscale: {
25: '#fcfcfd',
50: '#f9fafb',
100: '#f2f4f7',
200: '#eaecf0',
300: '#d0d5dd',
400: '#98a2b3',
500: '#667085',
600: '#475467',
700: '#344054',
800: '#182230',
900: '#101828',
950: '#0c111d',
},
accent: {
50: '#fef7ff',
100: '#feeeff',
200: '#fcddff',
300: '#f9bbff',
400: '#f589ff',
500: '#ee56ff',
600: '#d633e7',
700: '#b322c3',
800: '#921b9f',
900: '#771a82',
950: '#50045a',
},
// Enhanced semantic colors
success: {
50: '#ecfdf5',
100: '#d1fae5',
200: '#a7f3d0',
300: '#6ee7b7',
400: '#34d399',
500: '#10b981',
600: '#059669',
700: '#047857',
800: '#065f46',
900: '#064e3b',
},
warning: {
50: '#fffbeb',
100: '#fef3c7',
200: '#fde68a',
300: '#fcd34d',
400: '#fbbf24',
500: '#f59e0b',
600: '#d97706',
700: '#b45309',
800: '#92400e',
900: '#78350f',
},
error: {
50: '#fef2f2',
100: '#fee2e2',
200: '#fecaca',
300: '#fca5a5',
400: '#f87171',
500: '#ef4444',
600: '#dc2626',
700: '#b91c1c',
800: '#991b1b',
900: '#7f1d1d',
},
// Professional background colors
background: {
primary: '#ffffff',
secondary: '#f8fafc',
tertiary: '#f1f5f9',
inverse: '#0f172a',
},
// Text color system
text: {
primary: '#0f172a',
secondary: '#334155',
tertiary: '#64748b',
quaternary: '#94a3b8',
inverse: '#ffffff',
link: '#0ea5e9',
'link-hover': '#0284c7',
},
// Border color system
border: {
primary: '#e2e8f0',
secondary: '#cbd5e1',
tertiary: '#94a3b8',
focus: '#0ea5e9',
error: '#ef4444',
success: '#10b981',
},
},
fontFamily: {
sans: [
'Inter',
'system-ui',
'-apple-system',
'BlinkMacSystemFont',
'Segoe UI',
'Roboto',
'Helvetica Neue',
'Arial',
'sans-serif',
],
serif: ['Georgia', 'Times New Roman', 'serif'],
mono: [
'JetBrains Mono',
'Monaco',
'Consolas',
'Liberation Mono',
'Courier New',
'monospace',
],
display: ['Inter', 'system-ui', 'sans-serif'],
},
fontSize: {
'2xs': [
'0.625rem',
{ lineHeight: '0.875rem', letterSpacing: '0.025em' },
],
xs: ['0.75rem', { lineHeight: '1rem', letterSpacing: '0.025em' }],
sm: ['0.875rem', { lineHeight: '1.25rem', letterSpacing: '0.0125em' }],
base: ['1rem', { lineHeight: '1.5rem', letterSpacing: '0' }],
lg: ['1.125rem', { lineHeight: '1.75rem', letterSpacing: '-0.0125em' }],
xl: ['1.25rem', { lineHeight: '1.75rem', letterSpacing: '-0.025em' }],
'2xl': ['1.5rem', { lineHeight: '2rem', letterSpacing: '-0.025em' }],
'3xl': [
'1.875rem',
{ lineHeight: '2.25rem', letterSpacing: '-0.0375em' },
],
'4xl': ['2.25rem', { lineHeight: '2.5rem', letterSpacing: '-0.05em' }],
'5xl': ['3rem', { lineHeight: '1', letterSpacing: '-0.05em' }],
'6xl': ['3.75rem', { lineHeight: '1', letterSpacing: '-0.0625em' }],
'7xl': ['4.5rem', { lineHeight: '1', letterSpacing: '-0.0625em' }],
'8xl': ['6rem', { lineHeight: '1', letterSpacing: '-0.075em' }],
'9xl': ['8rem', { lineHeight: '1', letterSpacing: '-0.075em' }],
},
spacing: {
'18': '4.5rem',
'88': '22rem',
'128': '32rem',
'144': '36rem',
'160': '40rem',
'176': '44rem',
'192': '48rem',
},
animation: {
'fade-in': 'fadeIn 0.3s ease-in-out',
'fade-out': 'fadeOut 0.3s ease-in-out',
'slide-up': 'slideUp 0.3s ease-out',
'slide-down': 'slideDown 0.3s ease-out',
'slide-in-right': 'slideInRight 0.3s ease-out',
'slide-in-left': 'slideInLeft 0.3s ease-out',
'bounce-soft': 'bounceSoft 0.6s ease-in-out',
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
'scale-in': 'scaleIn 0.2s ease-out',
'scale-out': 'scaleOut 0.2s ease-out',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
fadeOut: {
'0%': { opacity: '1' },
'100%': { opacity: '0' },
},
slideUp: {
'0%': { transform: 'translateY(10px)', opacity: '0' },
'100%': { transform: 'translateY(0)', opacity: '1' },
},
slideDown: {
'0%': { transform: 'translateY(-10px)', opacity: '0' },
'100%': { transform: 'translateY(0)', opacity: '1' },
},
slideInRight: {
'0%': { transform: 'translateX(10px)', opacity: '0' },
'100%': { transform: 'translateX(0)', opacity: '1' },
},
slideInLeft: {
'0%': { transform: 'translateX(-10px)', opacity: '0' },
'100%': { transform: 'translateX(0)', opacity: '1' },
},
bounceSoft: {
'0%, 100%': { transform: 'scale(1)' },
'50%': { transform: 'scale(1.02)' },
},
scaleIn: {
'0%': { transform: 'scale(0.95)', opacity: '0' },
'100%': { transform: 'scale(1)', opacity: '1' },
},
scaleOut: {
'0%': { transform: 'scale(1)', opacity: '1' },
'100%': { transform: 'scale(0.95)', opacity: '0' },
},
},
boxShadow: {
soft: '0 1px 3px 0 rgba(0, 0, 0, 0.05), 0 1px 2px 0 rgba(0, 0, 0, 0.06)',
medium:
'0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03)',
large:
'0 10px 15px -3px rgba(0, 0, 0, 0.05), 0 4px 6px -2px rgba(0, 0, 0, 0.03)',
xl: '0 20px 25px -5px rgba(0, 0, 0, 0.05), 0 10px 10px -5px rgba(0, 0, 0, 0.02)',
'2xl': '0 25px 50px -12px rgba(0, 0, 0, 0.15)',
inner: 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.03)',
focus: '0 0 0 3px rgba(14, 165, 233, 0.1)',
'focus-error': '0 0 0 3px rgba(239, 68, 68, 0.1)',
'focus-success': '0 0 0 3px rgba(16, 185, 129, 0.1)',
},
borderRadius: {
'4xl': '2rem',
'5xl': '2.5rem',
'6xl': '3rem',
},
backdropBlur: {
xs: '2px',
},
backgroundImage: {
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
'gradient-conic':
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
},
zIndex: {
'60': '60',
'70': '70',
'80': '80',
'90': '90',
'100': '100',
},
},
},
plugins: [],
};
export default config;

33
tsconfig.json Normal file
View File

@@ -0,0 +1,33 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "es6"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@/components/*": ["./src/components/*"],
"@/lib/*": ["./src/lib/*"],
"@/types/*": ["./src/types/*"],
"@/hooks/*": ["./src/hooks/*"],
"@/utils/*": ["./src/utils/*"],
"@/styles/*": ["./src/styles/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}