"use client"; import { useEffect, useRef, useState } from "react"; import SafeImage from "./SafeImage"; interface Task { name: string; description: string; status: string; technologies: string[]; } interface ProjectLink { name: string; url: string; } interface Project { name: string; description: string; logo?: string; links?: ProjectLink[]; tasks: Task[]; } interface ProjectsProps { projects: Project[]; } function ProjectCard({ project, index }: { project: Project; index: number }) { const [expanded, setExpanded] = useState(false); const ref = useRef(null); useEffect(() => { const el = ref.current; if (!el) return; const obs = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) { setTimeout(() => el.classList.add("visible"), index * 80); obs.disconnect(); } }, { threshold: 0.1 } ); obs.observe(el); return () => obs.disconnect(); }, [index]); const allTechs = Array.from( new Set(project.tasks.flatMap((t) => t.technologies)) ); const doneCount = project.tasks.filter((t) => t.status === "Done").length; return (
{/* Header */}
{project.logo && ( )}
{String(index + 1).padStart(2, "0")}

{project.name}

{project.links?.map((link) => ( { (e.currentTarget as HTMLElement).style.borderColor = "#c8a96e"; (e.currentTarget as HTMLElement).style.color = "#c8a96e"; }} onMouseLeave={(e) => { (e.currentTarget as HTMLElement).style.borderColor = "#1c1f26"; (e.currentTarget as HTMLElement).style.color = "#4a5060"; }} > ↗ {link.name} ))}
{/* Description */}

{project.description}

{/* Tech tags */}
{allTechs.slice(0, 8).map((tech) => ( {tech} ))} {allTechs.length > 8 && ( +{allTechs.length - 8} )}
{/* Footer */}
{doneCount}/{project.tasks.length} tasks complete
{/* Tasks */} {expanded && (
{project.tasks.map((task) => (
{task.name} {task.status}

{task.description}

{task.technologies.map((tech) => ( {tech} ))}
))}
)}
); } export default function Projects({ projects }: ProjectsProps) { const headingRef = useRef(null); useEffect(() => { const el = headingRef.current; if (!el) return; const obs = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) { el.classList.add("visible"); obs.disconnect(); } }, { threshold: 0.1 } ); obs.observe(el); return () => obs.disconnect(); }, []); return (
{/* Section header */}
Selected Work

Projects

{String(projects.length).padStart(2, "0")} total
{/* Grid */}
{projects.map((project, i) => (
))}
); }