feat: Add landing page sections and localization for German and English
- Implemented ReferencesSection and WinningsSection components for the landing page. - Added localization files for German and English, covering various sections including About Me, Experience, Processes, Reasons, References, and Winnings. - Created useScrollReveal hook for scroll-based animations. - Developed styles for new sections: experience, processes, reasons, references, and winnings. - Updated LandingPage component to include new sections.
This commit is contained in:
parent
514b189030
commit
311f60abe6
|
|
@ -11,7 +11,6 @@
|
|||
"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"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -3364,15 +3363,6 @@
|
|||
"react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-icons": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
|
||||
"integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.17.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@
|
|||
"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"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import '../scss/App.scss';
|
|||
import Navbar from '../components/layout/Navigation';
|
||||
import Footer from '../components/layout/Footer';
|
||||
import HomePage from '../pages/HomePage';
|
||||
import LandingPage from '../pages/LandingPage';
|
||||
import ImprintPage from '../pages/ImprintPage';
|
||||
import PrivacyPolicy from '../pages/PrivacyPolicy';
|
||||
import BackToTopButton from '../components/BackToTopButton';
|
||||
|
|
@ -58,7 +59,8 @@ function AppRouter() {
|
|||
<Navbar theme={theme} setTheme={setTheme} />
|
||||
<main id="main-content" className="app__main">
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/" element={<LandingPage />} />
|
||||
<Route path="/technical" element={<HomePage />} />
|
||||
<Route path="/imprint" element={<ImprintPage />} />
|
||||
<Route path="/privacy-policy" element={<PrivacyPolicy />} />
|
||||
</Routes>
|
||||
|
|
|
|||
|
|
@ -1,21 +1,16 @@
|
|||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import type { Language } from '../data/types';
|
||||
import '../scss/language-toggle.scss';
|
||||
import { useScreenReaderAnnouncements } from '../hooks/useScreenReaderAnnouncements';
|
||||
|
||||
export default function LanguageToggle() {
|
||||
const { language, setLanguage } = useLanguage();
|
||||
const { announce } = useScreenReaderAnnouncements();
|
||||
|
||||
const handleLanguageChange = (newLang: Language) => {
|
||||
if (newLang !== language) {
|
||||
setLanguage(newLang);
|
||||
// Announce language change to screen readers
|
||||
const announcement = document.createElement('div');
|
||||
announcement.setAttribute('role', 'status');
|
||||
announcement.setAttribute('aria-live', 'polite');
|
||||
announcement.setAttribute('class', 'sr-only');
|
||||
announcement.textContent = `Language changed to ${newLang === 'en' ? 'English' : 'Deutsch'}`;
|
||||
document.body.appendChild(announcement);
|
||||
setTimeout(() => announcement.remove(), 1000);
|
||||
announce(`Language changed to ${newLang === 'en' ? 'English' : 'Deutsch'}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
import { useScrollReveal } from '../../hooks/useScrollReveal';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
|
||||
export default function ExperienceSection() {
|
||||
const { texts } = useLanguage();
|
||||
const t = texts.landingExperience;
|
||||
const sectionRef = useScrollReveal();
|
||||
|
||||
return (
|
||||
<section id="experience" className="experience-section" ref={sectionRef}>
|
||||
<div className="experience-section__container">
|
||||
<h2 className="experience-section__title reveal-item">
|
||||
{t.title}
|
||||
</h2>
|
||||
|
||||
<div className="experience-section__body">
|
||||
{t.paragraphs.map((paragraph, index) => (
|
||||
<p
|
||||
key={paragraph.substring(0, 40)}
|
||||
className={`experience-section__paragraph reveal-item${index === t.paragraphs.length - 1 ? ' experience-section__paragraph--outro' : ''}`}
|
||||
style={{ '--reveal-delay': `${index * 100}ms` } as React.CSSProperties}
|
||||
>
|
||||
{paragraph}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
import AboutSection from '../sections/AboutSection';
|
||||
|
||||
export default function LandingAboutMeSection() {
|
||||
const { texts } = useLanguage();
|
||||
const t = texts.landingAboutMe;
|
||||
|
||||
return (
|
||||
<AboutSection
|
||||
title={t.title}
|
||||
subtitle={t.subtitle}
|
||||
greeting={t.greeting}
|
||||
name={t.name}
|
||||
bio={t.bio}
|
||||
featureCards={[]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import { useScrollReveal } from '../../hooks/useScrollReveal';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
|
||||
export default function ProcessesSection() {
|
||||
const { texts } = useLanguage();
|
||||
const t = texts.landingProcesses;
|
||||
const sectionRef = useScrollReveal();
|
||||
|
||||
return (
|
||||
<section id="processes" className="processes-section" ref={sectionRef}>
|
||||
<div className="processes-section__container">
|
||||
<h2 className="processes-section__title reveal-item">{t.title}</h2>
|
||||
<p className="processes-section__intro reveal-item">{t.intro}</p>
|
||||
<ul className="processes-section__list" aria-label={t.title}>
|
||||
{t.items.map((item, index) => (
|
||||
<li
|
||||
key={item.text.substring(0, 40)}
|
||||
className="processes-section__item reveal-item"
|
||||
style={{ '--reveal-delay': `${index * 80}ms` } as React.CSSProperties}
|
||||
>
|
||||
<p className="processes-section__text">{item.text}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<p className="processes-section__outro reveal-item">{t.outro}</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import { useScrollReveal } from '../../hooks/useScrollReveal';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
|
||||
export default function ReasonsSection() {
|
||||
const { texts } = useLanguage();
|
||||
const t = texts.landingReasons;
|
||||
const sectionRef = useScrollReveal();
|
||||
|
||||
return (
|
||||
<section id="reasons" className="reasons-section" ref={sectionRef}>
|
||||
<div className="reasons-section__container">
|
||||
<h2 className="reasons-section__title reveal-item">
|
||||
{t.title}
|
||||
</h2>
|
||||
|
||||
|
||||
|
||||
|
||||
<ul className="reasons-section__list" aria-label="Reasons">
|
||||
{t.reasons.map((item, index) => (
|
||||
<li
|
||||
key={item.stat}
|
||||
className="reasons-section__item reveal-item"
|
||||
style={{ '--reveal-delay': `${index * 100}ms` } as React.CSSProperties}
|
||||
>
|
||||
<span className="reasons-section__stat" aria-hidden="true">
|
||||
{item.stat}
|
||||
</span>
|
||||
<p className="reasons-section__text">{item.text}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<p className="reasons-section__outro reveal-item">
|
||||
{t.outro}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { useScrollReveal } from '../../hooks/useScrollReveal';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
|
||||
export default function ReferencesSection() {
|
||||
const { texts } = useLanguage();
|
||||
const t = texts.landingReferences;
|
||||
const sectionRef = useScrollReveal();
|
||||
|
||||
return (
|
||||
<section id="references" className="references-section" ref={sectionRef}>
|
||||
<div className="references-section__container">
|
||||
<h2 className="references-section__title reveal-item">{t.title}</h2>
|
||||
<ul className="references-section__list" aria-label={t.title}>
|
||||
{t.items.map((item, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className="references-section__item reveal-item"
|
||||
style={{ '--reveal-delay': `${index * 100}ms` } as React.CSSProperties}
|
||||
>
|
||||
<p className="references-section__quote">{item.referenceText}</p>
|
||||
<div className="references-section__author">
|
||||
<p className="references-section__name">{item.name}</p>
|
||||
<p className="references-section__position">{item.position}</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import { useScrollReveal } from '../../hooks/useScrollReveal';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
|
||||
export default function WinningsSection() {
|
||||
const { texts } = useLanguage();
|
||||
const t = texts.landingWinnings;
|
||||
const sectionRef = useScrollReveal();
|
||||
|
||||
return (
|
||||
<section id="winnings" className="winnings-section" ref={sectionRef}>
|
||||
<div className="winnings-section__container">
|
||||
<h2 className="winnings-section__title reveal-item">
|
||||
{t.title}
|
||||
</h2>
|
||||
|
||||
<p className="winnings-section__intro reveal-item">
|
||||
{t.intro}
|
||||
</p>
|
||||
|
||||
<ul className="winnings-section__list" aria-label={t.title}>
|
||||
{t.items.map((item, index) => (
|
||||
<li
|
||||
key={item.text.substring(0, 40)}
|
||||
className="winnings-section__item reveal-item"
|
||||
style={{ '--reveal-delay': `${index * 80}ms` } as React.CSSProperties}
|
||||
>
|
||||
<p className="winnings-section__text">{item.text}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<p className="winnings-section__outro reveal-item">
|
||||
{t.outro}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
import { Github, Linkedin, Mail } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { personalConfig, createEmailLink } from '../../config/personal';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
|
||||
export default function Footer() {
|
||||
const { texts } = useLanguage();
|
||||
const location = useLocation();
|
||||
const isTechnicalPage = location.pathname === '/technical';
|
||||
// Email obfuscation function using config
|
||||
const handleEmailClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
|
@ -21,6 +23,13 @@ export default function Footer() {
|
|||
<Link to="/imprint" className="footer__link">
|
||||
{texts.footer.imprintText}
|
||||
</Link>
|
||||
<Link
|
||||
to={isTechnicalPage ? '/' : '/technical'}
|
||||
className="footer__link"
|
||||
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
|
||||
>
|
||||
{isTechnicalPage ? texts.footer.landingLinkText : texts.footer.technicalLinkText}
|
||||
</Link>
|
||||
<div className="footer__copyright">
|
||||
© {new Date().getFullYear()} {personalConfig.name}. {texts.footer.copyrightText}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -23,11 +23,14 @@ export default function Navbar({
|
|||
const navigate = useNavigate();
|
||||
const { texts } = useLanguage();
|
||||
|
||||
const menuItems = texts.navigation.menuItems;
|
||||
const isLandingPage = location.pathname === '/';
|
||||
const menuItems = isLandingPage
|
||||
? texts.navigation.landingMenuItems
|
||||
: texts.navigation.menuItems;
|
||||
|
||||
// Track active section based on scroll position
|
||||
useEffect(() => {
|
||||
if (location.pathname !== '/') {
|
||||
if (location.pathname !== '/technical' && location.pathname !== '/') {
|
||||
setActiveSection('');
|
||||
return;
|
||||
}
|
||||
|
|
@ -61,8 +64,17 @@ export default function Navbar({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="navbar">
|
||||
<Link to="/" className="navbar__name">{personalConfig.name}</Link>
|
||||
<div className={`navbar${isLandingPage ? ' navbar--landing' : ''}`}>
|
||||
<Link
|
||||
to={isLandingPage ? '/' : '/technical'}
|
||||
className="navbar__name"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
>
|
||||
{personalConfig.name}
|
||||
</Link>
|
||||
<div className="navbar__container">
|
||||
{menuItems.map((item) => (
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ interface AboutSectionProps {
|
|||
name?: string;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
greeting?: string;
|
||||
bio?: string[];
|
||||
profileImage?: string;
|
||||
featureCards?: Service[];
|
||||
|
|
@ -23,6 +24,7 @@ export default function AboutSection(props: AboutSectionProps = {}) {
|
|||
name = texts.name,
|
||||
title = texts.title,
|
||||
subtitle = texts.subtitle,
|
||||
greeting,
|
||||
bio = texts.bio,
|
||||
profileImage = saschaImage,
|
||||
featureCards = texts.featureCards.map((card, index) => ({
|
||||
|
|
@ -41,9 +43,11 @@ export default function AboutSection(props: AboutSectionProps = {}) {
|
|||
<h2 id="about-title" className="about-section__title">
|
||||
{title}
|
||||
</h2>
|
||||
{subtitle && (
|
||||
<p className="about-section__subtitle">
|
||||
{subtitle}
|
||||
</p>
|
||||
)}
|
||||
</header>
|
||||
|
||||
{/* Skip link for keyboard navigation */}
|
||||
|
|
@ -68,7 +72,7 @@ export default function AboutSection(props: AboutSectionProps = {}) {
|
|||
<div className="about-section__bio-section">
|
||||
<div className="about-section__bio-content">
|
||||
<h3 className="about-section__greeting">
|
||||
Hi, I'm {name}!
|
||||
{greeting ?? `Hi, I'm ${name}!`}
|
||||
</h3>
|
||||
<div className="about-section__bio-text">
|
||||
{bio.map((paragraph) => (
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { Mail, ExternalLink, Send } from 'lucide-react';
|
||||
import { personalConfig, getObfuscatedEmail } from '../../config/personal';
|
||||
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
||||
import { Mail, Send, Github, Linkedin } from 'lucide-react';
|
||||
import { personalConfig } from '../../config/personal';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
import { useScreenReaderAnnouncements } from '../../hooks/useScreenReaderAnnouncements';
|
||||
|
||||
interface ContactSectionProps {
|
||||
title?: string;
|
||||
|
|
@ -21,6 +23,7 @@ interface ContactSectionProps {
|
|||
export default function ContactSection(props: ContactSectionProps = {}) {
|
||||
const { texts: allTexts } = useLanguage();
|
||||
const texts = allTexts.contact;
|
||||
const { announce } = useScreenReaderAnnouncements();
|
||||
|
||||
const {
|
||||
title = texts.title,
|
||||
|
|
@ -44,18 +47,9 @@ export default function ContactSection(props: ContactSectionProps = {}) {
|
|||
const subject = encodeURIComponent(texts.emailSubject);
|
||||
const body = encodeURIComponent(texts.emailBody);
|
||||
|
||||
// Announce action to screen readers
|
||||
const announcement = document.createElement('div');
|
||||
announcement.setAttribute('aria-live', 'polite');
|
||||
announcement.setAttribute('class', 'sr-only');
|
||||
announcement.textContent = texts.emailAnnouncement;
|
||||
document.body.appendChild(announcement);
|
||||
announce(texts.emailAnnouncement);
|
||||
|
||||
globalThis.location.href = `mailto:${email}?subject=${subject}&body=${body}`;
|
||||
|
||||
setTimeout(() => {
|
||||
announcement.remove();
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const getEmailAddress = () => {
|
||||
|
|
@ -63,21 +57,6 @@ export default function ContactSection(props: ContactSectionProps = {}) {
|
|||
return parts[0] + '@' + parts[1] + '.' + parts[2];
|
||||
};
|
||||
|
||||
// Enhanced social link handler with announcements
|
||||
const handleSocialClick = (platform: string, url: string) => {
|
||||
const announcement = document.createElement('div');
|
||||
announcement.setAttribute('aria-live', 'polite');
|
||||
announcement.setAttribute('class', 'sr-only');
|
||||
announcement.textContent = texts.socialAnnouncement.replace('{platform}', platform);
|
||||
document.body.appendChild(announcement);
|
||||
|
||||
globalThis.open(url, '_blank', 'noopener,noreferrer');
|
||||
|
||||
setTimeout(() => {
|
||||
announcement.remove();
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="contact" className="contact-section" aria-labelledby="contact-title">
|
||||
<div className="contact-section__container">
|
||||
|
|
@ -166,45 +145,29 @@ export default function ContactSection(props: ContactSectionProps = {}) {
|
|||
<ul className="contact-section__social-list">
|
||||
|
||||
<li className="contact-section__detail-item">
|
||||
<button
|
||||
className="contact-section__social-button contact-section__social-button--email"
|
||||
onClick={handleEmailClick}
|
||||
aria-label={`Send email to ${getObfuscatedEmail()}`}
|
||||
type="button"
|
||||
<Github className="contact-section__social-icon" aria-hidden="true" />
|
||||
<a
|
||||
href={personalConfig.social.github.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="contact-section__detail-link"
|
||||
aria-label={`${texts.githubLinkText} (opens in new tab)`}
|
||||
>
|
||||
<Mail className="contact-section__social-icon" aria-hidden="true" />
|
||||
</button>
|
||||
<span className="contact-section__detail-text" aria-label="Email address">
|
||||
{getObfuscatedEmail()}
|
||||
</span>
|
||||
{texts.githubLinkText}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li className="contact-section__detail-item">
|
||||
<button
|
||||
className="contact-section__social-button contact-section__social-button--github"
|
||||
onClick={() => handleSocialClick('GitHub', personalConfig.social.github.url)}
|
||||
aria-label={`Visit GitHub profile: ${personalConfig.social.github.username} (opens in new tab)`}
|
||||
type="button"
|
||||
<Linkedin className="contact-section__social-icon" aria-hidden="true" />
|
||||
<a
|
||||
href={personalConfig.social.linkedin.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="contact-section__detail-link"
|
||||
aria-label={`${texts.linkedinLinkText} (opens in new tab)`}
|
||||
>
|
||||
<ExternalLink className="contact-section__social-icon" aria-hidden="true" />
|
||||
</button>
|
||||
<span className="contact-section__detail-text" aria-label="GitHub username">
|
||||
{personalConfig.social.github.username}
|
||||
</span>
|
||||
</li>
|
||||
|
||||
<li className="contact-section__detail-item">
|
||||
<button
|
||||
className="contact-section__social-button contact-section__social-button--linkedin"
|
||||
onClick={() => handleSocialClick('LinkedIn', personalConfig.social.linkedin.url)}
|
||||
aria-label={`Visit LinkedIn profile: ${personalConfig.social.linkedin.username} (opens in new tab)`}
|
||||
type="button"
|
||||
>
|
||||
<ExternalLink className="contact-section__social-icon" aria-hidden="true" />
|
||||
</button>
|
||||
<span className="contact-section__detail-text" aria-label="LinkedIn username">
|
||||
{personalConfig.social.linkedin.username}
|
||||
</span>
|
||||
{texts.linkedinLinkText}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useLocation, useNavigate } from 'react-router-dom';
|
|||
import type { StatItem } from '../../data/StatItem';
|
||||
import { scrollToProjects, scrollToSection } from '../../utils/scrollUtils';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
import { useScreenReaderAnnouncements } from '../../hooks/useScreenReaderAnnouncements';
|
||||
import resumePDF from '../../assets/CV_Sascha_Bach.pdf';
|
||||
|
||||
interface HeroSectionProps {
|
||||
|
|
@ -12,6 +13,7 @@ interface HeroSectionProps {
|
|||
secondaryButtonText?: string;
|
||||
tertiaryButtonText?: string;
|
||||
statItems?: StatItem[];
|
||||
hideStats?: boolean;
|
||||
}
|
||||
|
||||
export default function HeroSection(props: HeroSectionProps = {}) {
|
||||
|
|
@ -27,64 +29,44 @@ export default function HeroSection(props: HeroSectionProps = {}) {
|
|||
secondaryButtonText = texts.secondaryButtonText,
|
||||
tertiaryButtonText = texts.tertiaryButtonText,
|
||||
statItems = texts.statItems,
|
||||
hideStats = false,
|
||||
} = props;
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Enhanced announcement function for screen readers
|
||||
const announceToScreenReader = (message: string) => {
|
||||
const announcement = document.createElement('div');
|
||||
announcement.setAttribute('aria-live', 'polite');
|
||||
announcement.setAttribute('aria-atomic', 'true');
|
||||
announcement.setAttribute('class', 'sr-only');
|
||||
announcement.textContent = message;
|
||||
|
||||
document.body.appendChild(announcement);
|
||||
setTimeout(() => {
|
||||
announcement.remove();
|
||||
}, 1000);
|
||||
};
|
||||
const { announce } = useScreenReaderAnnouncements();
|
||||
|
||||
const handleViewWork = () => {
|
||||
announceToScreenReader('Navigating to projects section');
|
||||
announce('Navigating to projects section');
|
||||
scrollToProjects(location.pathname, navigate);
|
||||
};
|
||||
|
||||
const handleDownloadResume = () => {
|
||||
try {
|
||||
announceToScreenReader('Starting resume download');
|
||||
announce('Starting resume download');
|
||||
|
||||
// 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
|
||||
link.download = 'CV_Sascha_Bach.pdf';
|
||||
link.target = '_blank';
|
||||
|
||||
// Temporarily add to DOM and trigger click
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
// Clean up
|
||||
link.remove();
|
||||
|
||||
console.log('✅ Resume download initiated');
|
||||
announceToScreenReader('Resume download started successfully');
|
||||
announce('Resume download started successfully');
|
||||
} catch (error) {
|
||||
console.error('❌ Error downloading resume:', error);
|
||||
announceToScreenReader('Resume download failed, opening in new tab');
|
||||
announce('Resume download failed, opening in new tab');
|
||||
|
||||
// Fallback: open in new tab if download fails
|
||||
try {
|
||||
window.open(resumePDF, '_blank');
|
||||
} catch (fallbackError) {
|
||||
console.error('❌ Fallback method also failed:', fallbackError);
|
||||
announceToScreenReader('Unable to access resume file');
|
||||
} catch {
|
||||
announce('Unable to access resume file');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleGetInTouch = () => {
|
||||
announceToScreenReader('Navigating to contact section');
|
||||
announce('Navigating to contact section');
|
||||
scrollToSection('contact', location.pathname, navigate);
|
||||
};
|
||||
|
||||
|
|
@ -139,6 +121,7 @@ export default function HeroSection(props: HeroSectionProps = {}) {
|
|||
{accessibilityTexts.screenReader.downloadResumeDescription}
|
||||
</div>
|
||||
|
||||
{!hideStats && (
|
||||
<aside className="hero-section__stats" aria-labelledby="stats-heading">
|
||||
<h2 id="stats-heading" className="sr-only">{accessibilityTexts.screenReader.professionalStatistics}</h2>
|
||||
{statItems.map((item, index) => (
|
||||
|
|
@ -162,6 +145,7 @@ export default function HeroSection(props: HeroSectionProps = {}) {
|
|||
</div>
|
||||
))}
|
||||
</aside>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
// 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: '', // No longer used
|
||||
ENDPOINTS: {
|
||||
CONTACT: '/api/contact', // No longer used
|
||||
},
|
||||
};
|
||||
|
||||
// Export individual items for backward compatibility
|
||||
export const ENDPOINTS = API_CONFIG.ENDPOINTS;
|
||||
|
||||
// Helper function to get full API URL
|
||||
export const getApiUrl = (endpoint: string): string => {
|
||||
return `${API_CONFIG.BASE_URL}${endpoint}`;
|
||||
};
|
||||
|
||||
// Default export
|
||||
export default API_CONFIG;
|
||||
|
|
@ -18,4 +18,6 @@ export const contact = {
|
|||
'Hallo Sascha,\n\nIch habe Ihr Portfolio besucht und würde gerne ein potenzielles Projekt oder eine Zusammenarbeit besprechen.\n\nMit freundlichen Grüßen,',
|
||||
emailAnnouncement: 'E-Mail-Client wird mit vorausgefüllter Nachricht geöffnet',
|
||||
socialAnnouncement: '{platform}-Profil wird in neuem Tab geöffnet',
|
||||
githubLinkText: 'Sascha Bach auf GitHub',
|
||||
linkedinLinkText: 'Sascha Bach auf LinkedIn',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
export const footer = {
|
||||
imprintText: 'Impressum und Datenschutz',
|
||||
copyrightText: 'Alle Rechte vorbehalten.',
|
||||
technicalLinkText: 'Technisches Portfolio',
|
||||
landingLinkText: 'Landing Page',
|
||||
githubAriaLabel: 'GitHub-Profil',
|
||||
linkedinAriaLabel: 'LinkedIn-Profil',
|
||||
emailAriaLabel: 'E-Mail senden',
|
||||
|
|
|
|||
|
|
@ -3,6 +3,12 @@ import { themeToggle } from '../../theme-toggle';
|
|||
import { footer } from './footer';
|
||||
import { hero } from './hero';
|
||||
import { about } from './about';
|
||||
import { landingAboutMe } from './landing-aboutme';
|
||||
import { landingReasons } from './landing-reasons';
|
||||
import { landingExperience } from './landing-experience';
|
||||
import { landingWinnings } from './landing-winnings';
|
||||
import { landingProcesses } from './landing-processes';
|
||||
import { landingReferences } from './landing-references';
|
||||
import { services } from './services';
|
||||
import { skills } from './skills';
|
||||
import { projects } from './projects';
|
||||
|
|
@ -18,6 +24,12 @@ export const de = {
|
|||
footer,
|
||||
hero,
|
||||
about,
|
||||
landingAboutMe,
|
||||
landingReasons,
|
||||
landingExperience,
|
||||
landingWinnings,
|
||||
landingProcesses,
|
||||
landingReferences,
|
||||
services,
|
||||
skills,
|
||||
projects,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
export const landingAboutMe = {
|
||||
title: 'Mehr Kunden durch Barrierefreiheit',
|
||||
subtitle: '',
|
||||
greeting: 'Hi, ich bin Sascha.',
|
||||
name: 'Sascha',
|
||||
bio: [
|
||||
'Ich verhelfe Unternehmen zu Webseiten, die von allen genutzt werden können. Eine Webseite, die für alle funktioniert, erreicht alle.',
|
||||
],
|
||||
featureCards: [] as Array<{ title: string; description: string; }>,
|
||||
};
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
export const landingExperience = {
|
||||
title: 'Meine Erfahrung',
|
||||
paragraphs: [
|
||||
'Ich habe Unternehmen wie deins geholfen – von kleinen Läden bis hin zu Dienstleistern – ihre Webseiten barrierefrei zu machen.',
|
||||
'Bei ORWO habe ich einen Fotobuch-Designer entwickelt, der für tausende Kunden funktionierte, auch für Menschen mit Sehbeeinträchtigungen. Ich habe ihn nicht nur gut aussehen lassen, ich habe ihn für alle zum Laufen gebracht.',
|
||||
'Diese Sorgfalt bringe ich jetzt zu deiner Webseite, egal ob Bäckerei, Therapeut, Handwerker oder Kiosk.',
|
||||
'Barrierefreiheit geht alle was an.',
|
||||
],
|
||||
};
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
export const landingProcesses = {
|
||||
title: 'Wie ich arbeite',
|
||||
intro: 'Ich bin dein Partner.',
|
||||
items: [
|
||||
{ text: 'Ich arbeite meist remote - weniger Meetings, weniger Pendelzeit, weniger Aufwand.' },
|
||||
{ text: 'Wir vereinbaren einen Zeitplan - ich liefere.' },
|
||||
{ text: 'Ich nutze meine eigenen Tools - kein Setup auf deiner Seite nötig.' },
|
||||
{ text: 'Ich bin flexibel - egal ob du eine schnelle Lösung oder ein komplettes Redesign brauchst.' },
|
||||
],
|
||||
outro: 'Du bekommst meine volle Aufmerksamkeit.',
|
||||
};
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
export const landingReasons = {
|
||||
title: 'Warum es wichtig ist:',
|
||||
|
||||
reasons: [
|
||||
{
|
||||
stat: '15 %',
|
||||
text: 'aller Menschen in Deutschland haben eine Beeinträchtigung. Das sind 12 Millionen potenzielle Kunden.',
|
||||
},
|
||||
{
|
||||
stat: '80 %',
|
||||
text: 'dieser Menschen verlassen Webseiten, die nicht für sie funktionieren.',
|
||||
},
|
||||
{
|
||||
stat: 'SEO',
|
||||
text: 'Google belohnt barrierefreie Webseiten mit besseren Rankings, mehr Traffic und mehr Umsatz.',
|
||||
},
|
||||
{
|
||||
stat: 'Vertrauen',
|
||||
text: 'Kunden vertrauen barrierefreien Webseiten mehr. Sie fühlen sich respektiert, nicht ignoriert.',
|
||||
},
|
||||
],
|
||||
outro:
|
||||
'Du tust nicht nur das Richtige. Du machst den Unterschied.',
|
||||
};
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
export const landingReferences = {
|
||||
title: 'Was Kunden und Kollegen sagen',
|
||||
items: [
|
||||
{
|
||||
referenceText:
|
||||
'Das Audit zur digitalen Barrierefreiheit war äußerst aufschlussreich und praxisnah. Die identifizierten Schwachstellen wurden klar und verständlich erklärt, sodass konkrete Verbesserungsmaßnahmen abgeleitet werden konnten. Besonders positiv ist der ganzheitliche Blick auf Nutzerfreundlichkeit und Inklusion. Die Zusammenarbeit ist sehr empfehlenswert für alle, die ihre Website nachhaltig barrierefrei gestalten möchten.',
|
||||
name: 'Florian Adragna',
|
||||
position: 'Freiberufler, Florian Lucas Adragna',
|
||||
},
|
||||
{
|
||||
referenceText:
|
||||
'Ich habe die Zusammenarbeit mit dir immer sehr geschätzt. Bei der Fehlersuche und -behebung warst du besonders schnell und zuverlässig. Auch in neue Themen hast du dich problemlos und zügig eingearbeitet. Mit dir zu arbeiten war fachlich wie menschlich jederzeit angenehm – ich empfehle dich sehr gerne weiter.',
|
||||
name: 'Kollege',
|
||||
position: 'Teamlead, ORWO Net GmbH',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
export const landingWinnings = {
|
||||
title: 'Was du bekommst',
|
||||
intro: 'Wenn du mit mir arbeitest, bekommst du:',
|
||||
items: [
|
||||
{ text: 'Eine Webseite, die für alle funktioniert, egal wie sie surfen.' },
|
||||
{ text: 'Mehr Besucher, denn Google liebt barrierefreie Seiten.' },
|
||||
{ text: 'Mehr Umsatz, denn Menschen kaufen, wenn sie sich eingeschlossen fühlen.' },
|
||||
{ text: 'Mehr Vertrauen, denn deine Kunden fühlen sich respektiert.' },
|
||||
{ text: 'Entspannung, denn ich kümmere mich um alles, vom Testen bis zur rechtlichen Konformität.' },
|
||||
{ text: 'Kein Tech-Stress, denn ich erkläre alles in einfacher Sprache, ohne Fachjargon.' },
|
||||
],
|
||||
outro: 'Ich stelle sicher, dass ich dein Problem verstehe und meine Lösung passt.',
|
||||
};
|
||||
|
|
@ -7,5 +7,14 @@ export const navigation = {
|
|||
{ section: 'Projects', label: 'Projekte' },
|
||||
{ section: 'Contact', label: 'Kontakt' },
|
||||
],
|
||||
landingMenuItems: [
|
||||
{ section: 'About', label: 'Über mich' },
|
||||
{ section: 'Winnings', label: 'Vorteile' },
|
||||
{ section: 'Experience', label: 'Erfahrung' },
|
||||
{ section: 'Processes', label: 'Wie ich arbeite' },
|
||||
{ section: 'Projects', label: 'Projekte' },
|
||||
{ section: 'References', label: 'Referenzen' },
|
||||
{ section: 'Contact', label: 'Kontakt' },
|
||||
],
|
||||
mobileMenuAriaLabel: 'Menü öffnen',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,323 @@
|
|||
import type { MenuItem } from '../../../data/types';
|
||||
|
||||
export interface TextConfig {
|
||||
// Navigation
|
||||
navigation: {
|
||||
menuItems: MenuItem[];
|
||||
landingMenuItems: MenuItem[];
|
||||
mobileMenuAriaLabel: string;
|
||||
};
|
||||
|
||||
// Theme Toggle
|
||||
themeToggle: {
|
||||
lightIcon: string;
|
||||
darkIcon: string;
|
||||
lightModeText: string;
|
||||
darkModeText: string;
|
||||
};
|
||||
|
||||
// Footer
|
||||
footer: {
|
||||
imprintText: string;
|
||||
copyrightText: string;
|
||||
technicalLinkText: string;
|
||||
landingLinkText: string;
|
||||
githubAriaLabel: string;
|
||||
linkedinAriaLabel: string;
|
||||
emailAriaLabel: string;
|
||||
};
|
||||
|
||||
// Hero Section
|
||||
hero: {
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButtonText: string;
|
||||
secondaryButtonText: string;
|
||||
tertiaryButtonText: string;
|
||||
statItems: Array<{
|
||||
label: string;
|
||||
value: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
// About Section
|
||||
about: {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
name: string;
|
||||
bio: string[];
|
||||
featureCards: Array<{
|
||||
title: string;
|
||||
description: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
// Landing About Me Section
|
||||
landingAboutMe: {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
greeting: string;
|
||||
name: string;
|
||||
bio: string[];
|
||||
featureCards: Array<{
|
||||
title: string;
|
||||
description: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
// Landing Reasons Section
|
||||
landingReasons: {
|
||||
title: string;
|
||||
reasons: Array<{
|
||||
stat: string;
|
||||
text: string;
|
||||
}>;
|
||||
outro: string;
|
||||
};
|
||||
|
||||
// Landing Experience Section
|
||||
landingExperience: {
|
||||
title: string;
|
||||
paragraphs: string[];
|
||||
};
|
||||
|
||||
// Landing Winnings Section
|
||||
landingWinnings: {
|
||||
title: string;
|
||||
intro: string;
|
||||
items: Array<{ text: string; }>;
|
||||
outro: string;
|
||||
};
|
||||
|
||||
// Landing Processes Section
|
||||
landingProcesses: {
|
||||
title: string;
|
||||
intro: string;
|
||||
items: Array<{ text: string; }>;
|
||||
outro: string;
|
||||
};
|
||||
|
||||
// Landing References Section
|
||||
landingReferences: {
|
||||
title: string;
|
||||
items: Array<{
|
||||
referenceText: string;
|
||||
name: string;
|
||||
position: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
// Services Section
|
||||
services: {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
serviceItems: Array<{
|
||||
title: string;
|
||||
description: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
// Skills Section
|
||||
skills: {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
skillCategories: Array<{
|
||||
title: string;
|
||||
skills: Array<{
|
||||
skill: string;
|
||||
value: number;
|
||||
}>;
|
||||
}>;
|
||||
};
|
||||
|
||||
// Projects Section
|
||||
projects: {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
codeButtonText: string;
|
||||
liveButtonText: string;
|
||||
projectItems: Array<{
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
// Certifications Section
|
||||
certifications: {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
certificationItems: Array<{
|
||||
name: string;
|
||||
issuer: 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;
|
||||
emailSubject: string;
|
||||
emailBody: string;
|
||||
emailAnnouncement: string;
|
||||
socialAnnouncement: string;
|
||||
githubLinkText: string;
|
||||
linkedinLinkText: string;
|
||||
};
|
||||
|
||||
// Imprint Page
|
||||
imprint: {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
companyInfoTitle: string;
|
||||
contactTitle: string;
|
||||
responsibilityTitle: string;
|
||||
disclaimerTitle: string;
|
||||
contentLiabilityTitle: string;
|
||||
contentLiabilityText: string[];
|
||||
copyrightTitle: string;
|
||||
copyrightText: string[];
|
||||
privacyTitle: string;
|
||||
privacyText: string;
|
||||
detailedPrivacyPolicy: {
|
||||
title: string;
|
||||
introduction: string;
|
||||
responsibleTitle: string;
|
||||
responsibleText: string;
|
||||
responsibleContact: string;
|
||||
dataProtectionOfficerTitle: string;
|
||||
dataProtectionOfficerText: string;
|
||||
rightsTitle: string;
|
||||
rightsIntro: string;
|
||||
rights: Array<{ title: string; text: string; }>;
|
||||
revocationTitle: string;
|
||||
revocationText: string;
|
||||
objectionTitle: string;
|
||||
objectionText: string;
|
||||
objectionHighlight: string;
|
||||
objectionContact: string;
|
||||
dataDeletionTitle: string;
|
||||
dataDeletionIntro: string;
|
||||
dataDeletionReasons: string[];
|
||||
retentionText: string;
|
||||
webhostingTitle: string;
|
||||
webhostingText: string;
|
||||
webhostingDataTypes: string[];
|
||||
webhostingPurpose: string;
|
||||
webhostingDataCategories: {
|
||||
affectedData: string;
|
||||
affectedDataList: string[];
|
||||
affectedPersons: string;
|
||||
processingPurpose: string;
|
||||
legalBasis: string;
|
||||
provider: string;
|
||||
};
|
||||
hostingProvider: {
|
||||
name: string;
|
||||
address: string;
|
||||
website: string;
|
||||
};
|
||||
contactTitle: string;
|
||||
contactText: string;
|
||||
contactDataCategories: {
|
||||
affectedData: string[];
|
||||
affectedPersons: string;
|
||||
processingPurpose: string;
|
||||
legalBasis: string;
|
||||
};
|
||||
securityTitle: string;
|
||||
securityText: string;
|
||||
changesTitle: string;
|
||||
changesText: string;
|
||||
disclaimer: string;
|
||||
};
|
||||
address: {
|
||||
street: string;
|
||||
city: string;
|
||||
country: string;
|
||||
};
|
||||
contact: {
|
||||
phone: string;
|
||||
};
|
||||
};
|
||||
|
||||
// Privacy Policy Page
|
||||
privacyPolicy: {
|
||||
title: string;
|
||||
lastUpdated: string;
|
||||
introTitle: string;
|
||||
introText: string;
|
||||
dataCollectionTitle: string;
|
||||
dataCollectionText: string;
|
||||
dataCollectionList: string[];
|
||||
webhostingTitle: string;
|
||||
webhostingText: string;
|
||||
webhostingProvider: string;
|
||||
webhostingProviderName: string;
|
||||
webhostingProviderAddress: string;
|
||||
webhostingProviderWebsite: string;
|
||||
rightsTitle: string;
|
||||
rightsText: string;
|
||||
rightsList: string[];
|
||||
contactTitle: string;
|
||||
contactText: string;
|
||||
contactEmail: string;
|
||||
};
|
||||
|
||||
// Accessibility and Navigation
|
||||
accessibility: {
|
||||
skipLinks: {
|
||||
skipToHero: string;
|
||||
skipToAbout: string;
|
||||
skipToServices: string;
|
||||
skipToSkills: string;
|
||||
skipToProjects: string;
|
||||
skipToCertifications: string;
|
||||
skipToContact: string;
|
||||
};
|
||||
navigation: {
|
||||
keyboardHelp: string;
|
||||
keyboardHelpExpanded: string[];
|
||||
useTabToNavigate: string;
|
||||
useTabToNavigateCards: string;
|
||||
useTabToNavigateOptions: string;
|
||||
closeKeyboardHelp: string;
|
||||
};
|
||||
screenReader: {
|
||||
professionalStatistics: string;
|
||||
projectCard: string;
|
||||
viewSourceCode: string;
|
||||
viewLiveDemo: string;
|
||||
downloadResume: string;
|
||||
downloadResumeDescription: string;
|
||||
keyboardNavigationHelp: string;
|
||||
themeSwitchButton: string;
|
||||
switchToTheme: string;
|
||||
certificationCard: string;
|
||||
serviceCard: string;
|
||||
skillBadge: string;
|
||||
};
|
||||
ariaLabels: {
|
||||
mainNavigation: string;
|
||||
socialLinks: string;
|
||||
themeToggle: string;
|
||||
mobileMenuToggle: string;
|
||||
contactForm: string;
|
||||
projectsGrid: string;
|
||||
skillsGrid: string;
|
||||
certificationsGrid: string;
|
||||
servicesGrid: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -18,4 +18,6 @@ export const contact = {
|
|||
'Hello Sascha,\n\nI visited your portfolio and would like to discuss a potential project or collaboration.\n\nBest regards,',
|
||||
emailAnnouncement: 'Opening email client with pre-filled message',
|
||||
socialAnnouncement: 'Opening {platform} profile in new tab',
|
||||
githubLinkText: 'Sascha Bach on GitHub',
|
||||
linkedinLinkText: 'Sascha Bach on LinkedIn',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
export const footer = {
|
||||
imprintText: 'Impressum und Datenschutz',
|
||||
copyrightText: 'All rights reserved.',
|
||||
technicalLinkText: 'Technical Portfolio',
|
||||
landingLinkText: 'Landing Page',
|
||||
githubAriaLabel: 'GitHub Profile',
|
||||
linkedinAriaLabel: 'LinkedIn Profile',
|
||||
emailAriaLabel: 'Send Email',
|
||||
|
|
|
|||
|
|
@ -3,6 +3,12 @@ import { themeToggle } from '../../theme-toggle';
|
|||
import { footer } from './footer';
|
||||
import { hero } from './hero';
|
||||
import { about } from './about';
|
||||
import { landingAboutMe } from './landing-aboutme';
|
||||
import { landingReasons } from './landing-reasons';
|
||||
import { landingExperience } from './landing-experience';
|
||||
import { landingWinnings } from './landing-winnings';
|
||||
import { landingProcesses } from './landing-processes';
|
||||
import { landingReferences } from './landing-references';
|
||||
import { services } from './services';
|
||||
import { skills } from './skills';
|
||||
import { projects } from './projects';
|
||||
|
|
@ -11,270 +17,9 @@ import { contact } from './contact';
|
|||
import { imprint } from './imprint';
|
||||
import { privacyPolicy } from './privacy-policy';
|
||||
import { accessibility } from './accessibility';
|
||||
import type { MenuItem } from '../../../data/types';
|
||||
import type { TextConfig } from './TextConfig';
|
||||
|
||||
// TypeScript interfaces for type safety
|
||||
export interface TextConfig {
|
||||
// Navigation
|
||||
navigation: {
|
||||
menuItems: MenuItem[];
|
||||
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;
|
||||
tertiaryButtonText: string;
|
||||
statItems: Array<{
|
||||
label: string;
|
||||
value: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
// About Section
|
||||
about: {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
name: string;
|
||||
bio: string[];
|
||||
featureCards: Array<{
|
||||
title: string;
|
||||
description: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
// Services Section
|
||||
services: {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
serviceItems: Array<{
|
||||
title: string;
|
||||
description: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
// Skills Section
|
||||
skills: {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
skillCategories: Array<{
|
||||
title: string;
|
||||
skills: Array<{
|
||||
skill: string;
|
||||
value: number;
|
||||
}>;
|
||||
}>;
|
||||
};
|
||||
|
||||
// Projects Section
|
||||
projects: {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
codeButtonText: string;
|
||||
liveButtonText: string;
|
||||
projectItems: Array<{
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
// Certifications Section
|
||||
certifications: {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
certificationItems: Array<{
|
||||
name: string;
|
||||
issuer: 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;
|
||||
emailSubject: string;
|
||||
emailBody: string;
|
||||
emailAnnouncement: string;
|
||||
socialAnnouncement: string;
|
||||
};
|
||||
|
||||
// Imprint Page
|
||||
imprint: {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
companyInfoTitle: string;
|
||||
contactTitle: string;
|
||||
responsibilityTitle: string;
|
||||
disclaimerTitle: string;
|
||||
contentLiabilityTitle: string;
|
||||
contentLiabilityText: string[];
|
||||
copyrightTitle: string;
|
||||
copyrightText: string[];
|
||||
privacyTitle: string;
|
||||
privacyText: string;
|
||||
detailedPrivacyPolicy: {
|
||||
title: string;
|
||||
introduction: string;
|
||||
responsibleTitle: string;
|
||||
responsibleText: string;
|
||||
responsibleContact: string;
|
||||
dataProtectionOfficerTitle: string;
|
||||
dataProtectionOfficerText: string;
|
||||
rightsTitle: string;
|
||||
rightsIntro: string;
|
||||
rights: Array<{ title: string; text: string; }>;
|
||||
revocationTitle: string;
|
||||
revocationText: string;
|
||||
objectionTitle: string;
|
||||
objectionText: string;
|
||||
objectionHighlight: string;
|
||||
objectionContact: string;
|
||||
dataDeletionTitle: string;
|
||||
dataDeletionIntro: string;
|
||||
dataDeletionReasons: string[];
|
||||
retentionText: string;
|
||||
webhostingTitle: string;
|
||||
webhostingText: string;
|
||||
webhostingDataTypes: string[];
|
||||
webhostingPurpose: string;
|
||||
webhostingDataCategories: {
|
||||
affectedData: string;
|
||||
affectedDataList: string[];
|
||||
affectedPersons: string;
|
||||
processingPurpose: string;
|
||||
legalBasis: string;
|
||||
provider: string;
|
||||
};
|
||||
hostingProvider: {
|
||||
name: string;
|
||||
address: string;
|
||||
website: string;
|
||||
};
|
||||
contactTitle: string;
|
||||
contactText: string;
|
||||
contactDataCategories: {
|
||||
affectedData: string[];
|
||||
affectedPersons: string;
|
||||
processingPurpose: string;
|
||||
legalBasis: string;
|
||||
};
|
||||
securityTitle: string;
|
||||
securityText: string;
|
||||
changesTitle: string;
|
||||
changesText: string;
|
||||
disclaimer: string;
|
||||
};
|
||||
address: {
|
||||
street: string;
|
||||
city: string;
|
||||
country: string;
|
||||
};
|
||||
contact: {
|
||||
phone: string;
|
||||
};
|
||||
};
|
||||
|
||||
// Privacy Policy Page
|
||||
privacyPolicy: {
|
||||
title: string;
|
||||
lastUpdated: string;
|
||||
introTitle: string;
|
||||
introText: string;
|
||||
dataCollectionTitle: string;
|
||||
dataCollectionText: string;
|
||||
dataCollectionList: string[];
|
||||
webhostingTitle: string;
|
||||
webhostingText: string;
|
||||
webhostingProvider: string;
|
||||
webhostingProviderName: string;
|
||||
webhostingProviderAddress: string;
|
||||
webhostingProviderWebsite: string;
|
||||
rightsTitle: string;
|
||||
rightsText: string;
|
||||
rightsList: string[];
|
||||
contactTitle: string;
|
||||
contactText: string;
|
||||
contactEmail: string;
|
||||
};
|
||||
|
||||
// Accessibility and Navigation
|
||||
accessibility: {
|
||||
skipLinks: {
|
||||
skipToHero: string;
|
||||
skipToAbout: string;
|
||||
skipToServices: string;
|
||||
skipToSkills: string;
|
||||
skipToProjects: string;
|
||||
skipToCertifications: string;
|
||||
skipToContact: string;
|
||||
};
|
||||
navigation: {
|
||||
keyboardHelp: string;
|
||||
keyboardHelpExpanded: string[];
|
||||
useTabToNavigate: string;
|
||||
useTabToNavigateCards: string;
|
||||
useTabToNavigateOptions: string;
|
||||
closeKeyboardHelp: string;
|
||||
};
|
||||
screenReader: {
|
||||
professionalStatistics: string;
|
||||
projectCard: string;
|
||||
viewSourceCode: string;
|
||||
viewLiveDemo: string;
|
||||
downloadResume: string;
|
||||
downloadResumeDescription: string;
|
||||
keyboardNavigationHelp: string;
|
||||
themeSwitchButton: string;
|
||||
switchToTheme: string;
|
||||
certificationCard: string;
|
||||
serviceCard: string;
|
||||
skillBadge: string;
|
||||
};
|
||||
ariaLabels: {
|
||||
mainNavigation: string;
|
||||
socialLinks: string;
|
||||
themeToggle: string;
|
||||
mobileMenuToggle: string;
|
||||
contactForm: string;
|
||||
projectsGrid: string;
|
||||
skillsGrid: string;
|
||||
certificationsGrid: string;
|
||||
servicesGrid: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
export type { TextConfig } from './TextConfig';
|
||||
|
||||
// English text configuration
|
||||
export const en: TextConfig = {
|
||||
|
|
@ -283,6 +28,12 @@ export const en: TextConfig = {
|
|||
footer,
|
||||
hero,
|
||||
about,
|
||||
landingAboutMe,
|
||||
landingReasons,
|
||||
landingExperience,
|
||||
landingWinnings,
|
||||
landingProcesses,
|
||||
landingReferences,
|
||||
services,
|
||||
skills,
|
||||
projects,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
export const landingAboutMe = {
|
||||
title: 'More Clients Through Accessibility',
|
||||
subtitle: '',
|
||||
greeting: 'Hi, I\'m Sascha.',
|
||||
name: 'Sascha',
|
||||
bio: [
|
||||
'I help businesses build websites that can be used by everyone. A website that works for all reaches all.',
|
||||
],
|
||||
featureCards: [] as Array<{ title: string; description: string; }>,
|
||||
};
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
export const landingExperience = {
|
||||
title: 'My Experience',
|
||||
paragraphs: [
|
||||
"I've helped businesses like yours — from small shops to service providers — make their websites accessible.",
|
||||
"At ORWO, I built a photo book designer that worked for thousands of customers, even those with visual impairments. I didn't just make it look good, I made it work for everyone.",
|
||||
'Now, I bring that same care to your website, whether you are a bakery, a therapist, a craftsman, or a local shop.',
|
||||
'You do not need to be big. You just need to be accessible.',
|
||||
],
|
||||
};
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
export const landingProcesses = {
|
||||
title: 'How I Work',
|
||||
intro: "I'm your partner.",
|
||||
items: [
|
||||
{ text: 'I work mostly remotely - less meetings, less commute, less overhead.' },
|
||||
{ text: 'We agree on a schedule - I deliver.' },
|
||||
{ text: 'I use my own tools - no setup required on your side.' },
|
||||
{ text: "I'm flexible - whether you need a quick fix or a full redesign." },
|
||||
],
|
||||
outro: 'You get my full focus.',
|
||||
};
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
export const landingReasons = {
|
||||
title: 'Why it is important',
|
||||
reasons: [
|
||||
{
|
||||
stat: '15 %',
|
||||
text: 'of all people in Germany have a disability. That is 12 million potential customers.',
|
||||
},
|
||||
{
|
||||
stat: '80 %',
|
||||
text: 'of those people leave websites that do not work for them.',
|
||||
},
|
||||
{
|
||||
stat: 'SEO',
|
||||
text: 'Google rewards accessible websites with better rankings, more traffic, and more sales.',
|
||||
},
|
||||
{
|
||||
stat: 'Trust',
|
||||
text: 'Customers trust accessible websites more. They feel respected, not ignored.',
|
||||
},
|
||||
],
|
||||
outro:
|
||||
'You are not just doing the right thing. You are making a difference.',
|
||||
};
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
export const landingReferences = {
|
||||
title: 'What Clients and Colleagues Say',
|
||||
items: [
|
||||
{
|
||||
referenceText:
|
||||
'The digital accessibility audit was extremely insightful and practical. The identified weaknesses were explained clearly and in an understandable way, making it possible to derive concrete improvements. Particularly noteworthy is the holistic view of usability and inclusion. Working together is highly recommended for anyone who wants to make their website sustainably accessible.',
|
||||
name: 'Florian Adragna',
|
||||
position: 'Freelancer, Florian Lucas Adragna',
|
||||
},
|
||||
{
|
||||
referenceText:
|
||||
'I always greatly appreciated working with you. You were particularly fast and reliable when it came to finding and fixing bugs. You also picked up new topics quickly and without any issues. Working with you was pleasant both professionally and personally — I am very happy to recommend you.',
|
||||
name: 'Colleague',
|
||||
position: 'Team Lead, ORWO Net GmbH',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
export const landingWinnings = {
|
||||
title: 'What You Get',
|
||||
intro: 'When you work with me, you get:',
|
||||
items: [
|
||||
{ text: 'A website that works for everyone, no matter how they browse.' },
|
||||
{ text: 'More visitors, because Google loves accessible sites.' },
|
||||
{ text: 'More sales, because people buy when they feel included.' },
|
||||
{ text: 'More trust, because your customers feel respected.' },
|
||||
{ text: 'Peace of mind, because I handle everything from testing to legal compliance.' },
|
||||
{ text: 'No tech stress, because I explain things in plain language with no jargon.' },
|
||||
],
|
||||
outro: 'I make sure I understand your issue and my solution fits.',
|
||||
};
|
||||
|
|
@ -7,5 +7,14 @@ export const navigation = {
|
|||
{ section: 'Projects', label: 'Projects' },
|
||||
{ section: 'Contact', label: 'Contact' },
|
||||
],
|
||||
landingMenuItems: [
|
||||
{ section: 'About', label: 'About' },
|
||||
{ section: 'Winnings', label: 'Benefits' },
|
||||
{ section: 'Experience', label: 'Experience' },
|
||||
{ section: 'Processes', label: 'How I Work' },
|
||||
{ section: 'Projects', label: 'Projects' },
|
||||
{ section: 'References', label: 'References' },
|
||||
{ section: 'Contact', label: 'Contact' },
|
||||
],
|
||||
mobileMenuAriaLabel: 'Menü öffnen',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
import { useEffect, useRef } from 'react';
|
||||
|
||||
/**
|
||||
* Observes all `.reveal-item` descendants of the returned ref and adds
|
||||
* `.is-visible` when they enter the viewport, triggering the CSS transition
|
||||
* defined in globals.scss.
|
||||
*
|
||||
* The observer only starts after the first user interaction (scroll, keydown,
|
||||
* or pointer event), so elements already in the viewport on load stay hidden
|
||||
* until the user actually engages with the page.
|
||||
*/
|
||||
export function useScrollReveal() {
|
||||
const ref = useRef<HTMLElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const container = ref.current;
|
||||
if (!container) return;
|
||||
|
||||
let observer: IntersectionObserver | null = null;
|
||||
|
||||
const startObserving = () => {
|
||||
if (observer) return; // already started
|
||||
|
||||
observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('is-visible');
|
||||
observer!.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.12 }
|
||||
);
|
||||
|
||||
const items = container.querySelectorAll('.reveal-item');
|
||||
items.forEach((el) => observer!.observe(el));
|
||||
|
||||
// Clean up interaction listeners once started
|
||||
INTERACTION_EVENTS.forEach((event) =>
|
||||
window.removeEventListener(event, startObserving, { capture: true })
|
||||
);
|
||||
};
|
||||
|
||||
const INTERACTION_EVENTS = ['scroll', 'keydown', 'pointerdown', 'touchstart'] as const;
|
||||
INTERACTION_EVENTS.forEach((event) =>
|
||||
window.addEventListener(event, startObserving, { once: false, capture: true, passive: true })
|
||||
);
|
||||
|
||||
return () => {
|
||||
observer?.disconnect();
|
||||
INTERACTION_EVENTS.forEach((event) =>
|
||||
window.removeEventListener(event, startObserving, { capture: true })
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return ref;
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import LandingAboutMeSection from '../components/landing/LandingAboutMeSection';
|
||||
import ReasonsSection from '../components/landing/ReasonsSection';
|
||||
import ExperienceSection from '../components/landing/ExperienceSection';
|
||||
import WinningsSection from '../components/landing/WinningsSection';
|
||||
import ProcessesSection from '../components/landing/ProcessesSection';
|
||||
import ProjectsSection from '../components/sections/ProjectsSection';
|
||||
import ReferencesSection from '../components/landing/ReferencesSection';
|
||||
import ContactSection from '../components/sections/ContactSection';
|
||||
|
||||
export default function LandingPage() {
|
||||
return (
|
||||
<>
|
||||
<LandingAboutMeSection />
|
||||
<ReasonsSection />
|
||||
<WinningsSection />
|
||||
<ExperienceSection />
|
||||
<ProcessesSection />
|
||||
<ProjectsSection />
|
||||
<ReferencesSection />
|
||||
<ContactSection />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -22,14 +22,147 @@
|
|||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
// Standard Theme (Light)
|
||||
// Standard Theme (Light) + design system tokens
|
||||
:root {
|
||||
@include t.theme-vars(t.$light-theme);
|
||||
|
||||
// Spacing scale (multiples of 4px)
|
||||
--space-1: 0.25rem;
|
||||
--space-2: 0.5rem;
|
||||
--space-3: 0.75rem;
|
||||
--space-4: 1rem;
|
||||
--space-5: 1.25rem;
|
||||
--space-6: 1.5rem;
|
||||
--space-8: 2rem;
|
||||
--space-10: 2.5rem;
|
||||
--space-12: 3rem;
|
||||
--space-16: 4rem;
|
||||
--space-20: 5rem;
|
||||
--space-24: 6rem;
|
||||
|
||||
// Typography scale
|
||||
--font-size-xs: 0.75rem;
|
||||
--font-size-sm: 0.875rem;
|
||||
--font-size-base: 1rem;
|
||||
--font-size-lg: 1.125rem;
|
||||
--font-size-xl: 1.25rem;
|
||||
--font-size-2xl: 1.5rem;
|
||||
--font-size-3xl: 1.875rem;
|
||||
--font-size-4xl: 2.25rem;
|
||||
|
||||
// Line heights
|
||||
--leading-tight: 1.2;
|
||||
--leading-snug: 1.375;
|
||||
--leading-normal: 1.5;
|
||||
--leading-relaxed: 1.625;
|
||||
--leading-loose: 1.75;
|
||||
|
||||
// Letter spacing
|
||||
--tracking-tight: -0.025em;
|
||||
--tracking-normal: 0em;
|
||||
--tracking-wide: 0.05em;
|
||||
--tracking-wider: 0.1em;
|
||||
|
||||
// Border radius
|
||||
--radius-sm: 0.375rem;
|
||||
--radius-md: 0.5rem;
|
||||
--radius-lg: 0.75rem;
|
||||
--radius-xl: 1rem;
|
||||
|
||||
// Shadows (light)
|
||||
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||
--shadow-hover: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
|
||||
// Transitions
|
||||
--transition-fast: 150ms ease;
|
||||
--transition-base: 200ms ease;
|
||||
--transition-slow: 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
// Landing section background gradients (light) – vivid & saturated
|
||||
--bg-reasons: linear-gradient(160deg, #dbeafe 0%, #ede9fe 100%);
|
||||
--bg-winnings: linear-gradient(160deg, #ede9fe 0%, #e0e7ff 100%);
|
||||
--bg-experience: linear-gradient(160deg, #e0f2fe 0%, #ede9fe 100%);
|
||||
--bg-processes: linear-gradient(160deg, #e0e7ff 0%, #dbeafe 100%);
|
||||
--bg-references: linear-gradient(160deg, #ede9fe 0%, #dbeafe 100%);
|
||||
|
||||
// Reused section backgrounds (light)
|
||||
--about-background: linear-gradient(160deg, #f0f9ff 0%, #dbeafe 100%);
|
||||
--projects-background: linear-gradient(160deg, #dbeafe 0%, #ede9fe 100%);
|
||||
--contact-background: linear-gradient(160deg, #ede9fe 0%, #e0f2fe 100%);
|
||||
|
||||
// Tech section backgrounds (light)
|
||||
--hero-background: linear-gradient(
|
||||
160deg,
|
||||
#f8faff 0%,
|
||||
#f1f3ff 50%,
|
||||
#f5f0ff 100%
|
||||
);
|
||||
--services-background: linear-gradient(160deg, #ede9fe 0%, #dbeafe 100%);
|
||||
--skills-background: linear-gradient(160deg, #dbeafe 0%, #e0f2fe 100%);
|
||||
--certifications-background: linear-gradient(
|
||||
160deg,
|
||||
#e0e7ff 0%,
|
||||
#ede9fe 100%
|
||||
);
|
||||
|
||||
// Glassmorphism card tokens (light)
|
||||
--card-glass-bg: rgba(255, 255, 255, 0.65);
|
||||
--card-glass-border: rgba(255, 255, 255, 0.9);
|
||||
|
||||
// Glow shadows (light)
|
||||
--shadow-glow:
|
||||
0 0 0 1px rgba(99, 102, 241, 0.12), 0 8px 24px rgba(99, 102, 241, 0.1);
|
||||
--shadow-glow-hover:
|
||||
0 0 0 1px rgba(99, 102, 241, 0.28), 0 16px 48px rgba(99, 102, 241, 0.2);
|
||||
}
|
||||
|
||||
// Dark Theme aktiv, wenn `data-theme="dark"` am html oder body
|
||||
// Dark theme + shadow overrides
|
||||
[data-theme='dark'] {
|
||||
@include t.theme-vars(t.$dark-theme);
|
||||
|
||||
--shadow-xs: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.35);
|
||||
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.4);
|
||||
--shadow-hover: 0 8px 24px rgba(0, 0, 0, 0.5);
|
||||
|
||||
// Landing section backgrounds (dark) – cinematic deep
|
||||
--bg-reasons: linear-gradient(160deg, #0f172a 0%, #1a0f2e 100%);
|
||||
--bg-winnings: linear-gradient(160deg, #1a0f2e 0%, #0d1a2e 100%);
|
||||
--bg-experience: linear-gradient(160deg, #0d1a2e 0%, #0f172a 100%);
|
||||
--bg-processes: linear-gradient(160deg, #0f172a 0%, #190b2e 100%);
|
||||
--bg-references: linear-gradient(160deg, #190b2e 0%, #0f172a 100%);
|
||||
|
||||
// Reused section backgrounds (dark)
|
||||
--about-background: linear-gradient(160deg, #111827 0%, #0f172a 100%);
|
||||
--projects-background: linear-gradient(160deg, #0f172a 0%, #1a0f2e 100%);
|
||||
--contact-background: linear-gradient(160deg, #1a0f2e 0%, #0d1a2e 100%);
|
||||
|
||||
// Tech section backgrounds (dark) – full cinematic
|
||||
--hero-background: linear-gradient(
|
||||
160deg,
|
||||
#06080f 0%,
|
||||
#0c1220 50%,
|
||||
#120720 100%
|
||||
);
|
||||
--services-background: linear-gradient(160deg, #190b2e 0%, #0f172a 100%);
|
||||
--skills-background: linear-gradient(160deg, #0f172a 0%, #0d1a2e 100%);
|
||||
--certifications-background: linear-gradient(
|
||||
160deg,
|
||||
#0d1a2e 0%,
|
||||
#190b2e 100%
|
||||
);
|
||||
|
||||
// Glassmorphism card tokens (dark)
|
||||
--card-glass-bg: rgba(139, 92, 246, 0.06);
|
||||
--card-glass-border: rgba(139, 92, 246, 0.22);
|
||||
|
||||
// Glow shadows (dark)
|
||||
--shadow-glow:
|
||||
0 0 0 1px rgba(139, 92, 246, 0.2), 0 8px 32px rgba(139, 92, 246, 0.18);
|
||||
--shadow-glow-hover:
|
||||
0 0 0 1px rgba(139, 92, 246, 0.45), 0 16px 56px rgba(139, 92, 246, 0.32);
|
||||
}
|
||||
|
||||
body {
|
||||
|
|
@ -48,7 +181,6 @@ body {
|
|||
}
|
||||
|
||||
section {
|
||||
min-height: 100vh;
|
||||
padding: 3rem 1rem;
|
||||
margin: 0; // Remove any section margins
|
||||
}
|
||||
|
|
|
|||
|
|
@ -203,3 +203,21 @@ textarea,
|
|||
cursor: pointer;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
// ── Scroll reveal animation ───────────────────────────────────────────────────
|
||||
// Add class `reveal-item` to any element you want to animate in on scroll.
|
||||
// The hook useScrollReveal() adds `is-visible` when the element enters view.
|
||||
// Use `--reveal-delay` CSS custom property for staggered children.
|
||||
.reveal-item {
|
||||
opacity: 0;
|
||||
transform: translateY(28px);
|
||||
transition:
|
||||
opacity 0.55s ease,
|
||||
transform 0.55s ease;
|
||||
transition-delay: var(--reveal-delay, 0ms);
|
||||
|
||||
&.is-visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -148,8 +148,22 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Landing page has 7 menu items — switch to burger at a wider breakpoint
|
||||
@media (max-width: 1023px) {
|
||||
.navbar--landing .navbar__container__button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@include globals.desktop-only {
|
||||
.navbar__burger-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Keep burger visible for landing page up to 1023px
|
||||
@media (min-width: 769px) and (max-width: 1023px) {
|
||||
.navbar--landing .navbar__burger-button {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
// Section imports - organized and easy to manage
|
||||
@forward 'hero-section';
|
||||
@forward 'about-section';
|
||||
@forward 'reasons-section';
|
||||
@forward 'experience-section';
|
||||
@forward 'winnings-section';
|
||||
@forward 'processes-section';
|
||||
@forward 'references-section';
|
||||
@forward 'services-section';
|
||||
@forward 'skills-section';
|
||||
@forward 'certifications-section';
|
||||
|
|
|
|||
|
|
@ -4,6 +4,23 @@
|
|||
|
||||
.about-section {
|
||||
background: var(--about-background);
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
var(--color-primary) 30%,
|
||||
var(--color-secondary) 70%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
&__container {
|
||||
@include section-container;
|
||||
|
|
@ -15,6 +32,13 @@
|
|||
|
||||
&__title {
|
||||
@include section-title;
|
||||
font-size: clamp(2rem, 5vw, 2.75rem);
|
||||
font-weight: 800;
|
||||
background: var(--gradient-text);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
|
|
@ -113,8 +137,8 @@
|
|||
transition: $transition-base;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px var(--box-shadow-hover);
|
||||
transform: translateY(-2px) scale(1.05);
|
||||
box-shadow: var(--shadow-glow);
|
||||
}
|
||||
|
||||
&--primary {
|
||||
|
|
@ -161,14 +185,14 @@
|
|||
border: none;
|
||||
border-radius: $card-border-radius-sm;
|
||||
box-shadow: $shadow-card-lg;
|
||||
transition: $transition-base;
|
||||
transition: var(--transition-slow);
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-glow-hover);
|
||||
transform: translateY(-4px) scale(1.02);
|
||||
}
|
||||
|
||||
// Enhanced focus state for accessibility
|
||||
|
|
|
|||
|
|
@ -4,6 +4,23 @@
|
|||
|
||||
.certifications-section {
|
||||
background: var(--certifications-background);
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
var(--color-primary) 30%,
|
||||
var(--color-secondary) 70%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
&__container {
|
||||
@include section-container;
|
||||
|
|
@ -15,6 +32,13 @@
|
|||
|
||||
&__title {
|
||||
@include section-title;
|
||||
font-size: clamp(2rem, 5vw, 2.75rem);
|
||||
font-weight: 800;
|
||||
background: var(--gradient-text);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
|
|
@ -41,8 +65,10 @@
|
|||
border-radius: $card-border-radius-md;
|
||||
box-shadow: var(--certifications-card-shadow);
|
||||
background: var(--certifications-card-background);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
padding: 1.5rem 1rem;
|
||||
transition: $transition-base;
|
||||
transition: var(--transition-slow);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
|
|
@ -63,8 +89,8 @@
|
|||
}
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-0.25rem);
|
||||
box-shadow: var(--certifications-card-shadow-hover);
|
||||
transform: translateY(-4px) scale(1.02);
|
||||
box-shadow: var(--shadow-glow-hover);
|
||||
|
||||
&::before {
|
||||
opacity: 1;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,23 @@
|
|||
|
||||
.contact-section {
|
||||
background: var(--contact-background);
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
var(--color-primary) 30%,
|
||||
var(--color-secondary) 70%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
&__container {
|
||||
@include section-container;
|
||||
|
|
@ -15,6 +32,13 @@
|
|||
|
||||
&__title {
|
||||
@include section-title;
|
||||
font-size: clamp(2rem, 5vw, 2.75rem);
|
||||
font-weight: 800;
|
||||
background: var(--gradient-text);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
|
|
@ -236,6 +260,26 @@
|
|||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
&__detail-link {
|
||||
color: var(--color-primary);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: transparent;
|
||||
text-underline-offset: 3px;
|
||||
transition: text-decoration-color var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
text-decoration-color: var(--color-primary);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid var(--color-focus-ring, #2563eb);
|
||||
outline-offset: 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&__social {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
|
|
@ -474,12 +518,14 @@
|
|||
border: 1px solid var(--form-border);
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||
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),
|
||||
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);
|
||||
}
|
||||
|
|
@ -514,7 +560,8 @@
|
|||
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),
|
||||
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;
|
||||
|
|
@ -539,7 +586,8 @@
|
|||
&:hover {
|
||||
background: var(--contact-button-hover-bg);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px 0 rgba(37, 99, 235, 0.35),
|
||||
box-shadow:
|
||||
0 8px 25px 0 rgba(37, 99, 235, 0.35),
|
||||
0 4px 6px -2px rgba(0, 0, 0, 0.1);
|
||||
|
||||
&::before {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,76 @@
|
|||
.experience-section {
|
||||
padding-block: var(--space-20);
|
||||
padding-inline: var(--space-4);
|
||||
background: var(--bg-experience);
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
var(--color-primary) 30%,
|
||||
var(--color-secondary) 70%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding-block: var(--space-12);
|
||||
}
|
||||
|
||||
&__container {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-8);
|
||||
}
|
||||
|
||||
// ── Title ─────────────────────────────────────────────────────
|
||||
&__title {
|
||||
font-size: clamp(2rem, 5vw, var(--font-size-4xl));
|
||||
font-weight: 800;
|
||||
letter-spacing: var(--tracking-tight);
|
||||
line-height: var(--leading-tight);
|
||||
background: var(--gradient-text);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
// ── Body ──────────────────────────────────────────────────────
|
||||
&__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-5);
|
||||
}
|
||||
|
||||
&__paragraph {
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--leading-loose);
|
||||
color: var(--color-text);
|
||||
margin: 0;
|
||||
|
||||
// ── Outro (last paragraph) ───────────────────────────────
|
||||
&--outro {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
line-height: var(--leading-snug);
|
||||
background: var(--card-glass-bg);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
padding: var(--space-5) var(--space-6);
|
||||
border-radius: var(--radius-xl);
|
||||
border: 1px solid var(--card-glass-border);
|
||||
border-left: 4px solid var(--color-primary);
|
||||
box-shadow: var(--shadow-glow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,30 @@
|
|||
@include flex-center;
|
||||
min-height: 100vh;
|
||||
background: var(--hero-background);
|
||||
position: relative;
|
||||
|
||||
// Subtle radial glow at center for cinematic depth
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(
|
||||
ellipse 80% 60% at 50% 40%,
|
||||
rgba(99, 102, 241, 0.12) 0%,
|
||||
transparent 70%
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
[data-theme='dark'] & {
|
||||
&::before {
|
||||
background: radial-gradient(
|
||||
ellipse 80% 60% at 50% 40%,
|
||||
rgba(139, 92, 246, 0.22) 0%,
|
||||
transparent 70%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
&__container {
|
||||
max-width: 800px;
|
||||
|
|
@ -29,18 +53,22 @@
|
|||
@include flex-center;
|
||||
flex-direction: column;
|
||||
gap: $spacing-xl;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 3rem;
|
||||
font-weight: bold;
|
||||
font-size: clamp(2.5rem, 7vw, 5rem);
|
||||
font-weight: 900;
|
||||
margin: 0;
|
||||
letter-spacing: -0.03em;
|
||||
line-height: 1.1;
|
||||
@include gradient-text(var(--gradient-text));
|
||||
}
|
||||
|
||||
&__description {
|
||||
font-size: 1.2rem;
|
||||
line-height: 1.6;
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.65;
|
||||
color: var(--color-text-muted);
|
||||
max-width: 600px;
|
||||
margin-left: auto;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,107 @@
|
|||
.processes-section {
|
||||
padding-block: var(--space-20);
|
||||
padding-inline: var(--space-4);
|
||||
background: var(--bg-processes);
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
var(--color-secondary) 30%,
|
||||
var(--color-primary) 70%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding-block: var(--space-12);
|
||||
}
|
||||
|
||||
&__container {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-8);
|
||||
}
|
||||
|
||||
// ── Title ─────────────────────────────────────────────────────
|
||||
&__title {
|
||||
font-size: clamp(2rem, 5vw, var(--font-size-4xl));
|
||||
font-weight: 800;
|
||||
letter-spacing: var(--tracking-tight);
|
||||
line-height: var(--leading-tight);
|
||||
background: var(--gradient-text);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
// ── Intro line ────────────────────────────────────────────────
|
||||
&__intro {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
line-height: var(--leading-normal);
|
||||
margin-top: calc(var(--space-4) * -1);
|
||||
}
|
||||
|
||||
// ── List ──────────────────────────────────────────────────────
|
||||
&__list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
&__item {
|
||||
background: var(--card-glass-bg);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid var(--card-glass-border);
|
||||
border-left: 3px solid var(--color-secondary);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-4) var(--space-5);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition:
|
||||
box-shadow var(--transition-slow),
|
||||
transform var(--transition-slow);
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--shadow-glow-hover);
|
||||
transform: translateY(-3px) scale(1.01);
|
||||
}
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--leading-loose);
|
||||
color: var(--color-text);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// ── Outro ─────────────────────────────────────────────────────
|
||||
&__outro {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
line-height: var(--leading-snug);
|
||||
background: var(--card-glass-bg);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
padding: var(--space-5) var(--space-6);
|
||||
border-radius: var(--radius-xl);
|
||||
border: 1px solid var(--card-glass-border);
|
||||
border-left: 4px solid var(--color-secondary);
|
||||
box-shadow: var(--shadow-glow);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,23 @@
|
|||
|
||||
.projects-section {
|
||||
background: var(--projects-background);
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
var(--color-secondary) 30%,
|
||||
var(--color-primary) 70%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
&__container {
|
||||
@include section-container;
|
||||
|
|
@ -15,6 +32,13 @@
|
|||
|
||||
&__title {
|
||||
@include section-title;
|
||||
font-size: clamp(2rem, 5vw, 2.75rem);
|
||||
font-weight: 800;
|
||||
background: var(--gradient-text);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,134 @@
|
|||
.reasons-section {
|
||||
padding-block: var(--space-20);
|
||||
padding-inline: var(--space-4);
|
||||
background: var(--bg-reasons);
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
var(--color-primary) 30%,
|
||||
var(--color-secondary) 70%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding-block: var(--space-12);
|
||||
}
|
||||
|
||||
&__container {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-8);
|
||||
}
|
||||
|
||||
// ── Title ─────────────────────────────────────────────────────
|
||||
&__title {
|
||||
font-size: clamp(2rem, 5vw, var(--font-size-4xl));
|
||||
font-weight: 800;
|
||||
letter-spacing: var(--tracking-tight);
|
||||
line-height: var(--leading-tight);
|
||||
background: var(--gradient-text);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
// ── Intro quote ───────────────────────────────────────────────
|
||||
&__intro {
|
||||
font-size: var(--font-size-lg);
|
||||
font-style: italic;
|
||||
color: var(--color-text-muted);
|
||||
line-height: var(--leading-relaxed);
|
||||
margin-top: calc(var(--space-4) * -1);
|
||||
}
|
||||
|
||||
// ── Transition line ───────────────────────────────────────────
|
||||
&__transition {
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
line-height: var(--leading-normal);
|
||||
margin-top: calc(var(--space-4) * -1);
|
||||
}
|
||||
|
||||
// ── List ──────────────────────────────────────────────────────
|
||||
&__list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
&__item {
|
||||
display: grid;
|
||||
grid-template-columns: 5.5rem 1fr;
|
||||
align-items: start;
|
||||
gap: var(--space-4);
|
||||
background: var(--card-glass-bg);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid var(--card-glass-border);
|
||||
border-left: 3px solid var(--color-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-4) var(--space-5);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition:
|
||||
box-shadow var(--transition-slow),
|
||||
transform var(--transition-slow);
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--shadow-glow-hover);
|
||||
transform: translateY(-3px) scale(1.01);
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
}
|
||||
|
||||
&__stat {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 800;
|
||||
color: var(--color-primary);
|
||||
line-height: var(--leading-normal);
|
||||
white-space: nowrap;
|
||||
letter-spacing: var(--tracking-tight);
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--leading-loose);
|
||||
color: var(--color-text);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// ── Outro ─────────────────────────────────────────────────────
|
||||
&__outro {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
line-height: var(--leading-snug);
|
||||
background: var(--card-glass-bg);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
padding: var(--space-5) var(--space-6);
|
||||
border-radius: var(--radius-xl);
|
||||
border: 1px solid var(--card-glass-border);
|
||||
border-left: 4px solid var(--color-primary);
|
||||
box-shadow: var(--shadow-glow);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
.references-section {
|
||||
padding-block: var(--space-20);
|
||||
padding-inline: var(--space-4);
|
||||
background: var(--bg-references);
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
var(--color-primary) 30%,
|
||||
var(--color-secondary) 70%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding-block: var(--space-12);
|
||||
}
|
||||
|
||||
&__container {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-8);
|
||||
}
|
||||
|
||||
// ── Title ─────────────────────────────────────────────────────
|
||||
&__title {
|
||||
font-size: clamp(2rem, 5vw, var(--font-size-4xl));
|
||||
font-weight: 800;
|
||||
letter-spacing: var(--tracking-tight);
|
||||
line-height: var(--leading-tight);
|
||||
background: var(--gradient-text);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
// ── List ──────────────────────────────────────────────────────
|
||||
&__list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-6);
|
||||
}
|
||||
|
||||
&__item {
|
||||
background: var(--card-glass-bg);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid var(--card-glass-border);
|
||||
border-left: 3px solid var(--color-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-6);
|
||||
box-shadow: var(--shadow-sm);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-5);
|
||||
transition:
|
||||
box-shadow var(--transition-slow),
|
||||
transform var(--transition-slow);
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--shadow-glow-hover);
|
||||
transform: translateY(-3px) scale(1.01);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Quote text ────────────────────────────────────────────────
|
||||
&__quote {
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--leading-loose);
|
||||
color: var(--color-text);
|
||||
margin: 0;
|
||||
font-style: italic;
|
||||
|
||||
&::before {
|
||||
content: '\201C';
|
||||
font-size: 2.2em;
|
||||
line-height: 0;
|
||||
vertical-align: -0.45em;
|
||||
margin-right: 0.15em;
|
||||
color: var(--color-primary);
|
||||
font-style: normal;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '\201D';
|
||||
font-size: 2.2em;
|
||||
line-height: 0;
|
||||
vertical-align: -0.45em;
|
||||
margin-left: 0.15em;
|
||||
color: var(--color-primary);
|
||||
font-style: normal;
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Author attribution ────────────────────────────────────────
|
||||
&__author {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-1);
|
||||
padding-top: var(--space-4);
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 700;
|
||||
letter-spacing: var(--tracking-wide);
|
||||
color: var(--color-text);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__position {
|
||||
font-size: var(--font-size-xs);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
color: var(--color-text-muted);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,23 @@
|
|||
align-items: center;
|
||||
padding: $section-padding-y 0;
|
||||
background: var(--services-background);
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
var(--color-primary) 30%,
|
||||
var(--color-secondary) 70%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
// Only apply min-height on larger screens where we have more space
|
||||
@media (min-width: 1024px) and (min-height: 800px) {
|
||||
|
|
@ -30,10 +47,13 @@
|
|||
|
||||
&__title {
|
||||
@include section-title;
|
||||
|
||||
@include mobile-only {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
font-size: clamp(2rem, 5vw, 2.75rem);
|
||||
font-weight: 800;
|
||||
background: var(--gradient-text);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
|
|
@ -84,16 +104,17 @@
|
|||
&__card {
|
||||
background: white;
|
||||
border-radius: $card-border-radius-md;
|
||||
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 cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: var(--transition-slow);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
min-height: 250px; // Ensures consistent card heights
|
||||
min-height: 250px;
|
||||
position: relative;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid var(--card-glass-border);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
animation: cardFadeIn 0.6s ease-out;
|
||||
|
||||
// Add subtle backdrop blur for depth
|
||||
|
|
@ -111,9 +132,7 @@
|
|||
|
||||
&:hover {
|
||||
transform: translateY(-8px) scale(1.02);
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.3);
|
||||
filter: brightness(1.05);
|
||||
box-shadow: var(--shadow-glow-hover);
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
|
|
@ -133,17 +152,16 @@
|
|||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
// Enhanced hover and focus interaction
|
||||
&:hover,
|
||||
&:focus {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: $shadow-card-hover;
|
||||
transform: translateY(-4px) scale(1.015);
|
||||
box-shadow: var(--shadow-glow-hover);
|
||||
|
||||
// When both focused and hovered
|
||||
&:focus {
|
||||
outline: 3px solid var(--color-focus-ring, #2563eb);
|
||||
outline-offset: 2px;
|
||||
transform: translateY(-6px); // Slightly more lift when focused
|
||||
transform: translateY(-6px) scale(1.015);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -229,7 +247,8 @@
|
|||
|
||||
// Dark theme enhancements
|
||||
@media (prefers-color-scheme: dark) {
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3),
|
||||
box-shadow:
|
||||
0 4px 6px -1px rgba(0, 0, 0, 0.3),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
|
|
@ -443,7 +462,8 @@
|
|||
@media (prefers-color-scheme: dark) {
|
||||
.services-section__card {
|
||||
// Enhance card visibility with better shadows in dark mode
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3),
|
||||
box-shadow:
|
||||
0 4px 6px -1px rgba(0, 0, 0, 0.3),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.2);
|
||||
animation: cardFadeIn 0.6s ease-out;
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,24 @@
|
|||
padding: 3rem 0;
|
||||
}
|
||||
|
||||
// Gradient separator line at top
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
var(--color-secondary) 30%,
|
||||
var(--color-primary) 70%,
|
||||
transparent 100%
|
||||
);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
// Add decorative background elements
|
||||
&::before {
|
||||
content: '';
|
||||
|
|
@ -42,10 +60,13 @@
|
|||
|
||||
&__title {
|
||||
@include section-title;
|
||||
|
||||
@include mobile-only {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
font-size: clamp(2rem, 5vw, 2.75rem);
|
||||
font-weight: 800;
|
||||
background: var(--gradient-text);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
|
|
@ -111,7 +132,8 @@
|
|||
|
||||
&:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1),
|
||||
box-shadow:
|
||||
0 20px 25px -5px rgba(0, 0, 0, 0.1),
|
||||
0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
|
||||
&::before {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,107 @@
|
|||
.winnings-section {
|
||||
padding-block: var(--space-20);
|
||||
padding-inline: var(--space-4);
|
||||
background: var(--bg-winnings);
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
var(--color-secondary) 30%,
|
||||
var(--color-primary) 70%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding-block: var(--space-12);
|
||||
}
|
||||
|
||||
&__container {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-8);
|
||||
}
|
||||
|
||||
// ── Title ─────────────────────────────────────────────────────
|
||||
&__title {
|
||||
font-size: clamp(2rem, 5vw, var(--font-size-4xl));
|
||||
font-weight: 800;
|
||||
letter-spacing: var(--tracking-tight);
|
||||
line-height: var(--leading-tight);
|
||||
background: var(--gradient-text);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
// ── Intro line ────────────────────────────────────────────────
|
||||
&__intro {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
line-height: var(--leading-normal);
|
||||
margin-top: calc(var(--space-4) * -1);
|
||||
}
|
||||
|
||||
// ── List ──────────────────────────────────────────────────────
|
||||
&__list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
&__item {
|
||||
background: var(--card-glass-bg);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid var(--card-glass-border);
|
||||
border-left: 3px solid var(--color-secondary);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-4) var(--space-5);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition:
|
||||
box-shadow var(--transition-slow),
|
||||
transform var(--transition-slow);
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--shadow-glow-hover);
|
||||
transform: translateY(-3px) scale(1.01);
|
||||
}
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--leading-loose);
|
||||
color: var(--color-text);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// ── Outro ─────────────────────────────────────────────────────
|
||||
&__outro {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
line-height: var(--leading-snug);
|
||||
background: var(--card-glass-bg);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
padding: var(--space-5) var(--space-6);
|
||||
border-radius: var(--radius-xl);
|
||||
border: 1px solid var(--card-glass-border);
|
||||
border-left: 4px solid var(--color-secondary);
|
||||
box-shadow: var(--shadow-glow);
|
||||
}
|
||||
}
|
||||
|
|
@ -54,6 +54,11 @@ $base-light-theme: (
|
|||
bg-primary: #ffffff,
|
||||
bg-secondary: #f9fafb,
|
||||
border-color: #e5e7eb,
|
||||
|
||||
// Design system semantic tokens
|
||||
surface: #f8fafc,
|
||||
border: #e5e7eb,
|
||||
secondary: #2563eb,
|
||||
);
|
||||
|
||||
$base-dark-theme: (
|
||||
|
|
@ -107,4 +112,11 @@ $base-dark-theme: (
|
|||
bg-primary: #1f2937,
|
||||
bg-secondary: #374151,
|
||||
border-color: #4b5563,
|
||||
|
||||
// Design system semantic tokens
|
||||
// surface is a subtle step above background — not a stark jump.
|
||||
// Kept close to background so dark sections feel like one canvas.
|
||||
surface: #232f3e,
|
||||
border: #374151,
|
||||
secondary: #60a5fa,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -61,6 +61,9 @@ $dark-theme: map.merge(
|
|||
--color-background: #{map.get($theme, background)};
|
||||
--color-text: #{map.get($theme, text)};
|
||||
--color-text-muted: #{map.get($theme, text-muted)};
|
||||
--color-surface: #{map.get($theme, surface)};
|
||||
--color-border: #{map.get($theme, border)};
|
||||
--color-secondary: #{map.get($theme, secondary)};
|
||||
--box-shadow-hover: #{map.get($theme, hover-box-shadow)};
|
||||
--box-shadow-active: #{map.get($theme, active-box-shadow)};
|
||||
--gradient-primary: #{map.get($theme, gradient-primary)};
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ export function scrollToSection(
|
|||
currentPath: string,
|
||||
navigate?: NavigateFunction
|
||||
): void {
|
||||
// If we're on the homepage, scroll directly to the section
|
||||
if (currentPath === '/') {
|
||||
const isScrollablePage = currentPath === '/technical' || currentPath === '/';
|
||||
if (isScrollablePage) {
|
||||
const element = document.getElementById(sectionId);
|
||||
if (element) {
|
||||
const elementPosition = element.getBoundingClientRect().top;
|
||||
|
|
@ -22,10 +22,10 @@ export function scrollToSection(
|
|||
}
|
||||
} else if (navigate) {
|
||||
// If we're on another page, navigate to homepage with hash
|
||||
navigate(`/#${sectionId}`);
|
||||
navigate(`/technical#${sectionId}`);
|
||||
} else {
|
||||
// Fallback: direct navigation without router
|
||||
globalThis.location.href = `/#${sectionId}`;
|
||||
globalThis.location.href = `/technical#${sectionId}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue