diff --git a/backend/routes/contact.js b/backend/routes/contact.js index f06ed28..e94ab18 100644 --- a/backend/routes/contact.js +++ b/backend/routes/contact.js @@ -1,9 +1,11 @@ -import express from 'express'; -import rateLimit from 'express-rate-limit'; -import { body, validationResult } from 'express-validator'; -import { sendContactEmail } from '../services/emailService.js'; +// Currently not in use -const router = express.Router(); +import express from 'express' +import rateLimit from 'express-rate-limit' +import { body, validationResult } from 'express-validator' +import { sendContactEmail } from '../services/emailService.js' + +const router = express.Router() // GDPR compliant rate limiting const contactLimiter = rateLimit({ @@ -15,7 +17,7 @@ const contactLimiter = rateLimit({ }, standardHeaders: true, legacyHeaders: false, -}); +}) // GDPR compliant validation const validateContactForm = [ @@ -26,17 +28,17 @@ const validateContactForm = [ body('message').trim().isLength({ min: 10, max: 2000 }).escape(), body('gdprConsent').equals('true').withMessage('GDPR consent required'), body('website').optional().isEmpty(), // Honeypot -]; +] router.post('/', contactLimiter, validateContactForm, async (req, res) => { try { - const errors = validationResult(req); + const errors = validationResult(req) if (!errors.isEmpty()) { return res.status(400).json({ success: false, message: 'Invalid form data.', errors: errors.array(), - }); + }) } const { @@ -47,15 +49,15 @@ router.post('/', contactLimiter, validateContactForm, async (req, res) => { message, gdprConsent, website, - } = req.body; + } = req.body // Honeypot check if (website) { - console.warn('🚨 Spam detected:', req.ip); + console.warn('🚨 Spam detected:', req.ip) return res.status(400).json({ success: false, message: 'Invalid submission detected.', - }); + }) } // GDPR consent check @@ -63,7 +65,7 @@ router.post('/', contactLimiter, validateContactForm, async (req, res) => { return res.status(400).json({ success: false, message: 'GDPR consent required.', - }); + }) } // Send email with minimal logging for GDPR @@ -75,19 +77,19 @@ router.post('/', contactLimiter, validateContactForm, async (req, res) => { message, senderIP: req.ip, userAgent: req.get('User-Agent'), - }); + }) res.json({ success: true, message: "Thank you for your message! I'll get back to you soon.", - }); + }) } catch (error) { - console.error('❌ Contact error:', error.message); // No personal data in logs + console.error('❌ Contact error:', error.message) // No personal data in logs res.status(500).json({ success: false, message: 'Failed to send message. Please try again later.', - }); + }) } -}); +}) -export { router as contactRouter }; +export { router as contactRouter } diff --git a/src/assets/CV_Sascha_Bach.pdf b/src/assets/CV_Sascha_Bach.pdf index 34cff37..cbdeb61 100644 Binary files a/src/assets/CV_Sascha_Bach.pdf and b/src/assets/CV_Sascha_Bach.pdf differ diff --git a/src/assets/react.svg b/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/assets/vite.svg b/src/assets/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/src/assets/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/components/sections/ProjectsSection.tsx b/src/components/sections/ProjectsSection.tsx index e41e6be..243d8fe 100644 --- a/src/components/sections/ProjectsSection.tsx +++ b/src/components/sections/ProjectsSection.tsx @@ -25,8 +25,8 @@ export default function ProjectsSection(props: ProjectsSectionProps = {}) { title = texts.title, subtitle = texts.subtitle, projects = [...projectsData].sort((a, b) => { - const yearA = parseInt(a.year || '0'); - const yearB = parseInt(b.year || '0'); + const yearA = Number.parseInt(a.year || '0'); + const yearB = Number.parseInt(b.year || '0'); return yearB - yearA; // Descending order (newest first) }) } = props; diff --git a/src/config/certifications-config.ts b/src/config/certifications-config.ts index b58b17d..a375dae 100644 --- a/src/config/certifications-config.ts +++ b/src/config/certifications-config.ts @@ -1,46 +1,12 @@ import { certifications } from './locales/en/certifications'; import type { Certification } from '../data/Certification'; -const getCertificationText = (id: number) => { - const item = certifications.certificationItems.find(c => c.id === id); - if (!item) throw new Error(`Certification with id ${id} not found`); - return item; -}; - -export const certificationsData: Certification[] = [ - { - id: 1, - name: getCertificationText(1).name, - issuer: getCertificationText(1).issuer, - year: '2025', - icon: '', - }, - { - id: 2, - name: getCertificationText(2).name, - issuer: getCertificationText(2).issuer, - year: '2025', - icon: '', - }, - { - id: 3, - name: getCertificationText(3).name, - issuer: getCertificationText(3).issuer, - year: '2020', - icon: '', - }, - { - id: 4, - name: getCertificationText(4).name, - issuer: getCertificationText(4).issuer, - year: '2020', - icon: '', - }, - { - id: 5, - name: getCertificationText(5).name, - issuer: getCertificationText(5).issuer, - year: '2015', - icon: '', - }, -].sort((a, b) => Number.parseInt(b.year) - Number.parseInt(a.year)); +export const certificationsData: Certification[] = certifications.certificationItems + .map(item => ({ + id: item.id, + name: item.name, + issuer: item.issuer, + year: item.year, + icon: item.icon, + })) + .sort((a, b) => Number.parseInt(b.year) - Number.parseInt(a.year)); diff --git a/src/config/certifications-data.ts b/src/config/certifications-data.ts new file mode 100644 index 0000000..bc77d1f --- /dev/null +++ b/src/config/certifications-data.ts @@ -0,0 +1,43 @@ +export const certificationItems = [{ + id: 1, + name: 'React - The Complete Guide (incl. Next.js, Redux)', + issuer: 'Udemy Lecture - Maximilian Schwarzmüller', + year: '2026', + icon: '' +}, +{ + id: 2, + name: 'Practical Prompt Engineering Masterclass: Hands-On Learning', + issuer: 'Udemy Lecture - Asif Farooqi, Abdullah Dar', + year: '2025', + icon: '' +}, +{ + id: 3, + name: 'Understanding Typescript', + issuer: 'Udemy Lecture - Maximilian Schwarzmüller', + year: '2025', + icon: '' +}, +{ + id: 4, + name: 'Angular Step by Step', + issuer: 'Udemy Lecture - Shivprasad Koirala', + year: '2020', + icon: '' +}, +{ + id: 5, + name: 'Angular and Typescript', + issuer: 'LinkedIn TestDome', + year: '2020', + icon: '' +}, +{ + id: 6, + name: 'Bachelor of Science in Computer Science', + issuer: 'TU Darmstadt', + year: '2015', + icon: '' +}, +]; diff --git a/src/config/locales/de/certifications.ts b/src/config/locales/de/certifications.ts index 39bb08c..ef95eb3 100644 --- a/src/config/locales/de/certifications.ts +++ b/src/config/locales/de/certifications.ts @@ -1,31 +1,7 @@ +import { certificationItems } from '../../certifications-data'; + export const certifications = { title: 'Zertifizierungen & Erfolge', subtitle: 'Meine beruflichen Qualifikationen und kontinuierliche Weiterbildung', - certificationItems: [ - { - id: 1, - name: 'Practical Prompt Engineering Masterclass: Hands-On Learning', - issuer: 'Udemy Kurs - Asif Farooqi, Abdullah Dar', - }, - { - id: 2, - name: 'Understanding Typescript', - issuer: 'Udemy Kurs - Maximilian Schwarzmüller', - }, - { - id: 3, - name: 'Angular Step by Step', - issuer: 'Udemy Kurs - Shivprasad Koirala', - }, - { - id: 4, - name: 'Angular and Typescript', - issuer: 'LinkedIn TestDome', - }, - { - id: 5, - name: 'Bachelor of Science in Informatik', - issuer: 'TU Darmstadt', - }, - ], + certificationItems, }; diff --git a/src/config/locales/en/certifications.ts b/src/config/locales/en/certifications.ts index 7b9db80..5d91c51 100644 --- a/src/config/locales/en/certifications.ts +++ b/src/config/locales/en/certifications.ts @@ -1,31 +1,7 @@ +import { certificationItems } from '../../certifications-data'; + export const certifications = { title: 'Certifications & Achievements', subtitle: 'My professional qualifications and continuous learning', - certificationItems: [ - { - id: 1, - name: 'Practical Prompt Engineering Masterclass: Hands-On Learning', - issuer: 'Udemy Lecture - Asif Farooqi, Abdullah Dar', - }, - { - id: 2, - name: 'Understanding Typescript', - issuer: 'Udemy Lecture - Maximilian Schwarzmüller', - }, - { - id: 3, - name: 'Angular Step by Step', - issuer: 'Udemy Lecture - Shivprasad Koirala', - }, - { - id: 4, - name: 'Angular and Typescript', - issuer: 'LinkedIn TestDome', - }, - { - id: 5, - name: 'Bachelor of Science in Computer Science', - issuer: 'TU Darmstadt', - }, - ], + certificationItems, }; diff --git a/src/scss/sections/certifications-section.scss b/src/scss/sections/certifications-section.scss index d757c54..62c0863 100644 --- a/src/scss/sections/certifications-section.scss +++ b/src/scss/sections/certifications-section.scss @@ -23,15 +23,15 @@ &__grid { display: grid; - grid-template-columns: repeat(1, 1fr); + grid-template-columns: repeat(auto-fit, minmax(min(100%, 425px), 1fr)); gap: 2rem; - @include globals.mobile-only { + @media (min-width: 900px) and (max-width: 1799px) { grid-template-columns: repeat(2, 1fr); } - @include globals.desktop-only { - grid-template-columns: repeat(4, 1fr); + @media (min-width: 1200px) { + grid-template-columns: repeat(3, 1fr); } } @@ -41,10 +41,13 @@ border-radius: $card-border-radius-md; box-shadow: var(--certifications-card-shadow); background: var(--certifications-card-background); - padding: $card-padding-lg $card-padding-lg; + padding: 1.5rem 1rem; transition: $transition-base; position: relative; overflow: hidden; + display: flex; + flex-direction: column; + height: 100%; &::before { content: ''; @@ -70,25 +73,30 @@ } &__card-header { - margin-bottom: 1.5rem; + margin-bottom: 1em; position: relative; z-index: 1; + flex: 1; + display: flex; + flex-direction: column; } &__card-icon { width: 4rem; height: 4rem; - margin: 0 auto 1rem; + margin: 0 auto 0.75rem; color: var(--color-certifications-primary); display: block; + flex-shrink: 0; } &__card-icon-image { height: 4rem; width: 4rem; object-fit: contain; - margin: 0 auto 1rem; + margin: 0 auto 0.75rem; display: block; + flex-shrink: 0; } &__card-title { @@ -123,17 +131,25 @@ // Responsive adjustments @include globals.mobile-only { &__card { - padding: $card-padding-lg $card-padding-md; + padding: 1.25rem 1rem; } &__card-icon, &__card-icon-image { width: 3rem; height: 3rem; + margin-bottom: 0.5rem; } &__card-title { font-size: 1rem; } } + + // Reduce card height on larger screens + @media (min-width: 1200px) { + &__card { + height: 80%; + } + } }