From ddb5e16dc3983bc9473920bcf87f0f68faef0dde Mon Sep 17 00:00:00 2001 From: Ghost69 Date: Tue, 21 Jul 2026 13:35:47 +0100 Subject: [PATCH] feat(storefront): group Design Studio artworks into two collection folders Split Choose artwork into Signature gallery and Studio collections, and make every Collections piece studio-ready with approved garments. Co-authored-by: Cursor --- .../components/studio/design-studio.tsx | 166 ++++++++++++------ apps/storefront/lib/data/mock.ts | 12 +- .../lib/studio-artwork-folders.spec.ts | 69 ++++++++ apps/storefront/lib/studio-artwork-folders.ts | 64 +++++++ 4 files changed, 254 insertions(+), 57 deletions(-) create mode 100644 apps/storefront/lib/studio-artwork-folders.spec.ts create mode 100644 apps/storefront/lib/studio-artwork-folders.ts diff --git a/apps/storefront/components/studio/design-studio.tsx b/apps/storefront/components/studio/design-studio.tsx index 83fcb71..e19b4c8 100644 --- a/apps/storefront/components/studio/design-studio.tsx +++ b/apps/storefront/components/studio/design-studio.tsx @@ -1,7 +1,7 @@ 'use client'; import { Alert, cn, Eyebrow, Heading, Price, Text } from '@tms/ui'; -import { Check, Copy, Crop, Heart, Move, RotateCcw, ShoppingBag } from 'lucide-react'; +import { Check, ChevronDown, Copy, Crop, Heart, Move, RotateCcw, ShoppingBag } from 'lucide-react'; import Image from 'next/image'; import { useRouter } from 'next/navigation'; import { type ReactNode, useCallback, useMemo, useRef, useState } from 'react'; @@ -31,6 +31,11 @@ import { type StudioView, switchView, } from '@/lib/studio'; +import { + defaultOpenStudioFolderId, + groupArtworksForStudio, + type StudioArtworkFolder, +} from '@/lib/studio-artwork-folders'; function ChipButton({ selected, @@ -104,6 +109,10 @@ export function DesignStudio({ const [config, setConfig] = useState(() => resolveStudioConfig(initialConfig, initialOptions), ); + const artworkFolders = useMemo(() => groupArtworksForStudio(artworks), [artworks]); + const [openFolders, setOpenFolders] = useState>( + () => new Set([defaultOpenStudioFolderId(groupArtworksForStudio(artworks), initialConfig.artwork)]), + ); const [copied, setCopied] = useState(false); const [status, setStatus] = useState(null); const [added, setAdded] = useState(false); @@ -268,6 +277,8 @@ export function DesignStudio({ ); setOptions(nextOptions); setConfig(nextConfig); + const folderId = defaultOpenStudioFolderId(artworkFolders, slug); + setOpenFolders((prev) => new Set([...prev, folderId])); // Keep the URL shareable/refresh-safe without a Next navigation (which would reload). window.history.replaceState(null, '', `/design-studio?artwork=${encodeURIComponent(slug)}`); } catch { @@ -278,7 +289,7 @@ export function DesignStudio({ if (latestRequest.current === slug) setLoadingArtwork(null); } }, - [config.artwork, config.view, config.quantity, loadingArtwork], + [config.artwork, config.view, config.quantity, loadingArtwork, artworkFolders], ); // Placement chips are already limited to the side being viewed, so this just prints the active @@ -584,55 +595,110 @@ export function DesignStudio({
-
    - {artworks.map((a) => ( -
  • - -
  • - ))} -
+ + {folder.hint} + + + + +
+
+
    + {folderArtworks.map((a) => ( +
  • + +
  • + ))} +
+
+
+ + ); + })} +
diff --git a/apps/storefront/lib/data/mock.ts b/apps/storefront/lib/data/mock.ts index cdf74f2..d7f8fb5 100644 --- a/apps/storefront/lib/data/mock.ts +++ b/apps/storefront/lib/data/mock.ts @@ -348,13 +348,11 @@ const artworks: ArtworkSummary[] = [ title: suppliedArtworkTitle(seed.slug), collection: seed.collection, shortStory: `A studio-supplied piece from the ${seed.collection} collection.`, - availability: null, - startingPriceMinor: null, - currency: null, - compatibleGarments: - seed.slug === 'resilience-hands-high' || seed.slug.startsWith('africa-united-') - ? ['Classic T-shirt'] - : [], + availability: 'available', + startingPriceMinor: 1200000, + currency: 'NGN', + // Every Collections piece is Design Studio–ready on classic and oversized tees. + compatibleGarments: ['Classic T-shirt', 'Oversized T-shirt'], limitedEdition: false, })), ]; diff --git a/apps/storefront/lib/studio-artwork-folders.spec.ts b/apps/storefront/lib/studio-artwork-folders.spec.ts new file mode 100644 index 0000000..28c5271 --- /dev/null +++ b/apps/storefront/lib/studio-artwork-folders.spec.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from 'vitest'; +import type { ArtworkSummary } from './data/types'; +import { + defaultOpenStudioFolderId, + groupArtworksForStudio, + STUDIO_ARTWORK_FOLDERS, +} from './studio-artwork-folders'; + +function art(partial: Pick): ArtworkSummary { + return { + id: partial.slug, + slug: partial.slug, + title: partial.title, + collection: partial.collection, + shortStory: '', + availability: 'available', + startingPriceMinor: 100, + currency: 'NGN', + compatibleGarments: ['Classic T-shirt'], + limitedEdition: false, + }; +} + +describe('groupArtworksForStudio', () => { + it('puts every collection piece into one of the two folders', () => { + const items = [ + art({ slug: 'midnight-in-lagos', title: 'Midnight in Lagos', collection: 'Night Studies' }), + art({ slug: 'paper-tigers', title: 'Paper Tigers', collection: 'Comic Line' }), + art({ + slug: 'africa-united-heritage-duo', + title: 'Africa United Heritage Duo', + collection: 'Africa United', + }), + art({ slug: 'resilience-hands-high', title: 'Resilience Hands High', collection: 'Resilience' }), + art({ slug: 'studio-cap-muse', title: 'Studio Cap Muse', collection: 'Studio Muses' }), + ]; + + const groups = groupArtworksForStudio(items); + expect(groups).toHaveLength(2); + expect(groups.map((g) => g.folder.id)).toEqual(['signature', 'studio-collections']); + expect(groups[0]!.artworks.map((a) => a.slug)).toEqual([ + 'midnight-in-lagos', + 'paper-tigers', + ]); + expect(groups[1]!.artworks.map((a) => a.slug)).toEqual([ + 'africa-united-heritage-duo', + 'resilience-hands-high', + 'studio-cap-muse', + ]); + expect(groups.flatMap((g) => g.artworks)).toHaveLength(items.length); + }); + + it('drops empty folders', () => { + const groups = groupArtworksForStudio([ + art({ slug: 'market-day', title: 'Market Day', collection: 'City Portraits' }), + ]); + expect(groups).toHaveLength(1); + expect(groups[0]!.folder.id).toBe('signature'); + }); + + it('opens the folder that contains the selected artwork', () => { + const groups = groupArtworksForStudio([ + art({ slug: 'paper-tigers', title: 'Paper Tigers', collection: 'Comic Line' }), + art({ slug: 'city-sisters', title: 'City Sisters', collection: 'Africa United' }), + ]); + expect(defaultOpenStudioFolderId(groups, 'city-sisters')).toBe('studio-collections'); + expect(defaultOpenStudioFolderId(groups, null)).toBe(STUDIO_ARTWORK_FOLDERS[0]!.id); + }); +}); diff --git a/apps/storefront/lib/studio-artwork-folders.ts b/apps/storefront/lib/studio-artwork-folders.ts new file mode 100644 index 0000000..6560fe6 --- /dev/null +++ b/apps/storefront/lib/studio-artwork-folders.ts @@ -0,0 +1,64 @@ +import type { ArtworkSummary } from './data/types'; + +/** + * Design Studio groups the full Collections catalogue into two folders so the + * artwork picker stays scannable as the wall grows. + */ +export interface StudioArtworkFolder { + id: 'signature' | 'studio-collections'; + label: string; + hint: string; + /** Matches `ArtworkSummary.collection`. */ + collections: readonly string[]; +} + +export const STUDIO_ARTWORK_FOLDERS: readonly StudioArtworkFolder[] = [ + { + id: 'signature', + label: 'Signature gallery', + hint: 'Night Studies, Comic Line, Season Sketches, City Portraits', + collections: ['Night Studies', 'Comic Line', 'Season Sketches', 'City Portraits'], + }, + { + id: 'studio-collections', + label: 'Studio collections', + hint: 'Africa United, Resilience, Studio Muses', + collections: ['Africa United', 'Resilience', 'Studio Muses'], + }, +] as const; + +export interface StudioArtworkFolderGroup { + folder: StudioArtworkFolder; + artworks: ArtworkSummary[]; +} + +/** Split the catalogue into the two studio folders; leftovers join Signature. */ +export function groupArtworksForStudio(artworks: ArtworkSummary[]): StudioArtworkFolderGroup[] { + const buckets = new Map( + STUDIO_ARTWORK_FOLDERS.map((folder) => [folder.id, []]), + ); + + for (const artwork of artworks) { + const match = + STUDIO_ARTWORK_FOLDERS.find((folder) => folder.collections.includes(artwork.collection)) ?? + STUDIO_ARTWORK_FOLDERS[0]!; + buckets.get(match.id)!.push(artwork); + } + + return STUDIO_ARTWORK_FOLDERS.map((folder) => ({ + folder, + artworks: buckets.get(folder.id) ?? [], + })).filter((group) => group.artworks.length > 0); +} + +/** Which folder should open first given a selected (or deep-linked) artwork. */ +export function defaultOpenStudioFolderId( + groups: StudioArtworkFolderGroup[], + selectedSlug: string | null, +): StudioArtworkFolder['id'] { + if (selectedSlug) { + const hit = groups.find((group) => group.artworks.some((a) => a.slug === selectedSlug)); + if (hit) return hit.folder.id; + } + return groups[0]?.folder.id ?? 'signature'; +}