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 (
+
+ );
+ }
+
+ 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 (
+
+ );
+ }
+
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}
+
+
+
+
+
+ );
+ })}
);
}