Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 31x 31x 31x 31x 31x 31x 31x 31x 18x 31x 31x 31x 17x 17x 17x 17x 17x 17x 17x 17x 17x 17x 17x 17x 17x 17x 17x 17x 2x 2x 17x 17x 16x 17x 1x 1x 15x 15x 15x 17x 17x 17x 17x 2x 2x 2x 2x 17x 17x 17x 17x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 16x 31x 31x 31x 31x 31x 31x 31x 31x 31x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 3x 3x 3x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 2x 2x 2x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 2x 2x 2x 2x 2x 2x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 2x 2x 2x 2x 2x 2x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 2x 2x 2x 2x 2x 2x | /**
* Product Data Hooks
*
* Reusable React hooks for fetching products from the API.
* Provides built-in error handling, loading states, and retry logic.
*
* @module hooks/useProducts
*
* @example
* ```tsx
* // Basic usage - fetch products with filters
* import { useProducts, useNewArrivals, useBestSellers } from '@/hooks';
*
* function ProductGrid() {
* const { products, loading, error, refetch } = useProducts({
* categoryId: 1,
* limit: 12,
* });
*
* if (loading) return <Loading />;
* if (error) return <Error message={error.message} />;
*
* return (
* <div>
* {products.map(product => (
* <ProductCard key={product.id} product={product} />
* ))}
* </div>
* );
* }
* ```
*/
'use client';
import { useState, useEffect, useCallback, useMemo } from 'react';
import type { Product } from '@/types/product';
import { clientLogger } from '@/lib/logging/clientLogger';
/**
* Filter options for product queries
*/
export interface ProductFilters {
/** Page number for pagination (1-indexed) */
page?: number;
/** Number of items per page */
limit?: number;
/** Filter by category ID */
categoryId?: number;
/** Search query string */
search?: string;
/** Minimum price filter */
minPrice?: number;
/** Maximum price filter */
maxPrice?: number;
/** Fetch only latest/newest products */
latest?: boolean;
/** Fetch only bestseller products */
bestseller?: boolean;
/** Fetch random products */
random?: boolean;
/** Array of product IDs to exclude from results */
exclude?: number[];
}
/**
* Return type for useProducts hook and related product hooks
*/
export interface ProductsResponse {
/** Array of fetched products */
products: Product[];
/** Loading state indicator */
loading: boolean;
/** Error object if fetch failed */
error: Error | null;
/** Function to refetch products */
refetch: () => void;
/** Pagination metadata */
pagination?: {
page: number;
limit: number;
total: number;
totalPages: number;
};
}
/**
* Hook for fetching products with flexible filtering options
*
* Provides automatic loading states, error handling, and pagination support.
* Refetches automatically when filter parameters change.
*
* @param filters - Optional filter parameters for the product query
* @returns ProductsResponse with products array, loading state, error, and refetch function
*
* @example
* ```tsx
* // With pagination
* const { products, pagination, refetch } = useProducts({
* page: currentPage,
* limit: 12,
* categoryId: selectedCategory,
* });
*
* // With search
* const { products, loading } = useProducts({
* search: debouncedSearchTerm,
* limit: 20,
* });
*
* // With price range
* const { products } = useProducts({
* minPrice: 10,
* maxPrice: 100,
* });
* ```
*/
export function useProducts(filters?: ProductFilters): ProductsResponse {
const [products, setProducts] = useState<Product[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const [pagination, setPagination] = useState<ProductsResponse['pagination']>();
// Memoize exclude string to avoid complex expression in dependency array
const excludeString = useMemo(() => {
return filters?.exclude?.join(',') || '';
}, [filters?.exclude]);
const fetchProducts = useCallback(async () => {
try {
setLoading(true);
setError(null);
// Build query string
const params = new URLSearchParams();
if (filters?.page) params.set('page', filters.page.toString());
if (filters?.limit) params.set('limit', filters.limit.toString());
if (filters?.categoryId) params.set('categoryId', filters.categoryId.toString());
if (filters?.search) params.set('search', filters.search);
if (filters?.minPrice) params.set('minPrice', filters.minPrice.toString());
if (filters?.maxPrice) params.set('maxPrice', filters.maxPrice.toString());
if (filters?.latest) params.set('latest', 'true');
if (filters?.bestseller) params.set('bestseller', 'true');
if (filters?.random) params.set('random', 'true');
if (filters?.exclude && filters.exclude.length > 0) {
params.set('exclude', filters.exclude.join(','));
}
const response = await fetch(`/api/products?${params.toString()}`);
if (!response.ok) {
throw new Error(`Failed to fetch products: ${response.statusText}`);
}
const result = await response.json();
// Handle both direct response and wrapped response formats
const data = result?.data ?? result;
setProducts(data.products || []);
setPagination(data.pagination);
} catch (err) {
const error = err instanceof Error ? err : new Error('Failed to fetch products');
clientLogger.error('Error fetching products', error);
setError(error);
setProducts([]);
} finally {
setLoading(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
filters?.page,
filters?.limit,
filters?.categoryId,
filters?.search,
filters?.minPrice,
filters?.maxPrice,
filters?.latest,
filters?.bestseller,
filters?.random,
excludeString, // excludeString already tracks filters?.exclude via useMemo
]);
useEffect(() => {
fetchProducts();
}, [fetchProducts]);
return {
products,
loading,
error,
refetch: fetchProducts,
pagination};
}
/**
* Hook for fetching latest/newest products
*
* Convenience wrapper around useProducts for fetching new arrivals.
*
* @param limit - Maximum number of products to fetch (default: 8)
* @returns ProductsResponse with latest products
*
* @example
* ```tsx
* function NewArrivalsSection() {
* const { products, loading } = useNewArrivals(4);
* return <ProductGrid products={products} loading={loading} />;
* }
* ```
*/
export function useNewArrivals(limit: number = 8): ProductsResponse {
return useProducts({ limit, latest: true });
}
/**
* Hook for fetching bestseller products
*
* Convenience wrapper around useProducts for fetching top-selling items.
*
* @param limit - Maximum number of products to fetch (default: 6)
* @returns ProductsResponse with bestseller products
*
* @example
* ```tsx
* function BestSellerSection() {
* const { products, loading } = useBestSellers(6);
* return <ProductCarousel products={products} loading={loading} />;
* }
* ```
*/
export function useBestSellers(limit: number = 6): ProductsResponse {
return useProducts({ limit, bestseller: true });
}
/**
* Hook for fetching random products
*
* Useful for "You might also like" sections where specific products can be excluded.
*
* @param limit - Maximum number of products to fetch
* @param excludeIds - Array of product IDs to exclude from results
* @returns ProductsResponse with random products
*
* @example
* ```tsx
* function RelatedProducts({ currentProductId }: { currentProductId: number }) {
* const { products, loading } = useRandomProducts(4, [currentProductId]);
* return <ProductGrid products={products} loading={loading} />;
* }
* ```
*/
export function useRandomProducts(
limit: number,
excludeIds?: number[]
): ProductsResponse {
return useProducts({ limit, random: true, exclude: excludeIds });
}
/**
* Hook for fetching products by category
*
* Convenience wrapper for category-filtered product listings.
*
* @param categoryId - Category ID to filter by
* @param limit - Maximum number of products to fetch (default: 12)
* @returns ProductsResponse with products from the specified category
*
* @example
* ```tsx
* function CategoryPage({ categoryId }: { categoryId: number }) {
* const { products, pagination, loading } = useProductsByCategory(categoryId, 20);
* return <ProductListing products={products} pagination={pagination} />;
* }
* ```
*/
export function useProductsByCategory(
categoryId: number,
limit: number = 12
): ProductsResponse {
return useProducts({ categoryId, limit });
}
/**
* Hook for searching products
*
* Convenience wrapper for product search functionality.
* Note: Consider debouncing the searchTerm before passing to this hook.
*
* @param searchTerm - Search query string
* @param limit - Maximum number of results to return (default: 20)
* @returns ProductsResponse with search results
*
* @example
* ```tsx
* function SearchResults() {
* const [query, setQuery] = useState('');
* const debouncedQuery = useDebounce(query, 300);
* const { products, loading } = useProductSearch(debouncedQuery, 20);
*
* return <SearchResultsList products={products} loading={loading} />;
* }
* ```
*/
export function useProductSearch(
searchTerm: string,
limit: number = 20
): ProductsResponse {
return useProducts({ search: searchTerm, limit });
}
|