diff --git a/apps/storefront/app/products/[slug]/page.tsx b/apps/storefront/app/products/[slug]/page.tsx
index bfe9b76..03e3974 100644
--- a/apps/storefront/app/products/[slug]/page.tsx
+++ b/apps/storefront/app/products/[slug]/page.tsx
@@ -55,7 +55,7 @@ export default async function ProductPage({ params }: Params) {
{product.garment}
- {product.artworkTitle}
+ {product.title}
From the{' '}
diff --git a/apps/storefront/app/shop/page.tsx b/apps/storefront/app/shop/page.tsx
index a664c97..ebf4991 100644
--- a/apps/storefront/app/shop/page.tsx
+++ b/apps/storefront/app/shop/page.tsx
@@ -14,13 +14,16 @@ export const metadata: Metadata = {
export default async function ShopPage() {
const products = await dataProvider.listProducts();
+ const designSlugs = new Set(suppliedShopDesigns.map((d) => d.slug));
+ const clothingDesigns = products.filter((p) => designSlugs.has(p.slug));
+ const availablePieces = products.filter((p) => !designSlugs.has(p.slug));
return (
@@ -32,25 +35,30 @@ export default async function ShopPage() {
Clothing designs
- Every supplied shirt and worn-piece image lives here in Shop, linked to the artwork it
- carries.
+ Studio-supplied shirts you can buy as-is — open a piece to choose size, add to bag, and
+ leave a review.
-
- {suppliedShopDesigns.map((design, i) => (
-
-
-
-
-
- ))}
-
+ {clothingDesigns.length === 0 ? (
+
+
+
+ ) : (
+
+ {clothingDesigns.map((product, i) => (
+
+
+
+
+
+ ))}
+
+ )}
- {products.length === 0 ? (
-
-
-
- ) : (
+ {availablePieces.length === 0 ? null : (
- {products.map((product, i) => (
+ {availablePieces.map((product, i) => (
diff --git a/apps/storefront/components/product/product-card.tsx b/apps/storefront/components/product/product-card.tsx
index edc71eb..538a2cd 100644
--- a/apps/storefront/components/product/product-card.tsx
+++ b/apps/storefront/components/product/product-card.tsx
@@ -16,7 +16,7 @@ const availabilityLabel = {
* The image is the product's artwork (products have no photos; the drawing is the product).
*/
export function ProductCard({ product }: { product: ProductSummary }) {
- const src = artworkImage(product.artworkSlug);
+ const src = product.image ?? artworkImage(product.artworkSlug);
const badge = availabilityLabel[product.availability];
return (
@@ -27,7 +27,11 @@ export function ProductCard({ product }: { product: ProductSummary }) {
>
diff --git a/apps/storefront/components/product/product-configurator.tsx b/apps/storefront/components/product/product-configurator.tsx
index 9f329a0..aa20dd3 100644
--- a/apps/storefront/components/product/product-configurator.tsx
+++ b/apps/storefront/components/product/product-configurator.tsx
@@ -2,6 +2,7 @@
import { Alert, Badge, Price, cn } from '@tms/ui';
import { Minus, Plus, ShoppingBag } from 'lucide-react';
+import Image from 'next/image';
import { useMemo, useRef, useState } from 'react';
import { WishlistButton } from '@/components/account/wishlist-button';
import { Accordion } from '@/components/site/accordion';
@@ -33,6 +34,7 @@ export function ProductConfigurator({ product }: { product: ProductDetail }) {
const selectedColour = product.colours.find((c) => c.name === colour);
const soldOut = product.availability === 'sold_out';
const print = artworkImage(product.artworkSlug);
+ const productPhoto = product.image ?? null;
function addToBag() {
if (soldOut) return;
@@ -69,46 +71,67 @@ export function ProductConfigurator({ product }: { product: ProductDetail }) {
return (
- {/* Preview */}
+ {/* Preview — prefer a studio product photo when one was supplied. */}
-
-
- {view}
-
-
-
- {(['front', 'back'] as View[]).map((v) => (
-
setView(v)}
- aria-pressed={view === v}
- className={cn(
- 'rounded px-3 py-1.5 text-sm capitalize outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-focus-ring)]',
- view === v ? 'bg-accent text-on-accent' : 'text-ink-2 hover:text-ink',
- )}
- >
- {v}
-
- ))}
+ {productPhoto ? (
+
+
+
+ ) : (
+
+ )}
+ {productPhoto ? null : (
+
+ {view}
+
+ )}
+ {productPhoto ? null : (
+
+ {(['front', 'back'] as View[]).map((v) => (
+ setView(v)}
+ aria-pressed={view === v}
+ className={cn(
+ 'rounded px-3 py-1.5 text-sm capitalize outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-focus-ring)]',
+ view === v ? 'bg-accent text-on-accent' : 'text-ink-2 hover:text-ink',
+ )}
+ >
+ {v}
+
+ ))}
+
+ )}
{/* Configuration */}
diff --git a/apps/storefront/components/product/shop-design-card.tsx b/apps/storefront/components/product/shop-design-card.tsx
index b5d4453..87f0684 100644
--- a/apps/storefront/components/product/shop-design-card.tsx
+++ b/apps/storefront/components/product/shop-design-card.tsx
@@ -1,34 +1,40 @@
+import { Price } from '@tms/ui';
import Link from 'next/link';
import { TileBadge, TileImage } from '@/components/site/tile';
-import type { SuppliedShopDesign } from '@/lib/data/supplied-catalogue';
+import type { ProductSummary } from '@/lib/data';
-/** A supplied clothing photograph linked back to its immutable artwork canvas. */
+/** A studio-supplied clothing photograph that opens as a buyable product page. */
export function ShopDesignCard({
- design,
+ product,
priority = false,
}: {
- design: SuppliedShopDesign;
+ product: ProductSummary;
priority?: boolean;
}) {
+ const src = product.image ?? null;
+
return (
Clothing design}
+ badge={Ready to buy }
/>
- {design.title}
+ {product.title}
-
-
{design.garment}
-
Design yours
+
+
{product.garment}
+
+
+
+
Shop now
);
diff --git a/apps/storefront/lib/data/mock.ts b/apps/storefront/lib/data/mock.ts
index bd62800..60ed8c6 100644
--- a/apps/storefront/lib/data/mock.ts
+++ b/apps/storefront/lib/data/mock.ts
@@ -5,7 +5,7 @@ import { filterApproved } from '../community';
import { summariseReviews } from '../reviews';
import { artworkImage } from '../artwork-images';
import { countShoppableItems, storyHotspotTargets } from '../stories';
-import { suppliedArtworkSeeds } from './supplied-catalogue';
+import { suppliedArtworkSeeds, suppliedShopDesigns } from './supplied-catalogue';
import type {
ArtworkDetail,
ArtworkPassport,
@@ -57,6 +57,10 @@ interface ProductSeed {
colours: string[];
unavailableColours?: string[];
soldOutSizes?: string[];
+ /** Shop-facing title when it should differ from `artwork — garment`. */
+ displayTitle?: string;
+ /** Product photograph when the studio supplied a flat-lay / worn shot. */
+ image?: string;
}
const productSeeds: ProductSeed[] = [
@@ -124,13 +128,30 @@ const productSeeds: ProductSeed[] = [
availability: 'available',
colours: ['Black', 'Slate', 'Sand'],
},
+ // Studio-supplied clothing photographs — buyable products in Shop (not Design Studio links).
+ ...suppliedShopDesigns.map
((design) => {
+ const artwork = suppliedArtworkSeeds.find((a) => a.slug === design.artworkSlug);
+ const isBlack = design.garment.toLowerCase().includes('black');
+ return {
+ slug: design.slug,
+ artworkSlug: design.artworkSlug,
+ artworkTitle: suppliedArtworkTitle(design.artworkSlug),
+ collection: artwork?.collection ?? 'Africa United',
+ garment: 'Classic T-shirt',
+ displayTitle: design.title,
+ image: design.image,
+ priceMinor: 1400000,
+ availability: 'available',
+ colours: isBlack ? ['Black', 'Slate'] : ['Bone', 'Sand'],
+ };
+ }),
];
function toProductSummary(seed: ProductSeed): ProductSummary {
return {
id: seed.slug,
slug: seed.slug,
- title: `${seed.artworkTitle} — ${seed.garment}`,
+ title: seed.displayTitle ?? `${seed.artworkTitle} — ${seed.garment}`,
artworkSlug: seed.artworkSlug,
artworkTitle: seed.artworkTitle,
collection: seed.collection,
@@ -139,6 +160,7 @@ function toProductSummary(seed: ProductSeed): ProductSummary {
currency: 'NGN',
availability: seed.availability,
colourCount: seed.colours.length,
+ image: seed.image ?? null,
};
}
@@ -703,6 +725,50 @@ function toStorySummary(seed: StorySeed): StorySummary {
* read/write/moderation is server-authoritative (TMS-FBR-008).
*/
const reviewSeeds: Record = {
+ 'product:africa-united-white-tee': [
+ {
+ id: 'rv-auw-1',
+ rating: 5,
+ title: 'Wears like the studio shot',
+ body: 'Print is crisp on the white cotton and the fit is true to size. Exactly what I ordered from Shop.',
+ author: 'Amaka D.',
+ createdAt: '2026-07-12T10:00:00.000Z',
+ verifiedPurchase: true,
+ },
+ ],
+ 'product:africa-united-black-tee': [
+ {
+ id: 'rv-aub-1',
+ rating: 5,
+ title: 'Black tee, loud print',
+ body: 'The duo graphic pops on black. Soft hand-feel after one wash and no fading yet.',
+ author: 'Chidi O.',
+ createdAt: '2026-07-10T15:20:00.000Z',
+ verifiedPurchase: true,
+ },
+ ],
+ 'product:resilience-white-tee': [
+ {
+ id: 'rv-rw-1',
+ rating: 4,
+ title: 'Poster energy on cotton',
+ body: 'Love the type and the heritage symbols. Sized up for a looser street fit and it works.',
+ author: 'Zainab M.',
+ createdAt: '2026-07-08T09:10:00.000Z',
+ verifiedPurchase: true,
+ },
+ ],
+ 'product:africa-united-model-tee': [
+ {
+ id: 'rv-aum-1',
+ rating: 5,
+ title: 'Looks like the worn photo',
+ body: 'Bought the black worn piece and it matches the shop photo. Easy add-to-bag flow, arrived in five days.',
+ author: 'Ifeanyi B.',
+ createdAt: '2026-07-14T12:40:00.000Z',
+ verifiedPurchase: true,
+ },
+ ],
'product:midnight-in-lagos-classic-tee': [
{
id: 'rv-mil-1',
diff --git a/apps/storefront/lib/data/supplied-catalogue.spec.ts b/apps/storefront/lib/data/supplied-catalogue.spec.ts
index c63376e..7275c06 100644
--- a/apps/storefront/lib/data/supplied-catalogue.spec.ts
+++ b/apps/storefront/lib/data/supplied-catalogue.spec.ts
@@ -22,14 +22,28 @@ describe('studio-supplied catalogue media', () => {
}
});
- it('keeps clothing photographs in Shop and links them to artwork', () => {
+ it('keeps clothing photographs in Shop as buyable products', async () => {
expect(suppliedShopDesigns).toHaveLength(4);
+ const products = await mockProvider.listProducts();
+ const bySlug = new Map(products.map((p) => [p.slug, p]));
const artworkSlugs = new Set(suppliedArtworkSeeds.map(({ slug }) => slug));
+
for (const design of suppliedShopDesigns) {
expect(design.image).toMatch(/^\/products\/.+\.jpg$/);
expect(artworkSlugs.has(design.artworkSlug)).toBe(true);
expect(existsSync(join(process.cwd(), 'public', design.image.slice(1)))).toBe(true);
+
+ const product = bySlug.get(design.slug);
+ expect(product).toBeDefined();
+ expect(product?.image).toBe(design.image);
+ expect(product?.artworkSlug).toBe(design.artworkSlug);
+ expect(product?.availability).toBe('available');
+
+ const detail = await mockProvider.getProduct(design.slug);
+ expect(detail).not.toBeNull();
+ expect(detail?.colours.length).toBeGreaterThan(0);
+ expect(detail?.sizes.some((s) => s.available)).toBe(true);
}
});
diff --git a/apps/storefront/lib/data/types.ts b/apps/storefront/lib/data/types.ts
index 69c9728..0c49c68 100644
--- a/apps/storefront/lib/data/types.ts
+++ b/apps/storefront/lib/data/types.ts
@@ -258,6 +258,11 @@ export interface ProductSummary {
currency: string;
availability: Availability;
colourCount: number;
+ /**
+ * Optional product photograph (flat-lay / worn shot). When set, shop tiles and the product
+ * page prefer it over composing a garment mockup from the artwork plate alone.
+ */
+ image?: string | null;
}
/**