diff --git a/backend/.env.example b/backend/.env.example
deleted file mode 100644
index 2475d9a..0000000
--- a/backend/.env.example
+++ /dev/null
@@ -1,22 +0,0 @@
-# Server Configuration
-PORT=3001
-NODE_ENV=development
-
-# Email Configuration (Gmail example)
-EMAIL_SERVICE=gmail
-EMAIL_HOST=smtp.gmail.com
-EMAIL_PORT=587
-EMAIL_SECURE=false
-EMAIL_USER=your-email@gmail.com
-EMAIL_PASS=your-app-password
-
-# Recipient Configuration
-RECIPIENT_EMAIL=your-email@gmail.com
-RECIPIENT_NAME=Your Name
-
-# Frontend URL (for CORS)
-FRONTEND_URL=http://localhost:5173
-
-# Rate Limiting
-RATE_LIMIT_WINDOW_MS=900000
-RATE_LIMIT_MAX_REQUESTS=5
diff --git a/backend/.gitignore b/backend/.gitignore
deleted file mode 100644
index e9d6196..0000000
Binary files a/backend/.gitignore and /dev/null differ
diff --git a/backend/README.md b/backend/README.md
deleted file mode 100644
index 12a5022..0000000
--- a/backend/README.md
+++ /dev/null
@@ -1,239 +0,0 @@
-# Portfolio Backend API
-
-A secure Node.js backend API for handling contact form submissions from your portfolio website.
-
-## Features
-
-- 🔒 **Security**: Rate limiting, CORS protection, input validation, spam protection
-- 📧 **Email Integration**: Nodemailer with Gmail/SMTP support
-- ✅ **Validation**: Comprehensive form validation with Joi
-- 🚀 **Performance**: Express.js with async error handling
-- 🛡️ **Anti-Spam**: Honeypot fields and rate limiting
-- 📝 **Logging**: Request/response logging and error tracking
-- 🎨 **Email Templates**: Professional HTML email templates
-
-## Setup Instructions
-
-### 1. Install Dependencies
-
-```bash
-cd backend
-npm install
-```
-
-### 2. Configure Environment
-
-Copy the example environment file and configure your settings:
-
-```bash
-cp .env.example .env
-```
-
-Edit `.env` with your actual configuration:
-
-```env
-# Server Configuration
-PORT=3001
-NODE_ENV=development
-
-# Email Configuration (Gmail example)
-EMAIL_SERVICE=gmail
-EMAIL_HOST=smtp.gmail.com
-EMAIL_PORT=587
-EMAIL_SECURE=false
-EMAIL_USER=your-email@gmail.com
-EMAIL_PASS=your-app-password
-
-# Recipient Configuration
-RECIPIENT_EMAIL=your-email@gmail.com
-RECIPIENT_NAME=Your Name
-
-# Frontend URL (for CORS)
-FRONTEND_URL=http://localhost:5173
-
-# Rate Limiting
-RATE_LIMIT_WINDOW_MS=900000
-RATE_LIMIT_MAX_REQUESTS=5
-```
-
-### 3. Gmail Setup (Recommended)
-
-1. **Enable 2-Factor Authentication** on your Gmail account
-2. **Generate App Password**:
- - Go to Google Account settings
- - Security → 2-Step Verification → App passwords
- - Generate a password for "Mail"
- - Use this as `EMAIL_PASS` in your `.env` file
-
-### 4. Alternative Email Providers
-
-**Outlook/Hotmail:**
-
-```env
-EMAIL_SERVICE=hotmail
-EMAIL_HOST=smtp-mail.outlook.com
-EMAIL_PORT=587
-EMAIL_SECURE=false
-```
-
-**Custom SMTP:**
-
-```env
-EMAIL_SERVICE=
-EMAIL_HOST=your-smtp-host.com
-EMAIL_PORT=587
-EMAIL_SECURE=false
-```
-
-### 5. Run the Server
-
-**Development:**
-
-```bash
-npm run dev
-```
-
-**Production:**
-
-```bash
-npm start
-```
-
-### 6. Test the API
-
-Visit: `http://localhost:3001/health` to verify the server is running.
-
-For development, you can test the contact endpoint: `http://localhost:3001/api/contact/test`
-
-## API Endpoints
-
-### Health Check
-
-- **GET** `/health` - Server health status
-
-### Contact Form
-
-- **POST** `/api/contact` - Send contact email
-
-**Request Body:**
-
-```json
-{
- "firstName": "John",
- "lastName": "Doe",
- "email": "john@example.com",
- "subject": "Project Inquiry",
- "message": "Hello, I'd like to discuss a project..."
-}
-```
-
-**Response:**
-
-```json
-{
- "success": true,
- "message": "Your message has been sent successfully! I'll get back to you soon."
-}
-```
-
-## Security Features
-
-- **Rate Limiting**: 5 requests per 15 minutes per IP
-- **Input Validation**: Joi schema validation for all fields
-- **CORS Protection**: Configurable origin restrictions
-- **Helmet Security**: Security headers
-- **Honeypot Anti-Spam**: Hidden field detection
-- **Request Logging**: IP and user agent tracking
-
-## Deployment Options
-
-### 1. Heroku
-
-```bash
-# Install Heroku CLI, then:
-heroku create your-portfolio-api
-heroku config:set NODE_ENV=production
-heroku config:set EMAIL_USER=your-email@gmail.com
-# ... set other environment variables
-git push heroku main
-```
-
-### 2. Railway
-
-```bash
-# Install Railway CLI, then:
-railway login
-railway new
-railway add
-railway up
-```
-
-### 3. DigitalOcean App Platform
-
-- Create new app from GitHub repository
-- Set environment variables in the dashboard
-- Deploy automatically
-
-### 4. Vercel (Serverless)
-
-- Convert Express app to serverless functions
-- Deploy with `vercel` CLI
-
-## Environment Variables Reference
-
-| Variable | Description | Example |
-| ------------------------- | --------------------------- | ---------------------- |
-| `PORT` | Server port | `3001` |
-| `NODE_ENV` | Environment | `production` |
-| `EMAIL_SERVICE` | Email service provider | `gmail` |
-| `EMAIL_HOST` | SMTP host | `smtp.gmail.com` |
-| `EMAIL_PORT` | SMTP port | `587` |
-| `EMAIL_SECURE` | Use SSL/TLS | `false` |
-| `EMAIL_USER` | Email username | `your@gmail.com` |
-| `EMAIL_PASS` | Email password/app password | `app-password` |
-| `RECIPIENT_EMAIL` | Where to send emails | `your@gmail.com` |
-| `RECIPIENT_NAME` | Recipient display name | `Your Name` |
-| `FRONTEND_URL` | Frontend URL for CORS | `https://yoursite.com` |
-| `RATE_LIMIT_WINDOW_MS` | Rate limit window | `900000` (15 min) |
-| `RATE_LIMIT_MAX_REQUESTS` | Max requests per window | `5` |
-
-## Troubleshooting
-
-### Email Not Sending
-
-1. Check Gmail app password is correct
-2. Verify 2FA is enabled on Gmail
-3. Check console logs for detailed errors
-4. Test with `/health` endpoint first
-
-### CORS Issues
-
-1. Verify `FRONTEND_URL` matches your frontend domain exactly
-2. Include protocol (http/https) in the URL
-3. No trailing slashes
-
-### Rate Limiting
-
-- Default: 5 requests per 15 minutes per IP
-- Adjust `RATE_LIMIT_*` variables as needed
-- Check IP address in logs for debugging
-
-## Production Checklist
-
-- [ ] Environment variables configured
-- [ ] Email service tested
-- [ ] Rate limiting configured
-- [ ] CORS origins restricted
-- [ ] SSL/HTTPS enabled
-- [ ] Error monitoring setup
-- [ ] Logs configured
-- [ ] Backup strategy in place
-
-## Support
-
-If you encounter issues:
-
-1. Check the console logs
-2. Verify your `.env` configuration
-3. Test email configuration with `/health` endpoint
-4. Ensure your email provider allows SMTP access
diff --git a/backend/middleware/asyncHandler.js b/backend/middleware/asyncHandler.js
deleted file mode 100644
index c2d2a00..0000000
--- a/backend/middleware/asyncHandler.js
+++ /dev/null
@@ -1,4 +0,0 @@
-// Async error handler wrapper
-export const asyncHandler = (fn) => (req, res, next) => {
- Promise.resolve(fn(req, res, next)).catch(next);
-};
diff --git a/backend/middleware/errorHandlers.js b/backend/middleware/errorHandlers.js
deleted file mode 100644
index 6631c4e..0000000
--- a/backend/middleware/errorHandlers.js
+++ /dev/null
@@ -1,65 +0,0 @@
-// Error handling middleware
-
-export const errorHandler = (err, req, res, next) => {
- console.error('Error occurred:', {
- message: err.message,
- stack: err.stack,
- url: req.url,
- method: req.method,
- ip: req.ip,
- userAgent: req.get('User-Agent'),
- timestamp: new Date().toISOString(),
- });
-
- // Default error
- let error = {
- success: false,
- message: 'Internal server error',
- ...(process.env.NODE_ENV === 'development' && {
- error: err.message,
- stack: err.stack,
- }),
- };
-
- // Rate limit error
- if (err.status === 429) {
- error = {
- success: false,
- message: 'Too many requests. Please try again later.',
- retryAfter: err.retryAfter,
- };
- }
-
- // Validation errors
- if (err.name === 'ValidationError') {
- error = {
- success: false,
- message: 'Validation failed',
- errors: Object.values(err.errors).map((e) => e.message),
- };
- }
-
- // JSON parsing errors
- if (err instanceof SyntaxError && err.status === 400 && 'body' in err) {
- error = {
- success: false,
- message: 'Invalid JSON in request body',
- };
- }
-
- res.status(err.status || 500).json(error);
-};
-
-export const notFoundHandler = (req, res) => {
- res.status(404).json({
- success: false,
- message: `Route ${req.method} ${req.url} not found`,
- availableRoutes: {
- 'GET /health': 'Health check endpoint',
- 'POST /api/contact': 'Send contact form email',
- ...(process.env.NODE_ENV === 'development' && {
- 'GET /api/contact/test': 'Test contact API (development only)',
- }),
- },
- });
-};
diff --git a/backend/middleware/logger.js b/backend/middleware/logger.js
deleted file mode 100644
index 825eedb..0000000
--- a/backend/middleware/logger.js
+++ /dev/null
@@ -1,23 +0,0 @@
-// Request logging middleware
-export const requestLogger = (req, res, next) => {
- const start = Date.now();
-
- // Log request
- console.log(`📥 ${req.method} ${req.url}`, {
- ip: req.ip,
- userAgent: req.get('User-Agent'),
- timestamp: new Date().toISOString(),
- });
-
- // Log response when finished
- res.on('finish', () => {
- const duration = Date.now() - start;
- const statusEmoji = res.statusCode >= 400 ? '❌' : '✅';
-
- console.log(
- `📤 ${statusEmoji} ${req.method} ${req.url} - ${res.statusCode} - ${duration}ms`
- );
- });
-
- next();
-};
diff --git a/backend/middleware/validation.js b/backend/middleware/validation.js
deleted file mode 100644
index 19b5d44..0000000
--- a/backend/middleware/validation.js
+++ /dev/null
@@ -1,84 +0,0 @@
-import Joi from 'joi';
-
-// Validation schema for contact form
-const contactSchema = Joi.object({
- firstName: Joi.string()
- .trim()
- .min(1)
- .max(50)
- .pattern(/^[a-zA-Z\s\-']+$/)
- .required()
- .messages({
- 'string.empty': 'First name is required',
- 'string.min': 'First name must be at least 1 character long',
- 'string.max': 'First name cannot exceed 50 characters',
- 'string.pattern.base':
- 'First name can only contain letters, spaces, hyphens, and apostrophes',
- }),
-
- lastName: Joi.string()
- .trim()
- .min(1)
- .max(50)
- .pattern(/^[a-zA-Z\s\-']+$/)
- .required()
- .messages({
- 'string.empty': 'Last name is required',
- 'string.min': 'Last name must be at least 1 character long',
- 'string.max': 'Last name cannot exceed 50 characters',
- 'string.pattern.base':
- 'Last name can only contain letters, spaces, hyphens, and apostrophes',
- }),
-
- email: Joi.string()
- .trim()
- .email({ minDomainSegments: 2, tlds: { allow: true } })
- .max(254)
- .required()
- .messages({
- 'string.email': 'Please provide a valid email address',
- 'string.empty': 'Email address is required',
- 'string.max': 'Email address is too long',
- }),
-
- subject: Joi.string().trim().max(200).allow('').messages({
- 'string.max': 'Subject cannot exceed 200 characters',
- }),
-
- message: Joi.string().trim().min(10).max(5000).required().messages({
- 'string.empty': 'Message is required',
- 'string.min': 'Message must be at least 10 characters long',
- 'string.max': 'Message cannot exceed 5000 characters',
- }),
-
- // Honeypot field (should be empty)
- website: Joi.string().allow('').max(0).messages({
- 'string.max': 'Invalid form submission',
- }),
-});
-
-// Validation middleware
-export const validateContactForm = (req, res, next) => {
- const { error, value } = contactSchema.validate(req.body, {
- abortEarly: false,
- stripUnknown: true,
- convert: true,
- });
-
- if (error) {
- const errors = error.details.map((detail) => ({
- field: detail.path[0],
- message: detail.message,
- }));
-
- return res.status(400).json({
- success: false,
- message: 'Validation failed',
- errors,
- });
- }
-
- // Replace req.body with validated and sanitized data
- req.body = value;
- next();
-};
diff --git a/backend/package-lock.json b/backend/package-lock.json
deleted file mode 100644
index 9060552..0000000
--- a/backend/package-lock.json
+++ /dev/null
@@ -1,1351 +0,0 @@
-{
- "name": "portfolio-backend",
- "version": "1.0.0",
- "lockfileVersion": 3,
- "requires": true,
- "packages": {
- "": {
- "name": "portfolio-backend",
- "version": "1.0.0",
- "dependencies": {
- "cors": "^2.8.5",
- "dotenv": "^16.3.1",
- "express": "^4.18.2",
- "express-rate-limit": "^7.1.5",
- "helmet": "^7.1.0",
- "joi": "^17.11.0",
- "nodemailer": "^6.9.7",
- "resend": "^6.0.2"
- },
- "devDependencies": {
- "nodemon": "^3.0.2"
- },
- "engines": {
- "node": ">=18.0.0"
- }
- },
- "node_modules/@hapi/hoek": {
- "version": "9.3.0",
- "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
- "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==",
- "license": "BSD-3-Clause"
- },
- "node_modules/@hapi/topo": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz",
- "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "@hapi/hoek": "^9.0.0"
- }
- },
- "node_modules/@sideway/address": {
- "version": "4.1.5",
- "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
- "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "@hapi/hoek": "^9.0.0"
- }
- },
- "node_modules/@sideway/formula": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
- "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==",
- "license": "BSD-3-Clause"
- },
- "node_modules/@sideway/pinpoint": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
- "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==",
- "license": "BSD-3-Clause"
- },
- "node_modules/accepts": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
- "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
- "license": "MIT",
- "dependencies": {
- "mime-types": "~2.1.34",
- "negotiator": "0.6.3"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/anymatch": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
- "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "normalize-path": "^3.0.0",
- "picomatch": "^2.0.4"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/array-flatten": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
- "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
- "license": "MIT"
- },
- "node_modules/balanced-match": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/binary-extensions": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
- "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/body-parser": {
- "version": "1.20.3",
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
- "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
- "license": "MIT",
- "dependencies": {
- "bytes": "3.1.2",
- "content-type": "~1.0.5",
- "debug": "2.6.9",
- "depd": "2.0.0",
- "destroy": "1.2.0",
- "http-errors": "2.0.0",
- "iconv-lite": "0.4.24",
- "on-finished": "2.4.1",
- "qs": "6.13.0",
- "raw-body": "2.5.2",
- "type-is": "~1.6.18",
- "unpipe": "1.0.0"
- },
- "engines": {
- "node": ">= 0.8",
- "npm": "1.2.8000 || >= 1.4.16"
- }
- },
- "node_modules/brace-expansion": {
- "version": "1.1.12",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
- "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
- "node_modules/braces": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
- "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fill-range": "^7.1.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/bytes": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
- "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/call-bind-apply-helpers": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
- "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "function-bind": "^1.1.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/call-bound": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
- "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
- "license": "MIT",
- "dependencies": {
- "call-bind-apply-helpers": "^1.0.2",
- "get-intrinsic": "^1.3.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/chokidar": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
- "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "anymatch": "~3.1.2",
- "braces": "~3.0.2",
- "glob-parent": "~5.1.2",
- "is-binary-path": "~2.1.0",
- "is-glob": "~4.0.1",
- "normalize-path": "~3.0.0",
- "readdirp": "~3.6.0"
- },
- "engines": {
- "node": ">= 8.10.0"
- },
- "funding": {
- "url": "https://paulmillr.com/funding/"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.2"
- }
- },
- "node_modules/concat-map": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/content-disposition": {
- "version": "0.5.4",
- "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
- "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
- "license": "MIT",
- "dependencies": {
- "safe-buffer": "5.2.1"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/content-type": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
- "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/cookie": {
- "version": "0.7.1",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
- "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/cookie-signature": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
- "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
- "license": "MIT"
- },
- "node_modules/cors": {
- "version": "2.8.5",
- "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
- "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
- "license": "MIT",
- "dependencies": {
- "object-assign": "^4",
- "vary": "^1"
- },
- "engines": {
- "node": ">= 0.10"
- }
- },
- "node_modules/debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
- "license": "MIT",
- "dependencies": {
- "ms": "2.0.0"
- }
- },
- "node_modules/depd": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
- "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/destroy": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
- "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8",
- "npm": "1.2.8000 || >= 1.4.16"
- }
- },
- "node_modules/dotenv": {
- "version": "16.6.1",
- "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
- "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://dotenvx.com"
- }
- },
- "node_modules/dunder-proto": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
- "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
- "license": "MIT",
- "dependencies": {
- "call-bind-apply-helpers": "^1.0.1",
- "es-errors": "^1.3.0",
- "gopd": "^1.2.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/ee-first": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
- "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
- "license": "MIT"
- },
- "node_modules/encodeurl": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
- "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/es-define-property": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
- "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/es-errors": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
- "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/es-object-atoms": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
- "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/escape-html": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
- "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
- "license": "MIT"
- },
- "node_modules/etag": {
- "version": "1.8.1",
- "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
- "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/express": {
- "version": "4.21.2",
- "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
- "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
- "license": "MIT",
- "dependencies": {
- "accepts": "~1.3.8",
- "array-flatten": "1.1.1",
- "body-parser": "1.20.3",
- "content-disposition": "0.5.4",
- "content-type": "~1.0.4",
- "cookie": "0.7.1",
- "cookie-signature": "1.0.6",
- "debug": "2.6.9",
- "depd": "2.0.0",
- "encodeurl": "~2.0.0",
- "escape-html": "~1.0.3",
- "etag": "~1.8.1",
- "finalhandler": "1.3.1",
- "fresh": "0.5.2",
- "http-errors": "2.0.0",
- "merge-descriptors": "1.0.3",
- "methods": "~1.1.2",
- "on-finished": "2.4.1",
- "parseurl": "~1.3.3",
- "path-to-regexp": "0.1.12",
- "proxy-addr": "~2.0.7",
- "qs": "6.13.0",
- "range-parser": "~1.2.1",
- "safe-buffer": "5.2.1",
- "send": "0.19.0",
- "serve-static": "1.16.2",
- "setprototypeof": "1.2.0",
- "statuses": "2.0.1",
- "type-is": "~1.6.18",
- "utils-merge": "1.0.1",
- "vary": "~1.1.2"
- },
- "engines": {
- "node": ">= 0.10.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/express-rate-limit": {
- "version": "7.5.1",
- "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz",
- "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==",
- "license": "MIT",
- "engines": {
- "node": ">= 16"
- },
- "funding": {
- "url": "https://github.com/sponsors/express-rate-limit"
- },
- "peerDependencies": {
- "express": ">= 4.11"
- }
- },
- "node_modules/fill-range": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
- "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "to-regex-range": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/finalhandler": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
- "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
- "license": "MIT",
- "dependencies": {
- "debug": "2.6.9",
- "encodeurl": "~2.0.0",
- "escape-html": "~1.0.3",
- "on-finished": "2.4.1",
- "parseurl": "~1.3.3",
- "statuses": "2.0.1",
- "unpipe": "~1.0.0"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/forwarded": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
- "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/fresh": {
- "version": "0.5.2",
- "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
- "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/fsevents": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
- }
- },
- "node_modules/function-bind": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
- "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/get-intrinsic": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
- "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
- "license": "MIT",
- "dependencies": {
- "call-bind-apply-helpers": "^1.0.2",
- "es-define-property": "^1.0.1",
- "es-errors": "^1.3.0",
- "es-object-atoms": "^1.1.1",
- "function-bind": "^1.1.2",
- "get-proto": "^1.0.1",
- "gopd": "^1.2.0",
- "has-symbols": "^1.1.0",
- "hasown": "^2.0.2",
- "math-intrinsics": "^1.1.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/get-proto": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
- "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
- "license": "MIT",
- "dependencies": {
- "dunder-proto": "^1.0.1",
- "es-object-atoms": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/glob-parent": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
- "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "is-glob": "^4.0.1"
- },
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/gopd": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
- "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/has-flag": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
- "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/has-symbols": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
- "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/hasown": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
- "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
- "license": "MIT",
- "dependencies": {
- "function-bind": "^1.1.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/helmet": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.2.0.tgz",
- "integrity": "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==",
- "license": "MIT",
- "engines": {
- "node": ">=16.0.0"
- }
- },
- "node_modules/http-errors": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
- "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
- "license": "MIT",
- "dependencies": {
- "depd": "2.0.0",
- "inherits": "2.0.4",
- "setprototypeof": "1.2.0",
- "statuses": "2.0.1",
- "toidentifier": "1.0.1"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/iconv-lite": {
- "version": "0.4.24",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
- "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
- "license": "MIT",
- "dependencies": {
- "safer-buffer": ">= 2.1.2 < 3"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/ignore-by-default": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
- "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/inherits": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
- "license": "ISC"
- },
- "node_modules/ipaddr.js": {
- "version": "1.9.1",
- "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
- "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.10"
- }
- },
- "node_modules/is-binary-path": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
- "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "binary-extensions": "^2.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/is-extglob": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
- "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/is-glob": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
- "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "is-extglob": "^2.1.1"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/is-number": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
- "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.12.0"
- }
- },
- "node_modules/joi": {
- "version": "17.13.3",
- "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz",
- "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "@hapi/hoek": "^9.3.0",
- "@hapi/topo": "^5.1.0",
- "@sideway/address": "^4.1.5",
- "@sideway/formula": "^3.0.1",
- "@sideway/pinpoint": "^2.0.0"
- }
- },
- "node_modules/math-intrinsics": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
- "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/media-typer": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
- "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/merge-descriptors": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
- "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/methods": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
- "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/mime": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
- "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
- "license": "MIT",
- "bin": {
- "mime": "cli.js"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/mime-db": {
- "version": "1.52.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
- "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/mime-types": {
- "version": "2.1.35",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
- "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
- "license": "MIT",
- "dependencies": {
- "mime-db": "1.52.0"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^1.1.7"
- },
- "engines": {
- "node": "*"
- }
- },
- "node_modules/ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
- "license": "MIT"
- },
- "node_modules/negotiator": {
- "version": "0.6.3",
- "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
- "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/nodemailer": {
- "version": "6.10.1",
- "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz",
- "integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==",
- "license": "MIT-0",
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/nodemon": {
- "version": "3.1.10",
- "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz",
- "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "chokidar": "^3.5.2",
- "debug": "^4",
- "ignore-by-default": "^1.0.1",
- "minimatch": "^3.1.2",
- "pstree.remy": "^1.1.8",
- "semver": "^7.5.3",
- "simple-update-notifier": "^2.0.0",
- "supports-color": "^5.5.0",
- "touch": "^3.1.0",
- "undefsafe": "^2.0.5"
- },
- "bin": {
- "nodemon": "bin/nodemon.js"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/nodemon"
- }
- },
- "node_modules/nodemon/node_modules/debug": {
- "version": "4.4.1",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
- "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/nodemon/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/normalize-path": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
- "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/object-assign": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
- "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/object-inspect": {
- "version": "1.13.4",
- "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
- "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/on-finished": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
- "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
- "license": "MIT",
- "dependencies": {
- "ee-first": "1.1.1"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/parseurl": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
- "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/path-to-regexp": {
- "version": "0.1.12",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
- "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
- "license": "MIT"
- },
- "node_modules/picomatch": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/proxy-addr": {
- "version": "2.0.7",
- "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
- "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
- "license": "MIT",
- "dependencies": {
- "forwarded": "0.2.0",
- "ipaddr.js": "1.9.1"
- },
- "engines": {
- "node": ">= 0.10"
- }
- },
- "node_modules/pstree.remy": {
- "version": "1.1.8",
- "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
- "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/qs": {
- "version": "6.13.0",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
- "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "side-channel": "^1.0.6"
- },
- "engines": {
- "node": ">=0.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/range-parser": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
- "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/raw-body": {
- "version": "2.5.2",
- "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
- "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
- "license": "MIT",
- "dependencies": {
- "bytes": "3.1.2",
- "http-errors": "2.0.0",
- "iconv-lite": "0.4.24",
- "unpipe": "1.0.0"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/readdirp": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
- "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "picomatch": "^2.2.1"
- },
- "engines": {
- "node": ">=8.10.0"
- }
- },
- "node_modules/resend": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/resend/-/resend-6.0.2.tgz",
- "integrity": "sha512-um08qWpSVvEVqAePEy/bsa7pqtnJK+qTCZ0Et7YE7xuqM46J0C9gnSbIJKR3LIcRVMgO9jUeot8rH0UI84eqMQ==",
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "@react-email/render": "^1.1.0"
- },
- "peerDependenciesMeta": {
- "@react-email/render": {
- "optional": true
- }
- }
- },
- "node_modules/safe-buffer": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
- "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT"
- },
- "node_modules/safer-buffer": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
- "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
- "license": "MIT"
- },
- "node_modules/semver": {
- "version": "7.7.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
- "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/send": {
- "version": "0.19.0",
- "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
- "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
- "license": "MIT",
- "dependencies": {
- "debug": "2.6.9",
- "depd": "2.0.0",
- "destroy": "1.2.0",
- "encodeurl": "~1.0.2",
- "escape-html": "~1.0.3",
- "etag": "~1.8.1",
- "fresh": "0.5.2",
- "http-errors": "2.0.0",
- "mime": "1.6.0",
- "ms": "2.1.3",
- "on-finished": "2.4.1",
- "range-parser": "~1.2.1",
- "statuses": "2.0.1"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/send/node_modules/encodeurl": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
- "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/send/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "license": "MIT"
- },
- "node_modules/serve-static": {
- "version": "1.16.2",
- "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
- "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
- "license": "MIT",
- "dependencies": {
- "encodeurl": "~2.0.0",
- "escape-html": "~1.0.3",
- "parseurl": "~1.3.3",
- "send": "0.19.0"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/setprototypeof": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
- "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
- "license": "ISC"
- },
- "node_modules/side-channel": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
- "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "object-inspect": "^1.13.3",
- "side-channel-list": "^1.0.0",
- "side-channel-map": "^1.0.1",
- "side-channel-weakmap": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/side-channel-list": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
- "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "object-inspect": "^1.13.3"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/side-channel-map": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
- "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.2",
- "es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.5",
- "object-inspect": "^1.13.3"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/side-channel-weakmap": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
- "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.2",
- "es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.5",
- "object-inspect": "^1.13.3",
- "side-channel-map": "^1.0.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/simple-update-notifier": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
- "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "semver": "^7.5.3"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/statuses": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
- "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/supports-color": {
- "version": "5.5.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
- "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "has-flag": "^3.0.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/to-regex-range": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
- "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "is-number": "^7.0.0"
- },
- "engines": {
- "node": ">=8.0"
- }
- },
- "node_modules/toidentifier": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
- "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
- "license": "MIT",
- "engines": {
- "node": ">=0.6"
- }
- },
- "node_modules/touch": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
- "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "nodetouch": "bin/nodetouch.js"
- }
- },
- "node_modules/type-is": {
- "version": "1.6.18",
- "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
- "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
- "license": "MIT",
- "dependencies": {
- "media-typer": "0.3.0",
- "mime-types": "~2.1.24"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/undefsafe": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
- "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/unpipe": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
- "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/utils-merge": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
- "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4.0"
- }
- },
- "node_modules/vary": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
- "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- }
- }
-}
diff --git a/backend/package.json b/backend/package.json
deleted file mode 100644
index 55dc00f..0000000
--- a/backend/package.json
+++ /dev/null
@@ -1,29 +0,0 @@
-{
- "name": "portfolio-backend",
- "version": "1.0.0",
- "description": "Backend API for portfolio contact form",
- "main": "server.js",
- "type": "module",
- "scripts": {
- "dev": "nodemon server.js",
- "start": "node server.js",
- "build": "echo 'No build step needed for Node.js'",
- "test": "echo 'No tests yet'"
- },
- "dependencies": {
- "cors": "^2.8.5",
- "dotenv": "^16.3.1",
- "express": "^4.18.2",
- "express-rate-limit": "^7.1.5",
- "helmet": "^7.1.0",
- "joi": "^17.11.0",
- "nodemailer": "^6.9.7",
- "resend": "^6.0.2"
- },
- "devDependencies": {
- "nodemon": "^3.0.2"
- },
- "engines": {
- "node": ">=18.0.0"
- }
-}
diff --git a/backend/server.js b/backend/server.js
deleted file mode 100644
index b103b68..0000000
--- a/backend/server.js
+++ /dev/null
@@ -1,123 +0,0 @@
-import express from 'express';
-import cors from 'cors';
-import helmet from 'helmet';
-import rateLimit from 'express-rate-limit';
-import dotenv from 'dotenv';
-import { fileURLToPath } from 'url';
-import { dirname, join } from 'path';
-import { contactRouter } from './routes/contact.js';
-import { errorHandler, notFoundHandler } from './middleware/errorHandlers.js';
-import { requestLogger } from './middleware/logger.js';
-import { initializeEmailService } from './services/emailService.js';
-
-// Get the directory of the current module
-const __filename = fileURLToPath(import.meta.url);
-const __dirname = dirname(__filename);
-
-// Load environment variables from the backend directory
-dotenv.config({ path: join(__dirname, '.env') });
-
-async function startServer() {
- // Initialize email service after environment variables are loaded
- await initializeEmailService();
-
- const app = express();
- const PORT = process.env.PORT || 3001;
-
- // Trust proxy (required for Railway, Heroku, etc.)
- app.set('trust proxy', 1);
-
- // Security middleware
- app.use(
- helmet({
- crossOriginResourcePolicy: { policy: 'cross-origin' },
- })
- );
-
- // CORS configuration
- app.use(
- cors({
- origin: [
- 'http://localhost:5173',
- 'http://localhost:3000',
- 'https://portfolio-page-zeta-ten.vercel.app',
- ],
- credentials: true,
- methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
- allowedHeaders: ['Content-Type', 'Authorization'],
- })
- );
-
- // Rate limiting
- const limiter = rateLimit({
- windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000, // 15 minutes
- max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS) || 5, // limit each IP to 5 requests per windowMs
- message: {
- error: 'Too many requests from this IP, please try again later.',
- retryAfter: Math.ceil(
- (parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000) / 1000
- ),
- },
- standardHeaders: true,
- legacyHeaders: false,
- // Add this for better proxy support
- skip: (req) => {
- // Skip rate limiting for health checks
- return req.path === '/health';
- },
- });
-
- app.use('/api/', limiter);
-
- // Body parsing middleware
- app.use(express.json({ limit: '10mb' }));
- app.use(express.urlencoded({ extended: true, limit: '10mb' }));
-
- // Request logging
- app.use(requestLogger);
-
- // Health check endpoint
- app.get('/health', (req, res) => {
- res.json({
- status: 'ok',
- timestamp: new Date().toISOString(),
- environment: process.env.NODE_ENV || 'development',
- });
- });
-
- // API routes
- app.use('/api/contact', contactRouter);
-
- // Error handling middleware
- app.use(notFoundHandler);
- app.use(errorHandler);
-
- // Start server
- app.listen(PORT, '0.0.0.0', () => {
- console.log(`🚀 Server is running on port ${PORT}`);
- console.log(
- `📧 Email service: ${process.env.EMAIL_SERVICE || 'Not configured'}`
- );
- console.log(`🌍 Environment: ${process.env.NODE_ENV || 'development'}`);
- console.log(
- `🔗 Frontend URL: ${process.env.FRONTEND_URL || 'http://localhost:5173'}`
- );
- });
-
- // Graceful shutdown
- process.on('SIGTERM', () => {
- console.log('SIGTERM signal received: closing HTTP server');
- process.exit(0);
- });
-
- process.on('SIGINT', () => {
- console.log('SIGINT signal received: closing HTTP server');
- process.exit(0);
- });
-}
-
-// Start the server
-startServer().catch((error) => {
- console.error('Failed to start server:', error);
- process.exit(1);
-});
diff --git a/backend/services/emailService.js b/backend/services/emailService.js
deleted file mode 100644
index 3ce458e..0000000
--- a/backend/services/emailService.js
+++ /dev/null
@@ -1,195 +0,0 @@
-import nodemailer from 'nodemailer';
-
-// Create email transporter for custom SMTP server
-const createTransporter = () => {
- console.log('🔧 Creating custom SMTP transporter with config:', {
- host: process.env.SMTP_HOST,
- port: process.env.SMTP_PORT,
- secure: process.env.SMTP_SECURE === 'true',
- user: process.env.SMTP_USER ? 'Set' : 'Missing',
- pass: process.env.SMTP_PASS ? 'Set' : 'Missing',
- });
-
- if (
- !process.env.SMTP_HOST ||
- !process.env.SMTP_USER ||
- !process.env.SMTP_PASS
- ) {
- throw new Error('Custom SMTP credentials not configured');
- }
-
- return nodemailer.createTransport({
- host: process.env.SMTP_HOST,
- port: parseInt(process.env.SMTP_PORT) || 587,
- secure: process.env.SMTP_SECURE === 'true', // true for 465 (SSL), false for 587 (TLS)
- auth: {
- user: process.env.SMTP_USER,
- pass: process.env.SMTP_PASS,
- },
- tls: {
- rejectUnauthorized: false, // Accept self-signed certificates if needed
- ciphers: 'SSLv3', // For older servers if needed
- },
- timeout: 30000,
- connectionTimeout: 30000,
- greetingTimeout: 10000,
- socketTimeout: 30000,
- debug: process.env.NODE_ENV === 'development',
- logger: process.env.NODE_ENV === 'development',
- });
-};
-
-// Verify email configuration
-export const verifyEmailConfig = async () => {
- try {
- console.log('🔍 Verifying custom SMTP configuration...');
- console.log('SMTP_HOST:', process.env.SMTP_HOST || 'Missing');
- console.log(
- 'SMTP_PORT:',
- process.env.SMTP_PORT || 'Missing (default: 587)'
- );
- console.log(
- 'SMTP_SECURE:',
- process.env.SMTP_SECURE || 'Missing (default: false)'
- );
- console.log('SMTP_USER:', process.env.SMTP_USER ? 'Set' : 'Missing');
- console.log('SMTP_PASS:', process.env.SMTP_PASS ? 'Set' : 'Missing');
- console.log('FROM_EMAIL:', process.env.FROM_EMAIL ? 'Set' : 'Missing');
- console.log(
- 'RECIPIENT_EMAIL:',
- process.env.RECIPIENT_EMAIL ? 'Set' : 'Missing'
- );
-
- if (
- !process.env.SMTP_HOST ||
- !process.env.SMTP_USER ||
- !process.env.SMTP_PASS
- ) {
- console.error('❌ Missing required SMTP credentials');
- return false;
- }
-
- if (!process.env.FROM_EMAIL || !process.env.RECIPIENT_EMAIL) {
- console.error('❌ Missing sender or recipient email');
- return false;
- }
-
- // Skip verification in production to avoid Railway timeout issues
- if (process.env.NODE_ENV === 'production') {
- console.log('🔧 Skipping SMTP verification in production environment');
- return true;
- }
-
- // Test connection in development
- const transporter = createTransporter();
- await transporter.verify();
- console.log('✅ SMTP configuration verified successfully');
- return true;
- } catch (error) {
- console.error('❌ SMTP configuration verification failed:', error.message);
-
- if (process.env.NODE_ENV === 'production') {
- console.log(
- '🔧 Continuing in production mode despite verification failure'
- );
- return true;
- }
-
- return false;
- }
-};
-
-// Send contact form email
-export const sendContactEmail = async ({
- firstName,
- lastName,
- email,
- subject,
- message,
- senderIP,
- userAgent,
-}) => {
- const startTime = Date.now();
- console.log('📧 Starting email send process via custom SMTP...');
-
- try {
- const transporter = createTransporter();
- const fullName = `${firstName} ${lastName}`;
-
- const mailOptions = {
- from: {
- name: 'Portfolio Contact Form',
- address: process.env.FROM_EMAIL, // Your verified domain email
- },
- to: process.env.RECIPIENT_EMAIL,
- replyTo: {
- name: fullName,
- address: email,
- },
- subject: `[Portfolio] ${subject}`,
- html: `
-
New Contact Form Submission
- Name: ${fullName}
- Email: ${email}
- Subject: ${subject}
- Message:
-
- ${message.replace(/\n/g, ' ')}
-
-
- IP: ${senderIP || 'Unknown'}
- User Agent: ${userAgent || 'Unknown'}
- Sent: ${new Date().toISOString()}
- `,
- text: `
-New Contact Form Submission
-
-Name: ${fullName}
-Email: ${email}
-Subject: ${subject}
-
-Message:
-${message}
-
----
-IP: ${senderIP || 'Unknown'}
-User Agent: ${userAgent || 'Unknown'}
-Sent: ${new Date().toISOString()}
- `,
- };
-
- const result = await transporter.sendMail(mailOptions);
-
- const duration = Date.now() - startTime;
- console.log(
- `✅ Email sent successfully via custom SMTP in ${duration}ms:`,
- result.messageId
- );
-
- return result;
- } catch (error) {
- const duration = Date.now() - startTime;
- console.error(`❌ Custom SMTP email send failed after ${duration}ms:`, {
- message: error.message,
- code: error.code,
- command: error.command,
- });
-
- throw new Error(`Email sending failed: ${error.message}`);
- }
-};
-
-// Initialize email service
-export const initializeEmailService = async () => {
- console.log('🔧 Initializing custom SMTP email service...');
-
- const isValid = await verifyEmailConfig();
-
- if (!isValid) {
- console.warn('⚠️ Custom SMTP email service not properly configured.');
- } else {
- console.log('✅ Custom SMTP email service initialized successfully');
- }
-
- return isValid;
-};
diff --git a/backend/templates/emailTemplates.js b/backend/templates/emailTemplates.js
deleted file mode 100644
index c642ea2..0000000
--- a/backend/templates/emailTemplates.js
+++ /dev/null
@@ -1,158 +0,0 @@
-// Email templates for contact form submissions
-
-export const createContactEmailTemplate = ({
- fullName,
- email,
- subject,
- message,
- senderIP,
- userAgent,
- timestamp,
-}) => {
- // HTML template
- const html = `
-
-
-
-
-
- New Portfolio Contact
-
-
-
-
-
-
-
-
👤 From:
-
${fullName}
-
-
-
-
-
-
📋 Subject:
-
${subject}
-
-
-
-
💬 Message:
-
${message}
-
-
-
- 💡 Quick Reply: You can reply directly to this email to respond to ${fullName}.
-
-
-
-
-
-
- `;
-
- // Plain text version
- const text = `
-NEW PORTFOLIO CONTACT
-=====================
-
-From: ${fullName}
-Email: ${email}
-Subject: ${subject}
-
-Message:
---------
-${message}
-
-Metadata:
----------
-Received: ${new Date(timestamp).toLocaleString()}
-IP Address: ${senderIP}
-User Agent: ${userAgent || 'Not provided'}
-
-You can reply directly to this email to respond to the sender.
- `;
-
- return { html, text };
-};
diff --git a/backend/test-email.js b/backend/test-email.js
deleted file mode 100644
index bdedb62..0000000
--- a/backend/test-email.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import nodemailer from 'nodemailer';
-import dotenv from 'dotenv';
-import { fileURLToPath } from 'url';
-import { dirname, join } from 'path';
-
-// Get the directory of the current module
-const __filename = fileURLToPath(import.meta.url);
-const __dirname = dirname(__filename);
-
-// Load environment variables
-dotenv.config({ path: join(__dirname, '.env') });
-
-console.log('Testing Gmail configuration...');
-console.log('EMAIL_USER:', process.env.EMAIL_USER);
-console.log(
- 'EMAIL_PASS length:',
- process.env.EMAIL_PASS ? process.env.EMAIL_PASS.length : 'undefined'
-);
-
-const transporter = nodemailer.createTransport({
- service: 'gmail',
- auth: {
- user: process.env.EMAIL_USER,
- pass: process.env.EMAIL_PASS,
- },
-});
-
-transporter
- .verify()
- .then(() => {
- console.log('✅ Gmail connection successful!');
- process.exit(0);
- })
- .catch((error) => {
- console.error('❌ Gmail connection failed:');
- console.error('Error code:', error.code);
- console.error('Error message:', error.message);
- console.error('Response:', error.response);
- process.exit(1);
- });
diff --git a/package-lock.json b/package-lock.json
index 4e6ce6c..582dd76 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,7 +14,8 @@
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-icons": "^5.5.0",
- "react-router-dom": "^7.8.2"
+ "react-router-dom": "^7.8.2",
+ "resend": "^6.0.3"
},
"devDependencies": {
"@eslint/js": "^9.32.0",
@@ -3383,6 +3384,23 @@
"url": "https://paulmillr.com/funding/"
}
},
+ "node_modules/resend": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/resend/-/resend-6.0.3.tgz",
+ "integrity": "sha512-Yb7o2n52v91LjkUjq5vIZQbJrGiaPoMo2VPAyS0QTKA73tmvHMUQ7P1M56J1ux/Cw9Lf68PeaKidTl6GuS8DTA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@react-email/render": "^1.1.0"
+ },
+ "peerDependenciesMeta": {
+ "@react-email/render": {
+ "optional": true
+ }
+ }
+ },
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
diff --git a/package.json b/package.json
index ef56dc2..3ace978 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,6 @@
},
"dependencies": {
"@types/react-router-dom": "^5.3.3",
- "express-validator": "^7.2.1",
"lucide-react": "^0.542.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
diff --git a/src/app/AppRouter.tsx b/src/app/AppRouter.tsx
index 10b8ec9..20fb34a 100644
--- a/src/app/AppRouter.tsx
+++ b/src/app/AppRouter.tsx
@@ -5,6 +5,7 @@ import Navbar from '../components/layout/Navigation';
import Footer from '../components/layout/Footer';
import HomePage from '../pages/HomePage';
import ImprintPage from '../pages/ImprintPage';
+import PrivacyPolicy from '../pages/PrivacyPolicy';
function AppRouter() {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
@@ -56,6 +57,7 @@ function AppRouter() {
} />
} />
+ } />
diff --git a/src/assets/icarace.PNG b/src/assets/icarace.PNG
new file mode 100644
index 0000000..002c12e
Binary files /dev/null and b/src/assets/icarace.PNG differ
diff --git a/src/components/ThemeToggle.tsx b/src/components/ThemeToggle.tsx
index 9ba80f5..fd48ede 100644
--- a/src/components/ThemeToggle.tsx
+++ b/src/components/ThemeToggle.tsx
@@ -1,23 +1,29 @@
import React from 'react';
+import { getTexts } from '../config/texts';
type Props = {
theme: 'light' | 'dark';
setTheme: React.Dispatch>;
};
-export default function ThemeToggle({ theme, setTheme }: Props) {
+export default function ThemeToggle({
+ theme,
+ setTheme
+}: Props) {
+ const texts = getTexts();
+
const toggleTheme = () => {
setTheme(theme === 'light' ? 'dark' : 'light');
};
return (
-
- {theme === 'light' ? '🌙' : '☀️'}
+ {theme === 'light' ? texts.themeToggle.lightIcon : texts.themeToggle.darkIcon}
);
diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx
index 533437f..46d12f2 100644
--- a/src/components/layout/Footer.tsx
+++ b/src/components/layout/Footer.tsx
@@ -1,8 +1,10 @@
import { Github, Linkedin, Mail } from 'lucide-react';
import { Link } from 'react-router-dom';
import { personalConfig, createEmailLink } from '../../config/personal';
+import { getTexts } from '../../config/texts';
export default function Footer() {
+ const texts = getTexts();
// Email obfuscation function using config
const handleEmailClick = (e: React.MouseEvent) => {
e.preventDefault();
@@ -17,10 +19,10 @@ export default function Footer() {
- Impressum
+ {texts.footer.imprintText}
- © {new Date().getFullYear()} {personalConfig.name}. All rights reserved.
+ © {new Date().getFullYear()} {personalConfig.name}. {texts.footer.copyrightText}
@@ -28,7 +30,7 @@ export default function Footer() {
window.open(personalConfig.social.github.url, '_blank')}
- aria-label="GitHub Profile"
+ aria-label={texts.footer.githubAriaLabel}
>
@@ -36,7 +38,7 @@ export default function Footer() {
window.open(personalConfig.social.linkedin.url, '_blank')}
- aria-label="LinkedIn Profile"
+ aria-label={texts.footer.linkedinAriaLabel}
>
@@ -44,7 +46,7 @@ export default function Footer() {
diff --git a/src/components/layout/Navigation.tsx b/src/components/layout/Navigation.tsx
index 6d98bbd..3b57826 100644
--- a/src/components/layout/Navigation.tsx
+++ b/src/components/layout/Navigation.tsx
@@ -3,6 +3,7 @@ import { Link, useLocation, useNavigate } from 'react-router-dom';
import ThemeToggle from '../ThemeToggle';
import MobileMenu from './MobileMenu';
import { personalConfig } from '../../config/personal';
+import { getTexts } from '../../config/texts';
import { scrollToSection } from '../../utils/scrollUtils';
type Props = {
@@ -10,19 +11,16 @@ type Props = {
setTheme: React.Dispatch
>;
};
-export default function Navbar({ theme, setTheme }: Props) {
+export default function Navbar({
+ theme,
+ setTheme
+}: Props) {
const [menuOpen, setMenuOpen] = useState(false);
const location = useLocation();
const navigate = useNavigate();
+ const texts = getTexts();
- const menuItems = [
- 'About',
- 'Services',
- 'Skills',
- 'Certifications',
- 'Projects',
- 'Contact',
- ];
+ const menuItems = texts.navigation.menuItems;
function handleNavigation(section: string): void {
const sectionId = section.toLowerCase();
@@ -49,7 +47,7 @@ export default function Navbar({ theme, setTheme }: Props) {
{/* Burger Button für Mobile */}
setMenuOpen(!menuOpen)}
>
{[0, 1, 2].map(i =>
)}
diff --git a/src/components/sections/ContactSection.tsx b/src/components/sections/ContactSection.tsx
index 019a871..8181a90 100644
--- a/src/components/sections/ContactSection.tsx
+++ b/src/components/sections/ContactSection.tsx
@@ -1,218 +1,49 @@
-import { Mail, Github, Linkedin } from 'lucide-react';
-import { useState, useRef } from 'react';
-import type { FormEvent } from 'react';
-import { personalConfig, getObfuscatedEmail, createEmailLink } from '../../config/personal';
-import { API_CONFIG } from '../../config/api';
+import { Mail, Github, Linkedin, Send } from 'lucide-react';
+import { personalConfig, getObfuscatedEmail } from '../../config/personal';
+import { getTexts } from '../../config/texts';
interface ContactSectionProps {
title?: string;
subtitle?: string;
- email?: string;
- github?: string;
- linkedin?: string;
connectTitle?: string;
connectDescription?: string;
+ buttonText?: string;
+ socialTitle?: string;
+ availabilityText?: string;
+ responseTimeLabel?: string;
+ responseTimeValue?: string;
+ preferredContactLabel?: string;
+ preferredContactValue?: string;
+ locationLabel?: string;
+ locationValue?: string;
}
-// TODO Create Backend API for handling form submissions securely
-export default function ContactSection({
- title = "Get In Touch",
- subtitle = "Let's discuss your next project or collaboration opportunity",
- github = personalConfig.social.github.username,
- connectTitle = "Let's Connect",
- connectDescription = "I'm always interested in new opportunities and exciting projects. Whether you have a question or just want to say hi, feel free to reach out!"
-}: ContactSectionProps) {
- // Anti-spam honeypot state
- const [honeypot, setHoneypot] = useState('');
- const [isSubmitting, setIsSubmitting] = useState(false);
- const [formStatus, setFormStatus] = useState<{
- type: 'success' | 'error' | null;
- message: string;
- }>({ type: null, message: '' });
+// TODO Create contact form with backend access
+export default function ContactSection(props: ContactSectionProps = {}) {
+ const texts = getTexts().contact;
- // GDPR consent state
- const [gdprConsent, setGdprConsent] = useState(false);
+ // Use centralized texts as defaults, allow props to override
+ const {
+ title = texts.title,
+ subtitle = texts.subtitle,
+ connectTitle = texts.connectTitle,
+ connectDescription = texts.connectDescription,
+ buttonText = texts.buttonText,
+ socialTitle = texts.socialTitle,
+ availabilityText = texts.availabilityText,
+ responseTimeLabel = texts.responseTimeLabel,
+ responseTimeValue = texts.responseTimeValue,
+ preferredContactLabel = texts.preferredContactLabel,
+ preferredContactValue = texts.preferredContactValue,
+ locationLabel = texts.locationLabel,
+ locationValue = texts.locationValue,
+ } = props;
- // Email obfuscation function using config
- const handleEmailClick = (e: React.MouseEvent) => {
- e.preventDefault();
- window.location.href = createEmailLink();
- };
-
- // Add form ref
- const formRef = useRef
(null);
-
- const handleSubmit = async (e: FormEvent) => {
- e.preventDefault();
-
- // Check honeypot - if filled, it's likely spam
- if (honeypot) {
- console.warn('Spam detected - honeypot field was filled');
- setFormStatus({
- type: 'error',
- message: 'Invalid form submission detected.'
- });
- setIsSubmitting(false);
- return;
- }
-
- // GDPR consent validation
- if (!gdprConsent) {
- setFormStatus({
- type: 'error',
- message: 'Please accept the privacy policy to continue.'
- });
- return;
- }
-
- // Prevent rapid submissions
- if (isSubmitting) {
- return;
- }
-
- setIsSubmitting(true);
- setFormStatus({ type: null, message: '' }); // Clear previous status
-
- // Get form data
- const formData = new FormData(e.currentTarget);
- const data = {
- firstName: formData.get('firstName') as string,
- lastName: formData.get('lastName') as string,
- email: formData.get('email') as string,
- subject: formData.get('subject') as string,
- message: formData.get('message') as string,
- gdprConsent: gdprConsent.toString(), // Add this
- website: honeypot, // Anti-spam honeypot
- };
-
- // Basic validation
- if (!data.firstName || !data.lastName || !data.email || !data.message) {
- setFormStatus({
- type: 'error',
- message: 'Please fill in all required fields.'
- });
- setIsSubmitting(false);
- return;
- }
-
- // Email validation
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
- if (!emailRegex.test(data.email)) {
- setFormStatus({
- type: 'error',
- message: 'Please enter a valid email address.'
- });
- setIsSubmitting(false);
- return;
- }
-
- try {
- // Submit to your own backend API
- const response = await fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.CONTACT}`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- firstName: data.firstName,
- lastName: data.lastName,
- email: data.email,
- subject: data.subject || 'Contact from Portfolio',
- message: data.message,
- website: honeypot, // Include honeypot for spam detection
- }),
- });
-
- // Check if response is ok before trying to parse JSON
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
-
- // Check if response has content before parsing JSON
- const text = await response.text();
- if (!text) {
- throw new Error('Empty response from server');
- }
-
- const result = JSON.parse(text);
-
- if (result.success) {
- setFormStatus({
- type: 'success',
- message: result.message
- });
-
- // Reset form using ref
- if (formRef.current) {
- formRef.current.reset();
- }
- setHoneypot(''); // Reset honeypot
-
- // Clear success message after 5 seconds
- setTimeout(() => {
- setFormStatus({ type: null, message: '' });
- }, 5000);
- } else {
- // Handle different types of server errors
- let errorMessage = 'Form submission failed';
-
- if (response.status === 400) {
- if (result.errors && Array.isArray(result.errors)) {
- // Validation errors from server
- const validationErrors = result.errors.map((err: any) =>
- typeof err === 'object' ? err.message : err
- ).join(', ');
- errorMessage = `Validation errors: ${validationErrors}`;
- } else if (result.message) {
- errorMessage = result.message;
- } else {
- errorMessage = 'Please check your form data and try again.';
- }
- } else if (response.status === 429) {
- errorMessage = 'Too many requests. Please wait a moment and try again.';
- } else if (response.status === 500) {
- errorMessage = 'Server error. Our team has been notified. Please try again later.';
- } else if (result.message) {
- errorMessage = result.message;
- }
-
- throw new Error(errorMessage);
- }
- } catch (error) {
- console.error('Form submission error:', error);
-
- let userMessage = 'Sorry, there was an error sending your message.';
-
- if (error instanceof Error) {
- // Network errors
- if (error.message.includes('Failed to fetch') || error.message.includes('NetworkError')) {
- userMessage = 'Network error: Please check your internet connection and try again.';
- }
- // Timeout errors
- else if (error.message.includes('timeout')) {
- userMessage = 'Request timeout: The server is taking too long to respond. Please try again.';
- }
- // CORS errors
- else if (error.message.includes('CORS')) {
- userMessage = 'Connection error: Please refresh the page and try again.';
- }
- // Server validation errors or custom error messages
- else if (error.message.length > 0 && !error.message.includes('Form submission failed')) {
- userMessage = error.message;
- }
- // Generic fallback
- else {
- userMessage = 'Unexpected error occurred. Please try again or contact us directly.';
- }
- }
-
- setFormStatus({
- type: 'error',
- message: userMessage
- });
- }
-
- setIsSubmitting(false);
+ // Create email link with pre-filled subject and body
+ const handleEmailClick = () => {
+ const subject = encodeURIComponent("Portfolio Inquiry - Let's discuss my project");
+ const body = encodeURIComponent("Hello Sascha,\n\nI visited your portfolio and would like to discuss a potential project or collaboration.\n\nBest regards,");
+ window.location.href = `mailto:info@sascha-bach.de?subject=${subject}&body=${body}`;
};
return (
@@ -230,193 +61,94 @@ export default function ContactSection({
{/* Contact Content Grid */}
- {/* Contact Info */}
-
-
-
{connectTitle}
-
- {connectDescription}
-
-
-
-
-
-
- {getObfuscatedEmail()}
-
-
-
- {github}
-
-
-
-
{personalConfig.social.linkedin.username}
+ {/* Let's Connect Card */}
+
+
+
+
+
{connectTitle}
+
+ {connectDescription}
+
-
- window.open(personalConfig.social.github.url, '_blank')}
- aria-label="GitHub Profile"
- >
-
-
- window.open(personalConfig.social.linkedin.url, '_blank')}
- aria-label="LinkedIn Profile"
- >
-
-
-
-
-
+ {
+ const subject = encodeURIComponent("Portfolio Inquiry - Let's discuss your project");
+ const body = encodeURIComponent("Hello Sascha,\n\nI visited your portfolio and would like to discuss a potential project or collaboration.\n\nBest regards,");
+ window.location.href = `mailto:freelancer@sascha-bach.de?subject=${subject}&body=${body}`;
+ }}
+ className="contact-section__email-button"
+ type="button"
+ >
+
+
+ {buttonText}
+
+
+
+
+ {/* Contact Info Sidebar */}
+
+
+
+
+
+
+ {responseTimeLabel}
+ {responseTimeValue}
+
+
+ {preferredContactLabel}
+ {preferredContactValue}
+
+
+ {locationLabel}
+ {locationValue}
+
+
- {/* Contact Form */}
-
-
-
Send a Message
-
- I'll get back to you as soon as possible
-
+ {/* Social Contact Methods - Below connect card but same width */}
+
+
{socialTitle}
+
+
+
+
+
+ {getObfuscatedEmail()}
+
+
+ window.open(personalConfig.social.github.url, '_blank')}
+ aria-label="GitHub Profile"
+ >
+
+
+ {personalConfig.social.github.username}
+
+
+ window.open(personalConfig.social.linkedin.url, '_blank')}
+ aria-label="LinkedIn Profile"
+ >
+
+
+ {personalConfig.social.linkedin.username}
+
-
- {/* Status Message Display */}
- {formStatus.type && (
-
-
- {formStatus.message}
-
-
- )}
-
-
diff --git a/src/components/sections/ProjectsSection.tsx b/src/components/sections/ProjectsSection.tsx
index a411622..977034f 100644
--- a/src/components/sections/ProjectsSection.tsx
+++ b/src/components/sections/ProjectsSection.tsx
@@ -1,8 +1,10 @@
import { Github, ExternalLink } from 'lucide-react';
import { personalConfig } from '../../config/personal';
+import { getTexts } from '../../config/texts';
import type { Project } from '../../data/Project';
import ergoVRImage from '../../assets/ErgoVR.PNG';
import portfolioImage from '../../assets/portfolio.PNG'
+import icaraceImage from '../../assets/icarace.PNG'
interface ProjectsSectionProps {
@@ -12,8 +14,8 @@ interface ProjectsSectionProps {
}
export default function ProjectsSection({
- title = "Featured Projects",
- subtitle = "A showcase of my recent work and personal projects",
+ title,
+ subtitle,
projects = [
{
id: 1,
@@ -27,7 +29,7 @@ export default function ProjectsSection({
{
id: 2,
title: "ErgoVR",
- description: "A virtual reality application for analysis of motion sickness in VR environments.",
+ description: "A virtual reality application for analysis of motion sickness in VR environments from 2015.",
image: ergoVRImage,
technologies: ["Unity3D", "C#", "Oculus SDK"],
github: personalConfig.projects.ergoVR
@@ -36,38 +38,27 @@ export default function ProjectsSection({
id: 3,
title: "Icarace",
description: "Participated in the development of a web-platform for a fitness racing game for Icaros GmbH until 2018.",
- image: ergoVRImage,
+ image: icaraceImage,
technologies: ["Angular 4", "Typescript", "HTML", "CSS"],
github: personalConfig.projects.ergoVR
- },
- {
- id: 4,
- title: "ErgoVR",
- description: "Gravity is a fitness game for the Icaros Device developed by Icaros GmbH. I participated in developing behavior scripts in C#.",
- image: ergoVRImage,
- technologies: ["Unity3D", "C#", "Oculus SDK"],
- github: personalConfig.projects.ergoVR
- },
- {
- id: 5,
- title: "ErgoVR",
- description: "A virtual reality application for analysis of motion sickness in VR environments.",
- image: ergoVRImage,
- technologies: ["Unity3D", "C#", "Oculus SDK"],
- github: personalConfig.projects.ergoVR
}
]
}: ProjectsSectionProps) {
+ const texts = getTexts();
+
+ // Use texts from centralized config with fallbacks
+ const sectionTitle = title || texts.projects.title;
+ const sectionSubtitle = subtitle || texts.projects.subtitle;
return (
{/* Section Header */}
- {title}
+ {sectionTitle}
- {subtitle}
+ {sectionSubtitle}
@@ -91,7 +82,7 @@ export default function ProjectsSection({
className="projects-section__action-button projects-section__action-button--secondary"
>
- Code
+ {texts.projects.codeButtonText}
)}
{project.live && (
@@ -102,7 +93,7 @@ export default function ProjectsSection({
className="projects-section__action-button projects-section__action-button--primary"
>
- Live
+ {texts.projects.liveButtonText}
)}
diff --git a/src/config/api.ts b/src/config/api.ts
index e7133b9..f1b4867 100644
--- a/src/config/api.ts
+++ b/src/config/api.ts
@@ -1,11 +1,10 @@
-const isDevelopment = import.meta.env.DEV;
+// Note: API configuration no longer needed since we're using direct mailto links
+// This file is kept for reference but can be removed
export const API_CONFIG = {
- BASE_URL: isDevelopment
- ? 'http://localhost:3002'
- : 'https://portfolio-backend-production-39b3.up.railway.app',
+ BASE_URL: '', // No longer used
ENDPOINTS: {
- CONTACT: '/api/contact',
+ CONTACT: '/api/contact', // No longer used
},
};
diff --git a/src/config/texts.ts b/src/config/texts.ts
new file mode 100644
index 0000000..5953958
--- /dev/null
+++ b/src/config/texts.ts
@@ -0,0 +1,381 @@
+// Centralized text configuration for the portfolio
+// This file contains all configurable text used throughout the application
+//
+// Usage Examples:
+//
+// 1. Basic usage in components:
+// const texts = getTexts();
+// {texts.hero.title}
+//
+// 2. Using specific section texts:
+// const contactTexts = getTexts().contact;
+// {contactTexts.title}
+//
+// 3. Future internationalization ready:
+// const texts = getTexts('en'); // or 'de', 'fr', etc.
+//
+// Benefits:
+// - All text in one place for easy management
+// - Ready for internationalization
+// - Type-safe text references
+// - Easy bulk text updates
+// - Consistent text formatting
+
+export interface TextConfig {
+ // Navigation
+ navigation: {
+ menuItems: string[];
+ mobileMenuAriaLabel: string;
+ };
+
+ // Theme Toggle
+ themeToggle: {
+ lightIcon: string;
+ darkIcon: string;
+ lightModeText: string;
+ darkModeText: string;
+ };
+
+ // Footer
+ footer: {
+ imprintText: string;
+ copyrightText: string;
+ githubAriaLabel: string;
+ linkedinAriaLabel: string;
+ emailAriaLabel: string;
+ };
+
+ // Hero Section
+ hero: {
+ title: string;
+ description: string;
+ primaryButtonText: string;
+ secondaryButtonText: string;
+ };
+
+ // About Section
+ about: {
+ title: string;
+ subtitle: string;
+ description: string;
+ profileAlt: string;
+ experienceLabel: string;
+ projectsLabel: string;
+ clientsLabel: string;
+ features: {
+ responsive: { title: string; description: string };
+ performance: { title: string; description: string };
+ modern: { title: string; description: string };
+ accessible: { title: string; description: string };
+ };
+ };
+
+ // Services Section
+ services: {
+ title: string;
+ subtitle: string;
+ };
+
+ // Skills Section
+ skills: {
+ title: string;
+ subtitle: string;
+ };
+
+ // Projects Section
+ projects: {
+ title: string;
+ subtitle: string;
+ codeButtonText: string;
+ liveButtonText: string;
+ };
+
+ // Certifications Section
+ certifications: {
+ title: string;
+ subtitle: string;
+ };
+
+ // Contact Section
+ contact: {
+ title: string;
+ subtitle: string;
+ connectTitle: string;
+ connectDescription: string;
+ buttonText: string;
+ socialTitle: string;
+ availabilityText: string;
+ responseTimeLabel: string;
+ responseTimeValue: string;
+ preferredContactLabel: string;
+ preferredContactValue: string;
+ locationLabel: string;
+ locationValue: string;
+ nameLabel: string;
+ emailLabel: string;
+ subjectLabel: string;
+ messageLabel: string;
+ sendButtonText: string;
+ successMessage: string;
+ errorMessage: string;
+ requiredFieldError: string;
+ invalidEmailError: string;
+ githubText: string;
+ linkedinText: string;
+ emailText: string;
+ };
+
+ // Imprint Page
+ imprint: {
+ title: string;
+ subtitle: string;
+ companyInfoTitle: string;
+ contactTitle: string;
+ responsibilityTitle: string;
+ disclaimerTitle: string;
+ contentLiabilityTitle: string;
+ contentLiabilityText: string[];
+ linksLiabilityTitle: string;
+ linksLiabilityText: string[];
+ copyrightTitle: string;
+ copyrightText: string[];
+ privacyTitle: string;
+ privacyText: string;
+ address: {
+ street: string;
+ city: string;
+ country: string;
+ };
+ contact: {
+ phone: string;
+ };
+ };
+
+ // Privacy Policy Page
+ privacyPolicy: {
+ title: string;
+ lastUpdated: string;
+ dataCollectionTitle: string;
+ dataCollectionText: string;
+ dataCollectionList: string[];
+ purposeTitle: string;
+ purposeText: string;
+ providerTitle: string;
+ providerText: string;
+ retentionTitle: string;
+ retentionText: string;
+ rightsTitle: string;
+ rightsText: string;
+ rightsList: string[];
+ contactTitle: string;
+ contactText: string;
+ contactEmail: string;
+ legalBasisTitle: string;
+ legalBasisText: string;
+ };
+}
+
+// Default text configuration (English/German mix as currently used)
+export const defaultTexts: TextConfig = {
+ navigation: {
+ menuItems: [
+ 'About',
+ 'Services',
+ 'Skills',
+ 'Certifications',
+ 'Projects',
+ 'Contact',
+ ],
+ mobileMenuAriaLabel: 'Menü öffnen',
+ },
+
+ themeToggle: {
+ lightIcon: '🌙',
+ darkIcon: '☀️',
+ lightModeText: 'dark',
+ darkModeText: 'light',
+ },
+
+ footer: {
+ imprintText: 'Impressum',
+ copyrightText: 'All rights reserved.',
+ githubAriaLabel: 'GitHub Profile',
+ linkedinAriaLabel: 'LinkedIn Profile',
+ emailAriaLabel: 'Send Email',
+ },
+
+ hero: {
+ title: 'Building Digital Experiences',
+ description:
+ 'Full-Stack Developer specializing in modern web technologies and user-centered design.',
+ primaryButtonText: 'View Projects',
+ secondaryButtonText: 'Get in Touch',
+ },
+
+ about: {
+ title: 'About Me',
+ subtitle:
+ 'Passionate developer with a focus on clean code and innovative solutions',
+ description:
+ 'With years of experience in web development, I create robust and scalable applications using modern technologies. My approach combines technical expertise with user-centered design principles.',
+ profileAlt: 'Sascha Bach Profile',
+ experienceLabel: 'Years Experience',
+ projectsLabel: 'Projects Completed',
+ clientsLabel: 'Happy Clients',
+ features: {
+ responsive: {
+ title: 'Responsive Design',
+ description:
+ 'Mobile-first approach ensuring optimal experience across all devices',
+ },
+ performance: {
+ title: 'Performance Optimized',
+ description:
+ 'Fast loading times and smooth interactions for better user experience',
+ },
+ modern: {
+ title: 'Modern Technologies',
+ description:
+ 'Latest frameworks and tools for cutting-edge web applications',
+ },
+ accessible: {
+ title: 'Accessibility Focused',
+ description:
+ 'Inclusive design principles ensuring usability for everyone',
+ },
+ },
+ },
+
+ services: {
+ title: 'Services',
+ subtitle: 'Comprehensive web development solutions tailored to your needs',
+ },
+
+ skills: {
+ title: 'Skills & Technologies',
+ subtitle: 'Technical expertise across the full development stack',
+ },
+
+ projects: {
+ title: 'Featured Projects',
+ subtitle: 'A showcase of my recent work and personal projects',
+ codeButtonText: 'Code',
+ liveButtonText: 'Live',
+ },
+
+ certifications: {
+ title: 'Certifications & Achievements',
+ subtitle: 'My professional qualifications and continuous learning',
+ },
+
+ contact: {
+ title: 'Get In Touch',
+ subtitle: "Let's discuss your project and bring your ideas to life",
+ connectTitle: "Let's Connect",
+ connectDescription:
+ "I'm always interested in new opportunities and exciting projects. Whether you have a question or just want to say hi, feel free to reach out!",
+ buttonText: 'Contact via Email',
+ socialTitle: 'Other ways to connect',
+ availabilityText: 'Available for projects',
+ responseTimeLabel: 'Response time',
+ responseTimeValue: 'Within 24 hours',
+ preferredContactLabel: 'Preferred contact',
+ preferredContactValue: 'Email',
+ locationLabel: 'Location',
+ locationValue: 'Germany',
+ nameLabel: 'Your Name',
+ emailLabel: 'Your Email',
+ subjectLabel: 'Subject',
+ messageLabel: 'Your Message',
+ sendButtonText: 'Send Message',
+ successMessage: "Message sent successfully! I'll get back to you soon.",
+ errorMessage:
+ 'Failed to send message. Please try again or contact me directly.',
+ requiredFieldError: 'This field is required',
+ invalidEmailError: 'Please enter a valid email address',
+ githubText: 'Follow my work and contribute to open source projects',
+ linkedinText:
+ 'Connect with me for professional networking and opportunities',
+ emailText: 'Send me a direct message for inquiries and collaborations',
+ },
+
+ imprint: {
+ title: 'Impressum',
+ subtitle: 'Legal Information',
+ companyInfoTitle: 'Angaben gemäß § 5 TMG',
+ contactTitle: 'Kontakt',
+ responsibilityTitle: 'Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV',
+ disclaimerTitle: 'Haftungsausschluss',
+ contentLiabilityTitle: 'Haftung für Inhalte',
+ contentLiabilityText: [
+ 'Als Diensteanbieter sind wir gemäß § 7 Abs.1 TMG für eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 TMG sind wir als Diensteanbieter jedoch nicht unter der Verpflichtung, übermittelte oder gespeicherte fremde Informationen zu überwachen oder nach Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen.',
+ 'Verpflichtungen zur Entfernung oder Sperrung der Nutzung von Informationen nach den allgemeinen Gesetzen bleiben hiervon unberührt. Eine diesbezügliche Haftung ist jedoch erst ab dem Zeitpunkt der Kenntnis einer konkreten Rechtsverletzung möglich. Bei Bekanntwerden von entsprechenden Rechtsverletzungen werden wir diese Inhalte umgehend entfernen.',
+ ],
+ linksLiabilityTitle: 'Haftung für Links',
+ linksLiabilityText: [
+ 'Unser Angebot enthält Links zu externen Websites Dritter, auf deren Inhalte wir keinen Einfluss haben. Deshalb können wir für diese fremden Inhalte auch keine Gewähr übernehmen. Für die Inhalte der verlinkten Seiten ist stets der jeweilige Anbieter oder Betreiber der Seiten verantwortlich.',
+ 'Die verlinkten Seiten wurden zum Zeitpunkt der Verlinkung auf mögliche Rechtsverstöße überprüft. Rechtswidrige Inhalte waren zum Zeitpunkt der Verlinkung nicht erkennbar. Eine permanente inhaltliche Kontrolle der verlinkten Seiten ist jedoch ohne konkrete Anhaltspunkte einer Rechtsverletzung nicht zumutbar. Bei Bekanntwerden von Rechtsverletzungen werden wir derartige Links umgehend entfernen.',
+ ],
+ copyrightTitle: 'Urheberrecht',
+ copyrightText: [
+ 'Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unterliegen dem deutschen Urheberrecht. Die Vervielfältigung, Bearbeitung, Verbreitung und jede Art der Verwertung außerhalb der Grenzen des Urheberrechtes bedürfen der schriftlichen Zustimmung des jeweiligen Autors bzw. Erstellers.',
+ 'Downloads und Kopien dieser Seite sind nur für den privaten, nicht kommerziellen Gebrauch gestattet. Soweit die Inhalte auf dieser Seite nicht vom Betreiber erstellt wurden, werden die Urheberrechte Dritter beachtet. Insbesondere werden Inhalte Dritter als solche gekennzeichnet. Sollten Sie trotzdem auf eine Urheberrechtsverletzung aufmerksam werden, bitten wir um einen entsprechenden Hinweis. Bei Bekanntwerden von Rechtsverletzungen werden wir derartige Inhalte umgehend entfernen.',
+ ],
+ privacyTitle: 'Datenschutz',
+ privacyText:
+ 'Die Nutzung unserer Webseite ist in der Regel ohne Angabe personenbezogener Daten möglich. Soweit auf unseren Seiten personenbezogene Daten (beispielsweise Name, Anschrift oder E-Mail-Adressen) erhoben werden, erfolgt dies, soweit möglich, stets auf freiwilliger Basis. Diese Daten werden ohne Ihre ausdrückliche Zustimmung nicht an Dritte weitergegeben.',
+ address: {
+ street: 'Musterstraße 123',
+ city: '12345 Musterstadt',
+ country: 'Deutschland',
+ },
+ contact: {
+ phone: '+49 (0) 123 456789',
+ },
+ },
+
+ privacyPolicy: {
+ title: 'Privacy Policy (GDPR/DSGVO)',
+ lastUpdated: 'Last updated:',
+ dataCollectionTitle: 'Data Collection',
+ dataCollectionText:
+ 'We collect only the information you provide through our contact form:',
+ dataCollectionList: [
+ 'Name (first and last)',
+ 'Email address',
+ 'Subject and message content',
+ ],
+ purposeTitle: 'Purpose of Data Processing',
+ purposeText:
+ 'Your data is used solely to respond to your inquiry and establish contact.',
+ providerTitle: 'Email Service Provider',
+ providerText:
+ 'We use Resend (resend.com) to send emails from our contact form. Resend is GDPR compliant and processes data according to their privacy policy.',
+ retentionTitle: 'Data Retention',
+ retentionText:
+ 'Contact form data is processed immediately and not stored permanently on our servers. Your message is sent via email and then discarded from our systems.',
+ rightsTitle: 'Your Rights Under GDPR',
+ rightsText: 'You have the right to:',
+ rightsList: [
+ 'Request access to your personal data',
+ 'Request rectification of your personal data',
+ 'Request deletion of your personal data',
+ 'Data portability',
+ 'Object to processing',
+ ],
+ contactTitle: 'Contact',
+ contactText:
+ 'For any privacy-related questions or to exercise your rights, contact:',
+ contactEmail: 'info@sascha-bach.de',
+ legalBasisTitle: 'Legal Basis',
+ legalBasisText:
+ 'Processing is based on your explicit consent (Art. 6(1)(a) GDPR).',
+ },
+};
+
+// Export a function to get texts (can be extended for internationalization)
+export const getTexts = (): TextConfig => {
+ // For now, return default texts
+ // In the future, this can be extended to support multiple languages
+ return defaultTexts;
+};
diff --git a/src/pages/ImprintPage.tsx b/src/pages/ImprintPage.tsx
index 4f0f5d5..efc6273 100644
--- a/src/pages/ImprintPage.tsx
+++ b/src/pages/ImprintPage.tsx
@@ -1,108 +1,81 @@
import { personalConfig } from '../config/personal';
+import { getTexts } from '../config/texts';
export default function ImprintPage() {
+ const texts = getTexts().imprint;
return (
-
Impressum
-
Legal Information
+
{texts.title}
+
{texts.subtitle}
-
Angaben gemäß § 5 TMG
+
{texts.companyInfoTitle}
Name: {personalConfig.name}
Adresse:
-
Musterstraße 123
-
12345 Musterstadt
-
Deutschland
+
{texts.address.street}
+
{texts.address.city}
+
{texts.address.country}
-
Kontakt
+
{texts.contactTitle}
E-Mail: {personalConfig.email.full}
-
Telefon: +49 (0) 123 456789
+
Telefon: {texts.contact.phone}
-
Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV
+
{texts.responsibilityTitle}
{personalConfig.name}
-
Musterstraße 123
-
12345 Musterstadt
+
{texts.address.street}
+
{texts.address.city}
-
Haftungsausschluss
+
{texts.disclaimerTitle}
-
Haftung für Inhalte
-
- Als Diensteanbieter sind wir gemäß § 7 Abs.1 TMG für eigene Inhalte auf diesen Seiten nach den
- allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 TMG sind wir als Diensteanbieter jedoch nicht
- unter der Verpflichtung, übermittelte oder gespeicherte fremde Informationen zu überwachen oder nach
- Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen.
-
-
- Verpflichtungen zur Entfernung oder Sperrung der Nutzung von Informationen nach den allgemeinen
- Gesetzen bleiben hiervon unberührt. Eine diesbezügliche Haftung ist jedoch erst ab dem Zeitpunkt
- der Kenntnis einer konkreten Rechtsverletzung möglich. Bei Bekanntwerden von entsprechenden
- Rechtsverletzungen werden wir diese Inhalte umgehend entfernen.
-
+
{texts.contentLiabilityTitle}
+ {texts.contentLiabilityText.map((text, index) => (
+
+ {text}
+
+ ))}
-
Haftung für Links
-
- Unser Angebot enthält Links zu externen Websites Dritter, auf deren Inhalte wir keinen Einfluss haben.
- Deshalb können wir für diese fremden Inhalte auch keine Gewähr übernehmen. Für die Inhalte der
- verlinkten Seiten ist stets der jeweilige Anbieter oder Betreiber der Seiten verantwortlich.
-
-
- Die verlinkten Seiten wurden zum Zeitpunkt der Verlinkung auf mögliche Rechtsverstöße überprüft.
- Rechtswidrige Inhalte waren zum Zeitpunkt der Verlinkung nicht erkennbar. Eine permanente inhaltliche
- Kontrolle der verlinkten Seiten ist jedoch ohne konkrete Anhaltspunkte einer Rechtsverletzung nicht
- zumutbar. Bei Bekanntwerden von Rechtsverletzungen werden wir derartige Links umgehend entfernen.
-
+
{texts.linksLiabilityTitle}
+ {texts.linksLiabilityText.map((text, index) => (
+
+ {text}
+
+ ))}
-
Urheberrecht
-
- Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unterliegen dem
- deutschen Urheberrecht. Die Vervielfältigung, Bearbeitung, Verbreitung und jede Art der Verwertung
- außerhalb der Grenzen des Urheberrechtes bedürfen der schriftlichen Zustimmung des jeweiligen Autors
- bzw. Erstellers.
-
-
- Downloads und Kopien dieser Seite sind nur für den privaten, nicht kommerziellen Gebrauch gestattet.
- Soweit die Inhalte auf dieser Seite nicht vom Betreiber erstellt wurden, werden die Urheberrechte
- Dritter beachtet. Insbesondere werden Inhalte Dritter als solche gekennzeichnet. Sollten Sie trotzdem
- auf eine Urheberrechtsverletzung aufmerksam werden, bitten wir um einen entsprechenden Hinweis.
- Bei Bekanntwerden von Rechtsverletzungen werden wir derartige Inhalte umgehend entfernen.
-
+
{texts.copyrightTitle}
+ {texts.copyrightText.map((text, index) => (
+
+ {text}
+
+ ))}
-
Datenschutz
+
{texts.privacyTitle}
- Die Nutzung unserer Webseite ist in der Regel ohne Angabe personenbezogener Daten möglich.
- Soweit auf unseren Seiten personenbezogene Daten (beispielsweise Name, Anschrift oder
- E-Mail-Adressen) erhoben werden, erfolgt dies, soweit möglich, stets auf freiwilliger Basis.
- Diese Daten werden ohne Ihre ausdrückliche Zustimmung nicht an Dritte weitergegeben.
-
-
- Wir weisen darauf hin, dass die Datenübertragung im Internet (z.B. bei der Kommunikation per
- E-Mail) Sicherheitslücken aufweisen kann. Ein lückenloser Schutz der Daten vor dem Zugriff durch
- Dritte ist nicht möglich.
+ {texts.privacyText}
diff --git a/src/pages/PrivacyPolicy.tsx b/src/pages/PrivacyPolicy.tsx
index 64f95ce..658756e 100644
--- a/src/pages/PrivacyPolicy.tsx
+++ b/src/pages/PrivacyPolicy.tsx
@@ -1,25 +1,43 @@
-export default function PrivacyPolicy() {
- return (
-
-
Privacy Policy (GDPR/DSGVO)
-
Last updated: {new Date().toLocaleDateString()}
+import { getTexts } from '../config/texts';
-
Data Collection
-
We collect only the information you provide through our contact form:
+export default function PrivacyPolicy() {
+ const texts = getTexts().privacyPolicy;
+ return (
+
+
{texts.title}
+
{texts.lastUpdated} {new Date().toLocaleDateString()}
+
+
{texts.dataCollectionTitle}
+
{texts.dataCollectionText}
- Name (first and last)
- Email address
- Message content
+ {texts.dataCollectionList.map((item, index) => (
+ {item}
+ ))}
-
Purpose
-
Your data is used solely to respond to your inquiry.
+
{texts.purposeTitle}
+
{texts.purposeText}
-
Data Retention
-
Your data is not stored permanently. Email content is processed and forwarded immediately.
+
{texts.providerTitle}
+
{texts.providerText}
-
Your Rights
-
You have the right to access, rectify, or delete your personal data. Contact: info@sascha-bach.de
+
{texts.retentionTitle}
+
{texts.retentionText}
+
+
{texts.rightsTitle}
+
{texts.rightsText}
+
+ {texts.rightsList.map((right, index) => (
+ {right}
+ ))}
+
+
+
{texts.contactTitle}
+
{texts.contactText}
+
Email: {texts.contactEmail}
+
+
{texts.legalBasisTitle}
+
{texts.legalBasisText}
);
}
\ No newline at end of file
diff --git a/src/scss/sections/about-section.scss b/src/scss/sections/about-section.scss
index a441c2c..5791f8e 100644
--- a/src/scss/sections/about-section.scss
+++ b/src/scss/sections/about-section.scss
@@ -1,4 +1,5 @@
@use '../globals';
+@use '../variables' as *;
.about-section {
background: var(--about-background);
@@ -121,7 +122,7 @@
border-radius: 9999px;
font-size: 0.875rem;
font-weight: 500;
- transition: all 0.3s ease;
+ transition: $transition-base;
&:hover {
transform: translateY(-1px);
@@ -162,9 +163,9 @@
&__feature-card {
text-align: center;
border: none;
- border-radius: 0.5rem;
- box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
- transition: all 0.3s ease;
+ border-radius: $card-border-radius-sm;
+ box-shadow: $shadow-card-lg;
+ transition: $transition-base;
overflow: hidden;
&:hover {
@@ -186,7 +187,7 @@
}
&__feature-header {
- padding: 1.5rem;
+ padding: $card-padding-lg;
}
&__feature-icon {
@@ -249,7 +250,7 @@
// Mobile responsiveness
@include globals.mobile-only {
.about-section {
- padding: 3rem 0;
+ padding: $section-padding-y 0;
&__container {
padding: 0 1rem;
diff --git a/src/scss/sections/certifications-section.scss b/src/scss/sections/certifications-section.scss
index 1cae6b2..6f2bd85 100644
--- a/src/scss/sections/certifications-section.scss
+++ b/src/scss/sections/certifications-section.scss
@@ -1,4 +1,5 @@
@use '../globals';
+@use '../variables' as *;
.certifications-section {
background: var(--certifications-background);
@@ -57,11 +58,11 @@
&__card {
text-align: center;
border: none;
- border-radius: 1rem;
+ border-radius: $card-border-radius-md;
box-shadow: var(--certifications-card-shadow);
background: var(--certifications-card-background);
- padding: 2rem 1.5rem;
- transition: all 0.3s ease;
+ padding: $card-padding-lg $card-padding-lg;
+ transition: $transition-base;
position: relative;
overflow: hidden;
@@ -142,7 +143,7 @@
// Responsive adjustments
@include globals.mobile-only {
&__card {
- padding: 1.5rem 1rem;
+ padding: $card-padding-lg $card-padding-md;
}
&__card-icon,
diff --git a/src/scss/sections/contact-section.scss b/src/scss/sections/contact-section.scss
index 9ba63f0..28498ce 100644
--- a/src/scss/sections/contact-section.scss
+++ b/src/scss/sections/contact-section.scss
@@ -1,4 +1,5 @@
@use '../globals';
+@use '../variables' as *;
.contact-section {
background: var(--contact-background);
@@ -43,13 +44,179 @@
&__grid {
display: grid;
grid-template-columns: 1fr;
+ grid-template-areas:
+ 'connect'
+ 'sidebar'
+ 'social';
gap: 3rem;
+ align-items: start;
@include globals.desktop-only {
- grid-template-columns: 1fr 1fr;
+ grid-template-columns: 2fr 1fr;
+ grid-template-areas:
+ 'connect sidebar'
+ 'social .';
+ gap: 4rem;
+ align-items: stretch;
}
}
+ // Connect Card
+ &__connect-card {
+ grid-area: connect;
+ background: var(--form-background);
+ border-radius: var(--card-border-radius);
+ padding: var(--card-padding);
+ transition: var(--transition-default);
+ display: flex;
+ flex-direction: column;
+ }
+
+ &__connect-header {
+ display: flex;
+ gap: 1rem;
+ margin-bottom: 2rem;
+ align-items: flex-start;
+ }
+
+ &__connect-text {
+ flex: 1;
+ }
+
+ &__email-icon {
+ width: 2rem;
+ height: 2rem;
+ color: var(--primary-color);
+ background: rgba(147, 51, 234, 0.1);
+ border-radius: 0.75rem;
+ padding: 0.5rem;
+ flex-shrink: 0;
+ }
+
+ &__email-title {
+ font-size: 1.5rem;
+ font-weight: 700;
+ color: var(--color-text);
+ margin-bottom: 0.75rem;
+ background: var(--gradient-contact-title);
+ background-clip: text;
+ -webkit-background-clip: text;
+ color: transparent;
+ -webkit-text-fill-color: transparent;
+ }
+
+ &__email-description {
+ color: var(--color-text-muted);
+ font-size: 1rem;
+ line-height: 1.6;
+ margin-bottom: 2rem;
+ }
+
+ // Social Card - positioned below connect card
+ &__social-card {
+ grid-area: social;
+ background: var(--form-background);
+ border: 1px solid var(--form-border);
+ border-radius: var(--card-border-radius);
+ padding: var(--card-padding);
+ }
+
+ &__social-title {
+ font-size: 1.125rem;
+ font-weight: 600;
+ margin-bottom: 1rem;
+ color: var(--color-text);
+ margin-bottom: var(--card-padding-sm);
+ }
+
+ &__social-list {
+ display: flex;
+ flex-direction: column;
+ gap: var(--card-gap-sm);
+ }
+
+ // Sidebar
+ &__sidebar {
+ grid-area: sidebar;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ }
+
+ &__info-card {
+ background: var(--form-background);
+ border: 1px solid var(--form-border);
+ border-radius: var(--card-border-radius);
+ padding: var(--card-padding);
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ }
+
+ &__availability {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ margin-bottom: var(--card-padding);
+ padding: var(--card-gap-sm);
+ background: rgba(34, 197, 94, 0.1);
+ border: 1px solid rgba(34, 197, 94, 0.2);
+ border-radius: 0.75rem;
+ }
+
+ &__status-indicator {
+ width: 0.75rem;
+ height: 0.75rem;
+ background: #22c55e;
+ border-radius: 50%;
+ flex-shrink: 0;
+ animation: pulse 2s infinite;
+ }
+
+ @keyframes pulse {
+ 0% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.5;
+ }
+ 100% {
+ opacity: 1;
+ }
+ }
+
+ &__status-text {
+ font-size: 0.875rem;
+ font-weight: 500;
+ color: #22c55e;
+ }
+
+ &__quick-facts {
+ display: flex;
+ flex-direction: column;
+ gap: 1.25rem;
+ }
+
+ &__fact {
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+ }
+
+ &__fact-label {
+ font-size: 0.75rem;
+ color: var(--color-text-muted);
+ font-weight: 500;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ }
+
+ &__fact-value {
+ font-size: 0.875rem;
+ color: var(--color-text);
+ font-weight: 500;
+ }
+
// Contact Info Section
&__info {
display: flex;
@@ -80,6 +247,7 @@
display: flex;
align-items: center;
gap: 1rem;
+ margin-bottom: 1rem;
}
&__detail-icon {
@@ -112,12 +280,26 @@
color: var(--contact-social-text);
text-decoration: none;
transition: all 0.2s ease;
+ cursor: pointer;
&:hover {
background: var(--contact-social-hover-bg);
border-color: var(--contact-social-hover-border);
transform: translateY(-1px);
}
+
+ // Brand-specific icon colors with better contrast
+ &--email .contact-section__social-icon {
+ color: #2563eb; // Stronger blue for email
+ }
+
+ &--github .contact-section__social-icon {
+ color: #1f2937; // Darker for GitHub
+ }
+
+ &--linkedin .contact-section__social-icon {
+ color: #0d67b5; // Stronger LinkedIn blue
+ }
}
&__social-icon {
@@ -287,12 +469,14 @@
gap: 2rem;
}
- &__form-card {
+ &__form-card,
+ &__email-card {
border-radius: 0.75rem;
}
&__form-header,
- &__form {
+ &__form,
+ &__email-header {
padding-left: 1rem;
padding-right: 1rem;
}
@@ -302,4 +486,169 @@
margin-top: 1.5rem;
}
}
+
+ // Email Button Card Styles
+ &__email-card {
+ background: var(--form-background);
+ border-radius: 1rem;
+ border: 1px solid var(--form-border);
+ padding: 2rem;
+ text-align: center;
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
+ 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+ transition: all 0.3s ease;
+
+ &:hover {
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
+ 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+ transform: translateY(-2px);
+ }
+ }
+
+ &__email-header {
+ margin-bottom: 2rem;
+ padding: 0;
+ }
+
+ &__email-icon {
+ width: 3rem;
+ height: 3rem;
+ color: var(--primary-color);
+ margin: 0 auto 1rem;
+ display: block;
+ }
+
+ &__email-title {
+ font-size: 1.5rem;
+ font-weight: 700;
+ color: var(--text-primary);
+ margin-bottom: 0.5rem;
+ }
+
+ &__email-description {
+ color: var(--text-secondary);
+ font-size: 1rem;
+ line-height: 1.6;
+ }
+
+ &__email-button {
+ background: var(--contact-button-bg);
+ border: none;
+ border-radius: 0.75rem;
+ padding: 1rem 2rem;
+ font-size: 1.1rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.75rem;
+ margin: var(--card-padding-sm) 0;
+ text-decoration: none;
+ color: var(--contact-button-text);
+ box-shadow: 0 4px 14px 0 rgba(37, 99, 235, 0.25),
+ 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+ width: 100%;
+ position: relative;
+ overflow: hidden;
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: -100%;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(
+ 90deg,
+ transparent,
+ rgba(255, 255, 255, 0.2),
+ transparent
+ );
+ transition: left 0.5s ease;
+ }
+
+ &:hover {
+ background: var(--contact-button-hover-bg);
+ transform: translateY(-2px);
+ box-shadow: 0 8px 25px 0 rgba(37, 99, 235, 0.35),
+ 0 4px 6px -2px rgba(0, 0, 0, 0.1);
+
+ &::before {
+ left: 100%;
+ }
+ }
+
+ &:active {
+ transform: translateY(-1px);
+ }
+ }
+
+ &__button-text {
+ color: var(--contact-button-text);
+ font-weight: 600;
+ font-size: 1.1rem;
+ }
+
+ &__button-icon {
+ color: var(--contact-button-text);
+ width: 1.25rem;
+ height: 1.25rem;
+ }
+
+ &__email-info {
+ margin-top: 1.5rem;
+ }
+
+ &__email-note {
+ font-size: 0.875rem;
+ color: var(--text-secondary);
+ line-height: 1.5;
+
+ strong {
+ color: var(--primary-color);
+ font-weight: 600;
+ }
+ }
+}
+
+// Mobile responsive adjustments
+@include globals.mobile-only {
+ .contact-section {
+ &__grid {
+ gap: 2rem;
+ }
+
+ &__connect-card {
+ margin-bottom: 0.5rem;
+ }
+
+ &__social-card {
+ margin-top: 0.5rem;
+ }
+
+ &__email-button {
+ padding: 0.875rem 1.5rem;
+ font-size: 1rem;
+ }
+
+ &__email-icon {
+ width: 1.5rem;
+ height: 1.5rem;
+ padding: 0.375rem;
+ }
+
+ &__email-title {
+ font-size: 1.25rem;
+ }
+
+ &__connect-header {
+ margin-bottom: 1.5rem;
+ }
+
+ &__email-description {
+ margin-bottom: 1.5rem;
+ }
+ }
}
diff --git a/src/scss/sections/projects-section.scss b/src/scss/sections/projects-section.scss
index 669e76f..8bb74c5 100644
--- a/src/scss/sections/projects-section.scss
+++ b/src/scss/sections/projects-section.scss
@@ -1,4 +1,5 @@
@use '../globals';
+@use '../variables' as *;
.projects-section {
background: var(--projects-background);
@@ -56,10 +57,10 @@
&__card {
background: var(--projects-card-background);
- border-radius: 1rem;
+ border-radius: $card-border-radius-md;
box-shadow: var(--projects-card-shadow);
overflow: hidden;
- transition: all 0.3s ease;
+ transition: $transition-base;
border: 1px solid var(--projects-card-border);
&:hover {
@@ -146,7 +147,7 @@
}
&__content {
- padding: 1.5rem;
+ padding: $card-padding-lg;
}
&__header-content {
@@ -187,11 +188,11 @@
// Responsive adjustments
@include globals.mobile-only {
&__card {
- border-radius: 0.75rem;
+ border-radius: $card-border-radius-sm;
}
&__content {
- padding: 1rem;
+ padding: $card-padding-md;
}
&__image-container {
diff --git a/src/scss/sections/services-section.scss b/src/scss/sections/services-section.scss
index 0e52033..96ecb4c 100644
--- a/src/scss/sections/services-section.scss
+++ b/src/scss/sections/services-section.scss
@@ -5,9 +5,9 @@
min-height: 100vh;
display: flex;
align-items: center;
- padding: 2rem 0;
+ padding: $section-padding-y 0;
background: var(--services-background);
-
+
&__container {
max-width: 80rem;
margin: 0 auto;
@@ -98,9 +98,9 @@
&__card {
background: white;
- border-radius: $services-card-border-radius;
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
- transition: all 0.3s ease;
+ border-radius: $card-border-radius-md;
+ box-shadow: $shadow-card;
+ transition: $transition-base;
overflow: hidden;
display: flex;
flex-direction: column;
@@ -109,7 +109,7 @@
&:hover {
transform: translateY(-4px);
- box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
+ box-shadow: $shadow-card-hover;
}
// Responsive card heights
@@ -151,12 +151,12 @@
}
&__card-header {
- padding: $services-card-padding;
+ padding: $card-padding-lg;
text-align: center;
flex-shrink: 0;
@media (max-width: 767px) {
- padding: 1.25rem;
+ padding: $card-padding-sm;
}
}
@@ -237,7 +237,8 @@
}
&__card-content {
- padding: 0 $services-card-padding $services-card-padding $services-card-padding;
+ padding: 0 $services-card-padding $services-card-padding
+ $services-card-padding;
flex: 1;
display: flex;
align-items: center;
@@ -294,11 +295,11 @@
// For exactly 6 cards
@media (min-width: 768px) and (max-width: 1023px) {
grid-template-columns: repeat(2, 1fr);
-
- // Make last 2 cards center themselves
+
+ // Make last 2 cards center themselves if needed
.services-section__card:nth-child(5),
.services-section__card:nth-child(6) {
- // This ensures the last row centers properly
+ justify-self: center;
}
}
diff --git a/src/scss/sections/skills-section.scss b/src/scss/sections/skills-section.scss
index 2aee9ef..daf0c3c 100644
--- a/src/scss/sections/skills-section.scss
+++ b/src/scss/sections/skills-section.scss
@@ -2,7 +2,7 @@
@use '../variables' as *;
.skills-section {
- padding: 5rem 0;
+ padding: $section-padding-y 0;
background: var(--skills-background);
position: relative;
@@ -84,13 +84,13 @@
&__category {
background: var(--skills-category-bg);
border: 1px solid var(--skills-category-border);
- border-radius: 1rem;
+ border-radius: $card-border-radius-md;
padding: $skills-category-padding;
backdrop-filter: blur(10px);
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ transition: $transition-smooth;
position: relative;
overflow: hidden;
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+ box-shadow: $shadow-card;
// Add subtle gradient overlay for depth
&::before {
@@ -122,8 +122,9 @@
&:hover {
transform: translateY(-8px);
- box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
-
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1),
+ 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+
&::before {
opacity: 0.9;
}
@@ -142,11 +143,11 @@
// Web Frameworks - Blue theme (matching hero primary)
background: var(--skills-category-bg-primary);
border-color: var(--skills-category-border-primary);
-
+
&::before {
background: var(--skills-gradient-primary);
}
-
+
&::after {
background: var(--skills-accent-primary);
}
@@ -156,11 +157,11 @@
// Styling & Design - Green theme (matching about section)
background: var(--skills-category-bg-secondary);
border-color: var(--skills-category-border-secondary);
-
+
&::before {
background: var(--skills-gradient-secondary);
}
-
+
&::after {
background: var(--skills-accent-secondary);
}
@@ -170,11 +171,11 @@
// Backend Development - Purple theme (matching hero secondary)
background: var(--skills-category-bg-tertiary);
border-color: var(--skills-category-border-tertiary);
-
+
&::before {
background: var(--skills-gradient-tertiary);
}
-
+
&::after {
background: var(--skills-accent-tertiary);
}
@@ -184,11 +185,11 @@
// Development Tools - Teal theme (complementary)
background: var(--skills-category-bg-quaternary);
border-color: var(--skills-category-border-quaternary);
-
+
&::before {
background: var(--skills-gradient-quaternary);
}
-
+
&::after {
background: var(--skills-accent-quaternary);
}
@@ -198,11 +199,11 @@
// Testing & Quality - Orange theme (warm accent)
background: var(--skills-category-bg-quinary);
border-color: var(--skills-category-border-quinary);
-
+
&::before {
background: var(--skills-gradient-quinary);
}
-
+
&::after {
background: var(--skills-accent-quinary);
}
@@ -212,11 +213,11 @@
// AI-Tools - Indigo theme (tech/AI vibe)
background: var(--skills-category-bg-senary);
border-color: var(--skills-category-border-senary);
-
+
&::before {
background: var(--skills-gradient-senary);
}
-
+
&::after {
background: var(--skills-accent-senary);
}
@@ -251,7 +252,8 @@
}
&__skill {
- // Individual skill container
+ // Individual skill container styling
+ margin-bottom: 1rem;
}
&__skill-header {
@@ -327,7 +329,7 @@
border-radius: $skills-progress-radius;
transition: all 0.8s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
-
+
&::after {
content: '';
position: absolute;
@@ -335,7 +337,12 @@
left: 0;
right: 0;
bottom: 0;
- background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
+ background: linear-gradient(
+ 90deg,
+ transparent,
+ rgba(255, 255, 255, 0.4),
+ transparent
+ );
transform: translateX(-100%);
animation: shimmer 2s infinite;
}
diff --git a/src/scss/themes/_base.scss b/src/scss/themes/_base.scss
index f348adf..18fdbb3 100644
--- a/src/scss/themes/_base.scss
+++ b/src/scss/themes/_base.scss
@@ -16,6 +16,44 @@ $base-light-theme: (
box-shadow-sm: #{$shadow-sm},
box-shadow-md: #{$shadow-md},
box-shadow-lg: #{$shadow-lg},
+
+ // Common card shadows
+ shadow-card: #{$shadow-card},
+ shadow-card-hover: #{$shadow-card-hover},
+ shadow-card-lg: #{$shadow-card-lg},
+ shadow-button: #{$shadow-button},
+
+ // Common spacing and sizing
+ card-border-radius: #{$card-border-radius},
+ card-padding: #{$card-padding},
+ card-padding-sm: #{$card-padding-sm},
+ card-gap: #{$card-gap},
+ card-gap-sm: #{$card-gap-sm},
+
+ // Transitions
+ transition-default: #{$transition-default},
+ transition-fast: #{$transition-fast},
+
+ // Form and text variables
+ form-background: #ffffff,
+ form-border: #e5e7eb,
+ text-primary: #111827,
+ text-secondary: #6b7280,
+ primary-color: #9333ea,
+
+ // Contact status colors (light theme)
+ contact-status-success-bg: #d1fae5,
+ contact-status-success-border: #10b981,
+ contact-status-success-text: #065f46,
+ contact-status-error-bg: #fee2e2,
+ contact-status-error-border: #ef4444,
+ contact-status-error-text: #991b1b,
+
+ // Additional missing variables
+ box-shadow-hover: rgba(0, 0, 0, 0.15),
+ bg-primary: #ffffff,
+ bg-secondary: #f9fafb,
+ border-color: #e5e7eb,
);
$base-dark-theme: (
@@ -32,4 +70,41 @@ $base-dark-theme: (
box-shadow-sm: #{$shadow-sm},
box-shadow-md: #{$shadow-md},
box-shadow-lg: #{$shadow-lg},
-);
\ No newline at end of file
+
+ // Common card shadows (adjusted for dark theme)
+ shadow-card: 0 4px 6px -1px rgba(0, 0, 0, 0.3),
+ shadow-card-hover: 0 10px 15px -3px rgba(0, 0, 0, 0.3),
+ shadow-card-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.3),
+ shadow-button: 0 4px 6px -1px rgba(0, 0, 0, 0.3),
+ // Common spacing and sizing
+ card-border-radius: #{$card-border-radius},
+ card-padding: #{$card-padding},
+ card-padding-sm: #{$card-padding-sm},
+ card-gap: #{$card-gap},
+ card-gap-sm: #{$card-gap-sm},
+
+ // Transitions
+ transition-default: #{$transition-default},
+ transition-fast: #{$transition-fast},
+
+ // Form and text variables (dark theme)
+ form-background: #374151,
+ form-border: #4b5563,
+ text-primary: #f9fafb,
+ text-secondary: #9ca3af,
+ primary-color: #9333ea,
+
+ // Contact status colors (dark theme)
+ contact-status-success-bg: #064e3b,
+ contact-status-success-border: #059669,
+ contact-status-success-text: #a7f3d0,
+ contact-status-error-bg: #7f1d1d,
+ contact-status-error-border: #dc2626,
+ contact-status-error-text: #fecaca,
+
+ // Additional missing variables (dark theme)
+ box-shadow-hover: rgba(255, 255, 255, 0.15),
+ bg-primary: #1f2937,
+ bg-secondary: #374151,
+ border-color: #4b5563,
+);
diff --git a/src/scss/themes/_contact.scss b/src/scss/themes/_contact.scss
index 8d5fe5e..84587c7 100644
--- a/src/scss/themes/_contact.scss
+++ b/src/scss/themes/_contact.scss
@@ -8,31 +8,31 @@ $contact-light-theme: (
// Primary color for icons (indigo)
'color-contact-primary': #4f46e5,
- // Form styling
- 'contact-form-bg': linear-gradient(135deg, #ffffff 0%, #fefefe 100%),
- 'contact-form-border': rgba(79, 70, 229, 0.1),
+ // Form styling - warm cream/peach to complement cool blue background
+ 'contact-form-bg': linear-gradient(135deg, #fef7ed 0%, #fed7aa 100%),
+ 'contact-form-border': rgba(251, 146, 60, 0.2),
'contact-form-shadow': 0 10px 15px -3px rgba(0, 0, 0, 0.1),
- // Input styling
- 'contact-input-bg': #ffffff,
- 'contact-input-border': rgba(79, 70, 229, 0.2),
- 'contact-input-focus-ring': rgba(79, 70, 229, 0.1),
- // Button styling
- 'contact-button-bg': linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%),
+ // Input styling - warm background to complement form
+ 'contact-input-bg': #fef3c7,
+ 'contact-input-border': rgba(251, 146, 60, 0.3),
+ 'contact-input-focus-ring': rgba(251, 146, 60, 0.2),
+ // Button styling - improved contrast and readability
+ 'contact-button-bg': linear-gradient(135deg, #2563eb 0%, #3730a3 100%),
'contact-button-text': #ffffff,
- 'contact-button-hover-bg': linear-gradient(135deg, #4338ca 0%, #6d28d9 100%),
- // Social button styling
- 'contact-social-bg': rgba(255, 255, 255, 0.8),
- 'contact-social-text': #4b5563,
- 'contact-social-border': rgba(79, 70, 229, 0.2),
- 'contact-social-hover-bg': #ffffff,
- 'contact-social-hover-border': rgba(79, 70, 229, 0.3),
- // Status message styling (light theme)
- 'contact-status-success-bg': #dcfce7,
- 'contact-status-success-border': #16a34a,
- 'contact-status-success-text': #15803d,
- 'contact-status-error-bg': #fef2f2,
+ 'contact-button-hover-bg': linear-gradient(135deg, #1d4ed8 0%, #312e81 100%),
+ // Social button styling - warm peach background to complement section
+ 'contact-social-bg': #fed7aa,
+ 'contact-social-text': #92400e,
+ 'contact-social-border': #f59e0b,
+ 'contact-social-hover-bg': #fbbf24,
+ 'contact-social-hover-border': #d97706,
+ // Status message styling (light theme) - improved readability
+ 'contact-status-success-bg': #d1fae5,
+ 'contact-status-success-border': #059669,
+ 'contact-status-success-text': #047857,
+ 'contact-status-error-bg': #fee2e2,
'contact-status-error-border': #dc2626,
- 'contact-status-error-text': #dc2626
+ 'contact-status-error-text': #b91c1c
);
$contact-dark-theme: (
@@ -48,29 +48,29 @@ $contact-dark-theme: (
// Primary color for icons (lighter indigo)
'color-contact-primary': #6366f1,
- // Form styling (dark theme)
- 'contact-form-bg': linear-gradient(135deg, #111827 0%, #1f2937 100%),
- 'contact-form-border': rgba(99, 102, 241, 0.2),
+ // Form styling (dark theme) - warm dark tones to complement cool purple/indigo
+ 'contact-form-bg': linear-gradient(135deg, #451a03 0%, #78350f 100%),
+ 'contact-form-border': rgba(251, 146, 60, 0.3),
'contact-form-shadow': 0 10px 15px -3px rgba(0, 0, 0, 0.3),
- // Input styling (dark theme)
- 'contact-input-bg': #1f2937,
- 'contact-input-border': rgba(99, 102, 241, 0.3),
- 'contact-input-focus-ring': rgba(99, 102, 241, 0.2),
- // Button styling (dark theme)
- 'contact-button-bg': linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%),
+ // Input styling (dark theme) - warm dark background
+ 'contact-input-bg': #92400e,
+ 'contact-input-border': rgba(251, 146, 60, 0.4),
+ 'contact-input-focus-ring': rgba(251, 146, 60, 0.3),
+ // Button styling (dark theme) - improved contrast
+ 'contact-button-bg': linear-gradient(135deg, #3b82f6 0%, #2563eb 100%),
'contact-button-text': #ffffff,
- 'contact-button-hover-bg': linear-gradient(135deg, #6366f1 0%, #a855f7 100%),
- // Social button styling (dark theme)
- 'contact-social-bg': rgba(31, 41, 55, 0.8),
- 'contact-social-text': #e5e7eb,
- 'contact-social-border': rgba(99, 102, 241, 0.3),
- 'contact-social-hover-bg': #374151,
- 'contact-social-hover-border': rgba(99, 102, 241, 0.4),
- // Status message styling (dark theme)
- 'contact-status-success-bg': rgba(34, 197, 94, 0.1),
- 'contact-status-success-border': #22c55e,
- 'contact-status-success-text': #4ade80,
- 'contact-status-error-bg': rgba(239, 68, 68, 0.1),
- 'contact-status-error-border': #ef4444,
- 'contact-status-error-text': #f87171
+ 'contact-button-hover-bg': linear-gradient(135deg, #60a5fa 0%, #3b82f6 100%),
+ // Social button styling (dark theme) - warm brown/orange tones
+ 'contact-social-bg': #a16207,
+ 'contact-social-text': #fef3c7,
+ 'contact-social-border': #d97706,
+ 'contact-social-hover-bg': #ca8a04,
+ 'contact-social-hover-border': #f59e0b,
+ // Status message styling (dark theme) - improved readability
+ 'contact-status-success-bg': rgba(5, 150, 105, 0.15),
+ 'contact-status-success-border': #10b981,
+ 'contact-status-success-text': #6ee7b7,
+ 'contact-status-error-bg': rgba(239, 68, 68, 0.15),
+ 'contact-status-error-border': #f87171,
+ 'contact-status-error-text': #fca5a5
);
diff --git a/src/scss/themes/_index.scss b/src/scss/themes/_index.scss
index 77d4de5..2982436 100644
--- a/src/scss/themes/_index.scss
+++ b/src/scss/themes/_index.scss
@@ -66,9 +66,21 @@ $dark-theme: map.merge(
--gradient-primary: #{map.get($theme, gradient-primary)};
--gradient-text: #{map.get($theme, gradient-text)};
+ // Common card and form variables
+ --card-border-radius: #{map.get($theme, card-border-radius)};
+ --card-padding: #{map.get($theme, card-padding)};
+ --card-padding-sm: #{map.get($theme, card-padding-sm)};
+ --card-gap-sm: #{map.get($theme, card-gap-sm)};
+ --form-background: #{map.get($theme, form-background)};
+ --form-border: #{map.get($theme, form-border)};
+ --text-primary: #{map.get($theme, text-primary)};
+ --text-secondary: #{map.get($theme, text-secondary)};
+ --primary-color: #{map.get($theme, primary-color)};
+ --transition-default: #{map.get($theme, transition-default)};
+
// Hero section variables
--hero-background: #{map.get($theme, hero-background)};
-
+
// Stat variables using semantic naming
--stat-primary-bg: #{map.get($theme, stat-primary-bg)};
--stat-primary-value: #{map.get($theme, stat-primary-value)};
@@ -110,8 +122,14 @@ $dark-theme: map.merge(
--feature-title-secondary: #{map.get($theme, feature-title-secondary)};
--feature-title-tertiary: #{map.get($theme, feature-title-tertiary)};
--feature-description-primary: #{map.get($theme, feature-description-primary)};
- --feature-description-secondary: #{map.get($theme, feature-description-secondary)};
- --feature-description-tertiary: #{map.get($theme, feature-description-tertiary)};
+ --feature-description-secondary: #{map.get(
+ $theme,
+ feature-description-secondary
+ )};
+ --feature-description-tertiary: #{map.get(
+ $theme,
+ feature-description-tertiary
+ )};
// Services section variables
--services-background: #{map.get($theme, services-background)};
@@ -140,9 +158,18 @@ $dark-theme: map.merge(
--service-title-senary: #{map.get($theme, service-title-senary)};
--service-description-primary: #{map.get($theme, service-description-primary)};
- --service-description-secondary: #{map.get($theme, service-description-secondary)};
- --service-description-tertiary: #{map.get($theme, service-description-tertiary)};
- --service-description-quaternary: #{map.get($theme, service-description-quaternary)};
+ --service-description-secondary: #{map.get(
+ $theme,
+ service-description-secondary
+ )};
+ --service-description-tertiary: #{map.get(
+ $theme,
+ service-description-tertiary
+ )};
+ --service-description-quaternary: #{map.get(
+ $theme,
+ service-description-quaternary
+ )};
--service-description-quinary: #{map.get($theme, service-description-quinary)};
--service-description-senary: #{map.get($theme, service-description-senary)};
@@ -155,37 +182,64 @@ $dark-theme: map.merge(
--skills-category-title-color: #{map.get($theme, skills-category-title-color)};
--skills-skill-name-color: #{map.get($theme, skills-skill-name-color)};
--skills-skill-level-color: #{map.get($theme, skills-skill-level-color)};
- --skills-skill-percentage-color: #{map.get($theme, skills-skill-percentage-color)};
+ --skills-skill-percentage-color: #{map.get(
+ $theme,
+ skills-skill-percentage-color
+ )};
--skills-progress-bg: #{map.get($theme, skills-progress-bg)};
// Card-specific backgrounds and accents
--skills-category-bg-primary: #{map.get($theme, skills-category-bg-primary)};
- --skills-category-border-primary: #{map.get($theme, skills-category-border-primary)};
+ --skills-category-border-primary: #{map.get(
+ $theme,
+ skills-category-border-primary
+ )};
--skills-gradient-primary: #{map.get($theme, skills-gradient-primary)};
--skills-accent-primary: #{map.get($theme, skills-accent-primary)};
- --skills-category-bg-secondary: #{map.get($theme, skills-category-bg-secondary)};
- --skills-category-border-secondary: #{map.get($theme, skills-category-border-secondary)};
+ --skills-category-bg-secondary: #{map.get(
+ $theme,
+ skills-category-bg-secondary
+ )};
+ --skills-category-border-secondary: #{map.get(
+ $theme,
+ skills-category-border-secondary
+ )};
--skills-gradient-secondary: #{map.get($theme, skills-gradient-secondary)};
--skills-accent-secondary: #{map.get($theme, skills-accent-secondary)};
--skills-category-bg-tertiary: #{map.get($theme, skills-category-bg-tertiary)};
- --skills-category-border-tertiary: #{map.get($theme, skills-category-border-tertiary)};
+ --skills-category-border-tertiary: #{map.get(
+ $theme,
+ skills-category-border-tertiary
+ )};
--skills-gradient-tertiary: #{map.get($theme, skills-gradient-tertiary)};
--skills-accent-tertiary: #{map.get($theme, skills-accent-tertiary)};
- --skills-category-bg-quaternary: #{map.get($theme, skills-category-bg-quaternary)};
- --skills-category-border-quaternary: #{map.get($theme, skills-category-border-quaternary)};
+ --skills-category-bg-quaternary: #{map.get(
+ $theme,
+ skills-category-bg-quaternary
+ )};
+ --skills-category-border-quaternary: #{map.get(
+ $theme,
+ skills-category-border-quaternary
+ )};
--skills-gradient-quaternary: #{map.get($theme, skills-gradient-quaternary)};
--skills-accent-quaternary: #{map.get($theme, skills-accent-quaternary)};
--skills-category-bg-quinary: #{map.get($theme, skills-category-bg-quinary)};
- --skills-category-border-quinary: #{map.get($theme, skills-category-border-quinary)};
+ --skills-category-border-quinary: #{map.get(
+ $theme,
+ skills-category-border-quinary
+ )};
--skills-gradient-quinary: #{map.get($theme, skills-gradient-quinary)};
--skills-accent-quinary: #{map.get($theme, skills-accent-quinary)};
--skills-category-bg-senary: #{map.get($theme, skills-category-bg-senary)};
- --skills-category-border-senary: #{map.get($theme, skills-category-border-senary)};
+ --skills-category-border-senary: #{map.get(
+ $theme,
+ skills-category-border-senary
+ )};
--skills-gradient-senary: #{map.get($theme, skills-gradient-senary)};
--skills-accent-senary: #{map.get($theme, skills-accent-senary)};
@@ -199,15 +253,33 @@ $dark-theme: map.merge(
// Certifications section variables
--certifications-background: #{map.get($theme, certifications-background)};
- --gradient-certifications-title: #{map.get($theme, gradient-certifications-title)};
- --color-certifications-primary: #{map.get($theme, color-certifications-primary)};
- --certifications-card-background: #{map.get($theme, certifications-card-background)};
+ --gradient-certifications-title: #{map.get(
+ $theme,
+ gradient-certifications-title
+ )};
+ --color-certifications-primary: #{map.get(
+ $theme,
+ color-certifications-primary
+ )};
+ --certifications-card-background: #{map.get(
+ $theme,
+ certifications-card-background
+ )};
--certifications-card-shadow: #{map.get($theme, certifications-card-shadow)};
- --certifications-card-shadow-hover: #{map.get($theme, certifications-card-shadow-hover)};
+ --certifications-card-shadow-hover: #{map.get(
+ $theme,
+ certifications-card-shadow-hover
+ )};
--certifications-card-overlay: #{map.get($theme, certifications-card-overlay)};
- --certifications-badge-background: #{map.get($theme, certifications-badge-background)};
+ --certifications-badge-background: #{map.get(
+ $theme,
+ certifications-badge-background
+ )};
--certifications-badge-border: #{map.get($theme, certifications-badge-border)};
- --color-certifications-badge-text: #{map.get($theme, color-certifications-badge-text)};
+ --color-certifications-badge-text: #{map.get(
+ $theme,
+ color-certifications-badge-text
+ )};
// Projects section variables
--projects-background: #{map.get($theme, projects-background)};
@@ -218,13 +290,34 @@ $dark-theme: map.merge(
--projects-card-shadow-hover: #{map.get($theme, projects-card-shadow-hover)};
--projects-overlay-background: #{map.get($theme, projects-overlay-background)};
--projects-button-primary-bg: #{map.get($theme, projects-button-primary-bg)};
- --projects-button-primary-text: #{map.get($theme, projects-button-primary-text)};
- --projects-button-primary-border: #{map.get($theme, projects-button-primary-border)};
- --projects-button-primary-hover-bg: #{map.get($theme, projects-button-primary-hover-bg)};
- --projects-button-secondary-bg: #{map.get($theme, projects-button-secondary-bg)};
- --projects-button-secondary-text: #{map.get($theme, projects-button-secondary-text)};
- --projects-button-secondary-border: #{map.get($theme, projects-button-secondary-border)};
- --projects-button-secondary-hover-bg: #{map.get($theme, projects-button-secondary-hover-bg)};
+ --projects-button-primary-text: #{map.get(
+ $theme,
+ projects-button-primary-text
+ )};
+ --projects-button-primary-border: #{map.get(
+ $theme,
+ projects-button-primary-border
+ )};
+ --projects-button-primary-hover-bg: #{map.get(
+ $theme,
+ projects-button-primary-hover-bg
+ )};
+ --projects-button-secondary-bg: #{map.get(
+ $theme,
+ projects-button-secondary-bg
+ )};
+ --projects-button-secondary-text: #{map.get(
+ $theme,
+ projects-button-secondary-text
+ )};
+ --projects-button-secondary-border: #{map.get(
+ $theme,
+ projects-button-secondary-border
+ )};
+ --projects-button-secondary-hover-bg: #{map.get(
+ $theme,
+ projects-button-secondary-hover-bg
+ )};
--projects-tech-badge-bg: #{map.get($theme, projects-tech-badge-bg)};
--projects-tech-badge-text: #{map.get($theme, projects-tech-badge-text)};
--projects-tech-badge-border: #{map.get($theme, projects-tech-badge-border)};
@@ -247,4 +340,19 @@ $dark-theme: map.merge(
--contact-social-border: #{map.get($theme, contact-social-border)};
--contact-social-hover-bg: #{map.get($theme, contact-social-hover-bg)};
--contact-social-hover-border: #{map.get($theme, contact-social-hover-border)};
-}
\ No newline at end of file
+ --contact-status-success-bg: #{map.get($theme, contact-status-success-bg)};
+ --contact-status-success-border: #{map.get(
+ $theme,
+ contact-status-success-border
+ )};
+ --contact-status-success-text: #{map.get($theme, contact-status-success-text)};
+ --contact-status-error-bg: #{map.get($theme, contact-status-error-bg)};
+ --contact-status-error-border: #{map.get($theme, contact-status-error-border)};
+ --contact-status-error-text: #{map.get($theme, contact-status-error-text)};
+
+ // Additional variables
+ --box-shadow-hover: #{map.get($theme, box-shadow-hover)};
+ --bg-primary: #{map.get($theme, bg-primary)};
+ --bg-secondary: #{map.get($theme, bg-secondary)};
+ --border-color: #{map.get($theme, border-color)};
+}
diff --git a/src/scss/variables.scss b/src/scss/variables.scss
index e497540..4b96bc4 100644
--- a/src/scss/variables.scss
+++ b/src/scss/variables.scss
@@ -15,6 +15,7 @@ $shadow-lg: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
$border-radius-sm: 0.375rem;
$border-radius-md: 0.5rem;
$border-radius-lg: 1rem;
+$border-radius-xl: 1.5rem;
$border-radius-full: 9999px;
// Spacing variables
@@ -26,6 +27,37 @@ $spacing-xl: 2rem;
$spacing-2xl: 3rem;
$spacing-3xl: 4rem;
+// Section variables
+$section-padding-y: $spacing-2xl;
+
+// Card variables
+$card-border-radius: $border-radius-xl;
+$card-border-radius-sm: $border-radius-md;
+$card-border-radius-md: $border-radius-lg;
+$card-border-radius-lg: $border-radius-xl;
+$card-padding: $spacing-xl;
+$card-padding-sm: $spacing-lg;
+$card-padding-md: $spacing-md;
+$card-padding-lg: $spacing-xl;
+$card-gap: $spacing-xl;
+$card-gap-sm: $spacing-md;
+
+// Transition variables
+$transition-base: all 0.3s ease;
+$transition-default: all 0.3s ease;
+$transition-fast: all 0.2s ease;
+$transition-smooth: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+
+// Common shadow variables
+$shadow-card: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
+ 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+$shadow-card-hover: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
+ 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+$shadow-card-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
+ 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+$shadow-button: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
+ 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+
// Gradient variables
$gradient-primary: linear-gradient(
90deg,