Refactor code structure for improved readability and maintainability

This commit is contained in:
Sascha 2026-04-30 13:29:08 +02:00
parent dfe535eb1b
commit 8deb151241
9 changed files with 196 additions and 10 deletions

20
package-lock.json generated
View File

@ -20,6 +20,7 @@
"@types/react-dom": "^19.1.7", "@types/react-dom": "^19.1.7",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"@vitejs/plugin-react": "^4.7.0", "@vitejs/plugin-react": "^4.7.0",
"agentation": "^3.0.2",
"babel-plugin-react-compiler": "1.0.0", "babel-plugin-react-compiler": "1.0.0",
"dotenv": "^17.2.2", "dotenv": "^17.2.2",
"eslint": "^9.32.0", "eslint": "^9.32.0",
@ -2138,6 +2139,25 @@
"node": ">= 14" "node": ">= 14"
} }
}, },
"node_modules/agentation": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/agentation/-/agentation-3.0.2.tgz",
"integrity": "sha512-iGzBxFVTuZEIKzLY6AExSLAQH6i6SwxV4pAu7v7m3X6bInZ7qlZXAwrEqyc4+EfP4gM7z2RXBF6SF4DeH0f2lA==",
"dev": true,
"license": "PolyForm-Shield-1.0.0",
"peerDependencies": {
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/ajv": { "node_modules/ajv": {
"version": "6.12.6", "version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",

View File

@ -25,6 +25,7 @@
"@types/react-dom": "^19.1.7", "@types/react-dom": "^19.1.7",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"@vitejs/plugin-react": "^4.7.0", "@vitejs/plugin-react": "^4.7.0",
"agentation": "^3.0.2",
"babel-plugin-react-compiler": "1.0.0", "babel-plugin-react-compiler": "1.0.0",
"dotenv": "^17.2.2", "dotenv": "^17.2.2",
"eslint": "^9.32.0", "eslint": "^9.32.0",

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 70 KiB

View File

@ -1,8 +1,10 @@
import { useRef, useState, useEffect } from 'react';
import { Code, Palette, ShieldCheck, Eye } from 'lucide-react'; import { Code, Palette, ShieldCheck, Eye } from 'lucide-react';
import { useLanguage } from '../../contexts/LanguageContext'; import { useLanguage } from '../../contexts/LanguageContext';
import FeatureCardsGrid from '../FeatureCardsGrid'; import FeatureCardsGrid from '../FeatureCardsGrid';
import type { Service } from '../../data/Service'; import type { Service } from '../../data/Service';
import saschaImage from '../../assets/sascha.png'; import saschaImage from '../../assets/sascha.png';
import saschaPixel from '../../assets/sascha_pixel.svg';
// Define feature card icon and color mappings as constants // Define feature card icon and color mappings as constants
const FEATURE_CARD_ICONS = [Code, Palette, ShieldCheck, Eye] as const; const FEATURE_CARD_ICONS = [Code, Palette, ShieldCheck, Eye] as const;
@ -39,6 +41,26 @@ export default function AboutSection(props: AboutSectionProps = {}) {
})) }))
} = props; } = props;
const wrapperRef = useRef<HTMLDivElement>(null);
const [fading, setFading] = useState(false);
useEffect(() => {
const el = wrapperRef.current;
if (!el) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
const timer = setTimeout(() => setFading(true), 2000);
observer.disconnect();
return () => clearTimeout(timer);
}
},
{ threshold: 0.4 }
);
observer.observe(el);
return () => observer.disconnect();
}, []);
return ( return (
<section id="about" className="about-section" aria-labelledby="about-title"> <section id="about" className="about-section" aria-labelledby="about-title">
<div className="about-section__container"> <div className="about-section__container">
@ -63,7 +85,7 @@ export default function AboutSection(props: AboutSectionProps = {}) {
{/* Personal Photo and Bio */} {/* Personal Photo and Bio */}
<main className="about-section__content"> <main className="about-section__content">
<div className="about-section__image-wrapper"> <div className="about-section__image-wrapper" ref={wrapperRef}>
<img <img
src={profileImage} src={profileImage}
alt={`Professional headshot of ${name}, Web Developer and Software Engineer`} alt={`Professional headshot of ${name}, Web Developer and Software Engineer`}
@ -71,6 +93,16 @@ export default function AboutSection(props: AboutSectionProps = {}) {
loading="lazy" loading="lazy"
/> />
<div className="about-section__image-overlay" aria-hidden="true"></div> <div className="about-section__image-overlay" aria-hidden="true"></div>
<div
aria-hidden="true"
className={`about-section__pixel-bg${fading ? ' about-section__pixel-bg--fade' : ''}`}
/>
<img
src={saschaPixel}
alt=""
aria-hidden="true"
className={`about-section__pixel-overlay${fading ? ' about-section__pixel-overlay--fade' : ''}`}
/>
</div> </div>
<div className="about-section__bio-section"> <div className="about-section__bio-section">

View File

@ -1,3 +1,4 @@
import { useState, useEffect } from 'react';
import { Download } from 'lucide-react'; import { Download } from 'lucide-react';
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import type { StatItem } from '../../data/StatItem'; import type { StatItem } from '../../data/StatItem';
@ -41,6 +42,17 @@ export default function HeroSection(props: HeroSectionProps = {}) {
const navigate = useNavigate(); const navigate = useNavigate();
const { announce } = useScreenReaderAnnouncements(); const { announce } = useScreenReaderAnnouncements();
const [loaded, setLoaded] = useState(false);
useEffect(() => {
if (document.readyState === 'complete') {
setLoaded(true);
} else {
const onLoad = () => setLoaded(true);
window.addEventListener('load', onLoad);
return () => window.removeEventListener('load', onLoad);
}
}, []);
const handleViewWork = () => { const handleViewWork = () => {
announce('Navigating to projects section'); announce('Navigating to projects section');
scrollToProjects(location.pathname, navigate); scrollToProjects(location.pathname, navigate);
@ -72,7 +84,7 @@ export default function HeroSection(props: HeroSectionProps = {}) {
}; };
return ( return (
<main className="hero-section" aria-labelledby="hero-title"> <main className={`hero-section${loaded ? ' hero-section--loaded' : ''}`} aria-labelledby="hero-title">
<div className="hero-section__container"> <div className="hero-section__container">
{/* Skip link for keyboard navigation */} {/* Skip link for keyboard navigation */}
<div className="hero-section__skip"> <div className="hero-section__skip">

View File

@ -33,14 +33,20 @@
&__container { &__container {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; // Fluid gap: 0.25rem at 1050px 0.5rem at 1300px
gap: clamp(0.25rem, -0.8rem + 1.6vw, 0.5rem);
&__button { &__button {
@extend %button-reset; @extend %button-reset;
@extend %hover-lift; @extend %hover-lift;
padding: 0.6rem 1.2rem; // Fluid padding: 0.5rem 0.5rem at 1050px 0.6rem 1.2rem at 1300px
padding: clamp(0.5rem, 0.08rem + 0.64vw, 0.6rem)
clamp(0.5rem, -2.44rem + 4.48vw, 1.2rem);
border-radius: 8px; border-radius: 8px;
color: var(--color-text-on-dark); color: var(--color-text-on-dark);
white-space: nowrap;
// Fluid font-size: 0.85rem at 1050px 1rem at 1300px
font-size: clamp(0.85rem, 0.22rem + 0.96vw, 1rem);
transition: transition:
color 0.2s ease, color 0.2s ease,
box-shadow 0.2s ease, box-shadow 0.2s ease,
@ -113,6 +119,7 @@
top: 65px; top: 65px;
right: 0; right: 0;
background: var(--color-background); background: var(--color-background);
color: var(--color-text);
border: 1px solid var(--color-primary); border: 1px solid var(--color-primary);
border-radius: 8px; border-radius: 8px;
padding: 0; padding: 0;
@ -172,8 +179,8 @@
} }
} }
// Landing page has 7 menu items switch to burger at a wider breakpoint // Landing page has 7 menu items switch to burger at same breakpoint as regular nav
@media (max-width: 1023px) { @media (max-width: 1049px) {
.navbar--landing .navbar__container__button { .navbar--landing .navbar__container__button {
display: none; display: none;
} }
@ -185,8 +192,8 @@
} }
} }
// Keep burger visible for landing page up to 1023px // Keep burger visible for landing page up to 1049px
@media (min-width: 769px) and (max-width: 1023px) { @media (min-width: 769px) and (max-width: 1049px) {
.navbar--landing .navbar__burger-button { .navbar--landing .navbar__burger-button {
display: flex; display: flex;
} }

View File

@ -74,6 +74,50 @@
background: var(--gradient-image-overlay); background: var(--gradient-image-overlay);
} }
&__pixel-bg {
position: absolute;
inset: 0 0 0 -35px;
background-color: #dbeafe;
opacity: 1;
transition: opacity 1.2s ease-in-out;
will-change: opacity;
pointer-events: none;
[data-theme='dark'] & {
background-color: #1e3a8a;
}
&--fade {
opacity: 0;
}
@media (prefers-reduced-motion: reduce) {
display: none;
}
}
&__pixel-overlay {
position: absolute;
inset: 0 0 0 -35px;
width: calc(100% + 35px);
height: 100%;
object-fit: cover;
object-position: calc(30%) 50%;
border: 4px solid var(--color-background);
opacity: 1;
transition: opacity 1.2s ease-in-out;
will-change: opacity;
pointer-events: none;
&--fade {
opacity: 0;
}
@media (prefers-reduced-motion: reduce) {
display: none;
}
}
&__bio-section { &__bio-section {
flex: 1; flex: 1;
text-align: center; text-align: center;

View File

@ -17,6 +17,33 @@
} }
} }
@keyframes hero-fade-up {
from {
opacity: 0;
transform: translate3d(0, 20px, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
@keyframes hero-bubble-before {
0% { transform: scale(0.6) translate3d(0, 16px, 0); opacity: 0; }
40% { transform: scale(1.1) translate3d(0, -5px, 0); opacity: 1; }
65% { transform: scale(0.96) translate3d(0, 3px, 0); }
82% { transform: scale(1.03) translate3d(0, -1px, 0); }
100% { transform: scale(1) translate3d(0, 0, 0); opacity: 1; }
}
@keyframes hero-bubble-after {
0% { transform: scale(0.5) translate3d(0, -12px, 0); opacity: 0; }
45% { transform: scale(1.14) translate3d(0, 4px, 0); opacity: 1; }
68% { transform: scale(0.94) translate3d(0, -2px, 0); }
84% { transform: scale(1.04) translate3d(0, 1px, 0); }
100% { transform: scale(1) translate3d(0, 0, 0); opacity: 1; }
}
.hero-section { .hero-section {
@include flex-center; @include flex-center;
min-height: 100vh; min-height: 100vh;
@ -34,6 +61,7 @@
border-radius: 50%; border-radius: 50%;
border: 2px solid rgba(12, 68, 124, 0.15); border: 2px solid rgba(12, 68, 124, 0.15);
pointer-events: none; pointer-events: none;
will-change: transform, opacity;
} }
&::after { &::after {
@ -46,6 +74,7 @@
border-radius: 50%; border-radius: 50%;
background: rgba(239, 159, 39, 0.08); background: rgba(239, 159, 39, 0.08);
pointer-events: none; pointer-events: none;
will-change: transform, opacity;
} }
[data-theme='dark'] & { [data-theme='dark'] & {
@ -76,6 +105,7 @@
letter-spacing: -0.03em; letter-spacing: -0.03em;
line-height: 1.1; line-height: 1.1;
color: $color-heading; color: $color-heading;
will-change: transform, opacity;
} }
&__description { &__description {
@ -85,12 +115,14 @@
max-width: 600px; max-width: 600px;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
will-change: transform, opacity;
} }
&__buttons { &__buttons {
@include flex-center; @include flex-center;
gap: $spacing-lg; gap: $spacing-lg;
flex-wrap: wrap; flex-wrap: wrap;
will-change: transform, opacity;
} }
&__button { &__button {
@ -148,6 +180,7 @@
text-align: center; text-align: center;
padding: $spacing-md; padding: $spacing-md;
border-radius: $border-radius-md; border-radius: $border-radius-md;
will-change: transform, opacity;
&--1 { &--1 {
background: var(--stat-primary-bg); background: var(--stat-primary-bg);
@ -231,3 +264,37 @@
} }
} }
} }
// Animations fire only after the page has fully loaded (--loaded class added via JS)
@media (prefers-reduced-motion: no-preference) {
.hero-section--loaded {
&::before {
animation: hero-bubble-before 4s cubic-bezier(0.34, 1.2, 0.64, 1) both 0.2s;
}
&::after {
animation: hero-bubble-after 4s cubic-bezier(0.34, 1.2, 0.64, 1) both 0.7s;
}
.hero-section__title {
animation: hero-fade-up 0.6s ease-out both 0.2s;
}
.hero-section__description {
animation: hero-fade-up 0.6s ease-out both 0.35s;
}
.hero-section__buttons {
animation: hero-fade-up 0.6s ease-out both 0.5s;
}
.hero-section__stat-item {
animation: hero-fade-up 0.7s ease-out both;
&--1 { animation-delay: 0.7s; }
&--2 { animation-delay: 0.85s; }
&--3 { animation-delay: 1s; }
&--4 { animation-delay: 1.15s; }
}
}
}

View File

@ -1,6 +1,6 @@
// Breakpoint variables // Breakpoint variables
$mobile-breakpoint: 768px; $mobile-breakpoint: 1050px;
$desktop-breakpoint: 769px; $desktop-breakpoint: 1050px;
// Color variables // Color variables
$color-primary: #042c53; $color-primary: #042c53;