style: modernize project pages with warm organic design and improved readability

This commit is contained in:
2026-01-10 01:13:07 +01:00
parent 82b5ca4514
commit 2844b981bb
3 changed files with 280 additions and 204 deletions

View File

@@ -114,11 +114,13 @@ const Projects = () => {
className="object-cover transition-transform duration-1000 ease-out group-hover:scale-105" className="object-cover transition-transform duration-1000 ease-out group-hover:scale-105"
/> />
) : ( ) : (
<div className="absolute inset-0 bg-gradient-to-br from-stone-100/50 to-stone-200/50 flex items-center justify-center p-8 group-hover:from-stone-50/50 group-hover:to-stone-100/50 transition-colors duration-700 ease-out"> <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="w-full h-full border-2 border-dashed border-stone-300/50 rounded-xl flex items-center justify-center"> <div className="relative z-10 text-center">
<Layers className="text-stone-300 w-12 h-12" /> <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>
<div className="absolute inset-0 bg-gradient-to-tr from-liquid-mint/10 via-transparent to-liquid-rose/10 opacity-0 group-hover:opacity-100 transition-opacity duration-500" /> <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> </div>
)} )}

View File

@@ -1,11 +1,12 @@
"use client"; "use client";
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { ExternalLink, Calendar, Tag, ArrowLeft, Github as GithubIcon } from 'lucide-react'; import { ExternalLink, Calendar, Tag, ArrowLeft, Github as GithubIcon, Share2 } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import { useParams } from 'next/navigation'; import { useParams } from 'next/navigation';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import Image from 'next/image';
interface Project { interface Project {
id: number; id: number;
@@ -18,6 +19,7 @@ interface Project {
date: string; date: string;
github?: string; github?: string;
live?: string; live?: string;
imageUrl?: string;
} }
const ProjectDetail = () => { const ProjectDetail = () => {
@@ -48,139 +50,179 @@ const ProjectDetail = () => {
if (!project) { if (!project) {
return ( return (
<div className="min-h-screen animated-bg flex items-center justify-center"> <div className="min-h-screen bg-[#fdfcf8] flex items-center justify-center">
<div className="text-center"> <div className="text-center">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-500 mx-auto mb-4"></div> <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-stone-800 mx-auto mb-4"></div>
<p className="text-gray-400">Loading project...</p> <p className="text-stone-500 font-medium">Loading project...</p>
</div> </div>
</div> </div>
); );
} }
return ( return (
<div className="min-h-screen animated-bg"> <div className="min-h-screen bg-[#fdfcf8] pt-32 pb-20">
<div className="max-w-4xl mx-auto px-4 pt-32 pb-20"> <div className="max-w-4xl mx-auto px-4">
{/* Header */} {/* Navigation */}
<motion.div <motion.div
initial={{ opacity: 0, y: 30 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }} transition={{ duration: 0.6 }}
className="mb-12" className="mb-8"
> >
<Link <Link
href="/projects" href="/projects"
className="inline-flex items-center space-x-2 text-blue-400 hover:text-blue-300 transition-colors mb-6" className="inline-flex items-center space-x-2 text-stone-500 hover:text-stone-900 transition-colors group"
> >
<ArrowLeft size={20} /> <ArrowLeft size={20} className="group-hover:-translate-x-1 transition-transform" />
<span>Back to Projects</span> <span className="font-medium">Back to Projects</span>
</Link> </Link>
</motion.div>
<div className="flex items-center justify-between mb-6"> {/* Header & Meta */}
<h1 className="text-4xl md:text-5xl font-bold gradient-text"> <motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.1 }}
className="mb-12"
>
<div className="flex flex-col md:flex-row md:items-start md:justify-between gap-4 mb-6">
<h1 className="text-4xl md:text-6xl font-black font-sans text-stone-900 tracking-tight leading-tight">
{project.title} {project.title}
</h1> </h1>
<div className="flex gap-2 shrink-0 pt-2">
{project.featured && ( {project.featured && (
<span className="px-4 py-2 bg-gradient-to-r from-blue-500 to-purple-500 text-white text-sm font-semibold rounded-full"> <span className="px-4 py-1.5 bg-stone-900 text-stone-50 text-xs font-bold rounded-full shadow-sm">
Featured Featured
</span> </span>
)} )}
<span className="px-4 py-1.5 bg-white border border-stone-200 text-stone-600 text-xs font-medium rounded-full shadow-sm">
{project.category}
</span>
</div>
</div> </div>
<p className="text-xl text-gray-400 mb-6"> <p className="text-xl md:text-2xl text-stone-600 font-light leading-relaxed max-w-3xl mb-8">
{project.description} {project.description}
</p> </p>
{/* Project Meta */} <div className="flex flex-wrap items-center gap-6 text-stone-500 text-sm border-y border-stone-200 py-6">
<div className="flex flex-wrap items-center gap-6 text-gray-400 mb-8">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Calendar size={20} /> <Calendar size={18} />
<span>{project.date}</span> <span className="font-mono">{new Date(project.date).toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })}</span>
</div> </div>
<div className="flex items-center space-x-2"> <div className="h-4 w-px bg-stone-300 hidden sm:block"></div>
<Tag size={20} /> <div className="flex flex-wrap gap-2">
<span>{project.category}</span> {project.tags.map(tag => (
</div> <span key={tag} className="text-stone-700 font-medium">#{tag}</span>
</div>
{/* Tags */}
<div className="flex flex-wrap gap-3 mb-8">
{project.tags.map((tag) => (
<span
key={tag}
className="px-4 py-2 bg-gray-800/50 text-gray-300 rounded-full border border-gray-700"
>
{tag}
</span>
))} ))}
</div> </div>
</div>
</motion.div>
{/* Action Buttons */} {/* Featured Image / Fallback */}
{((project.github && project.github.trim() && project.github !== "#") || (project.live && project.live.trim() && project.live !== "#")) && ( <motion.div
<div className="flex flex-wrap gap-4"> initial={{ opacity: 0, scale: 0.95 }}
{project.github && project.github.trim() && project.github !== "#" && ( animate={{ opacity: 1, scale: 1 }}
<motion.a transition={{ duration: 0.8, delay: 0.2 }}
href={project.github} className="mb-16 rounded-2xl overflow-hidden shadow-2xl bg-stone-100 aspect-video relative"
target="_blank"
rel="noopener noreferrer"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="inline-flex items-center space-x-2 px-6 py-3 bg-gray-800/50 hover:bg-gray-700/50 text-white rounded-lg transition-colors border border-gray-700"
> >
<GithubIcon size={20} /> {project.imageUrl ? (
<span>View Code</span> <img
</motion.a> src={project.imageUrl}
)} alt={project.title}
className="w-full h-full object-cover"
{project.live && project.live.trim() && project.live !== "#" && ( />
<motion.a ) : (
href={project.live} <div className="absolute inset-0 bg-gradient-to-br from-stone-200 to-stone-300 flex items-center justify-center">
target="_blank" <span className="text-9xl font-serif font-bold text-stone-500/20 select-none">
rel="noopener noreferrer" {project.title.charAt(0)}
whileHover={{ scale: 1.05 }} </span>
whileTap={{ scale: 0.95 }}
className="inline-flex items-center space-x-2 px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
>
<ExternalLink size={20} />
<span>Live Demo</span>
</motion.a>
)}
</div> </div>
)} )}
</motion.div> </motion.div>
{/* Project Content */}
{/* Content & Sidebar Layout */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-12">
{/* Main Content */}
<motion.div <motion.div
initial={{ opacity: 0, y: 30 }} initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.2 }} transition={{ duration: 0.8, delay: 0.3 }}
className="glass-card p-8 rounded-2xl" className="lg:col-span-2"
> >
<div className="markdown prose prose-invert max-w-none text-white"> <div className="markdown prose prose-stone max-w-none prose-lg prose-headings:font-bold prose-headings:tracking-tight prose-a:text-stone-900 prose-a:decoration-stone-300 hover:prose-a:decoration-stone-900 prose-img:rounded-xl prose-img:shadow-lg">
<ReactMarkdown <ReactMarkdown
components={{ components={{
h1: ({children}) => <h1 className="text-3xl font-bold text-white mb-4">{children}</h1>, // Custom components to ensure styling matches
h2: ({children}) => <h2 className="text-2xl font-semibold text-white mb-3">{children}</h2>, h1: ({children}) => <h1 className="text-3xl font-bold text-stone-900 mt-8 mb-4">{children}</h1>,
h3: ({children}) => <h3 className="text-xl font-semibold text-white mb-2">{children}</h3>, h2: ({children}) => <h2 className="text-2xl font-bold text-stone-900 mt-8 mb-4">{children}</h2>,
p: ({children}) => <p className="text-gray-300 mb-3 leading-relaxed">{children}</p>, p: ({children}) => <p className="text-stone-700 leading-relaxed mb-6">{children}</p>,
ul: ({children}) => <ul className="list-disc list-inside text-gray-300 mb-3 space-y-1">{children}</ul>, li: ({children}) => <li className="text-stone-700">{children}</li>,
ol: ({children}) => <ol className="list-decimal list-inside text-gray-300 mb-3 space-y-1">{children}</ol>, code: ({children}) => <code className="bg-stone-100 text-stone-800 px-1.5 py-0.5 rounded text-sm font-mono font-medium">{children}</code>,
li: ({children}) => <li className="text-gray-300">{children}</li>, pre: ({children}) => <pre className="bg-stone-900 text-stone-50 p-6 rounded-xl overflow-x-auto my-6 shadow-lg">{children}</pre>,
a: ({href, children}) => (
<a href={href} className="text-blue-400 hover:text-blue-300 underline transition-colors" target="_blank" rel="noopener noreferrer">
{children}
</a>
),
code: ({children}) => <code className="bg-gray-800 text-blue-400 px-2 py-1 rounded text-sm">{children}</code>,
pre: ({children}) => <pre className="bg-gray-800 p-4 rounded-lg overflow-x-auto mb-3">{children}</pre>,
blockquote: ({children}) => <blockquote className="border-l-4 border-blue-500 pl-4 italic text-gray-300 mb-3">{children}</blockquote>,
strong: ({children}) => <strong className="font-semibold text-white">{children}</strong>,
em: ({children}) => <em className="italic text-gray-300">{children}</em>
}} }}
> >
{project.content} {project.content}
</ReactMarkdown> </ReactMarkdown>
</div> </div>
</motion.div> </motion.div>
{/* Sidebar / Actions */}
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8, delay: 0.4 }}
className="lg:col-span-1 space-y-8"
>
<div className="bg-white/50 backdrop-blur-xl border border-white/60 p-6 rounded-2xl shadow-sm sticky top-32">
<h3 className="font-bold text-stone-900 mb-4 flex items-center gap-2">
<Share2 size={18} />
Project Links
</h3>
<div className="space-y-3">
{project.live && project.live.trim() && project.live !== "#" ? (
<a
href={project.live}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-between w-full px-4 py-3 bg-stone-900 text-stone-50 rounded-xl font-medium hover:bg-stone-800 hover:scale-[1.02] transition-all shadow-md group"
>
<span>Live Demo</span>
<ExternalLink size={18} className="group-hover:translate-x-1 transition-transform" />
</a>
) : (
<div className="px-4 py-3 bg-stone-100 text-stone-400 rounded-xl font-medium text-sm text-center border border-stone-200 cursor-not-allowed">
Live demo not available
</div>
)}
{project.github && project.github.trim() && project.github !== "#" ? (
<a
href={project.github}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-between w-full px-4 py-3 bg-white border border-stone-200 text-stone-700 rounded-xl font-medium hover:bg-stone-50 hover:text-stone-900 hover:border-stone-300 transition-all shadow-sm group"
>
<span>View Source</span>
<GithubIcon size={18} className="group-hover:rotate-12 transition-transform" />
</a>
) : null}
</div>
<div className="mt-8 pt-6 border-t border-stone-100">
<h4 className="text-xs font-bold text-stone-400 uppercase tracking-wider mb-3">Tech Stack</h4>
<div className="flex flex-wrap gap-2">
{project.tags.map(tag => (
<span key={tag} className="px-2.5 py-1 bg-stone-100 text-stone-600 text-xs font-medium rounded-md border border-stone-200">
{tag}
</span>
))}
</div>
</div>
</div>
</motion.div>
</div>
</div> </div>
</div> </div>
); );

