Files
portfolio/app/_ui/ProjectsPageClient.tsx
denshooter 91eb446ac5 fix: cleanup footer, smart navigation, and projects redesign
Removed aggressive background text in footer. Implemented intelligent back button for projects. Redesigned project archive page. Stabilized idle quote logic in activity feed.
2026-02-16 01:35:35 +01:00

138 lines
6.2 KiB
TypeScript

"use client";
import { useEffect, useMemo, useState } from "react";
import { motion } from "framer-motion";
import { ArrowUpRight, ArrowLeft, Search } from "lucide-react";
import Link from "next/link";
import { useLocale, useTranslations } from "next-intl";
import Image from "next/image";
export type ProjectListItem = {
id: number;
slug: string;
title: string;
description: string;
tags: string[];
category: string;
date: string;
imageUrl?: string | null;
};
export default function ProjectsPageClient({
projects,
locale,
}: {
projects: ProjectListItem[];
locale: string;
}) {
const tCommon = useTranslations("common");
const tList = useTranslations("projects.list");
const [selectedCategory, setSelectedCategory] = useState("all");
const [searchQuery, setSearchQuery] = useState("");
const categories = useMemo(() => {
const unique = Array.from(new Set(projects.map((p) => p.category))).filter(Boolean);
return ["all", ...unique];
}, [projects]);
const filteredProjects = useMemo(() => {
let result = projects;
if (selectedCategory !== "all") {
result = result.filter((project) => project.category === selectedCategory);
}
if (searchQuery) {
const query = searchQuery.toLowerCase();
result = result.filter(
(p) => p.title.toLowerCase().includes(query) || p.description.toLowerCase().includes(query) || p.tags.some(t => t.toLowerCase().includes(query))
);
}
return result;
}, [projects, selectedCategory, searchQuery]);
return (
<div className="min-h-screen bg-[#fdfcf8] dark:bg-stone-950 pt-40 pb-20 px-6 transition-colors duration-500">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="mb-24">
<Link
href={`/${locale}`}
className="inline-flex items-center gap-2 text-stone-500 hover:text-stone-900 dark:hover:text-white transition-colors mb-10 group"
>
<ArrowLeft size={20} className="group-hover:-translate-x-1 transition-transform" />
<span className="font-bold uppercase tracking-widest text-xs">{tCommon("backToHome")}</span>
</Link>
<h1 className="text-6xl md:text-[10rem] font-black tracking-tighter text-stone-900 dark:text-stone-50 leading-[0.85] uppercase">
Archive<span className="text-liquid-mint">.</span>
</h1>
<p className="mt-8 text-xl md:text-3xl font-light text-stone-500 dark:text-stone-400 max-w-2xl leading-snug tracking-tight">
{tList("intro")}
</p>
</div>
{/* Filters */}
<div className="flex flex-col md:flex-row gap-8 justify-between items-start md:items-center mb-16">
<div className="flex flex-wrap gap-2">
{categories.map((cat) => (
<button
key={cat}
onClick={() => setSelectedCategory(cat)}
className={`px-6 py-2 rounded-full text-[10px] font-black uppercase tracking-widest transition-all ${
selectedCategory === cat
? "bg-stone-900 dark:bg-stone-100 text-white dark:text-stone-900"
: "bg-white dark:bg-stone-900 text-stone-500 border border-stone-200 dark:border-stone-800"
}`}
>
{cat === 'all' ? tList('all') : cat}
</button>
))}
</div>
<div className="relative w-full md:w-80">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 text-stone-400" size={18} />
<input
type="text"
placeholder={tList("searchPlaceholder")}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-white dark:bg-stone-900 border border-stone-200 dark:border-stone-800 rounded-2xl py-4 pl-12 pr-6 focus:outline-none focus:ring-2 focus:ring-liquid-mint/30 transition-all shadow-sm"
/>
</div>
</div>
{/* Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{filteredProjects.map((project) => (
<motion.div key={project.id} initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }}>
<Link href={`/${locale}/projects/${project.slug}`} className="group block h-full">
<div className="bg-white dark:bg-stone-900 rounded-[3rem] p-10 border border-stone-200/60 dark:border-stone-800/60 shadow-sm h-full hover:shadow-xl transition-all flex flex-col">
{project.imageUrl && (
<div className="relative aspect-[16/10] rounded-[2rem] overflow-hidden mb-8 border-4 border-stone-50 dark:border-stone-800 shadow-lg">
<Image src={project.imageUrl} alt={project.title} fill className="object-cover group-hover:scale-105 transition-transform duration-700" />
</div>
)}
<div className="flex-1 flex flex-col">
<div className="flex justify-between items-start mb-4">
<h3 className="text-3xl font-black text-stone-900 dark:text-white uppercase tracking-tight">{project.title}</h3>
<div className="w-12 h-12 rounded-full bg-stone-50 dark:bg-stone-800 flex items-center justify-center group-hover:bg-stone-900 dark:group-hover:bg-stone-50 group-hover:text-white dark:group-hover:text-stone-900 transition-all">
<ArrowUpRight size={20} />
</div>
</div>
<p className="text-stone-500 dark:text-stone-400 font-light text-lg mb-8 line-clamp-3 leading-relaxed">{project.description}</p>
<div className="mt-auto flex flex-wrap gap-2">
{project.tags.slice(0, 3).map(tag => (
<span key={tag} className="px-3 py-1 bg-stone-50 dark:bg-stone-800 rounded-lg text-[9px] font-black uppercase tracking-widest text-stone-400">{tag}</span>
))}
</div>
</div>
</div>
</Link>
</motion.div>
))}
</div>
</div>
</div>
);
}