From 46d504e48358bfa55688a0e9bd6892468cc22893 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 25 Jun 2026 16:58:13 +0700 Subject: [PATCH] x --- apps/spa/src/components/ProductCard.tsx | 22 ++++- apps/spa/src/pages/ProductDetailPage.tsx | 30 ++++-- .../src/components/AddToCartSection.tsx | 96 +++++++++++++------ packages/types/index.ts | 2 +- 4 files changed, 112 insertions(+), 38 deletions(-) diff --git a/apps/spa/src/components/ProductCard.tsx b/apps/spa/src/components/ProductCard.tsx index f13fdbe..dd7e4b2 100644 --- a/apps/spa/src/components/ProductCard.tsx +++ b/apps/spa/src/components/ProductCard.tsx @@ -13,6 +13,24 @@ function resolveStr(v: TranslatableString | null | undefined, lang: string): str return v[lang] ?? v[lang.slice(0, 2)] ?? v['th'] ?? v['en'] ?? Object.values(v)[0] ?? '' } +function resolveOptionValue(val: string, options: any[] | null | undefined, lang: string): string { + if (!val) return ''; + if (!options || !Array.isArray(options)) return val; + + for (const option of options) { + if (!option.values || !Array.isArray(option.values)) continue; + for (const v of option.values) { + const baseStr = typeof v === 'string' ? v : (v['th'] || v['en'] || Object.values(v)[0] || ''); + if (baseStr === val) { + if (typeof v === 'string') return v; + return v[lang] ?? v[lang.slice(0, 2)] ?? v['th'] ?? v['en'] ?? Object.values(v)[0] ?? baseStr; + } + } + } + + return val; +} + interface ProductCardProps { product: Product; enableQuickShop?: boolean; @@ -100,7 +118,7 @@ export default function ProductCard({ product, enableQuickShop = false, category : 'border-pop-black/10 bg-transparent text-pop-black/70 hover:border-pop-black/30 hover:bg-pop-black/5' }`} > - {[variant.option1, variant.option2, variant.option3].filter(Boolean).join(' / ')} + {[variant.option1, variant.option2, variant.option3].filter(Boolean).map(opt => resolveOptionValue(opt, product.options, i18n.language)).join(' / ')} ); })} @@ -131,7 +149,7 @@ export default function ProductCard({ product, enableQuickShop = false, category quantity: 1, price: selectedVariant.price || 0, productName: displayName, - variantName: [selectedVariant.option1, selectedVariant.option2, selectedVariant.option3].filter(Boolean).join(' / '), + variantName: [selectedVariant.option1, selectedVariant.option2, selectedVariant.option3].filter(Boolean).map(opt => resolveOptionValue(opt, product.options, i18n.language)).join(' / '), productImage: product.images?.[0]?.url || getCategoryImage(displayCategoryName) }); }} diff --git a/apps/spa/src/pages/ProductDetailPage.tsx b/apps/spa/src/pages/ProductDetailPage.tsx index e3665b9..d13278c 100644 --- a/apps/spa/src/pages/ProductDetailPage.tsx +++ b/apps/spa/src/pages/ProductDetailPage.tsx @@ -16,10 +16,28 @@ import DOMPurify from 'dompurify' function resolveTranslatable(value: TranslatableString | null | undefined, lang: string): string { if (!value) return '' if (typeof value === 'string') return value - // Record: ลอง lang เต็ม → ลอง prefix 2 ตัว → fallback 'th'/'en' + // Record: ลอง lang เต็ม → ลอง prefix 2 double-char → fallback 'th'/'en' return value[lang] ?? value[lang.slice(0, 2)] ?? value['th'] ?? value['en'] ?? Object.values(value)[0] ?? '' } +function resolveOptionValue(val: string, options: any[] | null | undefined, lang: string): string { + if (!val) return ''; + if (!options || !Array.isArray(options)) return val; + + for (const option of options) { + if (!option.values || !Array.isArray(option.values)) continue; + for (const v of option.values) { + const baseStr = typeof v === 'string' ? v : (v['th'] || v['en'] || Object.values(v)[0] || ''); + if (baseStr === val) { + if (typeof v === 'string') return v; + return v[lang] ?? v[lang.slice(0, 2)] ?? v['th'] ?? v['en'] ?? Object.values(v)[0] ?? baseStr; + } + } + } + + return val; +} + export default function ProductDetailPage() { const { id } = useParams<{ id: string }>() @@ -134,7 +152,7 @@ export default function ProductDetailPage() { quantity: 1, price: currentVariant.price || product.price || 0, productName: displayName, - variantName: [currentVariant.option1, currentVariant.option2, currentVariant.option3].filter(Boolean).join(' / '), + variantName: [currentVariant.option1, currentVariant.option2, currentVariant.option3].filter(Boolean).map(opt => resolveOptionValue(opt, product.options, i18n.language)).join(' / '), productImage: product.images?.[0]?.url || getCategoryImage(typeof product.category.name === 'string' ? product.category.name : (product.category.name as any)?.en || '') }) } @@ -204,7 +222,7 @@ export default function ProductDetailPage() { {formatPrice(currentVariant?.price)} {currentVariant && ( - / {[currentVariant.option1, currentVariant.option2, currentVariant.option3].filter(Boolean).join(' / ')} + / {[currentVariant.option1, currentVariant.option2, currentVariant.option3].filter(Boolean).map(opt => resolveOptionValue(opt, product.options, i18n.language)).join(' / ')} )} @@ -224,7 +242,7 @@ export default function ProductDetailPage() { : 'bg-spa-light text-spa-stone border-spa-stone/20 hover:border-spa-forest hover:text-spa-forest shadow-sm' }`} > - {[v.option1, v.option2, v.option3].filter(Boolean).join(' / ')} + {[v.option1, v.option2, v.option3].filter(Boolean).map(opt => resolveOptionValue(opt, product.options, i18n.language)).join(' / ')} {formatPrice(v.price)} @@ -337,7 +355,7 @@ export default function ProductDetailPage() { : 'bg-spa-light border-spa-stone/20 text-spa-stone' }`} > - {[v.option1, v.option2, v.option3].filter(Boolean).join(' / ')} + {[v.option1, v.option2, v.option3].filter(Boolean).map(opt => resolveOptionValue(opt, product.options, i18n.language)).join(' / ')} ))} @@ -346,7 +364,7 @@ export default function ProductDetailPage() {
- {product.variants && product.variants.length > 1 ? [currentVariant?.option1, currentVariant?.option2, currentVariant?.option3].filter(Boolean).join(' / ') : t('cart.totalPrice')} + {product.variants && product.variants.length > 1 ? [currentVariant?.option1, currentVariant?.option2, currentVariant?.option3].filter(Boolean).map(opt => resolveOptionValue(opt, product.options, i18n.language)).join(' / ') : t('cart.totalPrice')} {formatPrice(currentVariant?.price)} diff --git a/apps/website/src/components/AddToCartSection.tsx b/apps/website/src/components/AddToCartSection.tsx index 1dca4c7..9d3fb76 100644 --- a/apps/website/src/components/AddToCartSection.tsx +++ b/apps/website/src/components/AddToCartSection.tsx @@ -7,6 +7,30 @@ import { useCartStore } from '../store/cartStore' import { resolveCategoryName, resolveLocalized } from '../lib/localization' import { useLang } from '../hooks/useLang' +function getBaseString(val: any): string { + if (!val) return ''; + if (typeof val === 'string') return val; + return val['th'] || val['en'] || Object.values(val)[0] || ''; +} + +function resolveOptionValue(val: string, options: any[] | null | undefined, lang: string): string { + if (!val) return ''; + if (!options || !Array.isArray(options)) return val; + + for (const option of options) { + if (!option.values || !Array.isArray(option.values)) continue; + for (const v of option.values) { + const baseStr = typeof v === 'string' ? v : (v['th'] || v['en'] || Object.values(v)[0] || ''); + if (baseStr === val) { + if (typeof v === 'string') return v; + return v[lang] ?? v[lang.slice(0, 2)] ?? v['th'] ?? v['en'] ?? Object.values(v)[0] ?? baseStr; + } + } + } + + return val; +} + interface Props { product: Product } @@ -66,7 +90,10 @@ export default function AddToCartSection({ product }: Props) { const handleAddToCart = () => { if (!currentVariant) return - const variantName = [currentVariant.option1, currentVariant.option2, currentVariant.option3].filter(Boolean).join(' / '); + const variantName = [currentVariant.option1, currentVariant.option2, currentVariant.option3] + .filter(Boolean) + .map(opt => resolveOptionValue(opt, product.options, lang)) + .join(' / '); addToCart({ productId: product.id, @@ -80,7 +107,12 @@ export default function AddToCartSection({ product }: Props) { toggleCart(true) } - const currentVariantName = currentVariant ? [currentVariant.option1, currentVariant.option2, currentVariant.option3].filter(Boolean).join(' / ') : null; + const currentVariantName = currentVariant + ? [currentVariant.option1, currentVariant.option2, currentVariant.option3] + .filter(Boolean) + .map(opt => resolveOptionValue(opt, product.options, lang)) + .join(' / ') + : null; return ( <> @@ -108,34 +140,40 @@ export default function AddToCartSection({ product }: Props) { {product.options && product.options.length > 0 && (
- {product.options.map((option, optIndex) => ( -
-

Select {option.name}

-
- {option.values.map(val => { - const isSelected = selectedOptions[optIndex] === val; - const isAvailable = isOptionAvailable(optIndex, val); - - return ( - - ); - })} + {product.options.map((option, optIndex) => { + const optNameBase = getBaseString(option.name); + const optNameDisplay = resolveLocalized(option.name, lang); + return ( +
+

Select {optNameDisplay}

+
+ {option.values.map(val => { + const baseVal = getBaseString(val); + const displayVal = resolveLocalized(val, lang); + const isSelected = selectedOptions[optIndex] === baseVal; + const isAvailable = isOptionAvailable(optIndex, baseVal); + + return ( + + ); + })} +
-
- ))} + ); + })}
)} diff --git a/packages/types/index.ts b/packages/types/index.ts index b17e44c..ea3c9c1 100644 --- a/packages/types/index.ts +++ b/packages/types/index.ts @@ -60,7 +60,7 @@ export interface Product { categoryId: number category: Category brand: Brand - options?: Array<{ name: string; values: string[] }> | null + options?: Array<{ name: TranslatableString; values: TranslatableString[] }> | null variants: Variant[] images: ProductImage[] }