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:
@@ -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) => ({
|
||||||
|
|||||||
@@ -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);
|
||||||
key={`${item.title}-${index}`}
|
|
||||||
layer="single"
|
return (
|
||||||
interactive
|
<Link
|
||||||
className={[
|
key={`${item.title}-${index}`}
|
||||||
"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",
|
href={item.href}
|
||||||
index === 0 ? "lg:col-span-2" : "",
|
className={cn(
|
||||||
].join(" ")}
|
"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",
|
||||||
>
|
featured && "lg:col-span-2",
|
||||||
<div className="flex h-full flex-col gap-6 p-6">
|
)}
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
>
|
||||||
{item.meta.map((metaItem) => (
|
<div
|
||||||
<Badge
|
className={cn(
|
||||||
key={metaItem}
|
"relative overflow-hidden",
|
||||||
variant="outline"
|
featured ? "aspect-[16/10]" : "aspect-[4/3]",
|
||||||
className="border-border/80 bg-background text-muted-foreground transition-colors duration-300 group-hover:border-border-strong group-hover:text-foreground/80"
|
)}
|
||||||
>
|
>
|
||||||
{metaItem}
|
<PortfolioCover
|
||||||
</Badge>
|
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>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="flex flex-1 flex-col p-6">
|
||||||
<h3 className="text-2xl font-semibold tracking-[-0.05em] text-foreground transition-colors duration-300 group-hover:text-foreground/95">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
{item.title}
|
{item.meta.map((metaItem) => (
|
||||||
</h3>
|
|
||||||
<p className="text-sm leading-7 text-muted-foreground">{item.summary}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground">
|
|
||||||
{copy.stackLabel}
|
|
||||||
</p>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{item.stack.map((stackItem) => (
|
|
||||||
<Badge
|
<Badge
|
||||||
key={stackItem}
|
key={metaItem}
|
||||||
variant="secondary"
|
variant="outline"
|
||||||
className="bg-secondary/70 text-foreground transition-colors duration-300 group-hover:bg-secondary"
|
className="border-border text-caption text-muted-foreground"
|
||||||
>
|
>
|
||||||
{stackItem}
|
{metaItem}
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-auto">
|
<h3
|
||||||
<Link
|
className={cn(
|
||||||
href={item.href}
|
"mt-4 font-semibold tracking-tight text-foreground",
|
||||||
className="inline-flex items-center gap-2 text-sm font-medium text-foreground transition-all duration-200 group-hover:translate-x-0.5"
|
featured ? "text-h3 sm:text-h2" : "text-h3",
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
|
{item.title}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<p className="mt-2 line-clamp-2 text-small leading-relaxed text-muted-foreground">
|
||||||
|
{item.summary}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{item.stack.length > 0 ? (
|
||||||
|
<div className="mt-4 flex flex-wrap gap-2">
|
||||||
|
{item.stack.map((stackItem) => (
|
||||||
|
<Badge
|
||||||
|
key={stackItem}
|
||||||
|
variant="secondary"
|
||||||
|
className="bg-secondary/70 text-caption text-foreground"
|
||||||
|
>
|
||||||
|
{stackItem}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<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">
|
||||||
{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" />
|
||||||
</Link>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Link>
|
||||||
</AppCard>
|
);
|
||||||
))}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
Reference in New Issue
Block a user