diff --git a/.gitignore b/.gitignore index c814337..f1946d7 100644 Binary files a/.gitignore and b/.gitignore differ diff --git a/package-lock.json b/package-lock.json index 582dd76..8f3cf61 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,24 +9,24 @@ "version": "0.0.0", "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", "react-icons": "^5.5.0", - "react-router-dom": "^7.8.2", - "resend": "^6.0.3" + "react-router-dom": "^7.8.2" }, "devDependencies": { "@eslint/js": "^9.32.0", "@types/react": "^19.1.9", "@types/react-dom": "^19.1.7", "@vitejs/plugin-react": "^4.7.0", + "dotenv": "^17.2.2", "eslint": "^9.32.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", "globals": "^16.3.0", "sass": "^1.90.0", + "ssh2-sftp-client": "^12.0.1", "typescript": "~5.8.3", "typescript-eslint": "^8.39.0", "vite": "^7.1.0" @@ -2124,6 +2124,16 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2131,6 +2141,16 @@ "dev": true, "license": "MIT" }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -2188,6 +2208,23 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/buildcheck": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", + "integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==", + "dev": true, + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2279,6 +2316,22 @@ "dev": true, "license": "MIT" }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "dev": true, + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -2295,6 +2348,21 @@ "node": ">=18" } }, + "node_modules/cpu-features": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", + "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "dependencies": { + "buildcheck": "~0.0.6", + "nan": "^2.19.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2355,6 +2423,19 @@ "node": ">=0.10" } }, + "node_modules/dotenv": { + "version": "17.2.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.2.tgz", + "integrity": "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.211", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.211.tgz", @@ -2605,19 +2686,6 @@ "node": ">=0.10.0" } }, - "node_modules/express-validator": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.2.1.tgz", - "integrity": "sha512-CjNE6aakfpuwGaHQZ3m8ltCG2Qvivd7RHtVMS/6nVxOM7xVGqr4bhflsm4+N5FP5zI7Zxp+Hae+9RE+o8e3ZOQ==", - "license": "MIT", - "dependencies": { - "lodash": "^4.17.21", - "validator": "~13.12.0" - }, - "engines": { - "node": ">= 8.0.0" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2855,6 +2923,13 @@ "node": ">=0.8.19" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3002,12 +3077,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -3078,6 +3147,14 @@ "dev": true, "license": "MIT" }, + "node_modules/nan": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz", + "integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -3370,6 +3447,21 @@ "react-dom": ">=18" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -3384,23 +3476,6 @@ "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", @@ -3486,6 +3561,34 @@ "queue-microtask": "^1.2.2" } }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, "node_modules/sass": { "version": "1.91.0", "resolved": "https://registry.npmjs.org/sass/-/sass-1.91.0.tgz", @@ -3562,6 +3665,52 @@ "node": ">=0.10.0" } }, + "node_modules/ssh2": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz", + "integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "asn1": "^0.2.6", + "bcrypt-pbkdf": "^1.0.2" + }, + "engines": { + "node": ">=10.16.0" + }, + "optionalDependencies": { + "cpu-features": "~0.0.10", + "nan": "^2.23.0" + } + }, + "node_modules/ssh2-sftp-client": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/ssh2-sftp-client/-/ssh2-sftp-client-12.0.1.tgz", + "integrity": "sha512-ICJ1L2PmBel2Q2ctbyxzTFZCPKSHYYD6s2TFZv7NXmZDrDNGk8lHBb/SK2WgXLMXNANH78qoumeJzxlWZqSqWg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "concat-stream": "^2.0.0", + "ssh2": "^1.16.0" + }, + "engines": { + "node": ">=18.20.4" + }, + "funding": { + "type": "individual", + "url": "https://square.link/u/4g7sPflL" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -3662,6 +3811,13 @@ "typescript": ">=4.8.4" } }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true, + "license": "Unlicense" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -3675,6 +3831,13 @@ "node": ">= 0.8.0" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "dev": true, + "license": "MIT" + }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", @@ -3754,14 +3917,12 @@ "punycode": "^2.1.0" } }, - "node_modules/validator": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", - "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" }, "node_modules/vite": { "version": "7.1.3", diff --git a/package.json b/package.json index fc8d0a5..ec7eb4a 100644 --- a/package.json +++ b/package.json @@ -24,11 +24,13 @@ "@types/react": "^19.1.9", "@types/react-dom": "^19.1.7", "@vitejs/plugin-react": "^4.7.0", + "dotenv": "^17.2.2", "eslint": "^9.32.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", "globals": "^16.3.0", "sass": "^1.90.0", + "ssh2-sftp-client": "^12.0.1", "typescript": "~5.8.3", "typescript-eslint": "^8.39.0", "vite": "^7.1.0" diff --git a/src/assets/CV_Sascha_Bach.pdf b/src/assets/CV_Sascha_Bach.pdf new file mode 100644 index 0000000..34cff37 Binary files /dev/null and b/src/assets/CV_Sascha_Bach.pdf differ diff --git a/src/components/sections/AboutSection.tsx b/src/components/sections/AboutSection.tsx index 442ead4..2faca93 100644 --- a/src/components/sections/AboutSection.tsx +++ b/src/components/sections/AboutSection.tsx @@ -8,7 +8,7 @@ interface AboutSectionProps { name?: string; title?: string; subtitle?: string; - bio?: string; + bio?: string[]; profileImage?: string; skillBadges?: SkillBadge[]; featureCards?: Service[]; @@ -86,9 +86,11 @@ export default function AboutSection(props: AboutSectionProps = {}) {

