From e9f6e7201aae09494dccc43124462ac4e850da96 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 29 Sep 2026 17:03:36 +0700 Subject: [PATCH] x --- apps/spa/src/pages/CategoryPage.tsx | 176 ++++++++++++++++------------ 1 file changed, 98 insertions(+), 78 deletions(-) diff --git a/apps/spa/src/pages/CategoryPage.tsx b/apps/spa/src/pages/CategoryPage.tsx index b1ddc6f..22cd114 100644 --- a/apps/spa/src/pages/CategoryPage.tsx +++ b/apps/spa/src/pages/CategoryPage.tsx @@ -6,15 +6,17 @@ import Footer from '../components/Footer' import ProductCard from '../components/ProductCard' import DynamicPageRenderer from '../components/DynamicPageRenderer' import { API_BASE, getCategoryImage, type Category, type Product, type PageSection } from '../types' +import { getLocalizedField } from '../utils/localization' export default function CategoryPage() { const { id } = useParams<{ id: string }>() + const { t, i18n } = useTranslation() + const lang = i18n.language const [categories, setCategories] = useState([]) const [category, setCategory] = useState(null) const [products, setProducts] = useState([]) const [cmsSections, setCmsSections] = useState([]) - const { i18n } = useTranslation() // Local progressive rendering const [displayCount, setDisplayCount] = useState(24) @@ -23,28 +25,34 @@ export default function CategoryPage() { const [loading, setLoading] = useState(true) const [error, setError] = useState(null) + // Fetch all categories for navigation and pills useEffect(() => { - fetch(`${API_BASE}/categories?lang=${i18n.language}`) + fetch(`${API_BASE}/categories?lang=${lang}`) .then((r) => r.json()) - .then(setCategories) + .then((data: Category[]) => { + if (Array.isArray(data)) { + setCategories(data) + } + }) .catch(console.error) - }, [i18n.language]) + }, [lang]) + // Fetch CMS sections for category-layout useEffect(() => { - fetch(`${API_BASE}/pages/category-layout?lang=${i18n.language}`) + fetch(`${API_BASE}/pages/category-layout?lang=${lang}`) .then((r) => { - if (!r.ok) throw new Error('Page not found'); - return r.json(); + if (!r.ok) throw new Error('Page not found') + return r.json() }) .then((data) => { if (data && data.sections) { - setCmsSections(data.sections); + setCmsSections(data.sections) } }) .catch((err) => { - console.log('Category dynamic layout sections not loaded:', err.message); - }); - }, [i18n.language]) + console.log('Category dynamic layout sections not loaded:', err.message) + }) + }, [lang]) // Reset state when category changes useEffect(() => { @@ -52,28 +60,22 @@ export default function CategoryPage() { setDisplayCount(24) }, [id]) + // Fetch single category and its products useEffect(() => { if (!id) return setLoading(true) setError(null) - Promise.all([ - // Fetch up to 1000 items (essentially all) in one go - fetch(`${API_BASE}/categories/${id}/products?limit=1000&lang=${i18n.language}`), - fetch(`${API_BASE}/categories?lang=${i18n.language}`), - ]) - .then(async ([catRes, allCatsRes]) => { + fetch(`${API_BASE}/categories/${id}/products?limit=1000&lang=${lang}`) + .then(async (catRes) => { if (!catRes.ok) throw new Error('Category not found') const catPaginated = await catRes.json() - const allCats: Category[] = await allCatsRes.json() - setCategory(catPaginated.category) setProducts(catPaginated.data || []) - setCategories(allCats) }) .catch((err) => setError(err.message)) .finally(() => setLoading(false)) - }, [id, i18n.language]) + }, [id, lang]) // Intersection Observer for infinite scroll useEffect(() => { @@ -83,7 +85,7 @@ export default function CategoryPage() { setDisplayCount((prev) => Math.min(prev + 12, products.length)) } }, - { threshold: 0.1, rootMargin: '200px' } // Pre-load slightly before seeing the loader + { threshold: 0.1, rootMargin: '200px' } ) if (loaderRef.current) { @@ -93,12 +95,15 @@ export default function CategoryPage() { return () => observer.disconnect() }, [products.length, displayCount, loading]) - const announcementSections = cmsSections.filter(s => s.isActive && s.type.toUpperCase() === 'ANNOUNCEMENT_BAR'); - const heroSections = cmsSections.filter(s => s.isActive && (s.type.toUpperCase() === 'HERO' || s.type.toUpperCase() === 'BANNER')); - const bottomSections = cmsSections.filter(s => s.isActive && s.type.toUpperCase() !== 'ANNOUNCEMENT_BAR' && s.type.toUpperCase() !== 'HERO' && s.type.toUpperCase() !== 'BANNER'); + const announcementSections = cmsSections.filter(s => s.isActive && s.type.toUpperCase() === 'ANNOUNCEMENT_BAR') + const heroSections = cmsSections.filter(s => s.isActive && (s.type.toUpperCase() === 'HERO' || s.type.toUpperCase() === 'BANNER')) + const bottomSections = cmsSections.filter(s => s.isActive && s.type.toUpperCase() !== 'ANNOUNCEMENT_BAR' && s.type.toUpperCase() !== 'HERO' && s.type.toUpperCase() !== 'BANNER') + + const categoryName = getLocalizedField(category?.name, lang) + const categoryImageUrl = category?.imageUrl || (categoryName ? getCategoryImage(categoryName) : '') return ( -
+
{announcementSections.length > 0 && ( - {/* Hero Banner for category */} + {/* Hero Banner for Category */} {heroSections.length > 0 ? (
) : ( -
-
+
+
{/* Breadcrumb */} -
- Home +
+ {t('nav.home', 'Home')} / - Shop + {t('nav.shop', 'Shop')} / - {typeof category?.name === 'string' ? category.name : (category?.name as any)?.[localStorage.getItem('app_lang') || 'en'] || (category?.name as any)?.en || '...'} + {categoryName || '...'}
-

+ +

{loading ? ( -
+
) : ( - typeof category?.name === 'string' ? category.name : (category?.name as any)?.[i18n.language] || (category?.name as any)?.en || '' + categoryName )}

- {!loading && category?._count && ( -

- {category._count.products} products available + + {!loading && category?._count !== undefined && ( +

+ {category._count.products} {t('categories.itemCount', 'products available')}

)}
- -
- {typeof { (e.target as HTMLImageElement).style.display = 'none' }} - /> -
+ + {categoryImageUrl && ( +
+ {categoryName} { (e.target as HTMLImageElement).style.display = 'none' }} + /> +
+ )}
-
+
)} - {/* Products Grid */} -
+ + {/* Products & Category Navigation */} +
{/* Other Category Pills */} -
- {categories.map((cat) => ( - - {typeof cat.name === 'string' ? cat.name : (cat.name as any)?.[localStorage.getItem('app_lang') || 'en'] || (cat.name as any)?.en || ''} - - ))} +
+ {categories.map((cat) => { + const catItemName = getLocalizedField(cat.name, lang) + const isActive = String(cat.id) === String(id) + + return ( + + {catItemName} + + ) + })}
{error ? ( -
+

⚠️

-

{error}

- - Back to Shop +

{error}

+ + {t('shop.backToShop', 'Back to Shop')}
) : loading ? ( -
- {Array.from({ length: 9 }).map((_, i) => ( -
+
+ {Array.from({ length: 8 }).map((_, i) => ( +
))}
) : products.length === 0 ? ( -
-

🌿

-

No products in this category yet.

+
+

📦

+

+ {t('category.noProducts', 'No products in this category yet.')} +

+ + ← {t('shop.exploreAllProducts', 'Explore All Products')} +
) : (
-
+
{products.slice(0, displayCount).map((p) => ( - + ))}
{/* Invisible target for IntersectionObserver */} {displayCount < products.length && (
-
+
)}