View File

@@ -1,9 +1,8 @@
"use client"; "use client";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { ExternalLink, Github, Calendar, ArrowLeft } from 'lucide-react'; import { ExternalLink, Github, Calendar, ArrowLeft, Search } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
interface Project { interface Project {
@@ -17,10 +16,16 @@ interface Project {
date: string; date: string;
github?: string; github?: string;
live?: string; live?: string;
imageUrl?: string;
} }
const ProjectsPage = () => { const ProjectsPage = () => {
const [projects, setProjects] = useState<Project[]>([]); 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 // Load projects from API
useEffect(() => { useEffect(() => {
@@ -29,7 +34,12 @@ const ProjectsPage = () => {
const response = await fetch('/api/projects?published=true'); const response = await fetch('/api/projects?published=true');
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
setProjects(data.projects || []); 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) { } catch (error) {
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
@@ -39,31 +49,36 @@ const ProjectsPage = () => {
}; };
loadProjects(); 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); setMounted(true);
}, []); }, []);
if (!mounted) { // Filter projects
return null; useEffect(() => {
let result = projects;
if (selectedCategory !== "All") {
result = result.filter(project => project.category === selectedCategory);
} }
const filteredProjects = selectedCategory === "All" if (searchQuery) {
? projects const query = searchQuery.toLowerCase();
: projects.filter(project => project.category === selectedCategory); 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) { if (!mounted) {
return null; return null;
} }
return ( return (
<div className="min-h-screen animated-bg"> <div className="min-h-screen bg-[#fdfcf8] pt-32 pb-20">
<div className="max-w-7xl mx-auto px-4 pt-32 pb-20"> <div className="max-w-7xl mx-auto px-4">
{/* Header */} {/* Header */}
<motion.div <motion.div
initial={{ opacity: 0, y: 30 }} initial={{ opacity: 0, y: 30 }}
@@ -73,43 +88,56 @@ const ProjectsPage = () => {
> >
<Link <Link
href="/" href="/"
className="inline-flex items-center space-x-2 text-blue-400 hover:text-blue-300 transition-colors mb-6" className="inline-flex items-center space-x-2 text-stone-500 hover:text-stone-800 transition-colors mb-8 group"
> >
<ArrowLeft size={20} /> <ArrowLeft size={20} className="group-hover:-translate-x-1 transition-transform" />
<span>Back to Home</span> <span>Back to Home</span>
</Link> </Link>
<h1 className="text-5xl md:text-6xl font-bold mb-6 gradient-text"> <h1 className="text-5xl md:text-6xl font-black font-sans mb-6 text-stone-900 tracking-tight">
My Projects My Projects
</h1> </h1>
<p className="text-xl text-gray-400 max-w-3xl"> <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. Explore my portfolio of projects, from web applications to mobile apps.
Each project showcases different skills and technologies. Each project showcases different skills and technologies.
</p> </p>
</motion.div> </motion.div>
{/* Category Filter */} {/* Filters & Search */}
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.2 }} transition={{ duration: 0.8, delay: 0.2 }}
className="mb-12" className="mb-12 flex flex-col md:flex-row gap-6 justify-between items-start md:items-center"
> >
<div className="flex flex-wrap gap-3"> {/* Categories */}
<div className="flex flex-wrap gap-2">
{categories.map((category) => ( {categories.map((category) => (
<button <button
key={category} key={category}
onClick={() => setSelectedCategory(category)} onClick={() => setSelectedCategory(category)}
className={`px-6 py-3 rounded-lg font-medium transition-all duration-200 ${ className={`px-5 py-2 rounded-full text-sm font-medium transition-all duration-200 border ${
selectedCategory === category selectedCategory === category
? 'bg-gray-800 text-cream shadow-lg' ? 'bg-stone-800 text-stone-50 border-stone-800 shadow-md'
: 'bg-gray-800/50 text-gray-300 hover:bg-gray-700/50 hover:text-white' : 'bg-white text-stone-600 border-stone-200 hover:bg-stone-50 hover:border-stone-300'
}`} }`}
> >
{category} {category}
</button> </button>
))} ))}
</div> </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> </motion.div>
{/* Projects Grid */} {/* Projects Grid */}
@@ -120,95 +148,99 @@ const ProjectsPage = () => {
initial={{ opacity: 0, y: 30 }} initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: index * 0.1 }} transition={{ duration: 0.6, delay: index * 0.1 }}
whileHover={{ y: -10 }} whileHover={{ y: -8 }}
className="group relative overflow-hidden rounded-2xl glass-card card-hover" className="group 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"
> >
<div className="relative h-48 overflow-hidden"> {/* Image / Fallback */}
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/20 to-purple-500/20" /> <div className="relative h-56 overflow-hidden bg-stone-100">
<div className="absolute inset-0 bg-gray-800/50 flex flex-col items-center justify-center p-4"> {project.imageUrl ? (
<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"> <img // Using img for now if Image is tricky with dynamic urls without config
<span className="text-2xl font-bold text-white"> src={project.imageUrl}
{project.title.split(' ').map(word => word[0]).join('').toUpperCase()} alt={project.title}
</span> className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105"
</div> />
<span className="text-sm font-medium text-gray-400 text-center leading-tight"> ) : (
{project.title} <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">
<span className="text-6xl font-serif font-bold text-stone-500/20 group-hover:text-stone-500/30 select-none">
{project.title.charAt(0)}
</span> </span>
</div> </div>
)}
{project.featured && ( {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"> <div className="absolute top-4 right-4 px-3 py-1 bg-white/90 backdrop-blur-md text-stone-800 text-xs font-bold rounded-full shadow-sm border border-white/50">
Featured Featured
</div> </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>
<div className="p-6"> <div className="p-6 flex flex-col flex-1">
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<h3 className="text-xl font-bold text-white group-hover:text-blue-400 transition-colors"> <h3 className="text-xl font-bold text-stone-900 group-hover:text-stone-600 transition-colors">
{project.title} {project.title}
</h3> </h3>
<div className="flex items-center space-x-2 text-gray-400"> <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={16} /> <Calendar size={12} />
<span className="text-sm">{project.date}</span> <span>{new Date(project.date).getFullYear()}</span>
</div> </div>
</div> </div>
<p className="text-gray-300 mb-4 leading-relaxed"> <p className="text-stone-600 mb-6 leading-relaxed line-clamp-3 text-sm flex-1">
{project.description} {project.description}
</p> </p>
<div className="flex flex-wrap gap-2 mb-4"> <div className="flex flex-wrap gap-2 mb-6">
{project.tags.map((tag) => ( {project.tags.slice(0, 4).map((tag) => (
<span <span
key={tag} key={tag}
className="px-3 py-1 bg-gray-800/50 text-gray-300 text-sm rounded-full border border-gray-700" className="px-2.5 py-1 bg-white/60 border border-stone-100 text-stone-600 text-xs font-medium rounded-md"
> >
{tag} {tag}
</span> </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">
{project.github && (
<a href={project.github} target="_blank" rel="noopener noreferrer" 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" className="text-stone-400 hover:text-stone-900 transition-colors">
<ExternalLink size={18} />
</a>
)}
</div> </div>
<Link <Link
href={`/projects/${project.title.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`} 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" className="inline-flex items-center space-x-1 text-sm font-bold text-stone-800 hover:gap-2 transition-all"
> >
<span>View Project</span> <span>Read More</span>
<ExternalLink size={16} /> <ArrowLeft size={16} className="rotate-180" />
</Link> </Link>
</div> </div>
</div>
</motion.div> </motion.div>
))} ))}
</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>
</div> </div>
); );