diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index cce2cdd..a9ea3e2 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -7,6 +7,8 @@ import { getProductsPage, getCollections, getCollectionProductsPage, + isDefaultTitleOption, + isDefaultTitleSelection, } from '@/services/shopify/catalog'; // Streaming needs the Node runtime here because the Storefront helpers run @@ -63,10 +65,12 @@ const summariseProduct = (product: { description: product.description?.slice(0, 300) ?? null, productType: product.productType || null, tags: product.tags ?? [], - options: product.options?.map((option) => ({ - name: option.name, - values: option.values, - })), + options: product.options + ?.filter((option) => !isDefaultTitleOption(option)) + .map((option) => ({ + name: option.name, + values: option.values, + })), inStock: product.variants?.edges.some((edge) => edge.node.availableForSale), }); @@ -132,7 +136,9 @@ export async function POST(req: Request) { title: node.title, available: node.availableForSale, price: `${node.price.amount} ${node.price.currencyCode}`, - options: node.selectedOptions, + options: node.selectedOptions?.filter( + (option) => !isDefaultTitleSelection(option) + ), })), }; }, diff --git a/components/shopify/cart-drawer.tsx b/components/shopify/cart-drawer.tsx index 81ace6f..c8f7c63 100644 --- a/components/shopify/cart-drawer.tsx +++ b/components/shopify/cart-drawer.tsx @@ -25,6 +25,7 @@ import { EmptyDescription, EmptyContent, } from '@/components/ui/empty'; +import { isDefaultTitleSelection } from '@/services/shopify/catalog'; const CartDrawer: React.FC = () => { const isOpen = useCartStore((s) => s.isOpen); @@ -102,7 +103,10 @@ const CartDrawer: React.FC = () => { }; const getSelectedOptions = (item: (typeof items)[0]) => { - return item.merchandise.selectedOptions ?? []; + // Single-SKU items carry a synthetic `Title: Default Title` — not worth a line. + return (item.merchandise.selectedOptions ?? []).filter( + (option) => !isDefaultTitleSelection(option) + ); }; return ( diff --git a/components/shopify/product-detail/product-detail-info.tsx b/components/shopify/product-detail/product-detail-info.tsx index aadea71..b8318aa 100644 --- a/components/shopify/product-detail/product-detail-info.tsx +++ b/components/shopify/product-detail/product-detail-info.tsx @@ -5,6 +5,7 @@ import { RiSubtractLine, RiAddLine } from '@remixicon/react'; import ShopPayButton from '@/components/shopify/shop-pay-button'; import type { ProductOption, ProductOptionValue } from '@/hooks/use-shopify-products'; import { isSwatchOptionName, swatchColorForName } from '@/config/swatches'; +import { isDefaultTitleOption } from '@/services/shopify/catalog'; interface ProductPrice { amount: string; @@ -103,9 +104,10 @@ const ProductDetailInfo: React.FC = ({ isSwatchOptionName(option.name); // Some products return Size before Color; show the swatches first either way. - const orderedOptions = [...(product.options ?? [])].sort( - (a, b) => Number(isSwatchOption(b)) - Number(isSwatchOption(a)) - ); + // Single-SKU products expose a synthetic `Title: Default Title` option — drop it. + const orderedOptions = [...(product.options ?? [])] + .filter((option) => !isDefaultTitleOption(option)) + .sort((a, b) => Number(isSwatchOption(b)) - Number(isSwatchOption(a))); // `optionValues` carries the swatch data; fall back to plain `values`. const optionValuesFor = (option: ProductOption): ProductOptionValue[] => diff --git a/services/shopify/catalog.ts b/services/shopify/catalog.ts index 3b1b497..9c8461e 100644 --- a/services/shopify/catalog.ts +++ b/services/shopify/catalog.ts @@ -64,6 +64,25 @@ export interface ProductOption { optionValues?: ProductOptionValue[]; } +/** + * Single-variant products still carry one synthetic option — `Title` with the + * lone value `Default Title`. It isn't a real choice, so keep it out of the UI. + */ +export const isDefaultTitleOption = (option: { + name: string; + values: string[]; +}): boolean => + option.name === 'Title' && + option.values.length === 1 && + option.values[0] === 'Default Title'; + +/** Same synthetic option, as it appears on a variant's `selectedOptions`. */ +export const isDefaultTitleSelection = (selection: { + name: string; + value: string; +}): boolean => + selection.name === 'Title' && selection.value === 'Default Title'; + export interface Product { id: string; title: string;