251 lines
11 KiB
TypeScript
251 lines
11 KiB
TypeScript
"use client";
|
|
|
|
import { motion } from 'framer-motion';
|
|
import { ExternalLink, Calendar, ArrowLeft, Github as GithubIcon, Share2 } from 'lucide-react';
|
|
import Link from 'next/link';
|
|
import { useParams } from 'next/navigation';
|
|
import { useState, useEffect } from 'react';
|
|
import ReactMarkdown from 'react-markdown';
|
|
|
|
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 ProjectDetail = () => {
|
|
const params = useParams();
|
|
const slug = params.slug as string;
|
|
const [project, setProject] = useState<Project | null>(null);
|
|
|
|
// Load project from API by slug
|
|
useEffect(() => {
|
|
const loadProject = async () => {
|
|
try {
|
|
const response = await fetch(`/api/projects/search?slug=${slug}`);
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
if (data.projects && data.projects.length > 0) {
|
|
const loadedProject = data.projects[0];
|
|
setProject(loadedProject);
|
|
|
|
// Track page view
|
|
try {
|
|
await fetch('/api/analytics/track', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
type: 'pageview',
|
|
projectId: loadedProject.id.toString(),
|
|
page: `/projects/${slug}`
|
|
})
|
|
});
|
|
} catch (trackError) {
|
|
// Silently fail tracking
|
|
if (process.env.NODE_ENV === 'development') {
|
|
console.error('Error tracking page view:', trackError);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
if (process.env.NODE_ENV === 'development') {
|
|
console.error('Error loading project:', error);
|
|
}
|
|
}
|
|
};
|
|
|
|
loadProject();
|
|
}, [slug]);
|
|
|
|
if (!project) {
|
|
return (
|
|
<div className="min-h-screen bg-[#fdfcf8] flex items-center justify-center">
|
|
<div className="text-center">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-stone-800 mx-auto mb-4"></div>
|
|
<p className="text-stone-500 font-medium">Loading project...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-[#fdfcf8] pt-32 pb-20">
|
|
<div className="max-w-4xl mx-auto px-4">
|
|
{/* Navigation */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.6 }}
|
|
className="mb-8"
|
|
>
|
|
<Link
|
|
href="/projects"
|
|
className="inline-flex items-center space-x-2 text-stone-500 hover:text-stone-900 transition-colors group"
|
|
>
|
|
<ArrowLeft size={20} className="group-hover:-translate-x-1 transition-transform" />
|
|
<span className="font-medium">Back to Projects</span>
|
|
</Link>
|
|
</motion.div>
|
|
|
|
{/* Header & Meta */}
|
|
<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}
|
|
</h1>
|
|
<div className="flex gap-2 shrink-0 pt-2">
|
|
{project.featured && (
|
|
<span className="px-4 py-1.5 bg-stone-900 text-stone-50 text-xs font-bold rounded-full shadow-sm">
|
|
Featured
|
|
</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>
|
|
|
|
<p className="text-xl md:text-2xl text-stone-600 font-light leading-relaxed max-w-3xl mb-8">
|
|
{project.description}
|
|
</p>
|
|
|
|
<div className="flex flex-wrap items-center gap-6 text-stone-500 text-sm border-y border-stone-200 py-6">
|
|
<div className="flex items-center space-x-2">
|
|
<Calendar size={18} />
|
|
<span className="font-mono">{new Date(project.date).toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })}</span>
|
|
</div>
|
|
<div className="h-4 w-px bg-stone-300 hidden sm:block"></div>
|
|
<div className="flex flex-wrap gap-2">
|
|
{project.tags.map(tag => (
|
|
<span key={tag} className="text-stone-700 font-medium">#{tag}</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
|
|
{/* Featured Image / Fallback */}
|
|
<motion.div
|
|
initial={{ opacity: 0, scale: 0.95 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
transition={{ duration: 0.8, delay: 0.2 }}
|
|
className="mb-16 rounded-2xl overflow-hidden shadow-2xl bg-stone-100 aspect-video relative"
|
|
>
|
|
{project.imageUrl ? (
|
|
<img
|
|
src={project.imageUrl}
|
|
alt={project.title}
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
) : (
|
|
<div className="absolute inset-0 bg-gradient-to-br from-stone-200 to-stone-300 flex items-center justify-center">
|
|
<span className="text-9xl font-serif font-bold text-stone-500/20 select-none">
|
|
{project.title.charAt(0)}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</motion.div>
|
|
|
|
|
|
{/* Content & Sidebar Layout */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-12">
|
|
{/* Main Content */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 30 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.8, delay: 0.3 }}
|
|
className="lg:col-span-2"
|
|
>
|
|
<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
|
|
components={{
|
|
// Custom components to ensure styling matches
|
|
h1: ({children}) => <h1 className="text-3xl font-bold text-stone-900 mt-8 mb-4">{children}</h1>,
|
|
h2: ({children}) => <h2 className="text-2xl font-bold text-stone-900 mt-8 mb-4">{children}</h2>,
|
|
p: ({children}) => <p className="text-stone-700 leading-relaxed mb-6">{children}</p>,
|
|
li: ({children}) => <li className="text-stone-700">{children}</li>,
|
|
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>,
|
|
pre: ({children}) => <pre className="bg-stone-900 text-stone-50 p-6 rounded-xl overflow-x-auto my-6 shadow-lg">{children}</pre>,
|
|
}}
|
|
>
|
|
{project.content}
|
|
</ReactMarkdown>
|
|
</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>
|
|
);
|
|
};
|
|
|
|
export default ProjectDetail; |