feat: complete design overhaul with bento grid and island nav
Refactored About section to use a responsive Bento Grid layout. Redesigned Hero for stronger visual impact. Implemented floating Island navigation. Updated Project cards for cleaner aesthetic.
This commit is contained in:
@@ -1,35 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { motion, Variants } from "framer-motion";
|
||||
import { ExternalLink, Github, ArrowRight, Calendar } from "lucide-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";
|
||||
|
||||
const fadeInUp: Variants = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
duration: 0.5,
|
||||
ease: [0.25, 0.1, 0.25, 1],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const staggerContainer: Variants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.2,
|
||||
delayChildren: 0.1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
interface Project {
|
||||
id: number;
|
||||
slug: string;
|
||||
@@ -53,214 +30,83 @@ const Projects = () => {
|
||||
useEffect(() => {
|
||||
const loadProjects = async () => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
"/api/projects?featured=true&published=true&limit=6",
|
||||
);
|
||||
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) {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.error("Error loading projects:", error);
|
||||
}
|
||||
}
|
||||
} catch (error) {}
|
||||
};
|
||||
loadProjects();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section
|
||||
id="projects"
|
||||
className="py-24 px-4 relative bg-gradient-to-br from-liquid-peach/15 via-liquid-yellow/10 to-liquid-coral/15 overflow-hidden"
|
||||
>
|
||||
<section id="projects" className="py-32 px-4 bg-stone-50 dark:bg-stone-950">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, margin: "-50px" }}
|
||||
variants={fadeInUp}
|
||||
className="text-center mb-20"
|
||||
>
|
||||
<h2 className="text-4xl md:text-6xl font-bold mb-6 text-stone-900">
|
||||
{t("title")}
|
||||
</h2>
|
||||
<p className="text-lg text-stone-600 max-w-2xl mx-auto mt-4 font-light">
|
||||
{t("subtitle")}
|
||||
</p>
|
||||
</motion.div>
|
||||
<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-tight mb-4">
|
||||
Selected Work
|
||||
</h2>
|
||||
<p className="text-xl text-stone-500 max-w-xl">
|
||||
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-bold border-b-2 border-stone-900 dark:border-stone-100 pb-1 hover:opacity-70 transition-opacity">
|
||||
View Archive <ArrowUpRight className="group-hover:-translate-y-1 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, margin: "-50px" }}
|
||||
variants={staggerContainer}
|
||||
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"
|
||||
>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
{projects.map((project) => (
|
||||
<motion.div
|
||||
key={project.id}
|
||||
variants={fadeInUp}
|
||||
whileHover={{ y: -8 }}
|
||||
className="group flex flex-col bg-white/40 backdrop-blur-xl rounded-2xl overflow-hidden border border-white/60 shadow-[0_4px_20px_rgba(0,0,0,0.02)] hover:shadow-[0_20px_40px_rgba(0,0,0,0.06)] transition-[box-shadow,border-color,background-color] duration-500"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
className="group relative"
|
||||
>
|
||||
{/* Project Cover / Image Area */}
|
||||
<div className="relative aspect-[16/10] overflow-hidden bg-stone-100">
|
||||
{project.imageUrl ? (
|
||||
<>
|
||||
<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-1000 ease-out group-hover:scale-110"
|
||||
className="object-cover transition-transform duration-700 group-hover:scale-105"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-stone-900/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
||||
</>
|
||||
) : (
|
||||
<div className="absolute inset-0 bg-stone-200 flex items-center justify-center overflow-hidden">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-stone-300 via-stone-200 to-stone-300" />
|
||||
<div className="absolute top-[-20%] left-[-10%] w-[70%] h-[70%] bg-white/20 rounded-full blur-3xl animate-pulse" />
|
||||
<div className="absolute bottom-[-10%] right-[-5%] w-[60%] h-[60%] bg-stone-400/10 rounded-full blur-2xl" />
|
||||
|
||||
<div className="relative z-10">
|
||||
<span className="text-7xl font-serif font-black text-stone-800/10 group-hover:text-stone-800/20 transition-all duration-700 select-none tracking-tighter">
|
||||
{project.title.charAt(0)}
|
||||
) : (
|
||||
<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>
|
||||
)}
|
||||
|
||||
{/* Texture/Grain Overlay */}
|
||||
<div className="absolute inset-0 opacity-[0.03] pointer-events-none mix-blend-overlay bg-[url('https://grainy-gradients.vercel.app/noise.svg')]" />
|
||||
|
||||
{/* Animated Shine Effect */}
|
||||
<div className="absolute inset-0 translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-1000 ease-in-out bg-gradient-to-r from-transparent via-white/20 to-transparent skew-x-[-20deg] pointer-events-none" />
|
||||
|
||||
{/* Featured Badge */}
|
||||
{project.featured && (
|
||||
<div className="absolute top-3 left-3 z-20">
|
||||
<div className="px-3 py-1 bg-[#292524]/80 backdrop-blur-md text-[#fdfcf8] text-[10px] font-bold uppercase tracking-widest rounded-full shadow-sm border border-white/10">
|
||||
{t("featured")}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Overlay Links */}
|
||||
<div className="absolute inset-0 bg-stone-900/40 opacity-0 group-hover:opacity-100 transition-opacity duration-500 ease-out flex items-center justify-center gap-4 backdrop-blur-[2px] z-20 pointer-events-none">
|
||||
{project.github && (
|
||||
<a
|
||||
href={project.github}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-3 bg-white text-stone-900 rounded-full hover:scale-110 transition-all duration-300 shadow-xl border border-white/50 pointer-events-auto"
|
||||
aria-label="GitHub"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Github size={20} />
|
||||
</a>
|
||||
)}
|
||||
{project.live && !project.title.toLowerCase().includes('kernel panic') && (
|
||||
<a
|
||||
href={project.live}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-3 bg-white text-stone-900 rounded-full hover:scale-110 transition-all duration-300 shadow-xl border border-white/50 pointer-events-auto"
|
||||
aria-label="Live Demo"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<ExternalLink size={20} />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6 flex flex-col flex-1">
|
||||
{/* Stretched Link covering the whole card (including image area) */}
|
||||
<Link
|
||||
href={`/${locale}/projects/${project.slug}`}
|
||||
className="absolute inset-0 z-10"
|
||||
aria-label={`View project ${project.title}`}
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-xl font-bold text-stone-900 group-hover:text-stone-600 transition-colors">
|
||||
{project.title}
|
||||
</h3>
|
||||
<div className="flex items-center space-x-2 text-stone-400 text-xs font-mono bg-white/50 px-2 py-1 rounded border border-stone-100">
|
||||
<Calendar size={12} />
|
||||
<span>
|
||||
{(() => {
|
||||
const d = new Date(project.date);
|
||||
return isNaN(d.getTime()) ? project.date : d.getFullYear();
|
||||
})()}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-stone-600 mb-6 leading-relaxed line-clamp-3 text-sm flex-1">
|
||||
{project.description}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap gap-2 mb-6">
|
||||
{project.tags.slice(0, 4).map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="px-2.5 py-1 bg-white/60 border border-stone-100 text-stone-600 text-xs font-medium rounded-md"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
{project.tags.length > 4 && (
|
||||
<span className="px-2 py-1 text-stone-400 text-xs">+ {project.tags.length - 4}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-auto pt-4 border-t border-stone-100 flex items-center justify-between relative z-20">
|
||||
<div className="flex gap-3">
|
||||
{project.github && (
|
||||
<a
|
||||
href={project.github}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-stone-400 hover:text-stone-900 transition-colors relative z-20 hover:scale-110"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Github size={18} />
|
||||
</a>
|
||||
)}
|
||||
{project.live && !project.title.toLowerCase().includes('kernel panic') && (
|
||||
<a
|
||||
href={project.live}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-stone-400 hover:text-stone-900 transition-colors relative z-20 hover:scale-110"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<ExternalLink size={18} />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.8, delay: 0.3, ease: [0.25, 0.1, 0.25, 1] }}
|
||||
className="mt-16 text-center"
|
||||
>
|
||||
<Link
|
||||
href={`/${locale}/projects`}
|
||||
className="inline-flex items-center gap-2 px-8 py-4 bg-white border border-stone-200 rounded-full text-stone-700 font-medium hover:bg-stone-50 hover:border-stone-300 hover:gap-3 transition-all duration-500 ease-out shadow-sm hover:shadow-md"
|
||||
>
|
||||
{t("viewAll")} <ArrowRight size={16} />
|
||||
</Link>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user