Refactor code structure for improved readability and maintainability
This commit is contained in:
parent
dfe535eb1b
commit
8deb151241
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue