From 26256f0379ea207be8b67ea9fc13973b42c4d9d3 Mon Sep 17 00:00:00 2001 From: Matheo Guilbert Date: Fri, 17 Apr 2026 08:48:28 +0200 Subject: [PATCH] redesign(project-page): editorial redesign of /works/[slug] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ProjectHero: full-viewport dark hero with blurred cover image overlay, animated entrance (back link → date → title → description), scroll indicator - ProjectMeta: reveal-animated metadata strip with top/bottom dividers, refined label/value typography using brand color tokens - ProjectTechnologies: outline pill tags with per-tag staggered reveal, hover border/color transition - ProjectsColumns: cleaner 2-col prose grid with wider gap - ProjectPreviewGrid: rounded-xl with subtle border, responsive 1→2 col - ProjectVisitButton: pill-shaped dark CTA replacing bordered rectangle - OtherProjects: image-first card layout with scroll reveal, hover scale + brightness on covers, sliding arrow, refined "Voir aussi" header - page.tsx: passes cover prop to ProjectHero, 900px max-width content column, semantic MDX prose styles via article selector chains Co-Authored-By: Claude Sonnet 4.6 --- app/[locale]/works/[slug]/page.tsx | 63 ++++----- components/mdx/ProjectPreviewGrid.tsx | 48 +++---- components/mdx/ProjectVisitButton.tsx | 43 +++--- components/mdx/ProjectsColumns.tsx | 14 +- components/sections/OtherProjects.tsx | 81 ++++++++++-- components/sections/ProjectHero.tsx | 137 ++++++++++++++++---- components/sections/ProjectMeta.tsx | 72 ++++++---- components/sections/ProjectTechnologies.tsx | 41 +++++- 8 files changed, 357 insertions(+), 142 deletions(-) diff --git a/app/[locale]/works/[slug]/page.tsx b/app/[locale]/works/[slug]/page.tsx index 4619e55..5890829 100644 --- a/app/[locale]/works/[slug]/page.tsx +++ b/app/[locale]/works/[slug]/page.tsx @@ -36,45 +36,46 @@ export default async function ProjectPage({ params }: props) { slug: item.slug, title: item.title, description: item.description, - cover: item.cover + cover: item.cover, })); return ( -
-
- + + +
+ -
- + - - -
-
- -
-
- - -
+
+
+ +
+
+
); diff --git a/components/mdx/ProjectPreviewGrid.tsx b/components/mdx/ProjectPreviewGrid.tsx index 7a36531..d284763 100644 --- a/components/mdx/ProjectPreviewGrid.tsx +++ b/components/mdx/ProjectPreviewGrid.tsx @@ -1,31 +1,33 @@ type PreviewItem = { - title: string; - image: string; + title: string; + image: string; }; type props = { - items?: PreviewItem[]; + items?: PreviewItem[]; }; export function ProjectPreviewGrid({ items = [] }: props) { - return ( -
- {items.map((item) => ( -
- {item.image ? ( - {item.title} - ) : ( - {item.title} - )} -
- ))} -
- ); + return ( +
1 ? 'lg:grid-cols-2' : ''}`}> + {items.map((item) => ( +
+ {item.image ? ( + {item.title} + ) : ( +
+ {item.title} +
+ )} +
+ ))} +
+ ); } diff --git a/components/mdx/ProjectVisitButton.tsx b/components/mdx/ProjectVisitButton.tsx index 2f0b2ac..64d4f8c 100644 --- a/components/mdx/ProjectVisitButton.tsx +++ b/components/mdx/ProjectVisitButton.tsx @@ -1,24 +1,29 @@ -import { GoLinkExternal } from "react-icons/go"; - type VisitButtonProps = { - href: string; + href: string; }; export function ProjectVisitButton({ href }: VisitButtonProps) { + if (!href) return null; - if (href === "" || href === null) { return; } - - return ( -
- - Visiter le site - - -
- ); -} \ No newline at end of file + return ( +
+ + Visiter le projet + + +
+ ); +} diff --git a/components/mdx/ProjectsColumns.tsx b/components/mdx/ProjectsColumns.tsx index dfa55c8..9652fcb 100644 --- a/components/mdx/ProjectsColumns.tsx +++ b/components/mdx/ProjectsColumns.tsx @@ -1,11 +1,11 @@ type props = { - children: React.ReactNode; + children: React.ReactNode; }; export function ProjectsColumns({ children }: props) { - return ( -
- {children} -
- ); -} \ No newline at end of file + return ( +
+ {children} +
+ ); +} diff --git a/components/sections/OtherProjects.tsx b/components/sections/OtherProjects.tsx index ba198f0..e16916d 100644 --- a/components/sections/OtherProjects.tsx +++ b/components/sections/OtherProjects.tsx @@ -1,4 +1,7 @@ -import Link from "next/link"; +'use client'; + +import { useRef, useEffect } from 'react'; +import Link from 'next/link'; type props = { locale: string; @@ -11,27 +14,85 @@ type props = { }; export function OtherProjects({ locale, projects }: props) { + const ref = useRef(null); + + useEffect(() => { + const el = ref.current; + if (!el) return; + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + el.querySelectorAll('.reveal').forEach((node, i) => { + setTimeout(() => node.classList.add('visible'), i * 120); + }); + observer.unobserve(el); + } + }, + { threshold: 0.1 } + ); + observer.observe(el); + return () => observer.disconnect(); + }, []); + if (!projects.length) return null; return ( -
-

- Autres projets +
+ {/* Divider */} +
+ + {/* Label */} +
+ + + Autres projets + +
+ +

+ Voir aussi

- {projects.map((project) => ( + {projects.map((project, i) => ( -
{project.title}
-

{project.description}

- cover image + {/* Image */} +
+ {project.title} +
+ + {/* Text */} +
+
+

+ {project.title} +

+

+ {project.description} +

+
+ +
))}
); -} \ No newline at end of file +} diff --git a/components/sections/ProjectHero.tsx b/components/sections/ProjectHero.tsx index 236f152..77d0b6f 100644 --- a/components/sections/ProjectHero.tsx +++ b/components/sections/ProjectHero.tsx @@ -1,36 +1,125 @@ -import Link from "next/link"; +'use client'; -import { GoArrowLeft } from "react-icons/go"; +import { useEffect, useRef } from 'react'; +import Link from 'next/link'; type props = { title: string; description: string; dateLabel: string; locale: string; + cover?: string; }; -export function ProjectHero({ title, description, dateLabel, locale }: props) { +export function ProjectHero({ title, description, dateLabel, locale, cover }: props) { + const ref = useRef(null); + + useEffect(() => { + const section = ref.current; + if (!section) return; + const items = section.querySelectorAll('.hero-reveal'); + items.forEach((el, i) => { + setTimeout(() => { + el.style.opacity = '1'; + el.style.transform = 'translateY(0)'; + }, 80 + i * 100); + }); + }, []); + return ( -
-
- - Retour aux projets - - -
-

{title}

-

{description}

- -
- - {dateLabel} - -
+
+ {/* Background cover image */} + {cover && ( +
+ +
-
-
+ )} + + {/* Back link */} +
+
+ + + Retour aux projets + +
+
+ + {/* Main content — vertically centred */} +
+ {/* Category pill */} +
+ + + {dateLabel} + + +
+ + {/* Title */} +
+

+ {title} +

+
+ + {/* Description */} +
+

+ {description} +

+
+
+ + {/* Scroll indicator */} +
+
+ Défiler + +
+
+
); -} \ No newline at end of file +} diff --git a/components/sections/ProjectMeta.tsx b/components/sections/ProjectMeta.tsx index 4f616de..81b2471 100644 --- a/components/sections/ProjectMeta.tsx +++ b/components/sections/ProjectMeta.tsx @@ -1,3 +1,7 @@ +'use client'; + +import { useRef, useEffect } from 'react'; + type props = { dateLabel: string; category: string; @@ -5,34 +9,58 @@ type props = { client: string; }; -function Item({ - label, - children, -}: { - label: string; - children: React.ReactNode; -}) { +function MetaItem({ label, children }: { label: string; children: React.ReactNode }) { return ( -
-

{label}

-
{children}
+
+ + {label} + +
+ {children} +
); } export function ProjectMeta({ dateLabel, category, roles, client }: props) { + const ref = useRef(null); + + useEffect(() => { + const el = ref.current; + if (!el) return; + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + el.classList.add('visible'); + observer.unobserve(el); + } + }, + { threshold: 0.2 } + ); + observer.observe(el); + return () => observer.disconnect(); + }, []); + return ( -
- {dateLabel} - {category} - -
- {roles.map((role) => ( - {role} - ))} -
-
- {client} +
+ {/* Top divider */} +
+ +
+ {dateLabel} + {category} + +
+ {roles.map((role) => ( + {role} + ))} +
+
+ {client} +
+ + {/* Bottom divider */} +
); -} \ No newline at end of file +} diff --git a/components/sections/ProjectTechnologies.tsx b/components/sections/ProjectTechnologies.tsx index ba0526b..6902e9f 100644 --- a/components/sections/ProjectTechnologies.tsx +++ b/components/sections/ProjectTechnologies.tsx @@ -1,17 +1,46 @@ +'use client'; + +import { useRef, useEffect } from 'react'; + type props = { technologies: string[]; }; export function ProjectTechnologies({ technologies }: props) { - return ( -
-

Technologies

+ const ref = useRef(null); -
+ useEffect(() => { + const el = ref.current; + if (!el) return; + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + el.querySelectorAll('.reveal').forEach((node, i) => { + setTimeout(() => node.classList.add('visible'), i * 60); + }); + observer.unobserve(el); + } + }, + { threshold: 0.2 } + ); + observer.observe(el); + return () => observer.disconnect(); + }, []); + + return ( +
+
+ + + Technologies + +
+ +
{technologies.map((tech) => ( {tech} @@ -19,4 +48,4 @@ export function ProjectTechnologies({ technologies }: props) {
); -} \ No newline at end of file +}