freshyo/apps/web-ui/src/routes/home.search.tsx
2026-08-18 23:12:55 +05:30

184 lines
6 KiB
TypeScript

import { createFileRoute, useNavigate, useSearch } from '@tanstack/react-router'
import { useState, useEffect, useMemo, useRef, useCallback } from 'react'
import Fuse from 'fuse.js'
import { useAllProducts } from '../hooks/prominent-api-hooks'
import { SearchBar } from 'web-components'
import { ProductCard } from '../components/ProductCard'
import { SearchX, Loader2, ChevronLeft } from 'lucide-react'
import { AppLayout } from '../components/AppLayout'
import AddToCartDialog from '../components/AddToCartDialog'
import { useCartStore } from '../lib/stores/cart-store'
export const Route = createFileRoute('/home/search')({
component: SearchPage,
validateSearch: (search: Record<string, string>) => ({
q: search.q || '',
}),
})
// Debounce hook for search
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value)
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => {
clearTimeout(timer)
}
}, [value, delay])
return debouncedValue
}
function SearchPage() {
const navigate = useNavigate()
const { q } = useSearch({ from: '/home/search' })
const [inputQuery, setInputQuery] = useState(q)
const searchInputRef = useRef<HTMLInputElement>(null)
const { setAddedToCartProduct } = useCartStore()
// Debounce the search query for automatic search
const debouncedQuery = useDebounce(inputQuery, 300)
// Focus search input on mount
useEffect(() => {
requestAnimationFrame(() => {
searchInputRef.current?.focus()
})
}, [])
const { data: productsData, isLoading, error, refetch } = useAllProducts()
const allProducts = productsData?.products || []
// Client-side search filtering using Fuse.js
const products = useMemo(() => {
if (!debouncedQuery.trim()) return allProducts
const fuse = new Fuse(allProducts, {
keys: ['name', 'shortDescription'],
threshold: 0.3,
includeScore: true,
shouldSort: true,
})
const fuseResults = fuse.search(debouncedQuery)
return fuseResults.map((result) => result.item)
}, [allProducts, debouncedQuery])
const handleSearch = useCallback(() => {
// Search is now automatic via debounce, but keep this for manual submit
// Update URL with search query
navigate({
to: '/home/search',
search: { q: inputQuery },
})
}, [inputQuery, navigate])
const handleProductPress = (id: number) => {
navigate({
to: '/home/product/$id',
params: { id: String(id) },
})
}
const handleAddToCart = (product: any) => {
setAddedToCartProduct({ productId: product.id, product })
}
if (isLoading) {
return (
<AppLayout>
<div className="flex min-h-full flex-1 items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-brand-500" />
<p className="ml-2 font-medium text-gray-500">Loading products...</p>
</div>
</AppLayout>
)
}
if (error) {
return (
<AppLayout>
<div className="flex min-h-full flex-1 flex-col items-center justify-center px-4">
<SearchX className="h-16 w-16 text-gray-300" />
<p className="mt-4 text-lg font-medium text-gray-900">Failed to load products</p>
<button
onClick={() => refetch()}
className="mt-4 rounded-lg bg-brand-600 px-4 py-2 font-medium text-white transition-colors hover:bg-brand-700"
>
Retry
</button>
</div>
</AppLayout>
)
}
return (
<AppLayout>
<div className="flex min-h-full flex-1 flex-col bg-gray-50">
{/* Search Header */}
<div className="sticky top-0 z-10 border-b border-gray-100 bg-white px-4 pb-4 pt-4">
<div className="mx-auto flex w-full max-w-7xl items-center gap-3">
<button
onClick={() => navigate({ to: '/home' })}
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-gray-50 text-gray-700 transition-colors hover:bg-gray-100"
aria-label="Back to home"
>
<ChevronLeft className="h-5 w-5" />
</button>
<div className="flex-1">
<SearchBar
ref={searchInputRef}
placeholder="Search products..."
value={inputQuery}
onChange={setInputQuery}
onSearch={handleSearch}
/>
</div>
</div>
<div className="mx-auto mt-3 flex w-full max-w-7xl flex-row items-center justify-between">
<p className="text-lg font-bold text-gray-900">
{debouncedQuery ? `Search Results for "${debouncedQuery}"` : 'All Products'}
</p>
<p className="text-sm text-gray-500">{products.length} items</p>
</div>
</div>
{/* Products Grid */}
<div className="mx-auto w-full max-w-7xl flex-1 p-4">
{products.length === 0 ? (
<div className="flex flex-1 flex-col items-center justify-center px-4 py-12">
<SearchX className="h-16 w-16 text-gray-300" />
<p className="mt-4 text-center text-lg font-medium text-gray-500">
No products found
</p>
{debouncedQuery && (
<p className="mt-2 text-center text-gray-400">
Try adjusting your search for "{debouncedQuery}"
</p>
)}
</div>
) : (
<div className="grid grid-cols-2 gap-3 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
{products.map((product: any) => (
<ProductCard
key={product.id}
item={product}
onPress={() => handleProductPress(product.id)}
showDeliveryInfo={false}
miniView={true}
useAddToCartDialog={true}
/>
))}
</div>
)}
</div>
</div>
<AddToCartDialog />
</AppLayout>
)
}