developer-portfolios/app/components/ProjectsSection.tsx

266 lines
10 KiB
TypeScript
Raw Normal View History

2025-04-11 01:05:41 +00:00
"use client";
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { ExternalLinkIcon, GithubIcon, CodeIcon } from 'lucide-react';
interface Project {
title: string;
description: string;
techStack: string[];
link?: {
href: string;
label?: string;
};
}
interface ProjectsSectionProps {
projects: Project[];
}
const ProjectsSection: React.FC<ProjectsSectionProps> = ({ projects }) => {
const [activeFilter, setActiveFilter] = useState('All');
const [hoveredProject, setHoveredProject] = useState<string | null>(null);
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
// Extract unique tech stacks for filter
const techStacks = Array.from(
new Set(projects.flatMap(project => project.techStack))
);
// Filter projects based on tech stack
const filteredProjects = activeFilter === 'All'
? projects
: projects.filter(project => project.techStack.includes(activeFilter));
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
when: "beforeChildren"
},
},
};
const itemVariants = {
hidden: { y: 20, opacity: 0 },
visible: {
y: 0,
opacity: 1,
transition: {
type: "spring",
stiffness: 100,
},
},
};
// Generate a random image for each project (for demo purposes)
// In a real project, you'd use actual project images
const getProjectImage = (title: string) => {
const seed = title.charCodeAt(0) + title.length;
return `https://picsum.photos/seed/${seed}/600/400`;
};
return (
<div className="py-20 px-4 sm:px-6 md:px-12 overflow-hidden" id="projects">
<div className="container mx-auto max-w-6xl">
<motion.div
className="text-center mb-16"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.5 }}
>
<h2 className="text-3xl md:text-4xl font-bold mb-4">My Projects</h2>
<div className="w-20 h-1 bg-gradient-to-r from-primary to-secondary mx-auto rounded-full" />
<p className="mt-4 text-gray-600 dark:text-gray-300 max-w-2xl mx-auto">
A showcase of my technical projects and applications, demonstrating my skills and expertise in various technologies.
</p>
</motion.div>
{/* Filter controls */}
<motion.div
className="flex flex-wrap justify-center gap-3 mb-12"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<motion.button
className={`px-4 py-2 rounded-full transition-all duration-300 ${
activeFilter === 'All'
? "bg-primary text-white shadow-md shadow-primary/20"
: "bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700"
}`}
onClick={() => setActiveFilter('All')}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
All Projects
</motion.button>
{techStacks.map((tech, index) => (
<motion.button
key={index}
className={`px-4 py-2 rounded-full transition-all duration-300 ${
activeFilter === tech
? "bg-primary text-white shadow-md shadow-primary/20"
: "bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700"
}`}
onClick={() => setActiveFilter(tech)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
{tech}
</motion.button>
))}
</motion.div>
{/* Projects Grid */}
<motion.div
key={activeFilter}
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"
variants={containerVariants}
initial="hidden"
animate="visible"
>
{filteredProjects.map((project, index) => (
<motion.div
key={`${project.title}-${index}`}
layout
variants={itemVariants}
initial="hidden"
animate="visible"
exit="hidden"
className="bg-white dark:bg-gray-800 rounded-xl overflow-hidden shadow-lg hover:shadow-xl transition-all duration-300"
whileHover={{ y: -10 }}
onHoverStart={() => setHoveredProject(project.title)}
onHoverEnd={() => setHoveredProject(null)}
onClick={() => setSelectedProject(project)}
>
<div className="relative overflow-hidden h-48">
<img
src={getProjectImage(project.title)}
alt={project.title}
className="w-full h-full object-cover transition-transform duration-500"
style={{
transform: hoveredProject === project.title ? 'scale(1.1)' : 'scale(1)'
}}
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/70 to-transparent flex items-end p-4">
<h3 className="text-white text-xl font-bold">{project.title}</h3>
</div>
</div>
<div className="p-5">
<p className="text-gray-600 dark:text-gray-300 mb-4 line-clamp-3">
{project.description}
</p>
<div className="flex flex-wrap gap-2 mb-4">
{project.techStack.map((tech, techIndex) => (
<span
key={techIndex}
className="px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded-full"
>
{tech}
</span>
))}
</div>
<div className="flex justify-between items-center">
<button
className="text-primary dark:text-primary-light font-medium hover:underline flex items-center gap-1"
onClick={(e) => {
e.stopPropagation();
setSelectedProject(project);
}}
>
<CodeIcon className="w-4 h-4" />
View Details
</button>
{project.link && (
<a
href={project.link.href}
target="_blank"
rel="noopener noreferrer"
className="text-gray-600 dark:text-gray-300 hover:text-primary dark:hover:text-primary-light transition-colors"
onClick={(e) => e.stopPropagation()}
>
<GithubIcon className="w-5 h-5" />
</a>
)}
</div>
</div>
</motion.div>
))}
</motion.div>
{/* Project Modal */}
<AnimatePresence>
{selectedProject && (
<motion.div
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setSelectedProject(null)}
>
<motion.div
className="bg-white dark:bg-gray-800 rounded-xl shadow-2xl max-w-3xl w-full max-h-[90vh] overflow-y-auto"
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
transition={{ type: "spring", damping: 20 }}
onClick={(e) => e.stopPropagation()}
>
<div className="relative h-64 md:h-80">
<img
src={getProjectImage(selectedProject.title)}
alt={selectedProject.title}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/70 to-transparent flex flex-col justify-end p-6">
<h3 className="text-white text-2xl md:text-3xl font-bold">{selectedProject.title}</h3>
<div className="flex flex-wrap gap-2 mt-2">
{selectedProject.techStack.map((tech, techIndex) => (
<span
key={techIndex}
className="px-2 py-1 text-xs bg-white/20 text-white rounded-full"
>
{tech}
</span>
))}
</div>
</div>
</div>
<div className="p-6">
<p className="text-gray-600 dark:text-gray-300 mb-6">
{selectedProject.description}
</p>
{selectedProject.link && (
<div className="flex justify-end">
<a
href={selectedProject.link.href}
target="_blank"
rel="noopener noreferrer"
className="px-4 py-2 bg-primary text-white rounded-lg flex items-center gap-2 hover:bg-primary-dark transition-colors"
>
<GithubIcon className="w-4 h-4" />
View on GitHub
</a>
</div>
)}
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
);
};
export default ProjectsSection;