Fixed map parentheses syntax errors, resolved missing ActivityFeedClient imports, and corrected ActivityFeed prop types for idleQuote support. All systems green.
133 lines
5.1 KiB
TypeScript
133 lines
5.1 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { motion } from "framer-motion";
|
|
import { ArrowUpRight } from "lucide-react";
|
|
import Link from "next/link";
|
|
import Image from "next/image";
|
|
import { useLocale, useTranslations } from "next-intl";
|
|
import { Skeleton } from "./ui/Skeleton";
|
|
|
|
interface Project {
|
|
id: number;
|
|
slug: string;
|
|
title: string;
|
|
description: string;
|
|
content: string;
|
|
tags: string[];
|
|
featured: boolean;
|
|
category: string;
|
|
date: string;
|
|
github?: string;
|
|
live?: string;
|
|
imageUrl?: string;
|
|
}
|
|
|
|
const Projects = () => {
|
|
const [projects, setProjects] = useState<Project[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const locale = useLocale();
|
|
const t = useTranslations("home.projects");
|
|
|
|
useEffect(() => {
|
|
const loadProjects = async () => {
|
|
try {
|
|
const response = await fetch("/api/projects?featured=true&published=true&limit=6");
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
setProjects(data.projects || []);
|
|
}
|
|
} catch (error) {
|
|
console.error("Featured projects fetch failed:", error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
loadProjects();
|
|
}, []);
|
|
|
|
return (
|
|
<section id="projects" className="py-32 px-4 bg-stone-50 dark:bg-stone-950">
|
|
<div className="max-w-7xl mx-auto">
|
|
<div className="flex flex-col md:flex-row justify-between items-end mb-16 gap-6">
|
|
<div>
|
|
<h2 className="text-4xl md:text-6xl font-black text-stone-900 dark:text-stone-50 tracking-tighter mb-4 uppercase">
|
|
Selected Work<span className="text-liquid-mint">.</span>
|
|
</h2>
|
|
<p className="text-xl text-stone-500 max-w-xl font-light">
|
|
Projects that pushed my boundaries.
|
|
</p>
|
|
</div>
|
|
<Link href={`/${locale}/projects`} className="group flex items-center gap-2 text-stone-900 dark:text-stone-100 font-black border-b-2 border-stone-900 dark:border-stone-100 pb-1 hover:opacity-70 transition-all text-xs uppercase tracking-widest">
|
|
View Archive <ArrowUpRight className="group-hover:-translate-y-1 group-hover:translate-x-1 transition-transform" size={14} />
|
|
</Link>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-12">
|
|
{loading ? (
|
|
Array.from({ length: 2 }).map((_, i) => (
|
|
<div key={i} className="space-y-6">
|
|
<Skeleton className="aspect-[4/3] rounded-[2.5rem]" />
|
|
<div className="space-y-3">
|
|
<Skeleton className="h-8 w-1/2" />
|
|
<Skeleton className="h-4 w-3/4" />
|
|
</div>
|
|
</div>
|
|
))
|
|
) : (
|
|
projects.map((project) => (
|
|
<motion.div
|
|
key={project.id}
|
|
initial={{ opacity: 0, y: 20 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
className="group relative"
|
|
>
|
|
<Link href={`/${locale}/projects/${project.slug}`} className="block">
|
|
{/* Image Card */}
|
|
<div className="relative aspect-[4/3] rounded-3xl overflow-hidden bg-stone-200 dark:bg-stone-900 mb-6">
|
|
{project.imageUrl ? (
|
|
<Image
|
|
src={project.imageUrl}
|
|
alt={project.title}
|
|
fill
|
|
className="object-cover transition-transform duration-700 group-hover:scale-105"
|
|
/>
|
|
) : (
|
|
<div className="absolute inset-0 flex items-center justify-center bg-gradient-to-br from-stone-100 to-stone-200 dark:from-stone-800 dark:to-stone-900">
|
|
<span className="text-4xl font-bold text-stone-300 dark:text-stone-700">{project.title.charAt(0)}</span>
|
|
</div>
|
|
)}
|
|
{/* Overlay on Hover */}
|
|
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/10 transition-colors duration-500" />
|
|
</div>
|
|
|
|
{/* Text Content */}
|
|
<div className="flex justify-between items-start">
|
|
<div>
|
|
<h3 className="text-2xl font-bold text-stone-900 dark:text-stone-100 mb-2 group-hover:underline decoration-2 underline-offset-4">
|
|
{project.title}
|
|
</h3>
|
|
<p className="text-stone-500 dark:text-stone-400 line-clamp-2 max-w-md">
|
|
{project.description}
|
|
</p>
|
|
</div>
|
|
<div className="hidden md:flex gap-2">
|
|
{project.tags.slice(0, 2).map(tag => (
|
|
<span key={tag} className="px-3 py-1 rounded-full border border-stone-200 dark:border-stone-800 text-xs font-medium text-stone-600 dark:text-stone-400">
|
|
{tag}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</Link>
|
|
</motion.div>
|
|
)))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
export default Projects;
|