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%;
+ }
+ }
}