feat(home): image-led featured projects section

Bring imagery to the homepage projects block (Phase 3), matching the portfolio
gallery.

- Thread coverImagePath through ProjectCardCopy and the home mapping.
- Rewrite ProjectsSection as image-first cards (reusing PortfolioCover with the
  branded placeholder fallback): whole-card link, featured project spans two
  columns with a larger cover, type-scale title, meta + stack badges, hover
  lift. Replaces the previous text-only cards.

Verified: eslint clean; no new type errors (pre-existing Prisma-gen anys only).
This commit is contained in:
MohFarawati
2026-07-14 22:19:05 +02:00
parent f7355b5356
commit 4892f27006
3 changed files with 72 additions and 50 deletions
+1
View File
@@ -180,6 +180,7 @@ export default async function HomePage({ params }: HomePageProps) {
String(project.projectYear), String(project.projectYear),
], ],
featured: index === 0, featured: index === 0,
coverImagePath: project.coverImagePath,
})); }));
const fallbackProjectCards: ProjectCardCopy[] = projectsCopy.fallbackItems.map((item) => ({ const fallbackProjectCards: ProjectCardCopy[] = projectsCopy.fallbackItems.map((item) => ({
+52 -32
View File
@@ -1,8 +1,9 @@
import { ArrowUpRight } from "lucide-react"; import { ArrowUpRight } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { PortfolioCover } from "@/components/site/portfolio-cover";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { AppCard } from "@/components/ui/app-card"; import { cn } from "@/lib/utils";
import { SectionHeading } from "./section-heading"; import { SectionHeading } from "./section-heading";
import type { ProjectCardCopy, ProjectsSectionCopy } from "./types"; import type { ProjectCardCopy, ProjectsSectionCopy } from "./types";
@@ -20,66 +21,85 @@ export function ProjectsSection({ copy, items }: ProjectsSectionProps) {
description={copy.description} description={copy.description}
/> />
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3 xl:gap-6"> <div className="grid grid-cols-1 gap-5 lg:grid-cols-3">
{items.map((item, index) => ( {items.map((item, index) => {
<AppCard const featured = Boolean(item.featured);
return (
<Link
key={`${item.title}-${index}`} key={`${item.title}-${index}`}
layer="single" href={item.href}
interactive className={cn(
className={[ "group flex h-full flex-col overflow-hidden rounded-surface border border-border bg-surface-1 shadow-card transition duration-300 ease-out hover:-translate-y-1 hover:shadow-panel focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50",
"group h-full border-border/80 bg-surface-2 transition-all duration-300 hover:-translate-y-1 hover:border-border-strong hover:bg-background/80 hover:shadow-lg", featured && "lg:col-span-2",
index === 0 ? "lg:col-span-2" : "", )}
].join(" ")}
> >
<div className="flex h-full flex-col gap-6 p-6"> <div
className={cn(
"relative overflow-hidden",
featured ? "aspect-[16/10]" : "aspect-[4/3]",
)}
>
<PortfolioCover
src={item.coverImagePath ?? null}
title={item.title}
category={item.meta[0]}
sizes={
featured
? "(min-width: 1024px) 66vw, 100vw"
: "(min-width: 1024px) 33vw, 100vw"
}
/>
</div>
<div className="flex flex-1 flex-col p-6">
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
{item.meta.map((metaItem) => ( {item.meta.map((metaItem) => (
<Badge <Badge
key={metaItem} key={metaItem}
variant="outline" variant="outline"
className="border-border/80 bg-background text-muted-foreground transition-colors duration-300 group-hover:border-border-strong group-hover:text-foreground/80" className="border-border text-caption text-muted-foreground"
> >
{metaItem} {metaItem}
</Badge> </Badge>
))} ))}
</div> </div>
<div className="space-y-3"> <h3
<h3 className="text-2xl font-semibold tracking-[-0.05em] text-foreground transition-colors duration-300 group-hover:text-foreground/95"> className={cn(
"mt-4 font-semibold tracking-tight text-foreground",
featured ? "text-h3 sm:text-h2" : "text-h3",
)}
>
{item.title} {item.title}
</h3> </h3>
<p className="text-sm leading-7 text-muted-foreground">{item.summary}</p>
</div>
<div className="space-y-3"> <p className="mt-2 line-clamp-2 text-small leading-relaxed text-muted-foreground">
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground"> {item.summary}
{copy.stackLabel}
</p> </p>
<div className="flex flex-wrap gap-2">
{item.stack.length > 0 ? (
<div className="mt-4 flex flex-wrap gap-2">
{item.stack.map((stackItem) => ( {item.stack.map((stackItem) => (
<Badge <Badge
key={stackItem} key={stackItem}
variant="secondary" variant="secondary"
className="bg-secondary/70 text-foreground transition-colors duration-300 group-hover:bg-secondary" className="bg-secondary/70 text-caption text-foreground"
> >
{stackItem} {stackItem}
</Badge> </Badge>
))} ))}
</div> </div>
</div> ) : null}
<div className="mt-auto"> <span className="mt-auto inline-flex items-center gap-2 pt-6 text-caption font-medium text-foreground/70 transition-colors group-hover:text-brand-primary">
<Link
href={item.href}
className="inline-flex items-center gap-2 text-sm font-medium text-foreground transition-all duration-200 group-hover:translate-x-0.5"
>
{item.cta} {item.cta}
<ArrowUpRight className="h-4 w-4 transition-transform duration-200 group-hover:-translate-y-0.5 group-hover:translate-x-0.5" /> <ArrowUpRight className="h-4 w-4 transition-transform duration-300 group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
</span>
</div>
</Link> </Link>
</div> );
</div> })}
</AppCard>
))}
</div> </div>
</section> </section>
); );
+1
View File
@@ -71,6 +71,7 @@ export type ProjectCardCopy = {
cta: string; cta: string;
meta: string[]; meta: string[];
featured?: boolean; featured?: boolean;
coverImagePath?: string | null;
}; };
export type ProjectsSectionCopy = { export type ProjectsSectionCopy = {