redesign(homepage): elegant editorial redesign with scroll animations

- HeroSection: full-viewport split layout with staggered entrance animations,
  editorial typography, tech stack tags, animated scroll indicator
- WorksSection: refined header with section label, scroll-reveal on entry
- ProjectCard: per-card IntersectionObserver reveal, numbered projects,
  enhanced hover states (scale + brightness), improved CTA
- FeatureSection: card grid with border/glass treatment and staggered reveals
- AboutSection: animated timeline progress line, scroll-triggered card entrances
- FeatureColumn: icon container with hover treatment
- globals.css: fadeUp/lineGrow/arrowBounce keyframes, .reveal utility,
  prefers-reduced-motion support, brand-accent color token
- lib/useReveal.ts: IntersectionObserver hook for scroll reveals

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-17 08:39:54 +02:00
parent b717abb7de
commit 87efea3cc1
9 changed files with 600 additions and 178 deletions
+145 -20
View File
@@ -1,28 +1,153 @@
import { FiChevronsDown } from "react-icons/fi";
'use client';
import { useEffect, useRef } from 'react';
export default function HeroSection() {
return (
<section className="w-full h-[calc(100vh-(2*11px))] rounded-[10px] bg-brand-brown text-brand-beige flex flex-col items-center justify-center relative">
{/* Title + image container */}
<div className="flex flex-col w-9/10 gap-2.5
md:flex-row md:justify-between md:items-center md:w-2/3
">
{/* Rotating texts */}
<div className="flex flex-col">
<p className="text-[clamp(15px,2vw,24px)] font-sans">Ensemble nous allons...</p>
<h1 className="text-[clamp(30px,4vw,64px)] font-serif">Construire une solution qui répond à votre problème.</h1>
const sectionRef = useRef<HTMLElement>(null);
useEffect(() => {
const section = sectionRef.current;
if (!section) return;
const items = section.querySelectorAll<HTMLElement>('.hero-reveal');
items.forEach((el, i) => {
setTimeout(() => {
el.style.opacity = '1';
el.style.transform = 'translateY(0)';
}, 120 + i * 110);
});
}, []);
return (
<section
ref={sectionRef}
className="w-full min-h-[calc(100vh-(2*11px))] rounded-[10px] bg-brand-brown text-brand-beige flex flex-col overflow-hidden relative"
>
{/* Main content */}
<div className="flex flex-col lg:flex-row flex-1 px-8 md:px-14 lg:px-20 pt-16 pb-12 gap-10 lg:gap-0">
{/* Left — editorial text */}
<div className="flex flex-col justify-center lg:w-[55%] lg:pr-12 gap-8 lg:gap-10">
{/* Label */}
<div
className="hero-reveal"
style={{ opacity: 0, transform: 'translateY(28px)', transition: 'opacity 700ms cubic-bezier(0.22,1,0.36,1), transform 700ms cubic-bezier(0.22,1,0.36,1)' }}
>
<span className="inline-flex items-center gap-2 font-sans text-xs uppercase tracking-[0.2em] text-brand-beige/50">
<span className="w-6 h-px bg-brand-beige/30" />
Développeur Full Stack &amp; Architecte Web
</span>
</div>
{/* Headline */}
<div
className="hero-reveal"
style={{ opacity: 0, transform: 'translateY(28px)', transition: 'opacity 700ms cubic-bezier(0.22,1,0.36,1), transform 700ms cubic-bezier(0.22,1,0.36,1)' }}
>
<h1 className="font-serif leading-[0.95] text-[clamp(36px,5.5vw,88px)] text-brand-beige">
Ensemble,<br />
<em className="not-italic text-brand-beige/70">construisons</em><br />
ce qui dure.
</h1>
</div>
{/* Description */}
<div
className="hero-reveal"
style={{ opacity: 0, transform: 'translateY(28px)', transition: 'opacity 700ms cubic-bezier(0.22,1,0.36,1), transform 700ms cubic-bezier(0.22,1,0.36,1)' }}
>
<p className="font-sans text-base font-light text-brand-beige/60 max-w-md leading-relaxed">
Je conçois des architectures robustes, scalables et pérennes
des fondations qui supportent la croissance de votre produit dans le temps.
</p>
</div>
{/* CTA */}
<div
className="hero-reveal"
style={{ opacity: 0, transform: 'translateY(28px)', transition: 'opacity 700ms cubic-bezier(0.22,1,0.36,1), transform 700ms cubic-bezier(0.22,1,0.36,1)' }}
>
<a
href="/#works"
className="group inline-flex items-center gap-2 font-sans text-sm text-brand-beige/80 hover:text-brand-beige transition-colors duration-200 cursor-pointer"
onClick={(e) => {
e.preventDefault();
document.getElementById('works')?.scrollIntoView({ behavior: 'smooth' });
}}
>
<span className="relative">
Voir mes travaux
<span className="absolute bottom-0 left-0 w-full h-px bg-brand-beige/40 origin-left scale-x-0 group-hover:scale-x-100 transition-transform duration-300" />
</span>
<svg className="w-4 h-4 group-hover:translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</a>
</div>
</div>
{/* Image */}
<img className="rounded-[10px] w-full object-cover h-[clamp(200px,30vh,500px)] md:h-[clamp(250px,35vh,600px)] lg:h-[clamp(300px,45vh,700px)]" src="/images/pofile.png" alt="Image of me" />
{/* Right — portrait */}
<div
className="hero-reveal relative flex items-center justify-center lg:w-[45%]"
style={{ opacity: 0, transform: 'translateY(28px)', transition: 'opacity 900ms cubic-bezier(0.22,1,0.36,1), transform 900ms cubic-bezier(0.22,1,0.36,1)' }}
>
{/* Decorative accent ring */}
<div className="absolute inset-4 rounded-[14px] border border-brand-beige/8 pointer-events-none" />
<div className="relative w-full h-full min-h-[320px] lg:min-h-0 rounded-[10px] overflow-hidden">
<img
src="/images/pofile.png"
alt="Mathéo Guilbert, développeur Full Stack"
className="w-full h-full object-cover object-top"
style={{ filter: 'sepia(8%) brightness(0.95) contrast(1.02)' }}
/>
{/* Subtle vignette */}
<div className="absolute inset-0 bg-gradient-to-t from-brand-brown/30 via-transparent to-transparent pointer-events-none" />
</div>
</div>
</div>
{/* Call to scroll */}
<div className="flex flex-row justify-center items-center gap-2.5 absolute bottom-6">
<FiChevronsDown />
<p className="text-base font-sans">Voir mes travaux</p>
<FiChevronsDown />
{/* Bottom bar — scroll indicator */}
<div className="flex justify-between items-end px-8 md:px-14 lg:px-20 pb-8">
{/* Tech tags */}
<div
className="hero-reveal flex gap-3 flex-wrap"
style={{ opacity: 0, transform: 'translateY(28px)', transition: 'opacity 700ms cubic-bezier(0.22,1,0.36,1), transform 700ms cubic-bezier(0.22,1,0.36,1)' }}
>
{['FastAPI', 'React', 'PostgreSQL', 'Docker'].map((tag) => (
<span
key={tag}
className="font-sans text-[11px] uppercase tracking-widest text-brand-beige/35 border border-brand-beige/15 rounded-full px-3 py-1"
>
{tag}
</span>
))}
</div>
{/* Scroll arrow */}
<div
className="hero-reveal flex flex-col items-center gap-1.5 text-brand-beige/40"
style={{
opacity: 0,
transform: 'translateY(28px)',
transition: 'opacity 700ms cubic-bezier(0.22,1,0.36,1), transform 700ms cubic-bezier(0.22,1,0.36,1)',
animationDelay: '1s'
}}
>
<span className="font-sans text-[10px] uppercase tracking-[0.15em]">Défiler</span>
<svg
className="w-4 h-4"
style={{ animation: 'arrowBounce 1.8s ease-in-out infinite' }}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M19 9l-7 7-7-7" />
</svg>
</div>
</div>
</section>
);
</section>
);
}