Hi, I'm {name}!

-

- {bio} -

+
+ {bio.map((paragraph, index) => ( +

{paragraph}

+ ))} +
diff --git a/src/components/sections/HeroSection.tsx b/src/components/sections/HeroSection.tsx index 0a7ce87..c005f11 100644 --- a/src/components/sections/HeroSection.tsx +++ b/src/components/sections/HeroSection.tsx @@ -3,6 +3,7 @@ import { useLocation, useNavigate } from 'react-router-dom'; import type { StatItem } from '../../data/StatItem'; import { scrollToProjects } from '../../utils/scrollUtils'; import { getTexts } from '../../config/texts'; +import resumePDF from '../../assets/CV_Sascha_Bach.pdf'; interface HeroSectionProps { title?: string; @@ -31,8 +32,31 @@ export default function HeroSection(props: HeroSectionProps = {}) { }; const handleDownloadResume = () => { - // Add your resume download logic here - console.log('Download resume clicked'); + try { + // Create a temporary anchor element + const link = document.createElement('a'); + link.href = resumePDF; + link.download = 'CV_Sascha_Bach.pdf'; // Specify the filename for download + link.target = '_blank'; // Optional: open in new tab as fallback + + // Temporarily add to DOM and trigger click + document.body.appendChild(link); + link.click(); + + // Clean up + document.body.removeChild(link); + + console.log('✅ Resume download initiated'); + } catch (error) { + console.error('❌ Error downloading resume:', error); + + // Fallback: open in new tab if download fails + try { + window.open(resumePDF, '_blank'); + } catch (fallbackError) { + console.error('❌ Fallback method also failed:', fallbackError); + } + } }; return (
diff --git a/src/config/texts.ts b/src/config/texts.ts index e3327ce..1874694 100644 --- a/src/config/texts.ts +++ b/src/config/texts.ts @@ -39,7 +39,7 @@ export interface TextConfig { title: string; subtitle: string; name: string; - bio: string; + bio: string[]; }; // Services Section @@ -231,9 +231,15 @@ export const defaultTexts: TextConfig = { about: { title: 'About Me', subtitle: - 'Passionate developer with a focus on clean code and innovative solutions', + 'Passionate developer with a focus on clean code and innovative solutions.', name: 'Sascha Bach', - bio: "As a dedicated web developer, I specialize in creating modern, responsive and barrier free web applications. I'm passionate about writing clean, maintainable code and ensuring exceptional user experiences across all devices and browsers. ", + bio: [ + 'I studied Computer Science at the Technical University of Darmstadt and graduated in 2016 with my bachelor thesis on Motion Sickness in Virtual Reality. Since then I have been working as a Software and Web Developer with a focus on Angular and React.', + 'In my work I have seen that every company is different, but many face the same challenge: keeping their digital presence accessible, modern and future proof. A lot of businesses already know that accessibility is required by law, but in practice what matters most is that a website works for all users in a simple and enjoyable way.', + 'My specialization is Web Accessibility (WCAG 2.1, BFSG, European Accessibility Act). I help companies update their websites and applications so they are compliant and at the same time easy to use for everyone. What my clients value is that I combine technical expertise in Angular, React and TypeScript with a human centered approach. Because accessibility is not just about code, it is about people.', + 'If you are wondering whether your website already meets accessibility standards, or if you feel it is time to make your online presence more inclusive and legally compliant, I would be glad to support you. Often just a few clear steps can make a big difference, helping your website reach more people and stand out from the competition.', + "Let's connect and see how we can make your website barrier free, modern and ready for the future.", + ], }, services: { diff --git a/src/scss/sections/about-section.scss b/src/scss/sections/about-section.scss index 9e0c709..425aa58 100644 --- a/src/scss/sections/about-section.scss +++ b/src/scss/sections/about-section.scss @@ -104,6 +104,14 @@ font-size: 1.125rem; color: var(--color-text-muted); line-height: 1.625; + + p { + margin-bottom: 1.5rem; + + &:last-child { + margin-bottom: 0; + } + } } &__skill-badges { @@ -155,7 +163,11 @@ grid-template-columns: 1fr; gap: 2rem; - @include globals.desktop-only { + @media (min-width: 768px) and (max-width: 1199px) { + grid-template-columns: repeat(2, 1fr); + } + + @media (min-width: 1200px) { grid-template-columns: repeat(4, 1fr); } } @@ -287,6 +299,14 @@ &__bio-text { font-size: 1rem; + + p { + margin-bottom: 1.25rem; + + &:last-child { + margin-bottom: 0; + } + } } &__greeting {