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-router-dom": "^5.3.3",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"agentation": "^3.0.2",
|
||||
"babel-plugin-react-compiler": "1.0.0",
|
||||
"dotenv": "^17.2.2",
|
||||
"eslint": "^9.32.0",
|
||||
|
|
@ -2138,6 +2139,25 @@
|
|||
"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": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
"@types/react-dom": "^19.1.7",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"agentation": "^3.0.2",
|
||||
"babel-plugin-react-compiler": "1.0.0",
|
||||
"dotenv": "^17.2.2",
|
||||
"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 { useLanguage } from '../../contexts/LanguageContext';
|
||||
import FeatureCardsGrid from '../FeatureCardsGrid';
|
||||
import type { Service } from '../../data/Service';
|
||||
import saschaImage from '../../assets/sascha.png';
|
||||
import saschaPixel from '../../assets/sascha_pixel.svg';
|
||||
|
||||
// Define feature card icon and color mappings as constants
|
||||
const FEATURE_CARD_ICONS = [Code, Palette, ShieldCheck, Eye] as const;
|
||||
|
|
@ -39,6 +41,26 @@ export default function AboutSection(props: AboutSectionProps = {}) {
|
|||
}))
|
||||
} = 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 (
|
||||
<section id="about" className="about-section" aria-labelledby="about-title">
|
||||
<div className="about-section__container">
|
||||
|
|
@ -63,7 +85,7 @@ export default function AboutSection(props: AboutSectionProps = {}) {
|
|||
|
||||
{/* Personal Photo and Bio */}
|
||||
<main className="about-section__content">
|
||||
<div className="about-section__image-wrapper">
|
||||
<div className="about-section__image-wrapper" ref={wrapperRef}>
|
||||
<img
|
||||
src={profileImage}
|
||||
alt={`Professional headshot of ${name}, Web Developer and Software Engineer`}
|
||||
|
|
@ -71,6 +93,16 @@ export default function AboutSection(props: AboutSectionProps = {}) {
|
|||
loading="lazy"
|
||||
/>
|
||||
<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 className="about-section__bio-section">
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { Download } from 'lucide-react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import type { StatItem } from '../../data/StatItem';
|
||||
|
|
@ -41,6 +42,17 @@ export default function HeroSection(props: HeroSectionProps = {}) {
|
|||
const navigate = useNavigate();
|
||||
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 = () => {
|
||||
announce('Navigating to projects section');
|
||||
scrollToProjects(location.pathname, navigate);
|
||||
|
|
@ -72,7 +84,7 @@ export default function HeroSection(props: HeroSectionProps = {}) {
|
|||
};
|
||||
|
||||
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">
|
||||
{/* Skip link for keyboard navigation */}
|
||||
<div className="hero-section__skip">
|
||||
|
|
|
|||
|
|
@ -33,14 +33,20 @@
|
|||
&__container {
|
||||
display: flex;
|
||||
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 {
|
||||
@extend %button-reset;
|
||||
@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;
|
||||
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:
|
||||
color 0.2s ease,
|
||||
box-shadow 0.2s ease,
|
||||
|
|
@ -113,6 +119,7 @@
|
|||
top: 65px;
|
||||
right: 0;
|
||||
background: var(--color-background);
|
||||
color: var(--color-text);
|
||||
border: 1px solid var(--color-primary);
|
||||
border-radius: 8px;
|
||||
padding: 0;
|
||||
|
|
@ -172,8 +179,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Landing page has 7 menu items — switch to burger at a wider breakpoint
|
||||
@media (max-width: 1023px) {
|
||||
// Landing page has 7 menu items — switch to burger at same breakpoint as regular nav
|
||||
@media (max-width: 1049px) {
|
||||
.navbar--landing .navbar__container__button {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -185,8 +192,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Keep burger visible for landing page up to 1023px
|
||||
@media (min-width: 769px) and (max-width: 1023px) {
|
||||
// Keep burger visible for landing page up to 1049px
|
||||
@media (min-width: 769px) and (max-width: 1049px) {
|
||||
.navbar--landing .navbar__burger-button {
|
||||
display: flex;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,6 +74,50 @@
|
|||
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 {
|
||||
flex: 1;
|
||||
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 {
|
||||
@include flex-center;
|
||||
min-height: 100vh;
|
||||
|
|
@ -34,6 +61,7 @@
|
|||
border-radius: 50%;
|
||||
border: 2px solid rgba(12, 68, 124, 0.15);
|
||||
pointer-events: none;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
&::after {
|
||||
|
|
@ -46,6 +74,7 @@
|
|||
border-radius: 50%;
|
||||
background: rgba(239, 159, 39, 0.08);
|
||||
pointer-events: none;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
[data-theme='dark'] & {
|
||||
|
|
@ -76,6 +105,7 @@
|
|||
letter-spacing: -0.03em;
|
||||
line-height: 1.1;
|
||||
color: $color-heading;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
&__description {
|
||||
|
|
@ -85,12 +115,14 @@
|
|||
max-width: 600px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
@include flex-center;
|
||||
gap: $spacing-lg;
|
||||
flex-wrap: wrap;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
&__button {
|
||||
|
|
@ -148,6 +180,7 @@
|
|||
text-align: center;
|
||||
padding: $spacing-md;
|
||||
border-radius: $border-radius-md;
|
||||
will-change: transform, opacity;
|
||||
|
||||
&--1 {
|
||||
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
|
||||
$mobile-breakpoint: 768px;
|
||||
$desktop-breakpoint: 769px;
|
||||
$mobile-breakpoint: 1050px;
|
||||
$desktop-breakpoint: 1050px;
|
||||
|
||||
// Color variables
|
||||
$color-primary: #042c53;
|
||||
|
|
|
|||
Loading…
Reference in New Issue