diff --git a/src/components/ProjectsSection.tsx b/src/components/ProjectsSection.tsx index 7c28f01..1304779 100644 --- a/src/components/ProjectsSection.tsx +++ b/src/components/ProjectsSection.tsx @@ -1,9 +1,49 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useLayoutEffect, useRef, useState } from 'react'; import { ChevronRight, ExternalLink, GitFork, Layout, Star } from 'lucide-react'; import { PROJECTS } from '../data/contentData'; export function ProjectsSection() { const [repoStats, setRepoStats] = useState>({}); + const [orderedProjects, setOrderedProjects] = useState(PROJECTS); + const projectRefs = useRef(new Map()); + const projectPositions = useRef(new Map()); + + const handleProjectPromote = (projectId: string) => { + setOrderedProjects((prev) => { + const target = prev.find((project) => project.id === projectId); + if (!target) return prev; + const remaining = prev.filter((project) => project.id !== projectId); + return [target, ...remaining]; + }); + }; + + useLayoutEffect(() => { + const nextPositions = new Map(); + + projectRefs.current.forEach((node, id) => { + nextPositions.set(id, node.getBoundingClientRect()); + }); + + projectRefs.current.forEach((node, id) => { + const prev = projectPositions.current.get(id); + const next = nextPositions.get(id); + if (!prev || !next) return; + + const deltaX = prev.left - next.left; + const deltaY = prev.top - next.top; + + if (deltaX || deltaY) { + node.style.transform = `translate(${deltaX}px, ${deltaY}px)`; + node.style.transition = 'transform 0s'; + requestAnimationFrame(() => { + node.style.transform = ''; + node.style.transition = 'transform 320ms ease'; + }); + } + }); + + projectPositions.current = nextPositions; + }, [orderedProjects]); useEffect(() => { let isMounted = true; @@ -61,77 +101,117 @@ export function ProjectsSection() { recent_projects/
- {PROJECTS.map((project, idx) => ( -
( +
{ + if (node) { + projectRefs.current.set(project.id, node); + } else { + projectRefs.current.delete(project.id); + } + }} + className="transition-transform duration-300 ease-out" > - {project.ribbon && ( -
-
- {project.ribbon} +
+ {project.ribbon && ( +
+
+ {project.ribbon} +
-
- )} -
+ )} +
-
-
-
- - {project.icon} - - - {project.github && ( -
- - - {repoStats[project.id]?.stars ?? project.stars ?? '—'} - - - - {repoStats[project.id]?.forks ?? project.forks ?? '—'} - -
- )} -
-
-

- {project.link ? ( - - {project.title} - - - ) : ( - - {project.title} - - +
+
+
+ + {project.icon} + + + {project.github && ( +
+ + + {repoStats[project.id]?.stars ?? project.stars ?? '—'} + + + + {repoStats[project.id]?.forks ?? project.forks ?? '—'} + +
)} -

- {project.category} +
+
+

+ {project.link ? ( + + {project.title} + + + ) : ( + + {project.title} + + + )} +

+ {project.category} +
-
-

- {project.desc} -

+

+ {project.desc} +

-
- {project.tech.map((t) => ( - - {t} - - ))} -
-
+
+ {project.tech.map((t) => ( + + {t} + + ))} +
+ +
))} + {orderedProjects.length > 3 && ( +
+ {orderedProjects.slice(3).map((project) => ( +
{ + if (node) { + projectRefs.current.set(project.id, node); + } else { + projectRefs.current.delete(project.id); + } + }} + className="transition-transform duration-300 ease-out" + > + +
+ ))} +
+ )} ); }