From 623b237826d22eef6dd7ff9e7ba35051177c6dfb Mon Sep 17 00:00:00 2001 From: chris Date: Fri, 17 Apr 2026 15:39:31 -0400 Subject: [PATCH] feat: multi-category items and fix new items not appearing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Items can now belong to multiple Square categories and appear in all matching tabs (e.g. a Mother's Day balloon also shows under Easter). Also fixes new items not appearing when the Square account has no "online" category — previously this caused zero items to load; now it falls back to showing all items. Changes: - CatalogItem gains categories[] + categoryLabels[] (multi-category) - square.ts collects all non-skip categories per item; "online" filter is now optional (show all if category doesn't exist in Square) - catalog/route.ts propagates categoryOverride into categories[0] - FeaturedProducts: tabs and filter use the full categories array - Admin CategoryDisplayEditor sees all categories from multi-cat items Co-Authored-By: Claude Sonnet 4.6 --- estore/data.bak/categories-display.json | 11 ++++++++ estore/data.bak/occasions.json | 8 ++++++ estore/src/app/admin/page.tsx | 6 +++- estore/src/app/api/catalog/route.ts | 12 ++++++-- estore/src/components/FeaturedProducts.tsx | 10 +++++-- estore/src/data/mock-catalog.ts | 5 ++++ estore/src/lib/square.ts | 33 +++++++++++++--------- 7 files changed, 64 insertions(+), 21 deletions(-) create mode 100644 estore/data.bak/categories-display.json create mode 100644 estore/data.bak/occasions.json diff --git a/estore/data.bak/categories-display.json b/estore/data.bak/categories-display.json new file mode 100644 index 0000000..7a8335e --- /dev/null +++ b/estore/data.bak/categories-display.json @@ -0,0 +1,11 @@ +{ + "order": [ + "latex", + "birthday", + "mylar-bouquets", + "graduation", + "letters-and-numbers", + "other" + ], + "hidden": [] +} \ No newline at end of file diff --git a/estore/data.bak/occasions.json b/estore/data.bak/occasions.json new file mode 100644 index 0000000..2a68722 --- /dev/null +++ b/estore/data.bak/occasions.json @@ -0,0 +1,8 @@ +{ + "mothers-day": { + "windowStart": "04-10" + }, + "graduation": { + "windowStart": "04-01" + } +} \ No newline at end of file diff --git a/estore/src/app/admin/page.tsx b/estore/src/app/admin/page.tsx index dedc5ff..57dcbac 100644 --- a/estore/src/app/admin/page.tsx +++ b/estore/src/app/admin/page.tsx @@ -43,7 +43,11 @@ function CategoryDisplayEditor({ items }: { items: AdminItem[] }) { const catalogCats = useMemo(() => { const seen = new Map() items.forEach((item) => { - if (!seen.has(item.category)) seen.set(item.category, item.categoryLabel) + const cats = item.categories ?? [item.category] + const labels = item.categoryLabels ?? [item.categoryLabel] + cats.forEach((slug, i) => { + if (!seen.has(slug)) seen.set(slug, labels[i] ?? slug) + }) }) return Array.from(seen.entries()).map(([key, label]) => ({ key, label })) }, [items]) diff --git a/estore/src/app/api/catalog/route.ts b/estore/src/app/api/catalog/route.ts index 9748a42..9364511 100644 --- a/estore/src/app/api/catalog/route.ts +++ b/estore/src/app/api/catalog/route.ts @@ -12,9 +12,15 @@ function applyOverrides(items: CatalogItem[]): CatalogItem[] { if (!ov) return item return { ...item, - featured: ov.featured ?? item.featured, - category: ov.categoryOverride ?? item.category, - categoryLabel: ov.categoryLabelOverride ?? item.categoryLabel, + featured: ov.featured ?? item.featured, + category: ov.categoryOverride ?? item.category, + categoryLabel: ov.categoryLabelOverride ?? item.categoryLabel, + categories: ov.categoryOverride + ? [ov.categoryOverride, ...(item.categories ?? [item.category]).slice(1)] + : (item.categories ?? [item.category]), + categoryLabels: ov.categoryLabelOverride + ? [ov.categoryLabelOverride, ...(item.categoryLabels ?? [item.categoryLabel]).slice(1)] + : (item.categoryLabels ?? [item.categoryLabel]), showColors: ov.showColors != null ? ov.showColors : item.showColors, colorMin: ov.colorMin ?? item.colorMin, colorMax: ov.colorMax !== undefined ? ov.colorMax : item.colorMax, diff --git a/estore/src/components/FeaturedProducts.tsx b/estore/src/components/FeaturedProducts.tsx index 7c65d29..1820e7c 100644 --- a/estore/src/components/FeaturedProducts.tsx +++ b/estore/src/components/FeaturedProducts.tsx @@ -63,7 +63,11 @@ export default function FeaturedProducts() { const productCategories = useMemo(() => { const seen = new Map() items.forEach((item) => { - if (!seen.has(item.category)) seen.set(item.category, item.categoryLabel) + const cats = item.categories ?? [item.category] + const labels = item.categoryLabels ?? [item.categoryLabel] + cats.forEach((slug, i) => { + if (!seen.has(slug)) seen.set(slug, labels[i] ?? slug) + }) }) const all = Array.from(seen.entries()).map(([key, label]) => ({ key, label })) const visible = all.filter((c) => !catHidden.includes(c.key)) @@ -122,11 +126,11 @@ export default function FeaturedProducts() { const filtered = (activeOccasion ? activeOccasion.squareCategorySlug - ? items.filter((i) => i.category === activeOccasion.squareCategorySlug) + ? items.filter((i) => (i.categories ?? [i.category]).includes(activeOccasion.squareCategorySlug!)) : items : category === 'all' ? items - : items.filter((i) => i.category === category) + : items.filter((i) => (i.categories ?? [i.category]).includes(category)) ).filter((i) => !q || i.name.toLowerCase().includes(q) || i.description.toLowerCase().includes(q) ) diff --git a/estore/src/data/mock-catalog.ts b/estore/src/data/mock-catalog.ts index 8774a82..feff5b0 100644 --- a/estore/src/data/mock-catalog.ts +++ b/estore/src/data/mock-catalog.ts @@ -28,6 +28,9 @@ export interface CatalogItem { description: string category: string categoryLabel: string + /** All display categories this item belongs to (multi-category support). First entry matches category/categoryLabel. */ + categories: string[] + categoryLabels: string[] /** Price in cents of the default variation. null = custom quote required. */ price: number | null imageUrl: string | null @@ -150,5 +153,7 @@ export const MOCK_CATALOG: CatalogItem[] = (([ chromeSurchargePerColor: 0, imageUrls: item.imageUrl ? [item.imageUrl] : [], variations: item.price != null ? [{ id: item.id, name: 'Regular', priceCents: item.price, imageUrls: [], inventory: null }] : [], + categories: [item.category], + categoryLabels: [item.categoryLabel], ...item, })) as CatalogItem[] diff --git a/estore/src/lib/square.ts b/estore/src/lib/square.ts index dbf8dbd..83347c7 100644 --- a/estore/src/lib/square.ts +++ b/estore/src/lib/square.ts @@ -91,9 +91,10 @@ export async function getSquareCatalog(): Promise { const items = objects .filter((o) => o.type === 'ITEM') .filter((o) => - onlineCategoryId - ? (o.itemData?.categories ?? []).some((c: { id?: string }) => c.id === onlineCategoryId) - : false + // If an "online" category exists in Square, only show items tagged with it. + // If the category doesn't exist in this account, show all items. + !onlineCategoryId || + (o.itemData?.categories ?? []).some((c: { id?: string }) => c.id === onlineCategoryId) ) .map((item) => { const data = item.itemData! @@ -143,23 +144,27 @@ export async function getSquareCatalog(): Promise { .filter((ml): ml is ModifierList => ml !== null) const itemCategories: { id?: string }[] = data.categories ?? [] - const hasCategory = (id: string | undefined) => - !!id && itemCategories.some((c) => c.id === id) - // Derive display category from the first Square category that isn't 'online' or 'latex' + // Derive display categories from all Square categories that aren't 'online' or 'latex' const skipIds = new Set([onlineCategoryId, latexCategoryId].filter(Boolean) as string[]) - const displayCatName = itemCategories + const displayCatNames = itemCategories .filter((c) => c.id && !skipIds.has(c.id)) .map((c) => categoryNameMap.get(c.id!)) - .find(Boolean) ?? 'Other' - const categorySlug = displayCatName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') + .filter((n): n is string => !!n) + const primaryCatName = displayCatNames[0] ?? 'Other' + const toSlug = (n: string) => n.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') + const categorySlug = toSlug(primaryCatName) + const categorySlugs = displayCatNames.length ? displayCatNames.map(toSlug) : [categorySlug] + const categoryLbls = displayCatNames.length ? displayCatNames : [primaryCatName] return { - id: item.id!, - name: data.name ?? 'Unnamed item', - description: data.description ?? '', - category: categorySlug, - categoryLabel: displayCatName, + id: item.id!, + name: data.name ?? 'Unnamed item', + description: data.description ?? '', + category: categorySlug, + categoryLabel: primaryCatName, + categories: categorySlugs, + categoryLabels: categoryLbls, price: priceAmount ? Number(priceAmount) : null, imageUrl, imageUrls,