feat: multi-category items and fix new items not appearing
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 <noreply@anthropic.com>
This commit is contained in:
parent
84ab6bef2d
commit
623b237826
11
estore/data.bak/categories-display.json
Normal file
11
estore/data.bak/categories-display.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"order": [
|
||||||
|
"latex",
|
||||||
|
"birthday",
|
||||||
|
"mylar-bouquets",
|
||||||
|
"graduation",
|
||||||
|
"letters-and-numbers",
|
||||||
|
"other"
|
||||||
|
],
|
||||||
|
"hidden": []
|
||||||
|
}
|
||||||
8
estore/data.bak/occasions.json
Normal file
8
estore/data.bak/occasions.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"mothers-day": {
|
||||||
|
"windowStart": "04-10"
|
||||||
|
},
|
||||||
|
"graduation": {
|
||||||
|
"windowStart": "04-01"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -43,7 +43,11 @@ function CategoryDisplayEditor({ items }: { items: AdminItem[] }) {
|
|||||||
const catalogCats = useMemo(() => {
|
const catalogCats = useMemo(() => {
|
||||||
const seen = new Map<string, string>()
|
const seen = new Map<string, string>()
|
||||||
items.forEach((item) => {
|
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 }))
|
return Array.from(seen.entries()).map(([key, label]) => ({ key, label }))
|
||||||
}, [items])
|
}, [items])
|
||||||
|
|||||||
@ -15,6 +15,12 @@ function applyOverrides(items: CatalogItem[]): CatalogItem[] {
|
|||||||
featured: ov.featured ?? item.featured,
|
featured: ov.featured ?? item.featured,
|
||||||
category: ov.categoryOverride ?? item.category,
|
category: ov.categoryOverride ?? item.category,
|
||||||
categoryLabel: ov.categoryLabelOverride ?? item.categoryLabel,
|
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,
|
showColors: ov.showColors != null ? ov.showColors : item.showColors,
|
||||||
colorMin: ov.colorMin ?? item.colorMin,
|
colorMin: ov.colorMin ?? item.colorMin,
|
||||||
colorMax: ov.colorMax !== undefined ? ov.colorMax : item.colorMax,
|
colorMax: ov.colorMax !== undefined ? ov.colorMax : item.colorMax,
|
||||||
|
|||||||
@ -63,7 +63,11 @@ export default function FeaturedProducts() {
|
|||||||
const productCategories = useMemo(() => {
|
const productCategories = useMemo(() => {
|
||||||
const seen = new Map<string, string>()
|
const seen = new Map<string, string>()
|
||||||
items.forEach((item) => {
|
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 all = Array.from(seen.entries()).map(([key, label]) => ({ key, label }))
|
||||||
const visible = all.filter((c) => !catHidden.includes(c.key))
|
const visible = all.filter((c) => !catHidden.includes(c.key))
|
||||||
@ -122,11 +126,11 @@ export default function FeaturedProducts() {
|
|||||||
|
|
||||||
const filtered = (activeOccasion
|
const filtered = (activeOccasion
|
||||||
? activeOccasion.squareCategorySlug
|
? activeOccasion.squareCategorySlug
|
||||||
? items.filter((i) => i.category === activeOccasion.squareCategorySlug)
|
? items.filter((i) => (i.categories ?? [i.category]).includes(activeOccasion.squareCategorySlug!))
|
||||||
: items
|
: items
|
||||||
: category === 'all'
|
: category === 'all'
|
||||||
? items
|
? items
|
||||||
: items.filter((i) => i.category === category)
|
: items.filter((i) => (i.categories ?? [i.category]).includes(category))
|
||||||
).filter((i) =>
|
).filter((i) =>
|
||||||
!q || i.name.toLowerCase().includes(q) || i.description.toLowerCase().includes(q)
|
!q || i.name.toLowerCase().includes(q) || i.description.toLowerCase().includes(q)
|
||||||
)
|
)
|
||||||
|
|||||||
@ -28,6 +28,9 @@ export interface CatalogItem {
|
|||||||
description: string
|
description: string
|
||||||
category: string
|
category: string
|
||||||
categoryLabel: 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 in cents of the default variation. null = custom quote required. */
|
||||||
price: number | null
|
price: number | null
|
||||||
imageUrl: string | null
|
imageUrl: string | null
|
||||||
@ -150,5 +153,7 @@ export const MOCK_CATALOG: CatalogItem[] = (([
|
|||||||
chromeSurchargePerColor: 0,
|
chromeSurchargePerColor: 0,
|
||||||
imageUrls: item.imageUrl ? [item.imageUrl] : [],
|
imageUrls: item.imageUrl ? [item.imageUrl] : [],
|
||||||
variations: item.price != null ? [{ id: item.id, name: 'Regular', priceCents: item.price, imageUrls: [], inventory: null }] : [],
|
variations: item.price != null ? [{ id: item.id, name: 'Regular', priceCents: item.price, imageUrls: [], inventory: null }] : [],
|
||||||
|
categories: [item.category],
|
||||||
|
categoryLabels: [item.categoryLabel],
|
||||||
...item,
|
...item,
|
||||||
})) as CatalogItem[]
|
})) as CatalogItem[]
|
||||||
|
|||||||
@ -91,9 +91,10 @@ export async function getSquareCatalog(): Promise<CatalogItem[]> {
|
|||||||
const items = objects
|
const items = objects
|
||||||
.filter((o) => o.type === 'ITEM')
|
.filter((o) => o.type === 'ITEM')
|
||||||
.filter((o) =>
|
.filter((o) =>
|
||||||
onlineCategoryId
|
// If an "online" category exists in Square, only show items tagged with it.
|
||||||
? (o.itemData?.categories ?? []).some((c: { id?: string }) => c.id === onlineCategoryId)
|
// If the category doesn't exist in this account, show all items.
|
||||||
: false
|
!onlineCategoryId ||
|
||||||
|
(o.itemData?.categories ?? []).some((c: { id?: string }) => c.id === onlineCategoryId)
|
||||||
)
|
)
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
const data = item.itemData!
|
const data = item.itemData!
|
||||||
@ -143,23 +144,27 @@ export async function getSquareCatalog(): Promise<CatalogItem[]> {
|
|||||||
.filter((ml): ml is ModifierList => ml !== null)
|
.filter((ml): ml is ModifierList => ml !== null)
|
||||||
|
|
||||||
const itemCategories: { id?: string }[] = data.categories ?? []
|
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 skipIds = new Set([onlineCategoryId, latexCategoryId].filter(Boolean) as string[])
|
||||||
const displayCatName = itemCategories
|
const displayCatNames = itemCategories
|
||||||
.filter((c) => c.id && !skipIds.has(c.id))
|
.filter((c) => c.id && !skipIds.has(c.id))
|
||||||
.map((c) => categoryNameMap.get(c.id!))
|
.map((c) => categoryNameMap.get(c.id!))
|
||||||
.find(Boolean) ?? 'Other'
|
.filter((n): n is string => !!n)
|
||||||
const categorySlug = displayCatName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')
|
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 {
|
return {
|
||||||
id: item.id!,
|
id: item.id!,
|
||||||
name: data.name ?? 'Unnamed item',
|
name: data.name ?? 'Unnamed item',
|
||||||
description: data.description ?? '',
|
description: data.description ?? '',
|
||||||
category: categorySlug,
|
category: categorySlug,
|
||||||
categoryLabel: displayCatName,
|
categoryLabel: primaryCatName,
|
||||||
|
categories: categorySlugs,
|
||||||
|
categoryLabels: categoryLbls,
|
||||||
price: priceAmount ? Number(priceAmount) : null,
|
price: priceAmount ? Number(priceAmount) : null,
|
||||||
imageUrl,
|
imageUrl,
|
||||||
imageUrls,
|
imageUrls,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user