160 lines
6.3 KiB
TypeScript
160 lines
6.3 KiB
TypeScript
'use client';
|
|
import { useEffect, useState, useMemo } from 'react';
|
|
|
|
// Core columns for performance and usability
|
|
const columns = [
|
|
'uuid',
|
|
'sku',
|
|
'brandName',
|
|
'productName',
|
|
'department',
|
|
'category',
|
|
'subcategory',
|
|
'retailPrice',
|
|
'salePrice',
|
|
'imageUrl',
|
|
];
|
|
|
|
export default function AdminProductsPage() {
|
|
const [products, setProducts] = useState<any[]>([]);
|
|
const [total, setTotal] = useState(0);
|
|
const [page, setPage] = useState(1);
|
|
const [limit, setLimit] = useState(50);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
// Filter state
|
|
const [brand, setBrand] = useState('');
|
|
const [department, setDepartment] = useState('');
|
|
const [category, setCategory] = useState('');
|
|
const [subcategory, setSubcategory] = useState('');
|
|
|
|
useEffect(() => {
|
|
setLoading(true);
|
|
setError(null);
|
|
fetch(`/api/products?page=${page}&limit=${limit}`)
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
setProducts(data.data || []);
|
|
setTotal(data.total || 0);
|
|
setLoading(false);
|
|
})
|
|
.catch(err => {
|
|
setError(err.message || 'Error fetching products');
|
|
setLoading(false);
|
|
});
|
|
}, [page, limit]);
|
|
|
|
// Get unique filter options from current page's products
|
|
const brandOptions = useMemo(() => Array.from(new Set(products.map(p => p.brandName).filter(Boolean))).sort(), [products]);
|
|
const departmentOptions = useMemo(() => Array.from(new Set(products.map(p => p.department).filter(Boolean))).sort(), [products]);
|
|
const categoryOptions = useMemo(() => Array.from(new Set(products.map(p => p.category).filter(Boolean))).sort(), [products]);
|
|
const subcategoryOptions = useMemo(() => Array.from(new Set(products.map(p => p.subcategory).filter(Boolean))).sort(), [products]);
|
|
|
|
// Filter products before rendering
|
|
const filteredProducts = useMemo(() => {
|
|
return products.filter(p =>
|
|
(!brand || p.brandName === brand) &&
|
|
(!department || p.department === department) &&
|
|
(!category || p.category === category) &&
|
|
(!subcategory || p.subcategory === subcategory)
|
|
);
|
|
}, [products, brand, department, category, subcategory]);
|
|
|
|
// Reset to page 1 when filters change
|
|
useEffect(() => { setPage(1); }, [brand, department, category, subcategory]);
|
|
|
|
return (
|
|
<div className="max-w-7xl mx-auto px-4 py-8">
|
|
<h1 className="text-2xl font-bold mb-4">Admin Products</h1>
|
|
{error && <div className="text-red-600 mb-4">{error}</div>}
|
|
{/* Filters */}
|
|
<div className="flex flex-wrap gap-4 mb-4 items-end">
|
|
<div>
|
|
<label className="block text-xs font-semibold mb-1">Brand</label>
|
|
<select className="border rounded px-2 py-1 min-w-[120px]" value={brand} onChange={e => setBrand(e.target.value)}>
|
|
<option value="">All</option>
|
|
{brandOptions.map(opt => <option key={opt} value={opt}>{opt}</option>)}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-semibold mb-1">Department</label>
|
|
<select className="border rounded px-2 py-1 min-w-[120px]" value={department} onChange={e => setDepartment(e.target.value)}>
|
|
<option value="">All</option>
|
|
{departmentOptions.map(opt => <option key={opt} value={opt}>{opt}</option>)}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-semibold mb-1">Category</label>
|
|
<select className="border rounded px-2 py-1 min-w-[120px]" value={category} onChange={e => setCategory(e.target.value)}>
|
|
<option value="">All</option>
|
|
{categoryOptions.map(opt => <option key={opt} value={opt}>{opt}</option>)}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-semibold mb-1">Subcategory</label>
|
|
<select className="border rounded px-2 py-1 min-w-[120px]" value={subcategory} onChange={e => setSubcategory(e.target.value)}>
|
|
<option value="">All</option>
|
|
{subcategoryOptions.map(opt => <option key={opt} value={opt}>{opt}</option>)}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div className="overflow-x-auto border rounded bg-white">
|
|
<table className="min-w-full text-sm">
|
|
<thead>
|
|
<tr>
|
|
{columns.map(col => (
|
|
<th key={col} className="px-2 py-2 border-b text-left font-semibold bg-zinc-50">{col}</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{loading ? (
|
|
<tr><td colSpan={columns.length} className="text-center py-8">Loading...</td></tr>
|
|
) : filteredProducts.length === 0 ? (
|
|
<tr><td colSpan={columns.length} className="text-center py-8">No products found.</td></tr>
|
|
) : (
|
|
filteredProducts.map((product, i) => (
|
|
<tr key={product.uuid || i} className="border-b hover:bg-zinc-50">
|
|
{columns.map(col => (
|
|
<td key={col} className="px-2 py-1 max-w-xs truncate">
|
|
{col === 'imageUrl' && product[col] ? (
|
|
<img src={product[col]} alt="thumb" className="h-10 w-10 object-contain border rounded" />
|
|
) : (
|
|
product[col] ?? ''
|
|
)}
|
|
</td>
|
|
))}
|
|
</tr>
|
|
))
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{/* Pagination Controls */}
|
|
<div className="flex items-center gap-4 mt-4">
|
|
<button
|
|
className="px-3 py-1 border rounded disabled:opacity-50"
|
|
onClick={() => setPage(p => Math.max(1, p - 1))}
|
|
disabled={page === 1}
|
|
>Prev</button>
|
|
<span>Page {page} of {Math.ceil(total / limit) || 1}</span>
|
|
<button
|
|
className="px-3 py-1 border rounded disabled:opacity-50"
|
|
onClick={() => setPage(p => p + 1)}
|
|
disabled={page * limit >= total}
|
|
>Next</button>
|
|
<select
|
|
className="ml-4 border rounded px-2 py-1"
|
|
value={limit}
|
|
onChange={e => { setLimit(Number(e.target.value)); setPage(1); }}
|
|
>
|
|
{[25, 50, 100, 200].map(opt => (
|
|
<option key={opt} value={opt}>{opt} / page</option>
|
|
))}
|
|
</select>
|
|
<span className="ml-2 text-zinc-500">Total: {total}</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|