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:
@@ -1,61 +1,106 @@
|
||||
'use client';
|
||||
|
||||
import { useRef, useEffect } from 'react';
|
||||
import Link from "next/link";
|
||||
import { GoArrowRight } from "react-icons/go";
|
||||
import { Project } from "@/lib/projects/types";
|
||||
|
||||
type props = {
|
||||
project: Project;
|
||||
locale: string;
|
||||
isTheLast?: boolean;
|
||||
index?: number;
|
||||
};
|
||||
|
||||
export const ProjectCard = ({ project, locale, isTheLast = false }: props) => {
|
||||
export const ProjectCard = ({ project, locale, isTheLast = false, index = 0 }: props) => {
|
||||
const num = String(index + 1).padStart(2, '0');
|
||||
const cardRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const el = cardRef.current;
|
||||
if (!el) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
el.classList.add('visible');
|
||||
observer.unobserve(el);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.12 }
|
||||
);
|
||||
|
||||
observer.observe(el);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="w-full pb-8">
|
||||
<div className="w-full h-px bg-brand-brown/20 mb-8" />
|
||||
<div ref={cardRef} className="w-full pb-10 reveal">
|
||||
{/* Divider line */}
|
||||
<div className="w-full h-px bg-brand-brown/15 mb-10" />
|
||||
|
||||
<Link href={`/${locale}/works/${project.slug}`} className="group block pointer-cursor">
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:justify-between lg:items-center lg:gap-14 relative">
|
||||
<Link href={`/${locale}/works/${project.slug}`} className="group block cursor-pointer">
|
||||
<div className="flex flex-col gap-6 lg:flex-row lg:justify-between lg:items-start lg:gap-16 relative">
|
||||
|
||||
{/* Date label */}
|
||||
<div className="text-5xl font-serif text-brand-brown/40 mt-6 font-semibold lg:absolute lg:top-1 lg:right-1">
|
||||
{project.dateLabel}
|
||||
{/* Project number */}
|
||||
<div className="absolute top-0 right-0 font-sans text-xs tracking-[0.2em] text-brand-brown/25 uppercase">
|
||||
{num}
|
||||
</div>
|
||||
|
||||
{/* Left image */}
|
||||
<div className="w-full rounded-[10px] lg:w-3/5 h-92.5 bg-gray-600 overflow-hidden">
|
||||
{/* Left — image */}
|
||||
<div className="w-full rounded-[10px] lg:w-3/5 h-[340px] md:h-[420px] overflow-hidden bg-brand-brown/5">
|
||||
<img
|
||||
src={project.cover}
|
||||
alt={project.title}
|
||||
className="w-full h-full object-cover transition duration-500 group-hover:scale-[1.02]"
|
||||
className="w-full h-full object-cover transition-all duration-700 ease-out group-hover:scale-[1.04] group-hover:brightness-105"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right content */}
|
||||
<div className="flex flex-col items-start justify-center w-full gap-7.5">
|
||||
<div className="flex flex-col items-start justify-center w-full">
|
||||
<div className="p-1.5 bg-brand-brown rounded-px text-brand-beige font-sans">{project.category}</div>
|
||||
<div className="text-5xl text-brand-brown font-serif uppercase">{project.title}</div>
|
||||
<div className="text-2xl font-serif font-light">
|
||||
<p>
|
||||
(
|
||||
{project.roles.map((role, index) => role + (index < project.roles.length - 1 ? ", " : ""))}
|
||||
)
|
||||
</p>
|
||||
</div>
|
||||
{/* Right — content */}
|
||||
<div className="flex flex-col items-start justify-center w-full gap-6 lg:pt-4 lg:pr-6">
|
||||
{/* Category */}
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="w-4 h-px bg-brand-brown/40" />
|
||||
<span className="font-sans text-[11px] uppercase tracking-[0.18em] text-brand-brown/50">
|
||||
{project.category}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="text-base font-normal font-sans">{project.description}</div>
|
||||
{/* Title */}
|
||||
<div>
|
||||
<h3 className="text-[clamp(28px,3.5vw,52px)] text-brand-brown font-serif leading-tight uppercase">
|
||||
{project.title}
|
||||
</h3>
|
||||
<p className="font-serif font-light text-brand-brown/50 text-lg mt-1">
|
||||
({project.roles.join(', ')})
|
||||
</p>
|
||||
<p className="font-sans text-xs tracking-widest text-brand-brown/30 mt-1">
|
||||
{project.dateLabel}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="border-b text-base flex flex-row justify-center items-center gap-0.5 transition group-hover:gap-1.5">
|
||||
<p>READ</p>
|
||||
<GoArrowRight />
|
||||
{/* Description */}
|
||||
<p className="font-sans text-sm leading-relaxed text-brand-brown/70 max-w-sm">
|
||||
{project.description}
|
||||
</p>
|
||||
|
||||
{/* CTA */}
|
||||
<div className="flex items-center gap-2 font-sans text-xs uppercase tracking-[0.18em] text-brand-brown border-b border-brand-brown/30 pb-0.5 group-hover:border-brand-brown transition-colors duration-200">
|
||||
<span>Lire le cas</span>
|
||||
<svg
|
||||
className="w-3.5 h-3.5 group-hover:translate-x-1.5 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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{isTheLast && <div className="w-full h-px mt-8 bg-brand-brown/20" />}
|
||||
{isTheLast && <div className="w-full h-px mt-10 bg-brand-brown/15" />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user