diff --git a/components/site/portfolio-cover.tsx b/components/site/portfolio-cover.tsx new file mode 100644 index 0000000..65cf00a --- /dev/null +++ b/components/site/portfolio-cover.tsx @@ -0,0 +1,67 @@ +import Image from "next/image"; +import { ImageIcon } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +type PortfolioCoverProps = { + src: string | null; + title: string; + category?: string; + sizes?: string; + priority?: boolean; + className?: string; +}; + +const DEFAULT_SIZES = + "(min-width: 1024px) 33vw, (min-width: 640px) 50vw, 100vw"; + +/** + * Project cover. Renders the stored image when present, otherwise a restrained + * branded placeholder (owner will replace placeholders with real artwork). + * Fills a positioned, aspect-sized parent (`relative` + `aspect-*`). + */ +export function PortfolioCover({ + src, + title, + category, + sizes = DEFAULT_SIZES, + priority = false, + className, +}: PortfolioCoverProps) { + if (src) { + return ( + {title} + ); + } + + const monogram = title.trim().charAt(0).toUpperCase() || "•"; + + return ( +
+ + + {monogram} + + + + {category} + +
+ ); +} diff --git a/components/site/portfolio-project-grid.tsx b/components/site/portfolio-project-grid.tsx index 7e5a77e..627c43e 100644 --- a/components/site/portfolio-project-grid.tsx +++ b/components/site/portfolio-project-grid.tsx @@ -2,10 +2,10 @@ import { ArrowUpRight, CalendarDays } from "lucide-react"; import Link from "next/link"; import { MotionFade } from "@/components/motion-fade"; -import { AppCard } from "@/components/ui/app-card"; -import { CardContent } from "@/components/ui/card"; +import { PortfolioCover } from "@/components/site/portfolio-cover"; import { getLocalizedPath, type AppLocale } from "@/lib/locale"; import { getLocalizedValue, type PortfolioProjectView } from "@/lib/portfolio"; +import { cn } from "@/lib/utils"; type PortfolioProjectGridProps = { locale: AppLocale; @@ -22,48 +22,85 @@ export function PortfolioProjectGrid({ emptyLabel, openLabel, }: PortfolioProjectGridProps) { + if (projects.length === 0) { + return ( +
+
+ {emptyLabel} +
+
+ ); + } + return ( -
- {projects.map((item, index) => ( - - - - + {projects.map((item, index) => { + const featured = item.isFeatured; + const title = getLocalizedValue(item.title, locale); + const category = getLocalizedValue(item.category.name, locale); + + return ( + + +
+ +
+ +
-

- {getLocalizedValue(item.category.name, locale)} -

-

+ + {category} + + {item.projectYear} -

+
-

- {getLocalizedValue(item.title, locale)} + +

+ {title}

-

+ +

{getLocalizedValue(item.summary, locale)}

-

- {openLabel} - -

- - - - - ))} - {projects.length === 0 ? ( - - - {emptyLabel} - - - ) : null} + + {openLabel} + + +
+ +
+ ); + })}
); }