296 lines
13 KiB
TypeScript
296 lines
13 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { motion } from 'framer-motion';
|
|
import { ExternalLink, Github, Calendar, ArrowLeft, Search } from 'lucide-react';
|
|
import Link from 'next/link';
|
|
|
|
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 ProjectsPage = () => {
|
|
const [projects, setProjects] = useState<Project[]>([]);
|
|
const [filteredProjects, setFilteredProjects] = useState<Project[]>([]);
|
|
const [categories, setCategories] = useState<string[]>(["All"]);
|
|
const [selectedCategory, setSelectedCategory] = useState("All");
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
const [mounted, setMounted] = useState(false);
|
|
|
|
// Load projects from API
|
|
useEffect(() => {
|
|
const loadProjects = async () => {
|
|
try {
|
|
const response = await fetch('/api/projects?published=true');
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
const loadedProjects = data.projects || [];
|
|
setProjects(loadedProjects);
|
|
|
|
// Extract unique categories
|
|
const uniqueCategories = ["All", ...Array.from(new Set(loadedProjects.map((p: Project) => p.category))) as string[]];
|
|
setCategories(uniqueCategories);
|
|
}
|
|
} catch (error) {
|
|
if (process.env.NODE_ENV === 'development') {
|
|
console.error('Error loading projects:', error);
|
|
}
|
|
}
|
|
};
|
|
|
|
loadProjects();
|
|
setMounted(true);
|
|
}, []);
|
|
|
|
// Filter projects
|
|
useEffect(() => {
|
|
let result = projects;
|
|
|
|
if (selectedCategory !== "All") {
|
|
result = result.filter(project => project.category === selectedCategory);
|
|
}
|
|
|
|
if (searchQuery) {
|
|
const query = searchQuery.toLowerCase();
|
|
result = result.filter(project =>
|
|
project.title.toLowerCase().includes(query) ||
|
|
project.description.toLowerCase().includes(query) ||
|
|
project.tags.some(tag => tag.toLowerCase().includes(query))
|
|
);
|
|
}
|
|
|
|
setFilteredProjects(result);
|
|
}, [projects, selectedCategory, searchQuery]);
|
|
|
|
if (!mounted) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-[#fdfcf8] pt-32 pb-20">
|
|
<div className="max-w-7xl mx-auto px-4">
|
|
{/* Header */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 30 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.8 }}
|
|
className="mb-12"
|
|
>
|
|
<Link
|
|
href="/"
|
|
className="inline-flex items-center space-x-2 text-stone-500 hover:text-stone-800 transition-colors mb-8 group"
|
|
>
|
|
<ArrowLeft size={20} className="group-hover:-translate-x-1 transition-transform" />
|
|
<span>Back to Home</span>
|
|
</Link>
|
|
|
|
<h1 className="text-5xl md:text-6xl font-black font-sans mb-6 text-stone-900 tracking-tight">
|
|
My Projects
|
|
</h1>
|
|
<p className="text-xl text-stone-600 max-w-3xl font-light leading-relaxed">
|
|
Explore my portfolio of projects, from web applications to mobile apps.
|
|
Each project showcases different skills and technologies.
|
|
</p>
|
|
</motion.div>
|
|
|
|
{/* Filters & Search */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.8, delay: 0.2 }}
|
|
className="mb-12 flex flex-col md:flex-row gap-6 justify-between items-start md:items-center"
|
|
>
|
|
{/* Categories */}
|
|
<div className="flex flex-wrap gap-2">
|
|
{categories.map((category) => (
|
|
<button
|
|
key={category}
|
|
onClick={() => setSelectedCategory(category)}
|
|
className={`px-5 py-2 rounded-full text-sm font-medium transition-all duration-200 border ${
|
|
selectedCategory === category
|
|
? 'bg-stone-800 text-stone-50 border-stone-800 shadow-md'
|
|
: 'bg-white text-stone-600 border-stone-200 hover:bg-stone-50 hover:border-stone-300'
|
|
}`}
|
|
>
|
|
{category}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Search */}
|
|
<div className="relative w-full md:w-64">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-stone-400" size={18} />
|
|
<input
|
|
type="text"
|
|
placeholder="Search projects..."
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
className="w-full pl-10 pr-4 py-2 bg-white border border-stone-200 rounded-full text-stone-800 placeholder:text-stone-400 focus:outline-none focus:ring-2 focus:ring-stone-200 focus:border-stone-400 transition-all"
|
|
/>
|
|
</div>
|
|
</motion.div>
|
|
|
|
{/* Projects Grid */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
|
{filteredProjects.map((project, index) => (
|
|
<motion.div
|
|
key={project.id}
|
|
initial={{ opacity: 0, y: 30 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.6, delay: index * 0.1 }}
|
|
className="group relative flex flex-col bg-white/40 backdrop-blur-xl rounded-2xl overflow-hidden border border-white/60 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"
|
|
>
|
|
<Link
|
|
href={`/projects/${project.title.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`}
|
|
className="absolute inset-0 z-0"
|
|
/>
|
|
|
|
{/* Image / Fallback / Cover Area */}
|
|
<div className="relative aspect-[16/10] overflow-hidden bg-stone-100 pointer-events-none">
|
|
{project.imageUrl ? (
|
|
<>
|
|
<img
|
|
src={project.imageUrl}
|
|
alt={project.title}
|
|
className="w-full h-full object-cover transition-transform duration-1000 ease-out group-hover:scale-110"
|
|
/>
|
|
<div className="absolute inset-0 bg-gradient-to-t from-stone-900/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
|
</>
|
|
) : (
|
|
<div className="absolute inset-0 bg-stone-200 flex items-center justify-center overflow-hidden">
|
|
<div className="absolute inset-0 bg-gradient-to-br from-stone-300 via-stone-200 to-stone-300" />
|
|
<div className="absolute top-[-20%] left-[-10%] w-[70%] h-[70%] bg-white/20 rounded-full blur-3xl animate-pulse" />
|
|
<div className="absolute bottom-[-10%] right-[-5%] w-[60%] h-[60%] bg-stone-400/10 rounded-full blur-2xl" />
|
|
|
|
<div className="relative z-10">
|
|
<span className="text-7xl font-serif font-black text-stone-800/10 group-hover:text-stone-800/20 transition-all duration-700 select-none tracking-tighter">
|
|
{project.title.charAt(0)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Texture/Grain Overlay */}
|
|
<div className="absolute inset-0 opacity-[0.03] pointer-events-none mix-blend-overlay bg-[url('https://grainy-gradients.vercel.app/noise.svg')]" />
|
|
|
|
{/* Animated Shine Effect */}
|
|
<div className="absolute inset-0 translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-1000 ease-in-out bg-gradient-to-r from-transparent via-white/20 to-transparent skew-x-[-20deg] pointer-events-none" />
|
|
|
|
{project.featured && (
|
|
<div className="absolute top-3 left-3 z-20">
|
|
<div className="px-3 py-1.5 bg-white/90 backdrop-blur-md text-stone-800 text-[10px] font-bold uppercase tracking-widest rounded-full shadow-lg border border-white/50 flex items-center gap-1.5">
|
|
<span className="w-1.5 h-1.5 bg-red-500 rounded-full animate-pulse" />
|
|
Featured
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="p-6 flex flex-col flex-1 relative z-10 pointer-events-none">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<h3 className="text-xl font-bold text-stone-900 group-hover:text-stone-600 transition-colors">
|
|
{project.title}
|
|
</h3>
|
|
<div className="flex items-center space-x-2 text-stone-400 text-xs font-mono bg-white/50 px-2 py-1 rounded border border-stone-100">
|
|
<Calendar size={12} />
|
|
<span>{new Date(project.date).getFullYear()}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<p className="text-stone-600 mb-6 leading-relaxed line-clamp-3 text-sm flex-1">
|
|
{project.description}
|
|
</p>
|
|
|
|
<div className="flex flex-wrap gap-2 mb-6">
|
|
{project.tags.slice(0, 4).map((tag) => (
|
|
<span
|
|
key={tag}
|
|
className="px-2.5 py-1 bg-white/60 border border-stone-100 text-stone-600 text-xs font-medium rounded-md"
|
|
>
|
|
{tag}
|
|
</span>
|
|
))}
|
|
{project.tags.length > 4 && (
|
|
<span className="px-2 py-1 text-stone-400 text-xs">+ {project.tags.length - 4}</span>
|
|
)}
|
|
</div>
|
|
|
|
<div className="mt-auto pt-4 border-t border-stone-100 flex items-center justify-between">
|
|
<div className="flex gap-3 pointer-events-auto">
|
|
{project.github && (
|
|
<a href={project.github} target="_blank" rel="noopener noreferrer" onClick={(e) => e.stopPropagation()} className="text-stone-400 hover:text-stone-900 transition-colors">
|
|
<Github size={18} />
|
|
</a>
|
|
)}
|
|
{project.live && (
|
|
<a href={project.live} target="_blank" rel="noopener noreferrer" onClick={(e) => e.stopPropagation()} className="text-stone-400 hover:text-stone-900 transition-colors">
|
|
<ExternalLink size={18} />
|
|
</a>
|
|
)}
|
|
</div>
|
|
|
|
<div className="inline-flex items-center space-x-1 text-sm font-bold text-stone-800 hover:gap-2 transition-all">
|
|
<span>Read More</span>
|
|
<ArrowLeft size={16} className="rotate-180" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* External Overlay Links for full width on hover if you want to keep that style */}
|
|
<div className="absolute inset-0 bg-stone-900/40 opacity-0 group-hover:opacity-100 transition-opacity duration-500 ease-out flex items-center justify-center gap-4 backdrop-blur-[2px] z-20 pointer-events-none group-hover:pointer-events-auto">
|
|
{project.github && (
|
|
<a
|
|
href={project.github}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
onClick={(e) => e.stopPropagation()}
|
|
className="p-3 bg-white text-stone-900 rounded-full hover:scale-110 transition-all duration-300 shadow-xl border border-white/50"
|
|
aria-label="GitHub"
|
|
>
|
|
<Github size={20} />
|
|
</a>
|
|
)}
|
|
{project.live && (
|
|
<a
|
|
href={project.live}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
onClick={(e) => e.stopPropagation()}
|
|
className="p-3 bg-white text-stone-900 rounded-full hover:scale-110 transition-all duration-300 shadow-xl border border-white/50"
|
|
aria-label="Live Demo"
|
|
>
|
|
<ExternalLink size={20} />
|
|
</a>
|
|
)}
|
|
</div>
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
|
|
{filteredProjects.length === 0 && (
|
|
<div className="text-center py-20">
|
|
<p className="text-stone-500 text-lg">No projects found matching your criteria.</p>
|
|
<button
|
|
onClick={() => {setSelectedCategory("All"); setSearchQuery("");}}
|
|
className="mt-4 text-stone-800 font-medium hover:underline"
|
|
>
|
|
Clear filters
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ProjectsPage; |