222 lines
8.5 KiB
TypeScript
222 lines
8.5 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { motion, Variants } from "framer-motion";
|
|
import { ExternalLink, Github, Layers, ArrowRight } from "lucide-react";
|
|
import Link from "next/link";
|
|
import Image from "next/image";
|
|
|
|
const fadeInUp: Variants = {
|
|
hidden: { opacity: 0, y: 20 },
|
|
visible: {
|
|
opacity: 1,
|
|
y: 0,
|
|
transition: {
|
|
duration: 0.5,
|
|
ease: [0.25, 0.1, 0.25, 1],
|
|
},
|
|
},
|
|
};
|
|
|
|
const staggerContainer: Variants = {
|
|
hidden: { opacity: 0 },
|
|
visible: {
|
|
opacity: 1,
|
|
transition: {
|
|
staggerChildren: 0.2,
|
|
delayChildren: 0.1,
|
|
},
|
|
},
|
|
};
|
|
|
|
interface Project {
|
|
id: number;
|
|
title: string;
|
|
description: string;
|
|
content: string;
|
|
tags: string[];
|
|
featured: boolean;
|
|
category: string;
|
|
date: string;
|
|
github?: string;
|
|
live?: string;
|
|
imageUrl?: string;
|
|
}
|
|
|
|
const Projects = () => {
|
|
const [projects, setProjects] = useState<Project[]>([]);
|
|
|
|
useEffect(() => {
|
|
const loadProjects = async () => {
|
|
try {
|
|
const response = await fetch(
|
|
"/api/projects?featured=true&published=true&limit=6",
|
|
);
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
setProjects(data.projects || []);
|
|
}
|
|
} catch (error) {
|
|
if (process.env.NODE_ENV === "development") {
|
|
console.error("Error loading projects:", error);
|
|
}
|
|
}
|
|
};
|
|
loadProjects();
|
|
}, []);
|
|
|
|
return (
|
|
<section
|
|
id="projects"
|
|
className="py-24 px-4 relative bg-gradient-to-br from-liquid-peach/15 via-liquid-yellow/10 to-liquid-coral/15 overflow-hidden"
|
|
>
|
|
<div className="max-w-7xl mx-auto">
|
|
<motion.div
|
|
initial="hidden"
|
|
whileInView="visible"
|
|
viewport={{ once: true, margin: "-50px" }}
|
|
variants={fadeInUp}
|
|
className="text-center mb-20"
|
|
>
|
|
<h2 className="text-4xl md:text-6xl font-bold mb-6 text-stone-900">
|
|
Selected Works
|
|
</h2>
|
|
<p className="text-lg text-stone-600 max-w-2xl mx-auto mt-4 font-light">
|
|
A collection of projects I've worked on, ranging from web
|
|
applications to experiments.
|
|
</p>
|
|
</motion.div>
|
|
|
|
<motion.div
|
|
initial="hidden"
|
|
whileInView="visible"
|
|
viewport={{ once: true, margin: "-50px" }}
|
|
variants={staggerContainer}
|
|
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"
|
|
>
|
|
{projects.map((project) => (
|
|
<motion.div
|
|
key={project.id}
|
|
variants={fadeInUp}
|
|
whileHover={{
|
|
y: -8,
|
|
transition: { duration: 0.4, ease: "easeOut" },
|
|
}}
|
|
className="group relative flex flex-col bg-white/40 backdrop-blur-xl backdrop-saturate-150 rounded-2xl overflow-hidden shadow-[0_4px_20px_rgba(0,0,0,0.02)] hover:shadow-[0_20px_40px_rgba(0,0,0,0.06)] transition-all duration-500 ease-out border border-white/50 ring-1 ring-white/30"
|
|
>
|
|
{/* Project Cover / Header */}
|
|
<div className="relative aspect-[4/3] overflow-hidden bg-stone-100/50">
|
|
{project.imageUrl ? (
|
|
<Image
|
|
src={project.imageUrl}
|
|
alt={project.title}
|
|
fill
|
|
className="object-cover transition-transform duration-1000 ease-out group-hover:scale-105"
|
|
/>
|
|
) : (
|
|
<div className="absolute inset-0 bg-gradient-to-br from-stone-200 to-stone-300 flex items-center justify-center p-8 group-hover:from-stone-100 group-hover:to-stone-200 transition-colors duration-700 ease-out">
|
|
<div className="relative z-10 text-center">
|
|
<span className="text-6xl font-serif font-bold text-stone-500/20 group-hover:text-stone-500/30 transition-colors duration-500 select-none">
|
|
{project.title.charAt(0)}
|
|
</span>
|
|
</div>
|
|
<div className="absolute inset-0 bg-gradient-to-tr from-white/10 via-transparent to-white/10 opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
|
</div>
|
|
)}
|
|
|
|
{/* Overlay Links */}
|
|
<div className="absolute inset-0 bg-stone-900/10 opacity-0 group-hover:opacity-100 transition-opacity duration-500 ease-out flex items-center justify-center gap-4 backdrop-blur-[2px]">
|
|
{project.github && (
|
|
<a
|
|
href={project.github}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="p-3 bg-white/90 backdrop-blur-md rounded-full text-stone-800 hover:scale-110 transition-all duration-300 ease-out shadow-lg hover:shadow-xl border border-white/50"
|
|
aria-label="GitHub"
|
|
>
|
|
<Github size={20} />
|
|
</a>
|
|
)}
|
|
{project.live && (
|
|
<a
|
|
href={project.live}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="p-3 bg-white/90 backdrop-blur-md rounded-full text-stone-800 hover:scale-110 transition-all duration-300 ease-out shadow-lg hover:shadow-xl border border-white/50"
|
|
aria-label="Live Demo"
|
|
>
|
|
<ExternalLink size={20} />
|
|
</a>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="flex flex-col flex-1 p-6">
|
|
<div className="flex justify-between items-start mb-3">
|
|
<h3 className="text-xl font-bold text-stone-900 group-hover:text-stone-700 transition-colors duration-300 tracking-tight">
|
|
{project.title}
|
|
</h3>
|
|
<span className="text-xs font-mono text-stone-500 bg-white/60 border border-stone-200/50 px-2 py-1 rounded backdrop-blur-sm">
|
|
{new Date(project.date).getFullYear()}
|
|
</span>
|
|
</div>
|
|
|
|
<p className="text-stone-700 font-medium text-sm leading-relaxed mb-6 line-clamp-3 flex-1 opacity-90">
|
|
{project.description}
|
|
</p>
|
|
|
|
<div className="space-y-4 mt-auto">
|
|
<div className="flex flex-wrap gap-2">
|
|
{project.tags.slice(0, 3).map((tag, tIdx) => (
|
|
<span
|
|
key={`${project.id}-${tag}-${tIdx}`}
|
|
className="text-xs px-2.5 py-1 bg-white/50 border border-white/60 rounded-md text-stone-700 font-medium hover:bg-white/80 hover:border-white transition-all duration-300 ease-out shadow-sm"
|
|
>
|
|
{tag}
|
|
</span>
|
|
))}
|
|
{project.tags.length > 3 && (
|
|
<span className="text-xs px-2 py-1 text-stone-500 font-medium">
|
|
+ {project.tags.length - 3}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
<Link
|
|
href={`/projects/${project.title.toLowerCase().replace(/\s+/g, "-")}`}
|
|
className="inline-flex items-center text-sm font-bold text-stone-800 hover:gap-3 transition-all duration-300 ease-out group/link"
|
|
>
|
|
Read more{" "}
|
|
<ArrowRight
|
|
size={16}
|
|
className="ml-1 transition-transform duration-300 ease-out group-hover/link:translate-x-1"
|
|
/>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
))}
|
|
</motion.div>
|
|
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.8, delay: 0.3, ease: [0.25, 0.1, 0.25, 1] }}
|
|
className="mt-16 text-center"
|
|
>
|
|
<Link
|
|
href="/projects"
|
|
className="inline-flex items-center gap-2 px-8 py-4 bg-white border border-stone-200 rounded-full text-stone-700 font-medium hover:bg-stone-50 hover:border-stone-300 hover:gap-3 transition-all duration-500 ease-out shadow-sm hover:shadow-md"
|
|
>
|
|
View All Projects <ArrowRight size={16} />
|
|
</Link>
|
|
</motion.div>
|
|
</div>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
export default Projects;
|