Overview
The ProjectDetail component renders a comprehensive case study page for individual projects. It displays the project header, hero image, problem/solution breakdown, tech stack, stats, and navigation to the next project.
This is a routed page component that uses React Router’s useParams to fetch the correct project data based on the URL parameter.
Component Structure
Location : src/components/ProjectDetail/ProjectDetail.jsx
Styles : src/components/ProjectDetail/ProjectDetail.module.css
Data Source : src/data/projects.js
Route : /project/:id
Dependencies : react-router-dom (useParams, Link, useNavigate)
Implementation
Route Configuration
In your router setup:
import { BrowserRouter , Routes , Route } from 'react-router-dom' ;
import ProjectDetail from './components/ProjectDetail/ProjectDetail' ;
< BrowserRouter >
< Routes >
< Route path = "/project/:id" element = { < ProjectDetail /> } />
{ /* Other routes */ }
</ Routes >
</ BrowserRouter >
Full Component Code
import { useParams , Link , useNavigate } from 'react-router-dom' ;
import { projects } from '../../data/projects' ;
import styles from './ProjectDetail.module.css' ;
import { useEffect } from 'react' ;
export default function ProjectDetail () {
const { id } = useParams ();
const navigate = useNavigate ();
const project = projects . find ( p => p . id === id );
useEffect (() => {
window . scrollTo ({ top: 0 , behavior: 'smooth' });
}, [ id ]);
if ( ! project ) {
return (
< div className = { styles . container } >
< h2 > Project not found </ h2 >
< Link to = "/" className = { styles . backBtn } > Back Home </ Link >
</ div >
);
}
const nextProject = projects [( projects . indexOf ( project ) + 1 ) % projects . length ];
return (
< div className = { styles . page } >
< nav className = { styles . nav } >
< div className = { `container ${ styles . navInner } ` } >
< Link to = "/" className = { styles . backBtn } >
< span className = "material-icons" > arrow_back </ span >
BACK TO PROJECTS
</ Link >
</ div >
</ nav >
< main className = { `container ${ styles . main } ` } >
< header className = { styles . header } >
< div className = { styles . headerText } >
< div className = { styles . tagline } > { project . tagline } </ div >
< h1 className = { styles . title } >
{ project . title . split ( ' ' ). map (( word , i ) => (
< span key = { ` ${ word } - ${ i } ` } > { word } < br /></ span >
)) }
</ h1 >
</ div >
< div className = { styles . headerCtas } >
{ /* Placeholder for live demo / view code buttons */ }
</ div >
< p className = { styles . shortDesc } >
{ project . fullDesc }
</ p >
</ header >
< section className = { styles . heroImageWrap } >
< div className = { styles . imageDecor } />
< div className = { styles . imageContainer } >
< img src = { project . img } alt = { project . imgAlt } className = { styles . heroImg } />
< div className = { styles . imgBadge } >
< span className = { styles . imgName } >
{ project . title }
</ span >
</ div >
</ div >
</ section >
< div className = { styles . marquee } >
< div className = { styles . marqueeInner } >
{ [ ... Array ( 6 )]. map (( _ , i ) => (
< span key = { `tech- ${ i } ` } >
{ project . techList . join ( ' • ' ) } •
</ span >
)) }
</ div >
</ div >
< div className = { styles . contentGrid } >
< div className = { styles . storyCol } >
< div className = { styles . neoCard } >
< div className = { ` ${ styles . cardBadge } ${ styles . badgePink } ` } > The Problem </ div >
< h2 className = { styles . cardTitle } >
< span className = "material-icons" > warning </ span >
The Challenge
</ h2 >
< p className = { styles . cardText } > { project . challenge } </ p >
</ div >
< div className = { styles . neoCard } >
< div className = { ` ${ styles . cardBadge } ${ styles . badgeGreen } ` } > The Solution </ div >
< h2 className = { styles . cardTitle } >
< span className = "material-icons" > check_circle </ span >
Implementation
</ h2 >
< p className = { styles . cardText } > { project . solution } </ p >
</ div >
</ div >
< aside className = { styles . metaCol } >
< div className = { styles . statsBox } >
< h3 className = { styles . statsTitle } > Project Stats </ h3 >
< div className = { styles . statItem } >
< span className = { styles . statLabel } > Role </ span >
< span className = { styles . statValue } > { project . stats . role } </ span >
</ div >
< div className = { styles . statItem } >
< span className = { styles . statLabel } > Timeline </ span >
< span className = { styles . statValue } > { project . stats . timeline } </ span >
</ div >
< div className = { styles . statItem } >
< span className = { styles . statLabel } > Team </ span >
< span className = { styles . statValue } > { project . stats . team } </ span >
</ div >
</ div >
< div className = { styles . techBox } >
< h3 className = { styles . statsTitle } > Tech Stack </ h3 >
< div className = { styles . techList } >
{ project . techList . map ( tech => (
< span key = { tech } className = { styles . techTag } > { tech } </ span >
)) }
</ div >
</ div >
< Link to = { `/project/ ${ nextProject . id } ` } className = { styles . nextProject } >
< div className = { styles . nextDecor } />
< div className = { styles . nextContent } >
< span className = { styles . nextLabel } > Next Project </ span >
< h4 className = { styles . nextTitle } > { nextProject . title } → </ h4 >
</ div >
</ Link >
</ aside >
</ div >
</ main >
</ div >
);
}
Data Fetching
URL Parameter
const { id } = useParams ();
const project = projects . find ( p => p . id === id );
Retrieves the project ID from the URL (/project/chatverde) and finds matching project in data array.
Error Handling
if ( ! project ) {
return (
< div className = { styles . container } >
< h2 > Project not found </ h2 >
< Link to = "/" className = { styles . backBtn } > Back Home </ Link >
</ div >
);
}
Displays fallback UI if project ID doesn’t match any project in the data.
useEffect (() => {
window . scrollTo ({ top: 0 , behavior: 'smooth' });
}, [ id ]);
Scrolls to top when component mounts or when navigating between projects.
Page Sections
Navigation Bar
Hero Image
Tech Marquee
< nav className = { styles . nav } >
< div className = { `container ${ styles . navInner } ` } >
< Link to = "/" className = { styles . backBtn } >
< span className = "material-icons" > arrow_back </ span >
BACK TO PROJECTS
</ Link >
</ div >
</ nav >
Sticky navigation with back button to return to homepage. < section className = { styles . heroImageWrap } >
< div className = { styles . imageDecor } />
< div className = { styles . imageContainer } >
< img src = { project . img } alt = { project . imgAlt } className = { styles . heroImg } />
< div className = { styles . imgBadge } >
< span className = { styles . imgName } > { project . title } </ span >
</ div >
</ div >
</ section >
Large project image with decorative background and title badge. < div className = { styles . marquee } >
< div className = { styles . marqueeInner } >
{ [ ... Array ( 6 )]. map (( _ , i ) => (
< span key = { `tech- ${ i } ` } >
{ project . techList . join ( ' • ' ) } •
</ span >
)) }
</ div >
</ div >
Horizontally scrolling tech stack animation.
Content Grid Layout
Two-column responsive grid:
Story Column (Left)
Contains two neobrutalist cards:
< div className = { styles . neoCard } >
< div className = { ` ${ styles . cardBadge } ${ styles . badgePink } ` } > The Problem </ div >
< h2 className = { styles . cardTitle } >
< span className = "material-icons" > warning </ span >
The Challenge
</ h2 >
< p className = { styles . cardText } > { project . challenge } </ p >
</ div >
Pink-badged card describing the project challenge.
< div className = { styles . neoCard } >
< div className = { ` ${ styles . cardBadge } ${ styles . badgeGreen } ` } > The Solution </ div >
< h2 className = { styles . cardTitle } >
< span className = "material-icons" > check_circle </ span >
Implementation
</ h2 >
< p className = { styles . cardText } > { project . solution } </ p >
</ div >
Green-badged card explaining how the problem was solved.
Sidebar with project metadata:
Project Stats
Tech Stack
Next Project
< div className = { styles . statsBox } >
< h3 className = { styles . statsTitle } > Project Stats </ h3 >
< div className = { styles . statItem } >
< span className = { styles . statLabel } > Role </ span >
< span className = { styles . statValue } > { project . stats . role } </ span >
</ div >
< div className = { styles . statItem } >
< span className = { styles . statLabel } > Timeline </ span >
< span className = { styles . statValue } > { project . stats . timeline } </ span >
</ div >
< div className = { styles . statItem } >
< span className = { styles . statLabel } > Team </ span >
< span className = { styles . statValue } > { project . stats . team } </ span >
</ div >
</ div >
Next Project Navigation
Calculates the next project cyclically:
const nextProject = projects [( projects . indexOf ( project ) + 1 ) % projects . length ];
Uses modulo % to loop back to first project after the last
Creates seamless project browsing experience
Links to /project/${nextProject.id}
Title Line Break Effect
< h1 className = { styles . title } >
{ project . title . split ( ' ' ). map (( word , i ) => (
< span key = { ` ${ word } - ${ i } ` } > { word } < br /></ span >
)) }
</ h1 >
Splits title by spaces and renders each word on a new line for dramatic vertical layout.
Example :
Tech Marquee Animation
{[ ... Array ( 6 )]. map (( _ , i ) => (
< span key = { `tech- ${ i } ` } >
{ project . techList . join ( ' • ' ) } •
</ span >
))}
Creates 6 repetitions of the tech stack for infinite scroll effect. CSS handles the animation.
Customization
Uncomment and populate the header CTAs:
< div className = { styles . headerCtas } >
< a href = { project . liveUrl } className = { styles . btnPrimary } target = "_blank" rel = "noopener noreferrer" >
LIVE DEMO
</ a >
< a href = { project . repoUrl } className = { styles . btnSecondary } target = "_blank" rel = "noopener noreferrer" >
VIEW CODE
</ a >
</ div >
Ensure your project data includes liveUrl and repoUrl fields.
Modify Card Badges
< div className = { ` ${ styles . cardBadge } ${ styles . badgeBlue } ` } > Context </ div >
Add new badge color variants in CSS:
.badgeBlue {
background-color : #00b4d8 ;
color : #fff ;
}
Change Marquee Speed
Adjust CSS animation duration:
.marqueeInner {
animation : scroll 20 s linear infinite ; /* Slower: increase seconds */
}
Add More Stat Items
< div className = { styles . statItem } >
< span className = { styles . statLabel } > Industry </ span >
< span className = { styles . statValue } > { project . stats . industry } </ span >
</ div >
CSS Module Patterns
Key styling approaches:
Neobrutalist cards : .neoCard with bold borders and shadows
Badge overlays : Absolutely positioned colored badges
Grid layout : .contentGrid with responsive columns
Marquee animation : CSS keyframes for infinite scroll
Decorative elements : .imageDecor, .nextDecor for visual flair
Accessibility
Semantic HTML (<main>, <header>, <section>, <aside>)
Proper heading hierarchy (h1 → h2 → h3)
Descriptive alt text for images
Keyboard-navigable links
Material Icons used as visual enhancement (not essential for understanding)
Smooth scroll behavior for better UX
Efficient data lookup : .find() is O(n) but projects array is small
Conditional rendering : Only renders when project exists
CSS Modules : Scoped, optimized styles
Auto-scroll : Improves navigation UX
Lazy images : Could add loading="lazy" to hero image if below fold
Error States
Project not found : Shows fallback message with back link
Invalid route : Router handles 404 separately
Missing data fields : Component assumes all fields exist (ensure data integrity)
Projects : Grid listing that links to detail pages
Navbar : Can add “Back” navigation here too