feat: Project Carousel

This commit is contained in:
2026-03-02 17:06:47 +05:30
parent c5ef1bb840
commit 37832dac1a

View File

@@ -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<Record<string, { stars: number; forks: number }>>({});
const [orderedProjects, setOrderedProjects] = useState(PROJECTS);
const projectRefs = useRef(new Map<string, HTMLDivElement>());
const projectPositions = useRef(new Map<string, DOMRect>());
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<string, DOMRect>();
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() {
<Layout className="w-5 h-5 accent-text" /> recent_projects/
</h2>
<div className="flex flex-col gap-6">
{PROJECTS.map((project, idx) => (
<article
{orderedProjects.slice(0, 3).map((project, idx) => (
<div
key={project.id}
className={`group relative p-6 bg-(--bg-surface) sketch-border-subtle cursor-pointer overflow-visible ${idx % 2 === 0 ? 'transform rotate-[0.5deg]' : 'transform -rotate-[0.5deg]'}`}
ref={(node) => {
if (node) {
projectRefs.current.set(project.id, node);
} else {
projectRefs.current.delete(project.id);
}
}}
className="transition-transform duration-300 ease-out"
>
{project.ribbon && (
<div className="absolute -top-2 -right-10 rotate-12 z-10">
<div className="sketch-border-subtle bg-(--color-accent) text-(--bg-base) px-6 py-1 text-xs font-sketch uppercase tracking-wide shadow-lg">
{project.ribbon}
<article
className={`group relative p-6 bg-(--bg-surface) sketch-border-subtle cursor-pointer overflow-visible ${idx % 2 === 0 ? 'transform rotate-[0.5deg]' : 'transform -rotate-[0.5deg]'}`}
>
{project.ribbon && (
<div className="absolute -top-2 -right-10 rotate-12 z-10">
<div className="sketch-border-subtle bg-(--color-accent) text-(--bg-base) px-6 py-1 text-xs font-sketch uppercase tracking-wide shadow-lg">
{project.ribbon}
</div>
</div>
</div>
)}
<div className="absolute top-0 left-0 w-full h-1 bg-(--border-main) group-hover:bg-(--color-accent) transition-colors duration-300 opacity-50" />
)}
<div className="absolute top-0 left-0 w-full h-1 bg-(--border-main) group-hover:bg-(--color-accent) transition-colors duration-300 opacity-50" />
<div className="flex justify-between items-start mb-4">
<div className="flex items-center gap-4">
<div className="group/icon relative flex items-center gap-2 py-2.5 ps-2.5 pe-1.5 pr-6 sketch-border-subtle bg-(--bg-panel) text-(--text-muted) hover:text-(--color-accent) hover:scale-110 transition-all transform -rotate-3">
<span className="flex items-center justify-center">
{project.icon}
</span>
<ChevronRight className="absolute right-1.5 top-1/2 -translate-y-1/2 w-4 h-4 text-(--color-accent) opacity-90 animate-pulse transition-all drop-shadow-[0_0_16px_var(--color-accent)] group-hover/icon:opacity-0 group-hover/icon:scale-0 group-focus-within/icon:opacity-0 group-focus-within/icon:scale-0" />
{project.github && (
<div className="flex items-center gap-3 text-xs text-(--text-muted) overflow-hidden max-w-0 opacity-0 group-hover/icon:max-w-35 group-hover/icon:opacity-100 transition-all duration-300">
<span className="flex items-center gap-1">
<Star className="w-3 h-3 text-(--color-accent)" />
{repoStats[project.id]?.stars ?? project.stars ?? '—'}
</span>
<span className="flex items-center gap-1">
<GitFork className="w-3 h-3 text-(--color-accent)" />
{repoStats[project.id]?.forks ?? project.forks ?? '—'}
</span>
</div>
)}
</div>
<div>
<h3 className="text-(--text-main) font-bold text-lg font-mono">
{project.link ? (
<a
href={project.link}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-2 hover:text-(--color-accent) transition-colors"
>
{project.title}
<ExternalLink className="w-4 h-4 opacity-0 group-hover:opacity-100 transition-opacity accent-text" />
</a>
) : (
<span className="inline-flex items-center gap-2">
{project.title}
<ExternalLink className="w-4 h-4 opacity-0 group-hover:opacity-100 transition-opacity accent-text" />
</span>
<div className="flex justify-between items-start mb-4">
<div className="flex items-center gap-4">
<div className="group/icon relative flex items-center gap-2 py-2.5 ps-2.5 pe-1.5 pr-6 sketch-border-subtle bg-(--bg-panel) text-(--text-muted) hover:text-(--color-accent) hover:scale-110 transition-all transform -rotate-3">
<span className="flex items-center justify-center">
{project.icon}
</span>
<ChevronRight className="absolute right-1.5 top-1/2 -translate-y-1/2 w-4 h-4 text-(--color-accent) opacity-90 animate-pulse transition-all drop-shadow-[0_0_16px_var(--color-accent)] group-hover/icon:opacity-0 group-hover/icon:scale-0 group-focus-within/icon:opacity-0 group-focus-within/icon:scale-0" />
{project.github && (
<div className="flex items-center gap-3 text-xs text-(--text-muted) overflow-hidden max-w-0 opacity-0 group-hover/icon:max-w-35 group-hover/icon:opacity-100 transition-all duration-300">
<span className="flex items-center gap-1">
<Star className="w-3 h-3 text-(--color-accent)" />
{repoStats[project.id]?.stars ?? project.stars ?? '—'}
</span>
<span className="flex items-center gap-1">
<GitFork className="w-3 h-3 text-(--color-accent)" />
{repoStats[project.id]?.forks ?? project.forks ?? '—'}
</span>
</div>
)}
</h3>
<span className="font-sketch text-sm text-(--text-muted) accent-text">{project.category}</span>
</div>
<div>
<h3 className="text-(--text-main) font-bold text-lg font-mono">
{project.link ? (
<a
href={project.link}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-2 hover:text-(--color-accent) transition-colors"
>
{project.title}
<ExternalLink className="w-4 h-4 opacity-0 group-hover:opacity-100 transition-opacity accent-text" />
</a>
) : (
<span className="inline-flex items-center gap-2">
{project.title}
<ExternalLink className="w-4 h-4 opacity-0 group-hover:opacity-100 transition-opacity accent-text" />
</span>
)}
</h3>
<span className="font-sketch text-sm text-(--text-muted) accent-text">{project.category}</span>
</div>
</div>
</div>
</div>
<p className="text-sm text-(--text-muted) mb-5 leading-relaxed font-mono">
{project.desc}
</p>
<p className="text-sm text-(--text-muted) mb-5 leading-relaxed font-mono">
{project.desc}
</p>
<div className="flex flex-wrap gap-2">
{project.tech.map((t) => (
<span key={t} className="text-xs px-2 py-1 bg-(--bg-panel) sketch-border-subtle text-(--text-muted) font-mono">
{t}
</span>
))}
</div>
</article>
<div className="flex flex-wrap gap-2">
{project.tech.map((t) => (
<span key={t} className="text-xs px-2 py-1 bg-(--bg-panel) sketch-border-subtle text-(--text-muted) font-mono">
{t}
</span>
))}
</div>
</article>
</div>
))}
</div>
{orderedProjects.length > 3 && (
<div className="flex flex-wrap gap-3">
{orderedProjects.slice(3).map((project) => (
<div
key={project.id}
ref={(node) => {
if (node) {
projectRefs.current.set(project.id, node);
} else {
projectRefs.current.delete(project.id);
}
}}
className="transition-transform duration-300 ease-out"
>
<button
type="button"
onClick={() => handleProjectPromote(project.id)}
className="group/icon relative flex items-center gap-2 py-2.5 ps-2.5 pe-1.5 pr-6 sketch-border-subtle bg-(--bg-panel) text-(--text-muted) hover:text-(--color-accent) hover:scale-105 transition-all"
aria-label={`Promote ${project.title}`}
>
<span className="flex items-center justify-center">
{project.icon}
</span>
<ChevronRight className="absolute right-1.5 top-1/2 -translate-y-1/2 w-4 h-4 text-(--color-accent) opacity-90 animate-pulse transition-all drop-shadow-[0_0_16px_var(--color-accent)] group-hover/icon:opacity-0 group-hover/icon:scale-0 group-focus-within/icon:opacity-0 group-focus-within/icon:scale-0" />
</button>
</div>
))}
</div>
)}
</section>
);
}