186 lines
6.8 KiB
TypeScript
186 lines
6.8 KiB
TypeScript
"use client";
|
|
|
|
import { motion } from 'framer-motion';
|
|
import { ExternalLink, Calendar, Tag, ArrowLeft, Github as GithubIcon } 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;
|
|
}
|
|
|
|
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) {
|
|
setProject(data.projects[0]);
|
|
}
|
|
} else {
|
|
console.error('Failed to fetch project from API');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading project:', error);
|
|
}
|
|
};
|
|
|
|
loadProject();
|
|
}, [slug]);
|
|
|
|
if (!project) {
|
|
return (
|
|
<div className="min-h-screen animated-bg flex items-center justify-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>
|
|
<p className="text-gray-400">Loading project...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen animated-bg">
|
|
<div className="max-w-4xl 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="/projects"
|
|
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 Projects</span>
|
|
</Link>
|
|
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h1 className="text-4xl md:text-5xl font-bold gradient-text">
|
|
{project.title}
|
|
</h1>
|
|
{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">
|
|
Featured
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
<p className="text-xl text-gray-400 mb-6">
|
|
{project.description}
|
|
</p>
|
|
|
|
{/* Project Meta */}
|
|
<div className="flex flex-wrap items-center gap-6 text-gray-400 mb-8">
|
|
<div className="flex items-center space-x-2">
|
|
<Calendar size={20} />
|
|
<span>{project.date}</span>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<Tag size={20} />
|
|
<span>{project.category}</span>
|
|
</div>
|
|
</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>
|
|
|
|
{/* Action Buttons */}
|
|
<div className="flex flex-wrap gap-4">
|
|
<motion.a
|
|
href={project.github}
|
|
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} />
|
|
<span>View Code</span>
|
|
</motion.a>
|
|
|
|
{project.live !== "#" && (
|
|
<motion.a
|
|
href={project.live}
|
|
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-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
|
|
>
|
|
<ExternalLink size={20} />
|
|
<span>Live Demo</span>
|
|
</motion.a>
|
|
)}
|
|
</div>
|
|
</motion.div>
|
|
|
|
{/* Project Content */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 30 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.8, delay: 0.2 }}
|
|
className="glass-card p-8 rounded-2xl"
|
|
>
|
|
<div className="markdown prose prose-invert max-w-none text-white">
|
|
<ReactMarkdown
|
|
components={{
|
|
h1: ({children}) => <h1 className="text-3xl font-bold text-white mb-4">{children}</h1>,
|
|
h2: ({children}) => <h2 className="text-2xl font-semibold text-white mb-3">{children}</h2>,
|
|
h3: ({children}) => <h3 className="text-xl font-semibold text-white mb-2">{children}</h3>,
|
|
p: ({children}) => <p className="text-gray-300 mb-3 leading-relaxed">{children}</p>,
|
|
ul: ({children}) => <ul className="list-disc list-inside text-gray-300 mb-3 space-y-1">{children}</ul>,
|
|
ol: ({children}) => <ol className="list-decimal list-inside text-gray-300 mb-3 space-y-1">{children}</ol>,
|
|
li: ({children}) => <li className="text-gray-300">{children}</li>,
|
|
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}
|
|
</ReactMarkdown>
|
|
</div>
|
|
</motion.div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ProjectDetail;
|