221 lines
8.0 KiB
TypeScript
221 lines
8.0 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
|
|
import { motion } from 'framer-motion';
|
|
import { ExternalLink, Github, Calendar, ArrowLeft } 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;
|
|
}
|
|
|
|
const ProjectsPage = () => {
|
|
const [projects, setProjects] = useState<Project[]>([]);
|
|
|
|
// 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();
|
|
setProjects(data.projects || []);
|
|
} else {
|
|
console.error('Failed to fetch projects from API');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading projects:', error);
|
|
}
|
|
};
|
|
|
|
loadProjects();
|
|
}, []);
|
|
|
|
const categories = ["All", "Web Development", "Full-Stack", "Web Application", "Mobile App"];
|
|
const [selectedCategory, setSelectedCategory] = useState("All");
|
|
const [mounted, setMounted] = useState(false);
|
|
|
|
useEffect(() => {
|
|
setMounted(true);
|
|
}, []);
|
|
|
|
if (!mounted) {
|
|
return null;
|
|
}
|
|
|
|
const filteredProjects = selectedCategory === "All"
|
|
? projects
|
|
: projects.filter(project => project.category === selectedCategory);
|
|
|
|
console.log('Selected category:', selectedCategory);
|
|
console.log('Filtered projects:', filteredProjects);
|
|
|
|
if (!mounted) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen animated-bg">
|
|
<div className="max-w-7xl mx-auto px-4 py-20">
|
|
{/* 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-blue-400 hover:text-blue-300 transition-colors mb-6"
|
|
>
|
|
<ArrowLeft size={20} />
|
|
<span>Back to Home</span>
|
|
</Link>
|
|
|
|
<h1 className="text-5xl md:text-6xl font-bold mb-6 gradient-text">
|
|
My Projects
|
|
</h1>
|
|
<p className="text-xl text-gray-400 max-w-3xl">
|
|
Explore my portfolio of projects, from web applications to mobile apps.
|
|
Each project showcases different skills and technologies.
|
|
</p>
|
|
</motion.div>
|
|
|
|
{/* Category Filter */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.8, delay: 0.2 }}
|
|
className="mb-12"
|
|
>
|
|
<div className="flex flex-wrap gap-3">
|
|
{categories.map((category) => (
|
|
<button
|
|
key={category}
|
|
onClick={() => setSelectedCategory(category)}
|
|
className={`px-6 py-3 rounded-lg font-medium transition-all duration-200 ${
|
|
selectedCategory === category
|
|
? 'bg-blue-600 text-white shadow-lg'
|
|
: 'bg-gray-800/50 text-gray-300 hover:bg-gray-700/50 hover:text-white'
|
|
}`}
|
|
>
|
|
{category}
|
|
</button>
|
|
))}
|
|
</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 }}
|
|
whileHover={{ y: -10 }}
|
|
className="group relative overflow-hidden rounded-2xl glass-card card-hover"
|
|
>
|
|
<div className="relative h-48 overflow-hidden">
|
|
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/20 to-purple-500/20" />
|
|
<div className="absolute inset-0 bg-gray-800/50 flex flex-col items-center justify-center p-4">
|
|
<div className="w-16 h-16 bg-gradient-to-br from-blue-500 to-purple-500 rounded-full flex items-center justify-center mb-2">
|
|
<span className="text-2xl font-bold text-white">
|
|
{project.title.split(' ').map(word => word[0]).join('').toUpperCase()}
|
|
</span>
|
|
</div>
|
|
<span className="text-sm font-medium text-gray-400 text-center leading-tight">
|
|
{project.title}
|
|
</span>
|
|
</div>
|
|
|
|
{project.featured && (
|
|
<div className="absolute top-4 right-4 px-3 py-1 bg-gradient-to-r from-blue-500 to-purple-500 text-white text-xs font-semibold rounded-full">
|
|
Featured
|
|
</div>
|
|
)}
|
|
|
|
{((project.github && project.github.trim() && project.github !== "#") || (project.live && project.live.trim() && project.live !== "#")) && (
|
|
<div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center space-x-4">
|
|
{project.github && project.github.trim() && project.github !== "#" && (
|
|
<motion.a
|
|
href={project.github}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
whileHover={{ scale: 1.1 }}
|
|
whileTap={{ scale: 0.95 }}
|
|
className="p-3 bg-gray-800/80 rounded-lg text-white hover:bg-gray-700/80 transition-colors"
|
|
>
|
|
<Github size={20} />
|
|
</motion.a>
|
|
)}
|
|
{project.live && project.live.trim() && project.live !== "#" && (
|
|
<motion.a
|
|
href={project.live}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
whileHover={{ scale: 1.1 }}
|
|
whileTap={{ scale: 0.95 }}
|
|
className="p-3 bg-blue-600/80 rounded-lg text-white hover:bg-blue-500/80 transition-colors"
|
|
>
|
|
<ExternalLink size={20} />
|
|
</motion.a>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="p-6">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<h3 className="text-xl font-bold text-white group-hover:text-blue-400 transition-colors">
|
|
{project.title}
|
|
</h3>
|
|
<div className="flex items-center space-x-2 text-gray-400">
|
|
<Calendar size={16} />
|
|
<span className="text-sm">{project.date}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<p className="text-gray-300 mb-4 leading-relaxed">
|
|
{project.description}
|
|
</p>
|
|
|
|
<div className="flex flex-wrap gap-2 mb-4">
|
|
{project.tags.map((tag) => (
|
|
<span
|
|
key={tag}
|
|
className="px-3 py-1 bg-gray-800/50 text-gray-300 text-sm rounded-full border border-gray-700"
|
|
>
|
|
{tag}
|
|
</span>
|
|
))}
|
|
</div>
|
|
|
|
<Link
|
|
href={`/projects/${project.title.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`}
|
|
className="inline-flex items-center space-x-2 text-blue-400 hover:text-blue-300 transition-colors font-medium"
|
|
>
|
|
<span>View Project</span>
|
|
<ExternalLink size={16} />
|
|
</Link>
|
|
</div>
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ProjectsPage;
|