210 lines
7.3 KiB
TypeScript
210 lines
7.3 KiB
TypeScript
import { createFileRoute, useNavigate, useParams } from '@tanstack/react-router'
|
|
import { useStoreWithProducts, useAllProducts } from '../hooks/prominent-api-hooks'
|
|
import { useState, useMemo } from 'react'
|
|
import { p } from 'web-components'
|
|
import { AppLayout } from '../components/AppLayout'
|
|
import AddToCartDialog from '../components/AddToCartDialog'
|
|
import { ProductCard } from '../components/ProductCard'
|
|
import { usePopulateCentralStores } from '../hooks/usePopulateCentralStores'
|
|
import { ArrowLeft, Store, X, Grid3X3 } from 'lucide-react'
|
|
|
|
export const Route = createFileRoute('/stores/$storeId')({
|
|
component: StoreDetailPage,
|
|
})
|
|
|
|
interface Tag {
|
|
id: number
|
|
tagName: string
|
|
productIds?: number[]
|
|
}
|
|
|
|
function StoreDetailPage() {
|
|
const { storeId } = useParams({ from: '/stores/$storeId' })
|
|
const navigate = useNavigate()
|
|
const storeIdNum = Number(storeId)
|
|
const [selectedTagId, setSelectedTagId] = useState<number | null>(null)
|
|
|
|
// Populate central stores with slots and product data for out-of-stock checking
|
|
usePopulateCentralStores()
|
|
|
|
const { data: storeData, isLoading, error, refetch } = useStoreWithProducts(storeIdNum)
|
|
const { data: productsData, isLoading: isProductsLoading } = useAllProducts()
|
|
|
|
const productById = useMemo(() => {
|
|
const map = new Map<number, any>()
|
|
productsData?.products?.forEach((product) => {
|
|
map.set(product.id, product)
|
|
})
|
|
return map
|
|
}, [productsData])
|
|
|
|
const storeProducts = useMemo(() => {
|
|
if (!storeData?.products) return []
|
|
return storeData.products
|
|
.map((product: any) => productById.get(product.id))
|
|
.filter(Boolean)
|
|
}, [storeData, productById])
|
|
|
|
// Filter products based on selected tag
|
|
const filteredProducts = selectedTagId
|
|
? storeProducts.filter((product: any) => {
|
|
const selectedTag = storeData?.tags.find((t: Tag) => t.id === selectedTagId)
|
|
return selectedTag?.productIds?.includes(product.id) ?? false
|
|
})
|
|
: storeProducts
|
|
|
|
const isMeatStore = storeData?.store?.name?.toLowerCase().includes('meat')
|
|
|
|
if (isLoading || isProductsLoading) {
|
|
return (
|
|
<AppLayout>
|
|
<div className="flex min-h-screen items-center justify-center bg-gray-50">
|
|
<p className="font-medium text-gray-500">
|
|
{isLoading ? 'Loading store...' : 'Loading products...'}
|
|
</p>
|
|
</div>
|
|
</AppLayout>
|
|
)
|
|
}
|
|
|
|
if (error || !storeData) {
|
|
return (
|
|
<AppLayout>
|
|
<div className="flex min-h-screen flex-col items-center justify-center bg-gray-50">
|
|
<svg className="mb-4 h-12 w-12 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
<p className="font-bold mb-2 text-lg text-gray-900">
|
|
Oops!
|
|
</p>
|
|
<p className="text-gray-500">Store not found or error loading</p>
|
|
</div>
|
|
</AppLayout>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<AppLayout>
|
|
<div className="min-h-screen bg-gray-50 pb-24">
|
|
{/* Back Button */}
|
|
<div className="sticky top-0 z-10 border-b border-gray-200 bg-white px-4 py-3">
|
|
<div className="flex items-center gap-3">
|
|
<div onClick={() => navigate({ to: '/stores' })} className="p-2">
|
|
<ArrowLeft className="h-5 w-5 text-gray-700" />
|
|
</div>
|
|
<p className="font-bold text-lg text-gray-900">
|
|
{storeData?.store?.name || 'Store'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="px-4 pt-4">
|
|
|
|
{/* Store Info Card */}
|
|
<div className="flex items-center gap-2 mb-6 rounded-2xl border border-gray-100 bg-white p-6 text-center shadow-sm">
|
|
<div className="mb-4 flex h-16 w-16 items-center justify-center self-center rounded-full bg-pink-50">
|
|
<Store className="h-7 w-7 text-brand-500" />
|
|
</div>
|
|
<p className="font-bold mb-2 text-center text-2xl text-gray-900">
|
|
{storeData?.store?.name}
|
|
</p>
|
|
{storeData?.store?.description && (
|
|
<p className="px-4 text-center leading-5 text-gray-500">
|
|
{storeData?.store?.description}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Tags Section */}
|
|
{storeData?.tags && storeData.tags.length > 0 && (
|
|
<div className="mb-6 flex gap-2 overflow-x-auto pb-2 scrollbar-hide">
|
|
{storeData.tags.map((tag: Tag) => (
|
|
<TagChip
|
|
key={tag.id}
|
|
tag={tag}
|
|
isSelected={selectedTagId === tag.id}
|
|
onPress={() => setSelectedTagId(selectedTagId === tag.id ? null : tag.id)}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Products Count & Clear Filter */}
|
|
<div className="mb-4 flex items-center justify-between">
|
|
<div className="flex items-center">
|
|
<Grid3X3 className="mr-2 h-5 w-5 text-gray-700" />
|
|
<p className="font-bold text-lg text-gray-900">
|
|
{selectedTagId
|
|
? `${storeData?.tags.find((t: Tag) => t.id === selectedTagId)?.tagName} items`
|
|
: `${filteredProducts.length} products`}
|
|
</p>
|
|
</div>
|
|
{selectedTagId && (
|
|
<div
|
|
onClick={() => setSelectedTagId(null)}
|
|
className="flex items-center"
|
|
>
|
|
<p className="mr-1 text-sm font-medium text-brand-500">Clear</p>
|
|
<X className="h-4 w-4 text-brand-500" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Products Grid */}
|
|
<div className="grid gap-4 sm:grid-cols-2" style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))' }}>
|
|
{filteredProducts.map((product: any) => (
|
|
<ProductCard
|
|
key={product.id}
|
|
item={product}
|
|
onPress={() =>
|
|
navigate({
|
|
to: '/stores/$storeId/product/$productId',
|
|
params: { storeId, productId: String(product.id) },
|
|
})
|
|
}
|
|
showDeliveryInfo={false}
|
|
miniView={true}
|
|
useAddToCartDialog={true}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
{filteredProducts.length === 0 && (
|
|
<div className="py-10 text-center">
|
|
<p className="font-medium text-gray-400">
|
|
{selectedTagId ? 'No products in this category' : 'No products available'}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<AddToCartDialog />
|
|
</AppLayout>
|
|
)
|
|
}
|
|
|
|
interface TagChipProps {
|
|
tag: Tag
|
|
isSelected: boolean
|
|
onPress: () => void
|
|
}
|
|
|
|
function TagChip({ tag, isSelected, onPress }: TagChipProps) {
|
|
const productCount = tag.productIds?.length || 0
|
|
|
|
return (
|
|
<button
|
|
onClick={onPress}
|
|
className={`whitespace-nowrap rounded-lg border px-4 py-2 ${
|
|
isSelected
|
|
? 'border-brand-500 bg-brand-500 text-white'
|
|
: 'border-brand-500 bg-white text-brand-500'
|
|
}`}
|
|
>
|
|
<span className={`text-sm font-medium ${isSelected ? 'text-white' : 'text-brand-500'}`}>
|
|
{tag.tagName} ({productCount})
|
|
</span>
|
|
</button>
|
|
)
|
|
}
|