Some checks failed
Dev Deployment (Zero Downtime) / deploy-dev (push) Has been cancelled
- Hero: smoother font scaling (text-[2.75rem] -> sm -> md -> lg), smaller photo on mobile, reduced gaps and padding - About: responsive bento grid with smaller border-radius, compact hobbies grid (2-col on mobile), hidden descriptions on small screens - Projects: wider aspect ratio on mobile (16/10), show tags from sm:, smoother title scaling - Contact: compact form inputs, responsive connect links, smaller gaps - Footer: reduced top padding and gap on mobile - HomePage: smaller wave separators (h-12 on mobile) - 404: compact card padding and button sizing - ActivityFeed: smaller quote text and min-height on mobile Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
133 lines
5.3 KiB
TypeScript
133 lines
5.3 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();
|
|
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-16 sm:py-24 md:py-32 px-4 sm:px-6 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-start md:items-end mb-8 sm:mb-12 md:mb-16 gap-4 sm:gap-6">
|
|
<div>
|
|
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-black text-stone-900 dark:text-stone-50 tracking-tighter mb-2 sm:mb-4 uppercase">
|
|
Selected Work<span className="text-liquid-mint">.</span>
|
|
</h2>
|
|
<p className="text-base sm:text-lg md: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-6 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-[16/10] sm:aspect-[4/3] rounded-2xl sm:rounded-3xl overflow-hidden bg-stone-200 dark:bg-stone-900 mb-4 sm: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-lg sm:text-xl md:text-2xl font-bold text-stone-900 dark:text-stone-100 mb-1 sm:mb-2 group-hover:underline decoration-2 underline-offset-4">
|
|
{project.title}
|
|
</h3>
|
|
<p className="text-sm sm:text-base text-stone-500 dark:text-stone-400 line-clamp-2 max-w-md">
|
|
{project.description}
|
|
</p>
|
|
</div>
|
|
<div className="hidden sm: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;
|