From 231803db2da642b7f2f6f21c1f6eded67e03b003 Mon Sep 17 00:00:00 2001 From: Sascha Date: Tue, 20 Jan 2026 15:18:09 +0100 Subject: [PATCH] adding learning certificate, styling fixes --- backend/routes/contact.js | 40 +++++++------- src/assets/CV_Sascha_Bach.pdf | Bin 1546636 -> 1547067 bytes src/assets/react.svg | 1 - src/assets/vite.svg | 1 - src/components/sections/ProjectsSection.tsx | 4 +- src/config/certifications-config.ts | 52 +++--------------- src/config/certifications-data.ts | 43 +++++++++++++++ src/config/locales/de/certifications.ts | 30 +--------- src/config/locales/en/certifications.ts | 30 +--------- src/scss/sections/certifications-section.scss | 34 +++++++++--- 10 files changed, 106 insertions(+), 129 deletions(-) delete mode 100644 src/assets/react.svg delete mode 100644 src/assets/vite.svg create mode 100644 src/config/certifications-data.ts 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 34cff3728927775150455fe496d2a9c8972947ca..cbdeb6168c17dd255c33be1e91ae317a48c571d8 100644 GIT binary patch delta 15373 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 NoG@f$V)EiJ{|CzE-B|zt delta 14925 zcmai)Q*bU^u%=_%wrz9Awr$(VPQKW-ZQHhO+fH_j`KM|w&bgYZb}cEd4=qSf5k*yV z{Gt~pH8@E3lKqE!PHCCd2cJo9V)0WQ)&7~z@UtJx zpw{%JjQ88$t2u3GfjNUIay%b&am_iN(yIA?YUOYL?i8Ma9KI|B_Y?%b3n`!majgJl zpJn2ypJRJ>#m(B%|0Mf_rF|1n$OQ%Snq8i2L!a%v&%VaK9hUlTi@K%S`x0K@peUVq ztQyts_i6-KcKuB^IF~KXarr9X-a`Z@E)S~ToR+J2XrgDrb zbqE>u+-%I^NUenZS;JdtUv!Q4oy40%D^d_{*&t4v)HSHOi9w5-MQ_iK;DNX3w4jx^ zrjPfa?s1IPse2e<&?$AVGbt&g$mcXLIrD+`o`>c_{{9xv)Y`d)f7pXy$r92$db~Lzq8D9ly0xyXUT&MdrG7Ktf+HP2c$+8i4$GL+ZGGT z90nVMt+<=d2{aMFGh-RV8Y2eCgNnP3o=((`q?-I93o zJbf2()W)x53V=-=uAaoX;BW!lr||vT^~~MX&KtY>z{9(Aw$CPX2S&xJFS$5r|K{0~oGSab6)Co6)$pN}`|Z z198sfD7EX8Gdq=FEE;B`ooeCJ?Ww0emMfZdB&XQ2v>3eTS_~U!KAC{p#6i1N1wHku zmbDa$2GLbA7@289f3DS=j3t%L$MphF*lUC??FM29^Qn5w3x2&?KgqbIC!^sxWC=v> zmi_pwWkUG=4&*OHJ=jlyFNLtPW31_4YuQa{M{{ov8-eLr|0bQ$zr@FQ!kEIj_?%s^ zJ~p#-FQAi%DuX*C#sI*25gc}vA!9GW0Cl74fu{<0LK?QhX*`t(_t z8qP}#Z{!tx{@&M6N#3B~dE?PX5fTlV_<)zkBoXt4LU2AzuucF^nYy$kDuT-_4x|0z z8V#XB!2FDTeJ;!UFu78Dw? zlMN^}4oe(6S7f!-KG`mMc7`2;giaw;&jt<@t#4G<*ec9nxJY=B%HRNOVKj*?{Aue~ zuC-g6%za1W*)u>ZtTyCL1BsXZcr?3xx{NT<(NW3=1C4gqW-cSej)oNx|Xx*Kd#<3@x|#{*-4*2oyDQp|D339lpA#)+gTxyW`H z3z?6ZUT$E9Mlu~uUgAJYrqlMJ@OR|qgT-hD{Sv_w;Jq* ztu^c<(BPzl=bW(@?-!5rOVLB< z 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⁡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 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%; + } + } }