import { useQuery } from "@tanstack/react-query";
import { useCriteria } from "@/hooks/use-criteria";
import { Filter } from "@/components/filter/filter";
import type { QueryFilter } from "@/lib/filter-utils";
import { PaginatedResult } from "@woltz/rich-domain";
interface Product {
id: string;
name: string;
category: string;
price: number;
stock: number;
status: "active" | "draft" | "archived";
createdAt: Date;
}
const productFields: QueryFilter[] = [
{ field: "name", fieldLabel: "Name", type: "string" },
{
field: "category",
fieldLabel: "Category",
type: "string",
options: [
{ value: "electronics", label: "Electronics" },
{ value: "clothing", label: "Clothing" },
{ value: "books", label: "Books" },
],
},
{ field: "price", fieldLabel: "Price", type: "number" },
{ field: "stock", fieldLabel: "Stock", type: "number" },
{
field: "status",
fieldLabel: "Status",
type: "string",
options: [
{ value: "active", label: "Active" },
{ value: "draft", label: "Draft" },
{ value: "archived", label: "Archived" },
],
},
{ field: "createdAt", fieldLabel: "Created At", type: "date" },
];
async function fetchProducts(criteria: Criteria<Product>) {
const params = new URLSearchParams();
// Add filters
criteria.getFilters().forEach((f) => {
params.set(`${f.field}:${f.operator}`, String(f.value));
});
// Add pagination
const pagination = criteria.getPagination();
params.set("page", String(pagination.page));
params.set("limit", String(pagination.limit));
// Add sorting
const orders = criteria.getOrders();
if (orders.length > 0) {
params.set(
"orderBy",
orders.map((o) => `${o.field}:${o.direction}`).join(",")
);
}
const response = await fetch(`/api/products?${params}`);
return response.json() as Promise<PaginatedResult<Product>>;
}
export function ProductList() {
const {
criteria,
filters,
sorting,
pagination,
addOrReplaceByIndex,
removeFilter,
clearFilters,
setSort,
setPage,
} = useCriteria<Product>({
pageSize: 10,
syncWithUrl: true,
});
const { data, isLoading, error } = useQuery({
queryKey: ["products", criteria.toJSON()],
queryFn: () => fetchProducts(criteria),
});
if (error) {
return <div>Error loading products</div>;
}
return (
<div className="space-y-4">
{/* Filters */}
<Filter
fields={productFields}
filters={filters}
addOrReplaceByIndex={addOrReplaceByIndex}
removeFilter={removeFilter}
clearFilters={clearFilters}
/>
{/* Table */}
{isLoading ? (
<div>Loading...</div>
) : (
<table className="w-full">
<thead>
<tr>
<th onClick={() => setSort("name", "asc")}>Name</th>
<th onClick={() => setSort("category", "asc")}>Category</th>
<th onClick={() => setSort("price", "asc")}>Price</th>
<th onClick={() => setSort("stock", "asc")}>Stock</th>
<th onClick={() => setSort("status", "asc")}>Status</th>
</tr>
</thead>
<tbody>
{data?.data.map((product) => (
<tr key={product.id}>
<td>{product.name}</td>
<td>{product.category}</td>
<td>${product.price}</td>
<td>{product.stock}</td>
<td>{product.status}</td>
</tr>
))}
</tbody>
</table>
)}
{/* Pagination */}
{data && (
<div className="flex items-center justify-between">
<span>
Showing {pagination.offset + 1} to{" "}
{Math.min(pagination.offset + pagination.limit, data.meta.total)} of{" "}
{data.meta.total} results
</span>
<div className="flex gap-2">
<button
onClick={() => setPage(pagination.page - 1)}
disabled={!data.meta.hasPrevious}
>
Previous
</button>
<span>
Page {pagination.page} of {data.meta.totalPages}
</span>
<button
onClick={() => setPage(pagination.page + 1)}
disabled={!data.meta.hasNext}
>
Next
</button>
</div>
</div>
)}
</div>
);
}