freshyo/apps/admin-web/src/routes/dashboard.products.tsx
2026-09-06 23:05:20 +05:30

228 lines
8.2 KiB
TypeScript

import React, { useState, useMemo } from 'react'
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { Image as ImageIcon, Eye, Pencil, Package } from 'lucide-react'
import { AppContainer, p as MyText, MyButton, SearchBar, MyFlatList } from 'web-components'
import { trpc } from '@/lib/trpc-client'
import { useManualRefresh, useMarkDataFetchers } from '@/lib/refresh-context'
import type { AdminSku } from '@packages/shared'
export const Route = createFileRoute('/dashboard/products')({
component: Products,
})
type FilterType = 'all' | 'in-stock' | 'out-of-stock'
type SerializedAdminSku = Omit<AdminSku, 'createdAt'> & { createdAt: string | Date }
function getDefaultSku(product: { skus: SerializedAdminSku[] }): SerializedAdminSku | null {
return product.skus?.[0] ?? null
}
function FilterButton({ filter, label, count, activeFilter, onSelect }: { filter: FilterType, label: string, count: number, activeFilter: FilterType, onSelect: (f: FilterType) => void }) {
return (
<button
onClick={() => onSelect(filter)}
className={`rounded-lg px-4 py-2 ${activeFilter === filter ? 'bg-blue-500' : 'bg-gray-200'}`}
>
<MyText className={`${activeFilter === filter ? 'text-white' : 'text-gray-700'} font-semibold`}>
{label} ({count})
</MyText>
</button>
)
}
function Products() {
const navigate = useNavigate()
const [searchTerm, setSearchTerm] = useState('')
const [activeFilter, setActiveFilter] = useState<FilterType>('all')
const { data: productsData, isLoading, error, refetch } = trpc.admin.product.getProducts.useQuery()
useManualRefresh(refetch)
useMarkDataFetchers(() => {
refetch()
})
const products = productsData?.products || []
const filteredProducts = useMemo(() => {
return products.filter((product) => {
const defaultSku = getDefaultSku(product)
const matchesSearch = product.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
(product.shortDescription?.toLowerCase().includes(searchTerm.toLowerCase()))
const matchesFilter = activeFilter === 'all' ||
(activeFilter === 'in-stock' && !defaultSku?.isOutOfStock) ||
(activeFilter === 'out-of-stock' && defaultSku?.isOutOfStock)
return matchesSearch && matchesFilter
})
}, [products, searchTerm, activeFilter])
const handleEdit = (productId: number) => {
navigate({ to: '/dashboard/products/edit', search: { id: String(productId) } })
}
const handleViewDetails = (productId: number) => {
navigate({ to: '/dashboard/products/$id', params: { id: String(productId) } })
}
if (isLoading) {
return (
<AppContainer>
<div className="flex flex-1 items-center justify-center">
<MyText className="text-gray-600">Loading products...</MyText>
</div>
</AppContainer>
)
}
if (error) {
return (
<AppContainer>
<div className="flex flex-1 flex-col items-center justify-center">
<MyText className="text-red-600">Error loading products</MyText>
<button
onClick={() => refetch()}
className="mt-4 rounded-lg bg-blue-500 px-4 py-2"
>
<MyText className="font-semibold text-white">Retry</MyText>
</button>
</div>
</AppContainer>
)
}
const inStockCount = products.filter((p) => getDefaultSku(p) && !getDefaultSku(p)!.isOutOfStock).length
const outOfStockCount = products.filter((p) => getDefaultSku(p)?.isOutOfStock).length
const listHeader = (
<>
<div className="mb-6 flex flex-row items-center justify-between">
<MyText className="text-2xl font-bold text-gray-800">Products</MyText>
<MyButton onClick={() => navigate({ to: '/dashboard/products/new' })}>
Add Product
</MyButton>
</div>
<div className="mb-4">
<SearchBar
value={searchTerm}
onChange={setSearchTerm}
placeholder="Search products..."
className="mb-0"
/>
</div>
<div className="mb-6 flex flex-row gap-2">
<FilterButton filter="all" label="All" count={products.length} activeFilter={activeFilter} onSelect={setActiveFilter} />
<FilterButton filter="in-stock" label="In Stock" count={inStockCount} activeFilter={activeFilter} onSelect={setActiveFilter} />
<FilterButton filter="out-of-stock" label="Out of Stock" count={outOfStockCount} activeFilter={activeFilter} onSelect={setActiveFilter} />
</div>
</>
)
const renderItem = (product: any) => {
const defaultSku = getDefaultSku(product)
const skuImages = defaultSku?.images ?? null
const isOut = defaultSku?.isOutOfStock ?? false
return (
<div key={product.id} className="mb-4 overflow-hidden rounded-2xl bg-white shadow-lg">
{skuImages && skuImages.length > 0 ? (
<img
src={skuImages[0]}
className="h-48 w-full object-cover"
alt={product.name}
/>
) : (
<div className="flex h-48 w-full items-center justify-center bg-gray-200">
<ImageIcon size={48} color="#9CA3AF" />
</div>
)}
<div className="p-4">
<div className="mb-2 flex flex-row items-start justify-between">
<MyText className="mr-2 flex-1 text-lg font-bold text-gray-800" numberOfLines={2}>
{product.name}
</MyText>
<div className="flex flex-row items-center">
<div className={`mr-1 h-3 w-3 rounded-full ${isOut ? 'bg-red-500' : 'bg-green-500'}`} />
<MyText className={`text-sm font-semibold ${isOut ? 'text-red-500' : 'text-green-500'}`}>
{isOut ? 'Out' : 'In'}
</MyText>
</div>
</div>
{product.shortDescription && (
<MyText className="mb-2 text-gray-600" numberOfLines={2}>
{product.shortDescription}
</MyText>
)}
<div className="mb-3 flex flex-row items-center justify-between">
<MyText className="text-sm font-medium text-gray-500">
{product.skus?.length ?? 0} Variants
</MyText>
</div>
<div className="flex flex-row gap-2">
<button
onClick={() => handleViewDetails(product.id)}
className="flex flex-1 flex-row items-center justify-center rounded-lg bg-gray-500 p-3"
>
<Eye size={16} color="white" />
<MyText className="ml-1 font-semibold text-white">View</MyText>
</button>
<button
onClick={() => handleEdit(product.id)}
className="flex flex-1 flex-row items-center justify-center rounded-lg bg-blue-500 p-3"
>
<Pencil size={16} color="white" />
<MyText className="ml-1 font-semibold text-white">Edit</MyText>
</button>
</div>
</div>
</div>
)
}
return (
<AppContainer>
<div className="flex flex-1 flex-col">
<MyFlatList
data={filteredProducts}
renderItem={renderItem}
keyExtractor={(item) => item.id.toString()}
ListHeaderComponent={listHeader}
ListEmptyComponent={
<div className="flex flex-1 flex-col items-center justify-center py-10">
<Package size={64} color="#D1D5DB" />
<MyText className="mt-4 text-center text-lg text-gray-500">
{searchTerm || activeFilter !== 'all' ? 'No products match your filters' : 'No products found'}
</MyText>
<MyText className="mt-2 text-center text-gray-400">
{searchTerm || activeFilter !== 'all' ? 'Try adjusting your search or filters' : 'Start by adding your first product'}
</MyText>
{(searchTerm || activeFilter !== 'all') && (
<button
onClick={() => {
setSearchTerm('')
setActiveFilter('all')
}}
className="mt-4 rounded-lg bg-blue-500 px-4 py-2"
>
<MyText className="font-semibold text-white">Clear Filters</MyText>
</button>
)}
</div>
}
className="pb-4"
/>
</div>
</AppContainer>
)
}