diff --git a/features/landing/frontend/src/components/Header/Header.css b/features/landing/frontend/src/components/Header/Header.css index 9d93e65df..713c2ebca 100644 --- a/features/landing/frontend/src/components/Header/Header.css +++ b/features/landing/frontend/src/components/Header/Header.css @@ -5,7 +5,7 @@ ============================================ */ /* Import shared glass utilities */ -@import '../../styles/glass.css'; +@import '@transquinnftw/ui-glassmorphism/styles'; .site-header { position: sticky; diff --git a/features/landing/frontend/src/main.tsx b/features/landing/frontend/src/main.tsx index 3d2397c70..c66e4eb83 100644 --- a/features/landing/frontend/src/main.tsx +++ b/features/landing/frontend/src/main.tsx @@ -13,8 +13,7 @@ import { workers } from './config' import { CartProvider, DevUserProvider } from './contexts' import { bundledResources, useApiMode, LANDING_NAMESPACES } from './locales' -// Glassmorphism tokens defined in styles/glass.css (imported by Header.css) -// When @transquinnftw/ui-glassmorphism is installed, can use: import '@transquinnftw/ui-glassmorphism/styles' +// Glassmorphism tokens imported from @transquinnftw/ui-glassmorphism (imported by Header.css and other components) import './index.css' // i18n Strategy: diff --git a/features/landing/frontend/src/pages/apps/AppPage.css b/features/landing/frontend/src/pages/apps/AppPage.css index f8aeb6e33..9bee1fda6 100644 --- a/features/landing/frontend/src/pages/apps/AppPage.css +++ b/features/landing/frontend/src/pages/apps/AppPage.css @@ -3,7 +3,7 @@ Individual app showcase with screenshots ============================================ */ -@import '../../styles/glass.css'; +@import '@transquinnftw/ui-glassmorphism/styles'; .app-page { min-height: 100vh; diff --git a/features/landing/frontend/src/pages/apps/AppsGallery.css b/features/landing/frontend/src/pages/apps/AppsGallery.css index 12d212878..e206f9c05 100644 --- a/features/landing/frontend/src/pages/apps/AppsGallery.css +++ b/features/landing/frontend/src/pages/apps/AppsGallery.css @@ -3,7 +3,7 @@ Showcases all platform apps in a visual grid ============================================ */ -@import '../../styles/glass.css'; +@import '@transquinnftw/ui-glassmorphism/styles'; .apps-gallery { min-height: 100vh; diff --git a/features/landing/frontend/src/pages/shop/ShopApparelPage.tsx b/features/landing/frontend/src/pages/shop/ShopApparelPage.tsx index f842c5c93..6211978b4 100644 --- a/features/landing/frontend/src/pages/shop/ShopApparelPage.tsx +++ b/features/landing/frontend/src/pages/shop/ShopApparelPage.tsx @@ -1,7 +1,7 @@ import { m } from 'framer-motion' import { useRef, useMemo } from 'react' import { useParams, useNavigate } from 'react-router-dom' -import { Shirt, Heart, ShoppingCart } from 'lucide-react' +import { Shirt, Heart, ShoppingCart, Loader2 } from 'lucide-react' import { useTranslation } from '@lilith/i18n' import SEOHead from '../../components/SEOHead' import AIBackground from '../../components/AIBackground' @@ -9,13 +9,13 @@ import ProductDetailModal from '../../components/ProductDetailModal' import { useScrollTrigger } from '@ui/themes' import { useReducedMotion } from '@ui/accessibility' import { useSoundEngine } from '@ui/effects-sound' -import { useCart, type Product } from '../../contexts' +import { useCart, type Product, type ProductType } from '../../contexts' import { Routes } from '../../routes' +import { useProducts, ProductStatus, ShopProductType, type ProductApiResponse } from '../../hooks/useProductsApi' import './Shop.css' -// Define apparel products with full details -// IDs use productId-product-slug format for SEO-friendly URLs -const APPAREL_PRODUCTS_LIST: Product[] = [ +// Fallback products for when API is unavailable or during initial load +const FALLBACK_PRODUCTS: Product[] = [ { id: 'tshirt-lilith-classic', name: 'lilith Classic T-Shirt', @@ -58,11 +58,41 @@ const APPAREL_PRODUCTS_LIST: Product[] = [ }, ] -// Map of products by ID for URL lookup -const APPAREL_PRODUCTS: Record = {} -APPAREL_PRODUCTS_LIST.forEach((product) => { - APPAREL_PRODUCTS[product.id] = product -}) +/** + * Map backend ProductApiResponse to frontend Product type + */ +function mapApiProductToProduct(apiProduct: ProductApiResponse): Product { + // Map backend product type to frontend type + const typeMap: Record = { + [ShopProductType.PHYSICAL_MERCHANDISE]: 'apparel', + [ShopProductType.PHYSICAL_ACCESSORY]: 'accessory', + [ShopProductType.DIGITAL_PRODUCT]: 'accessory', + [ShopProductType.GIFT_CARD]: 'gift-card', + } + + // Extract sizes and colors from variants + const sizes = apiProduct.variants + ?.filter(v => v.size) + .map(v => v.size!) + .filter((v, i, a) => a.indexOf(v) === i) // unique + const colors = apiProduct.variants + ?.filter(v => v.color) + .map(v => v.color!) + .filter((v, i, a) => a.indexOf(v) === i) // unique + + return { + id: apiProduct.id, + name: apiProduct.name, + description: apiProduct.description, + category: apiProduct.category || 'Uncategorized', + price: parseFloat(apiProduct.basePriceUsd), + type: typeMap[apiProduct.productType] || 'accessory', + image: apiProduct.thumbnailUrl || undefined, + comingSoon: apiProduct.status === ProductStatus.COMING_SOON, + sizes: sizes?.length ? sizes : undefined, + colors: colors?.length ? colors : undefined, + } +} export default function ShopApparelPage() { const { t } = useTranslation('landing-merch') @@ -75,11 +105,31 @@ export default function ShopApparelPage() { const merchPreviewRef = useRef(null) const merchPreviewVisible = useScrollTrigger(merchPreviewRef, { rootMargin: '-50px', threshold: 0.1 }) + // Fetch products from API + const { products: apiProducts, loading, error } = useProducts({ category: 'Apparel' }) + + // Map API products to frontend format, fall back to hardcoded if API unavailable + const apparelProducts = useMemo(() => { + if (apiProducts.length > 0) { + return apiProducts.map(mapApiProductToProduct) + } + // Fall back to hardcoded products if API fails or returns empty + return FALLBACK_PRODUCTS.filter(p => p.type === 'apparel' || p.type === 'accessory') + }, [apiProducts]) + + // Create lookup map for modal + const productsMap = useMemo(() => { + return apparelProducts.reduce((acc, product) => { + acc[product.id] = product + return acc + }, {} as Record) + }, [apparelProducts]) + // Derive modal state from URL const selectedProduct = useMemo(() => { if (!productId) return null - return APPAREL_PRODUCTS[productId] || null - }, [productId]) + return productsMap[productId] || null + }, [productId, productsMap]) const isModalOpen = !!selectedProduct @@ -160,8 +210,21 @@ export default function ShopApparelPage() { animate={merchPreviewVisible ? { opacity: 1, y: 0 } : undefined} transition={{ duration: 0.8 }} > + {loading && ( +
+ + {t('merchPreview.loading', 'Loading products...')} +
+ )} + + {error && !loading && apparelProducts.length === 0 && ( +
+ {t('merchPreview.error', 'Unable to load products. Showing preview items.')} +
+ )} +
- {APPAREL_PRODUCTS_LIST.map((product, index) => ( + {apparelProducts.map((product, index) => (