This is the current products/page.jsx:
import React from 'react';
import ProductList from '@/components/ProductList';
import AsyncNavBar from '@/components/AsyncNavBar';
import { unstable_cache } from 'next/cache';
import Link from 'next/link';

const ITEMS_PER_PAGE = 15;
export const revalidate = 0;
export const dynamic = 'force-dynamic';



const fetchSubBrands = unstable_cache(
    async () => {
        const response = await fetch(`${process.env.NEXT_PUBLIC_API}/subcategories/brands`, {
            headers: { 'Content-Type': 'application/json' },
            cache: "force-cache",
            next: {revalidate: 3600}
        });

        if (response.status === 200) {
            return response.json();
        }
        throw new Error(`Failed to fetch sub-brands: ${response.statusText}`);
    },
    {
        revalidate: 1440,
    }
);

const fetchProducts = async () => {
    const response = await fetch(`${process.env.NEXT_PUBLIC_API}/products`, {
        headers: {'Content-Type': 'application/json'},
        next: {revalidate: 3600}
    });

    if (!response.ok) {
        throw new Error('Failed to fetch products');
    }
    return response.json();
}

const getFilteredProducts = async (
    page = 1,
    query = '',
    c = '',
    sc = '',
    b = '',
    minPriceA = 0,
    maxPriceA = Infinity,
    availability = 'all',
    sortBy = ''
) => {
    const [productsData, subBrands] = await Promise.all([
        fetchProducts(),
        b ? fetchSubBrands() : Promise.resolve([]), // Uses unstable_cache: fetched once every 4 hours if 'b' is truthy
    ]);

    let products = productsData.products;

    const subCategories = b
        ? subBrands
            .filter((item) => item.category_id.toString() === b)
            .map((item) => item.id.toString())
        : [];

    // Calculate min and max prices
    let minPrice = Infinity;
    let maxPrice = -Infinity;
    products.forEach((product) => {
        product.sizes.forEach((size) => {
            const price = parseFloat(size.price);
            if (price < minPrice) minPrice = price;
            if (price > maxPrice) maxPrice = price;
        });
    });

    const filterProduct = (product) => {
        if (
            c &&
            !product.categories.some((cat) => cat.main_category_id?.toString() === c)
        )
            return false;
        if (
            sc &&
            product?.brand_id.toString() !== sc &&
            !product.categories.some((cat) => cat.id?.toString() === sc)
        )
            return false;
        if (b && !subCategories.includes(product?.brand_id.toString())) return false;

        // Price range filter
        const productPrices = product.sizes.map((size) => parseFloat(size.price));
        const productMinPrice = Math.min(...productPrices);
        const productMaxPrice = Math.max(...productPrices);

        if (productMinPrice < minPriceA || productMaxPrice > maxPriceA) {
            return false;
        }

        // Availability filter
        if (availability !== 'all') {
            const inStock = product.sizes.some((size) => size.qty > 0);
            if (
                (availability === 'in_stock' && !inStock) ||
                (availability === 'out_of_stock' && inStock)
            ) {
                return false;
            }
        }

        // Query filter
        if (query) {
            const normalizedName = product.name.normalize('NFC').toLowerCase();
            const cleanName = normalizedName.replace(
                /[\u200E\u200F\u202A-\u202E\u2066-\u2069]/g,
                ''
            );
            const queryWords = query.toLowerCase().split(/\s+/);
            return queryWords.every((word) => cleanName.includes(word));
        }

        return true;
    };

    // Apply the filter
    let filteredProducts = products.filter(filterProduct);

    // Calculate total sold for each product
    filteredProducts.forEach((product) => {
        product.totalSold = product.sizes.reduce((sum, size) => sum + size.sold, 0);
    });

    // Apply sorting
    switch (sortBy) {
        case 'Price: Low to High':
            filteredProducts.sort(
                (a, b) =>
                    Math.min(...a.sizes.map((s) => parseFloat(s.price))) -
                    Math.min(...b.sizes.map((s) => parseFloat(s.price)))
            );
            break;
        case 'Price: High to Low':
            filteredProducts.sort(
                (a, b) =>
                    Math.max(...b.sizes.map((s) => parseFloat(s.price))) -
                    Math.max(...a.sizes.map((s) => parseFloat(s.price)))
            );
            break;
        case 'Name: A to Z':
            filteredProducts.sort((a, b) => a.name.localeCompare(b.name));
            break;
        case 'Name: Z to A':
            filteredProducts.sort((a, b) => b.name.localeCompare(a.name));
            break;
        case 'Best Selling':
            filteredProducts.sort((a, b) => b.totalSold - a.totalSold);
            break;
        case 'Newest Arrivals':
            filteredProducts.sort(
                (a, b) => new Date(b.created_at) - new Date(a.created_at)
            );
            break;
        default:
            // Default sorting (optional)
            break;
    }

    // Additional query-based sorting if applicable
    if (query && !sortBy) {
        const lowerQuery = query.toLowerCase();
        filteredProducts.sort((a, b) => {
            const aIndex = a.name.toLowerCase().indexOf(lowerQuery);
            const bIndex = b.name.toLowerCase().indexOf(lowerQuery);
            return (aIndex === -1 ? Infinity : aIndex) - (bIndex === -1 ? Infinity : bIndex);
        });
    }

    const totalProducts = filteredProducts.length;
    const end = page * ITEMS_PER_PAGE;

    return {
        products: filteredProducts.slice(0, end),
        totalProducts,
        hasMore: end < totalProducts,
        minPrice,
        maxPrice,
    };
};

// The main ProductsPage component
const ProductsPage = async ({ searchParams }) => {
    // Destructure and parse search parameters
    const {
        page = '1',
        query = '',
        c = '',
        sc = '',
        b = '',
        minPrice = '0',
        maxPrice = '',
        availability = 'all',
        sortBy = '',
        title = '',
    } = searchParams;

    const pageNum = parseInt(page, 10) || 1;
    const minPriceNum = parseFloat(minPrice) || 0;
    const maxPriceNum = maxPrice ? parseFloat(maxPrice) : Infinity;

    try {
        // Get filtered products based on search parameters
        const { products, totalProducts, hasMore, minPrice, maxPrice } = await getFilteredProducts(
            pageNum,
            query,
            c,
            sc,
            b,
            minPriceNum,
            maxPriceNum,
            availability,
            sortBy
        );

        return (
            <>
                <AsyncNavBar />
                <ProductList
                    initialProducts={products}
                    totalProducts={totalProducts}
                    hasMore={hasMore}
                    initialPage={pageNum}
                    initialQuery={query}
                    initialC={c}
                    initialSc={sc}
                    initialB={b}
                    minPrice={minPrice}
                    maxPrice={maxPrice}
                    title={sortBy || title || 'All Products'}
                />
            </>
        );
    } catch (error) {
        // Enhanced error handling  user-friendly message
        console.error('Error fetching products:', error);
        return (
            <div className="font-serif flex flex-col justify-center items-center p-4">
                <p>Oops! Something went wrong while loading products. Please try again later.</p>
                <Link href="/nay-store/public" className="text-blue-500 underline mt-2">
                    Go back home
                </Link>
            </div>
        );
    }
};

export default ProductsPage;
However I switched the api to work on pagination:
export const getProducts = async (req: Request, res: Response): Promise<void> => {
    try {
        // Retrieve cached products
        let cacheData: CacheData | undefined = productCache.get('cacheData');

        if (!cacheData) {
            // Cache miss: Load from DB and cache
            cacheData = await loadAndCacheAllProducts();
            console.log('Loaded products from database and cached them.');
        } else {
            console.log('Loaded products from cache.');
        }

        const { allProducts, minPrice, maxPrice } = cacheData;

        // Log total products before any filtering
        console.log(`Total products before filtering: ${allProducts.length}`);

        // Extract and sanitize query parameters
        let {
            search = '',
            c,
            sc,
            b,
            minPriceA = '0',
            maxPriceA = '',
            availability = 'all',
            sortBy = '',
            page = '1',
            itemsPerPage = '15',
        } = req.query;

        // Parse numerical values
        const pageNum = parseInt(page as string, 10) || 1;
        const itemsPerPageNum = parseInt(itemsPerPage as string, 10) || 15;
        const minPriceANum = parseFloat(minPriceA as string) || 0;
        let maxPriceANum = maxPriceA ? parseFloat(maxPriceA as string) : Infinity;

        // If maxPriceA is less than minPriceA, reset to Infinity
        if (!isNaN(maxPriceANum) && maxPriceANum < minPriceANum) {
            console.warn('Provided maxPriceA is less than minPriceA. Resetting maxPriceANum to Infinity.');
            maxPriceANum = Infinity;
        } else if (isNaN(maxPriceANum)) {
            maxPriceANum = Infinity;
        }

        // Parse c, sc, b as numbers if present
        const cNum = c ? parseInt(c as string, 10) : undefined;
        const scNum = sc ? parseInt(sc as string, 10) : undefined;
        const bNum = b ? parseInt(b as string, 10) : undefined;

        // Initialize brand-related variables if 'b' is provided
        let allBrands: Brand[] = [];
        if (bNum !== undefined && !isNaN(bNum)) {
            // Retrieve cached brands
            let brandsCacheData: BrandsCacheData | undefined = brandsCache.get('allBrands');

            if (!brandsCacheData) {
                // Cache miss: Load from DB and cache
                brandsCacheData = await loadAndCacheAllBrands();
                console.log('Loaded brands from database and cached them.');
            } else {
                console.log('Loaded brands from cache.');
            }
            console.log(brandsCacheData)
            allBrands = brandsCacheData.allBrands;

            // Check if 'bNum' is a valid brand ID
            const isValidBrand = allBrands.some(brand => brand.category_id === bNum);
            if (!isValidBrand) {
                console.warn(`Brand ID ${bNum} is invalid.`);
                // Return zero products since 'b' is invalid
                res.json({
                    products: [],
                    total: 0,
                    page: pageNum,
                    itemsPerPage: itemsPerPageNum,
                    totalPages: 0,
                    hasMore: false,
                    minPrice,
                    maxPrice,
                });
                return;
            }
        }

        // Start with all products
        let filteredProducts = [...allProducts];

        // Apply Search Filter
        if (search && typeof search === 'string') {
            const searchLower = search.toLowerCase();
            const searchWords = searchLower.split(/\s+/).filter(Boolean);
            filteredProducts = filteredProducts.filter(product =>
                searchWords.every(word => product.name.toLowerCase().includes(word))
            );
        }

        // Apply Category Filter (c)
        if (cNum !== undefined && !isNaN(cNum)) {
            filteredProducts = filteredProducts.filter(product =>
                product.categories.some(cat => cat.main_category_id === cNum)
            );
        }

        // Apply Subcategory Filter (sc)
        if (scNum !== undefined && !isNaN(scNum)) {
            filteredProducts = filteredProducts.filter(product =>
                product.brand_id === scNum ||
                product.categories.some(cat => cat.id === scNum)
            );
        }

        // Apply Brand Filter (b)
        if (bNum !== undefined && !isNaN(bNum)) {
            console.log(`Applying Brand Filter: brand_id === ${bNum}`);
            const preFilterCount = filteredProducts.length;
            allBrands = allBrands.filter(brand => brand.category_id === bNum)
            filteredProducts = filteredProducts.filter(product =>
                allBrands.some(brand => brand.id === product.brand_id)
            );
            console.log(`Filtered from ${preFilterCount} to ${filteredProducts.length} products after applying brand filter.`);
        }

        // Apply Price Range Filter
        if (minPriceANum > 0 || maxPriceANum < Infinity) {
            filteredProducts = filteredProducts.filter(product => {
                const productPrices = product.sizes.map(size => size.price);
                if (productPrices.length === 0) return false;
                const productMinPrice = Math.min(...productPrices);
                const productMaxPrice = Math.max(...productPrices);
                return productMinPrice >= minPriceANum && productMaxPrice <= maxPriceANum;
            });
        }

        // Apply Availability Filter
        if (availability === 'in_stock') {
            filteredProducts = filteredProducts.filter(product =>
                product.sizes.some(size => size.qty > 0)
            );
        } else if (availability === 'out_of_stock') {
            filteredProducts = filteredProducts.filter(product =>
                product.sizes.every(size => size.qty === 0)
            );
        }

        // Log total after filtering
        console.log(`Total products after filtering: ${filteredProducts.length}`);

        // Apply Sorting
        if (sortBy && typeof sortBy === 'string') {
            switch (sortBy) {
                case 'Price: Low to High':
                    filteredProducts.sort((a, b) => {
                        const aMin = Math.min(...a.sizes.map(s => s.price));
                        const bMin = Math.min(...b.sizes.map(s => s.price));
                        return aMin - bMin;
                    });
                    break;
                case 'Price: High to Low':
                    filteredProducts.sort((a, b) => {
                        const aMax = Math.max(...a.sizes.map(s => s.price));
                        const bMax = Math.max(...b.sizes.map(s => s.price));
                        return bMax - aMax;
                    });
                    break;
                case 'Name: A to Z':
                    filteredProducts.sort((a, b) => a.name.localeCompare(b.name));
                    break;
                case 'Name: Z to A':
                    filteredProducts.sort((a, b) => b.name.localeCompare(a.name));
                    break;
                case 'Best Selling':
                    filteredProducts.sort((a, b) => b.sold - a.sold);
                    break;
                case 'Newest Arrivals':
                    filteredProducts.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
                    break;
                default:
                    // If sortBy parameter doesn't match any case, do not sort
                    break;
            }
        }

        // Apply Pagination
        const totalFilteredProducts = filteredProducts.length;
        const totalPages = Math.ceil(totalFilteredProducts / itemsPerPageNum);
        const startIdx = (pageNum - 1) * itemsPerPageNum;
        const endIdx = startIdx + itemsPerPageNum;
        const paginatedProducts = filteredProducts.slice(startIdx, endIdx);

        // Construct Response
        const response = {
            products: paginatedProducts,
            total: totalFilteredProducts,
            page: pageNum,
            itemsPerPage: itemsPerPageNum,
            totalPages,
            hasMore: endIdx < totalFilteredProducts,
            minPrice, // Global minPrice across all products
            maxPrice, // Global maxPrice across all products
        };

        // Send Response
        res.json(response);
    } catch (error) {
        console.error('Error fetching products:', error);
        res.status(500).json({ error: 'Internal server error' });
    } finally {
        // Optional: Any cleanup operations can be performed here
        // For example, monitoring memory usage or logging request completion
    }
};
I want you to mirgate the new api with the clientside of ProductList and get rid of searchParams, except params like c and sc and b  and title will be constant and totally optional!!!... 
so the new products/page.jsx will be like this:

import React from 'react';
import ProductList from '@/components/ProductList';
import AsyncNavBar from '@/components/AsyncNavBar';
import Link from 'next/link';

export const dynamic = 'force-dynamic';

const ProductsPage = async ({ searchParams }) => {
    try {
    const {
        c = '',
        sc = '',
        b = '',
        title = '',
    } = searchParams;

        return (
            <>
                <AsyncNavBar />
                <ProductList
                    c={c}
                    sc={sc}
                    b={b}
                    title={title}
                />
            </>
        );
    } catch (error) {
        console.error('Error fetching products:', error);
        return (
            <div className="font-serif flex flex-col justify-center items-center p-4">
                <p>Oops! Something went wrong while loading products. Please try again later.</p>
                <Link href="/nay-store/public" className="text-blue-500 underline mt-2">
                    Go back home
                </Link>
            </div>
        );
    }
};

export default ProductsPage;
ProductList.jsx:
'use client'
import React, {useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react';
import { useRouter, useSearchParams, usePathname } from 'next/navigation';
import { useInView } from 'react-intersection-observer';
import dynamic from 'next/dynamic';
import ProductItem from './ProductItem';
import ProductLoading from '@/components/ProductLoading';
import NoProductsFound from '@/components/NoProductsFound';
import ProductDetail from '@/components/ProductDetail';
import { NotificationProvider } from "@/components/NotificationContext";

const SearchComponent = dynamic(() => import('@/components/SearchComponent'), {
    ssr: false,
    loading: () => <SearchComponentSkeleton />
});

const ProductModal = dynamic(() => import("@/components/ProductDetailModal"), {
    ssr: false
});

export default function ProductList({
                                        initialProducts,
                                        hasMore,
                                        initialPage,
                                        initialQuery,
                                        minPrice,
                                        maxPrice,
                                        title
                                    }) {
    const [products, setProducts] = useState(initialProducts);
    const [page, setPage] = useState(initialPage);
    const [loading, setLoading] = useState(false);
    const [noMoreProducts, setNoMoreProducts] = useState(!hasMore);
    const [query, setQuery] = useState(initialQuery);
    const [detail, setDetail] = useState(null);
    const [selectedProduct, setSelectedProduct] = useState(null);
    const [shouldScroll, setShouldScroll] = useState(false);
    const scroll = useRef(0);

    const { ref, inView } = useInView({ threshold: 0 });
    const router = useRouter();
    const searchParams = useSearchParams();
    const pathname = usePathname();
    const loadMoreProducts = useCallback(() => {
        if (loading || noMoreProducts) return;

        setLoading(true);
        const nextPage = page + 1;
        const params = new URLSearchParams(Array.from(searchParams.entries()));
        params.set('page', nextPage.toString());
        router.push(`/products?${params.toString()}`, { scroll: false });
    }, [page, loading, noMoreProducts, searchParams, router]);

    useEffect(() => {
        if (inView && !noMoreProducts) {
            loadMoreProducts();
        }
    }, [inView, loadMoreProducts, noMoreProducts]);

    const handleSearch = useCallback((newQuery) => {
        setQuery(newQuery);
        const params = new URLSearchParams(Array.from(searchParams.entries()));
        params.set('query', newQuery);
        params.set('page', '1');
        router.push(`/products?${params.toString()}`, { scroll: false });
    }, [searchParams, router]);

    useEffect(() => {
        setProducts(initialProducts);
        setPage(initialPage);
        setNoMoreProducts(!hasMore);
        setQuery(initialQuery);
        setLoading(false);
    }, [initialProducts, initialPage, hasMore, initialQuery]);

    const memoizedProducts = useMemo(() => products.map((product) => ({
        ...product,
        cheapestPrice: getCheapestPrice(product),
    })), [products]);

    useEffect(() => {
        if (shouldScroll) {
            window.scrollTo(0, scroll.current);
            setShouldScroll(false);
        }
    }, [shouldScroll]);

    useLayoutEffect(() => {
        if (pathname === '/products') {
            setDetail(null);
            setShouldScroll(true);
        }
    }, [pathname]);

    const handleProductClick = useCallback((product) => {
        scroll.current = window.scrollY;
        setDetail(product);
        const newURL = `/products/${product.id}`;
        window.history.pushState({ path: newURL }, '', newURL);
    }, []);

    const handleCartClick = useCallback((product) => {
        setSelectedProduct(product);
    }, []);

    const handleModalClose = useCallback(() => {
        setSelectedProduct(null);
    }, []);

    return (
        <>
            {detail ? (
                <NotificationProvider>
                    <ProductDetail product={detail} />
                </NotificationProvider>
            ) : (
                <>
                    <header className="flex justify-center relative mt-20 mb-4">
                        <h1
                            className="text-3xl z-10 text-[#181717] font-sans text-center font-medium">
                            {title || 'All Products'}
                        </h1>
                    </header>
                    <SearchComponent minPrice={minPrice} maxPrice={maxPrice} query={query} setQuery={handleSearch}/>
                    {memoizedProducts.length > 0 ? (
                        <div
                            className="grid grid-cols-2 w-full justify-between gap-4 sm:gap-6 ssm3:grid-cols-3 md:grid-cols-4 lg:grid-cols-5">
                            {memoizedProducts.map((product) => (
                                <ProductItem
                                    key={product.id}
                                    id={product.id}
                                    name={product.name}
                                    product={product}
                                    handleClick={() => handleProductClick(product)}
                                    price={formatPrice(product.cheapestPrice)}
                                    imageUrl={product.images[0]}
                                    onCartClick={handleCartClick}
                                />
                            ))}
                        </div>
                    ) : (
                        <NoProductsFound />
                    )}
                    {loading && <ProductLoading />}
                    {!loading && !noMoreProducts && <div ref={ref} style={{height: '20px'}}></div>}
                </>
            )}
            {selectedProduct && (
                <NotificationProvider>
                    <ProductModal
                        productData={{ product: selectedProduct }}
                        isOpen={!!selectedProduct}
                        onClose={handleModalClose}
                    />
                </NotificationProvider>
            )}
        </>
    );
}

const getCheapestPrice = (product) => {
    const prices = [
        product.price && product.price !== '0.00'
            ? parseFloat(product.price)
            : Infinity,
        ...(product.sizes?.map((size) => parseFloat(size.price)) || []),
        ...(product.colors?.flatMap((color) => [
            parseFloat(color.price),
            ...(color.sizes?.map((size) => parseFloat(size.price)) || []),
        ]) || []),
    ].filter((price) => !isNaN(price) && isFinite(price));

    return prices.length > 0 ? Math.min(...prices) : null;
};

const formatPrice = (price) => {
    if (price == null || price === Infinity) return 'N/A';
    return `${price >= 10000 ? price.toLocaleString() : price} IQD`;
};

const SearchComponentSkeleton = () => (
    <div className="flex mb-4 items-center space-x-2 animate-pulse">
        <div className="h-10 bg-gray-200 rounded-md flex-grow"></div>
        <div className="h-10 w-[5.5rem] bg-gray-200 rounded-md"></div>
    </div>
);
ProductItem.jsx:
'use client'
import React, {memo, useCallback, useMemo, useState} from 'react';
import Image from 'next/image';
import WishlistHeart from "@/components/WishlistHeart";

const CartIcon = memo(() => (
    <svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
        <rect width="32" height="32" rx="16" fill="#3B5345"/>
        <path d="M12.5715 12.7009V11.9618C12.5715 10.2476 13.9506 8.56375 15.6649 8.40375C17.7068 8.20565 19.4287 9.81327 19.4287 11.8171V12.8685" stroke="white" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round"/>
        <path d="M13.7143 23.619H18.2858C21.3486 23.619 21.8972 22.3923 22.0572 20.899L22.6286 16.3276C22.8343 14.4685 22.301 12.9523 19.0477 12.9523H12.9524C9.69909 12.9523 9.16576 14.4685 9.37147 16.3276L9.9429 20.899C10.1029 22.3923 10.6515 23.619 13.7143 23.619Z" stroke="white" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round"/>
        <path d="M18.6631 15.9999H18.67" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
        <path d="M13.3292 15.9999H13.336" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
    </svg>
));

const ImageSkeleton = () => (
    <div className="absolute inset-0 bg-gray-200 animate-pulse rounded-t-lg" />
);

const ProductItem = memo(({ id, name, price, imageUrl, product, handleClick, onCartClick }) => {
    const [imageLoaded, setImageLoaded] = useState(false);

    const handleProductClick = useCallback((e) => {
        if (!e.target.closest('.heart-icon')) {
            handleClick();
        }
    }, [handleClick]);

    const handleCartButtonClick = useCallback((e) => {
        e.stopPropagation();
        onCartClick(product);
    }, [onCartClick, product]);

    const handleImageLoad = useCallback(() => {
        setImageLoaded(true);
    }, []);
    const isOutOfStock = useMemo(() => {
        return product.sizes.every(size => !size.qty) && !product.qty && product.colors.every(color => !color.qty);
    }, [product.sizes]);

    return (
        <div className="rounded-lg" onClick={handleProductClick}>
            <div className="bg-white aspect-[186/275] relative flex flex-col w-full drop-shadow overflow-hidden group">
                <button className="absolute left-1 top-4 z-20 heart-icon" onClick={(e) => e.stopPropagation()}>
                    <WishlistHeart product={product} />
                </button>

                {!imageLoaded && <ImageSkeleton />}
                <Image
                    src={`https://storage.naayiq.com/resources/${imageUrl}`}
                    alt={name}
                    fill={true}
                    sizes="(max-width: 768px) 50vw, (max-width: 1200px) 25vw, 10vw"
                    onError={(e) => {
                        e.target.src = "https://storage.naayiq.com/resources/noimage.webp";
                    }}
                    onLoad={handleImageLoad}
                    className={`object-cover relative rounded-t-lg w-full h-full overflow-hidden transition-transform duration-300 group-hover:scale-110 ${imageLoaded ? 'opacity-100' : 'opacity-0'}`}
                />

                <div className="absolute min-h-[84px] bottom-0 left-0 right-0 bg-gradient-custom rounded-t-lg p-2 flex flex-col justify-end">
                    <h3 className="font-serif line-clamp-2 capitalize overflow-ellipsis font-medium text-xs sm:text-sm leading-tight tracking-tight text-[#181717] mb-auto">
                        {name}
                    </h3>
                    <div className="flex justify-between items-center mt-2">
                        {isOutOfStock ? (
                            <p className="font-medium font-sans text-lg leading-tight tracking-tight text-[#C91C1C]">
                                Out of stock
                            </p>
                        ) : (
                            <p className="font-semibold font-serif text-lg leading-tight tracking-tight text-[#181717]">
                                {price}
                            </p>
                        )}
                        <button
                            className={`w-8 h-8 rounded-full flex items-center justify-center ${isOutOfStock ? 'bg-gray-400 cursor-not-allowed' : 'bg-[#3B5345]'}`}
                            onClick={handleCartButtonClick}
                            disabled={isOutOfStock}
                        >
                            <CartIcon />
                        </button>
                    </div>
                </div>
            </div>
        </div>
    );
});

ProductItem.displayName = "ProductItem";
CartIcon.displayName = "CartIcon";

export default ProductItem;
FilterComponent.jsx:
'use client'
import React, { useState, useEffect, useCallback } from 'react';
import { Slider, ThemeProvider, createTheme } from '@mui/material';
import { useRouter, useSearchParams } from 'next/navigation';
import "./NotificationStyles.css"
const theme = createTheme({
    palette: {
        primary: {
            main: '#97C86C',
        },
    },
});

const ProductFilterComponent = ({ onFilter, modalRef, filter, setFilter, minPrice, maxPrice }) => {
    const router = useRouter();
    const searchParams = useSearchParams();

    const [priceRange, setPriceRange] = useState([
        parseInt(searchParams.get('minPrice')) || minPrice,
        parseInt(searchParams.get('maxPrice')) || maxPrice
    ]);
    const [inStock, setInStock] = useState(searchParams.get('availability') !== 'out_of_stock');
    const [sortBy, setSortBy] = useState(searchParams.get('sortBy') || '');

    const handleFilterClose = useCallback(() => {
        setFilter(false);
        document.body.style.overflow = 'auto';
    }, [setFilter]);

    const handleOutsideClick = useCallback((e) => {
        if (modalRef.current && !modalRef.current.contains(e.target)) {
            handleFilterClose();
        }
    }, [handleFilterClose, modalRef]);

    const handleApplyFilter = (e) => {
        e.preventDefault(); // Prevent default form submission
        const params = new URLSearchParams(searchParams);
        params.set('minPrice', priceRange[0]);
        params.set('maxPrice', priceRange[1]);
        params.set('availability', inStock ? 'in_stock' : 'out_of_stock');
        if (sortBy) {
            params.set('sortBy', sortBy);
        } else {
            params.delete('sortBy');
        }

        // Update the URL without navigating
        router.push(`?${params.toString()}`, { scroll: false });

        onFilter({ priceRange, inStock, sortBy });
        handleFilterClose();
    };

    const handleResetFilter = (e) => {
        e.preventDefault(); // Prevent default button behavior
        setPriceRange([minPrice, maxPrice]);
        setInStock(true);
        setSortBy('');

        // Clear all filter-related params from the URL
        const params = new URLSearchParams(searchParams);
        ['minPrice', 'maxPrice', 'availability', 'sortBy'].forEach(param => params.delete(param));
        router.push(`?${params.toString()}`, { scroll: false });
    };

    const handlePriceRangeChange = (event, newValue, activeThumb) => {
        if (!Array.isArray(newValue)) {
            return;
        }

        const minDistance = 1;

        if (activeThumb === 0) {
            const newMinValue = Math.min(newValue[0], priceRange[1] - minDistance);
            setPriceRange([newMinValue, priceRange[1]]);
        } else {
            const newMaxValue = Math.max(newValue[1], priceRange[0] + minDistance);
            setPriceRange([priceRange[0], newMaxValue]);
        }
    };

    const valuetext = (value) => {
        return `${value} IQD`;
    };

    useEffect(() => {
        if (filter) {
            document.addEventListener('mousedown', handleOutsideClick);
            document.addEventListener('touchstart', handleOutsideClick);
        } else {
            document.removeEventListener('mousedown', handleOutsideClick);
            document.removeEventListener('touchstart', handleOutsideClick);
        }

        return () => {
            document.removeEventListener('mousedown', handleOutsideClick);
            document.removeEventListener('touchstart', handleOutsideClick);
        };
    }, [handleOutsideClick, filter]);

    return (
        <form onSubmit={handleApplyFilter}>
            <div className="flex justify-center mb-2">
                <div className="w-10 h-1 bg-black rounded-full"></div>
            </div>
            <div className="flex justify-between items-center mb-6">
                <h2 className="text-2xl font-medium font-sans">Filter Products</h2>
                <button
                    type="button"
                    onClick={handleResetFilter}
                    className="text-[#97C86C] font-sans text-sm font-medium"
                >
                    Reset Filter
                </button>
            </div>

            <div className="space-y-6">
                {/* Price Range section */}
                <div>
                    <h3 className="text-lg font-medium font-sans mb-4">Price Range</h3>
                    <ThemeProvider theme={theme}>
                        <div className="relative pt-6 mx-16 pb-8">
                            <Slider
                                getAriaLabel={() => 'Price range'}
                                value={priceRange}
                                onChange={handlePriceRangeChange}
                                valueLabelDisplay="off"
                                getAriaValueText={valuetext}
                                min={minPrice}
                                max={maxPrice}
                                disableSwap
                                sx={{
                                    '& .MuiSlider-thumb': {
                                        height: 24,
                                        width: 24,
                                        backgroundColor: 'currentColor',
                                        '&:focus, &:hover, &.Mui-active, &.Mui-focusVisible': {
                                            boxShadow: 'inherit',
                                        },
                                    },
                                    '& .MuiSlider-track': {
                                        height: 4,
                                    },
                                    '& .MuiSlider-rail': {
                                        height: 4,
                                        opacity: 0.5,
                                        backgroundColor: '#bfbfbf',
                                    },
                                }}
                            />
                            <div className="absolute bottom-0 left-0 right-0 flex justify-between">
                                <span
                                    className="font-serif text-sm"
                                    style={{
                                        position: 'absolute',
                                        left: `${((priceRange[0] - minPrice) / (maxPrice - minPrice)) * 100}%`,
                                        transform: 'translateX(-100%) translateY(-100%)',
                                    }}
                                >
                                    {priceRange[0]} IQD
                                </span>
                                <span
                                    className="font-serif text-sm"
                                    style={{
                                        position: 'absolute',
                                        right: `${100 - ((priceRange[1] - minPrice) / (maxPrice - minPrice)) * 100}%`,
                                        transform: 'translateX(100%) translateY(-100%)',
                                    }}
                                >
                                    {priceRange[1]} IQD
                                </span>
                            </div>
                        </div>
                    </ThemeProvider>
                </div>

                {/* Availability section */}
                <div>
                    <h3 className="text-lg font-sans font-medium mb-4">Availability</h3>
                    <div className="flex font-sans items-center space-x-4">
                        <button
                            type="button"
                            onClick={() => setInStock(true)}
                            className={`flex items-center text-black`}
                        >
                            <div
                                className={`w-8 h-8 rounded-[100%] border flex items-center justify-center mr-2 ${inStock ? 'border-[#3B5345]' : 'border-[#695C5C]/50'}`}>
                                {inStock && (
                                    <div className="w-5 h-5 bg-[#97C86C] rounded-full"></div>
                                )}
                            </div>
                            In Stock
                        </button>
                        <button
                            type="button"
                            onClick={() => setInStock(false)}
                            className={`flex items-center text-black`}
                        >
                            <div
                                className={`w-8 h-8 rounded-[100%] border flex items-center justify-center mr-2 ${!inStock ? 'border-[#3B5345]' : 'border-[#695C5C]/50'}`}>
                                {!inStock && (
                                    <div className="w-5 h-5 bg-[#97C86C] rounded-full"></div>
                                )}
                            </div>
                            Out of Stock
                        </button>
                    </div>
                </div>

                {/* Sort by section */}
                <div>
                    <h3 className="text-lg font-sans font-medium  mb-4">Sort by</h3>
                    <div className="grid font-serif font-medium text-xs ssm:text-base grid-cols-2 gap-9">
                        {['Price: Low to High', 'Name: Z to A', 'Price: High to Low', 'Best Selling', 'Name: A to Z', 'Newest Arrivals'].map((option) => (
                            <button
                                type="button"
                                key={option}
                                onClick={() => setSortBy(option)}
                                className={`py-6 px-1 rounded-md border ${
                                    sortBy === option
                                        ? 'bg-[#3B5345] text-white'
                                        : 'bg-white text-black'
                                }`}
                            >
                                {option}
                            </button>
                        ))}
                    </div>
                </div>
            </div>

            <button
                type="submit"
                className="w-full bg-[#3B5345] font-sans text-white py-3 rounded-lg mt-6 text-lg font-medium"
            >
                Apply Filter
            </button>
        </form>
    );
};

export default ProductFilterComponent;

Keep the design as it is... However maintain the performance and I'm using infinite scrolling... since each time should get a new page till it reaches the final page...
the api response may be something like this:
{"products":[{"id":39,"name":"Vichy Normaderm ‏Phytosolution Intensive Purifying Gel","description":"‏جل منظف ​​للبشرة الدهنية والمعرضة لحب الشباب، مدعوم بالمعادن والبروبايوتيك. يزيل جزيئات الغبار والتلوث بشكل مثالي من الاستعمال الأول.يزيل لمعان البشرة فوراً ويؤخر إنتاج الزيوت خلال النهار. يقلل مظهر المسام ويقضي بشكلٍ فعال على الرؤوس السوداء.\nينقي ويزيل المكياج \nيخفض إنتاج الزهم الزائد وزيوت البشرة\nخالي من الكبريتات - خالي من الصابون - خالي من الكحول","brand_id":71,"has_color":false,"has_size":true,"product_type":"size","price":0,"qty":0,"sold":0,"created_at":"2024-08-24T19:11:33.876Z","updated_at":"2024-09-17T21:46:38.398Z","categories":[{"id":168,"name":"Face Cleansers","main_category_id":1,"main_category_name":"Face Care"}],"colors":[],"sizes":[{"id":896,"name":"200 ml","price":25000,"qty":14,"sold":0,"color_id":null,"images":["1737.jpeg"]}],"images":[{"id":3244,"url":"1737.jpeg","color_id":null,"size_id":896}]},{"id":41,"name":"‏Vichy Liftactiv A Retinol Specialist Deep Wrinkles Serum","description":"‏سيروم الريتنول من vichy\nيوفر فعالية بفضل تركيبته القوية لتقديم فوائد\nيعمل على تجديد البشرة ، ويعزز إنتاج الكولاجين ، وله خصائص مضادة للأكسدة. \n فهو فعال للغاية في تقليل التجاعيد العميقة بشكل واضح وتحسين تماسك البشرة. \nإلى جانب ذلك بفضل مفعولها المقوي تصبح البشرة أكثر  تناسقًا وإشراقًا. \nبفضل تقنية Retinol Guard ، فإن هذا المكون قادر على اختراق أعماق الجلد والعمل على جميع أنواع التجاعيد ، حتى أعمق التجاعيد \nملمسة حليبي يمتص بسرعة  ويترك ملمس غير دهني\n-مناسبة لجميع  أنواع البشرة.","brand_id":71,"has_color":false,"has_size":true,"product_type":"size","price":0,"qty":0,"sold":0,"created_at":"2024-08-24T19:21:15.242Z","updated_at":"2024-09-08T12:08:37.048Z","categories":[{"id":170,"name":"Serums","main_category_id":1,"main_category_name":"Face Care"}],"colors":[],"sizes":[{"id":897,"name":"30 ml","price":55000,"qty":3,"sold":0,"color_id":null,"images":["1738.jpeg"]}],"images":[{"id":1073,"url":"1738.jpeg","color_id":null,"size_id":897}]},{"id":42,"name":"Vichy Dercos Densi-solutions spray","description":"بخاخ ڤيشي ديركوس لتكثيف وزياده سمك الشعر\n-ڤيشي دينسي يحتوي علي ماده ستيموكسيدين التي تعمل على تنشيط للخلايا الجذعيه والبصيلات الخامله في فروه الراس. \nوبالتالي إعاده إنبات الشعر من جديد ويزيد من كثافته. \n-يحتوي أيضًا ماده الرامنوز التي تحفز نمو الشعر وتغذيه.\n-الريسفيراتول مضاد أكسده قوي ضد العوامل اللي تؤذي الفروه ويزيد من كفاءه ماده الستيموكسدين.\n-١٠ بخات صباحا و ١٠ بخات مساءا على الفروه وتوزع جيدا .","brand_id":71,"has_color":false,"has_size":true,"product_type":"size","price":0,"qty":0,"sold":0,"created_at":"2024-08-24T19:26:52.874Z","updated_at":"2024-09-08T12:05:14.585Z","categories":[{"id":9,"name":"Hair Treatments","main_category_id":3,"main_category_name":"Hair Care"}],"colors":[],"sizes":[{"id":895,"name":"100 ml","price":55000,"qty":1,"sold":0,"color_id":null,"images":["1736.jpeg"]}],"images":[{"id":1071,"url":"1736.jpeg","color_id":null,"size_id":895}]},{"id":43,"name":"Vichy Nutri Protein Restorative Shampoo","description":"شامبو مطور خصيصًا لمعالجة الجفاف بالشعر و الفروة بمزيج من المكونات الغذائية الطبيعية تصلح الجفاف الحاصل بسبب الحرارة و الاشعه الفوق البنفسجية يزيد ترطيب الشعر بشكل فوري يقلل من النهايات المتقصفة ينعم ملمس الشعر و يخلي حريري و صحي ولامع \nخالي من البارابين و السيليكون مناسب للاستخدام اليومي او اي وقت لغسل الشعر","brand_id":71,"has_color":false,"has_size":true,"product_type":"size","price":0,"qty":0,"sold":0,"created_at":"2024-08-24T19:28:42.186Z","updated_at":"2024-09-17T21:48:35.090Z","categories":[{"id":1,"name":"Shampoos","main_category_id":3,"main_category_name":"Hair Care"}],"colors":[],"sizes":[{"id":892,"name":"250 ml","price":25000,"qty":1,"sold":0,"color_id":null,"images":["1733.jpeg"]}],"images":[{"id":3245,"url":"1733.jpeg","color_id":null,"size_id":892}]},{"id":44,"name":"Vichy ‏Beauty Deo Anti-perspirant","description":"‏مزيل تعرق طبي يساعد بعلاج التعرق المفرط ويخفي رائحة العرق المزعجه \nبمفعول يصل لمده 48 ساعة \nبالاضافه على قدرته بتفتيح منطقة الابط وتبيضها\nملائم للجلد الحساس يحتوي على حمض الليبوهيدروكسي و الحبيبات اللؤلؤية تعمل على تحسين مظهر البشرة وتفتيحها","brand_id":71,"has_color":false,"has_size":true,"product_type":"size","price":0,"qty":0,"sold":0,"created_at":"2024-08-24T19:37:55.009Z","updated_at":"2024-09-17T11:16:11.775Z","categories":[{"id":182,"name":"Deodorant","main_category_id":2,"main_category_name":"Body Care"}],"colors":[],"sizes":[{"id":886,"name":"50 ml","price":20000,"qty":14,"sold":0,"color_id":null,"images":["1728.jpeg"]}],"images":[{"id":2956,"url":"1728.jpeg","color_id":null,"size_id":886}]},{"id":45,"name":"VICHY DEODORANT MINERAL 48 H","description":"مزيل عرق من فيجي\nخالي من أملاح الألمنيوم، مع أقصى درجة من الفعالية والكفاءة، من خلال ربط المعادن الموجودة في مياه فيتشي الحرارية مع الzinc gluconate \nلفعالية تستمر لمدة 48 ساعة بالاضافة الى  Provitamine B5 الذي يعمل كمهدئ للبشرة.\nمناسب لجميع انواع البشرة خاصة البشرة الحساسة\nخالي من الكحول والبرابين.","brand_id":71,"has_color":false,"has_size":true,"product_type":"size","price":0,"qty":0,"sold":0,"created_at":"2024-08-24T19:40:27.610Z","updated_at":"2024-09-17T11:17:48.913Z","categories":[{"id":182,"name":"Deodorant","main_category_id":2,"main_category_name":"Body Care"}],"colors":[],"sizes":[{"id":888,"name":"50 ml","price":20000,"qty":33,"sold":0,"color_id":null,"images":["1730.jpeg"]}],"images":[{"id":2961,"url":"1730.jpeg","color_id":null,"size_id":888}]},{"id":46,"name":"Vichy Clinical Control Deodorant & Antiperspirant 96 hour","description":"Vichy Clinical Control Deodorant & Antiperspirant 96 hour \nديودرنت مزيل للتعرق بفاعليته المقاومة للتعرق حتى تحت الضغوط اليومية.\nيناسب الرجال و النساء الذين يعانون من التعرق المفرط و يريدون الجمع بين مزيل للعرق فعال و كفاءة عالية تتحمله بشرتهم.\nخالي من الباربين والكحول يناسب البشره الحساسة","brand_id":71,"has_color":false,"has_size":true,"product_type":"size","price":0,"qty":0,"sold":0,"created_at":"2024-08-24T19:42:13.987Z","updated_at":"2024-09-23T19:02:26.409Z","categories":[{"id":182,"name":"Deodorant","main_category_id":2,"main_category_name":"Body Care"}],"colors":[],"sizes":[{"id":887,"name":"50 ml","price":20000,"qty":1,"sold":0,"color_id":null,"images":["1729.webp"]}],"images":[{"id":26,"url":"1729.webp","color_id":null,"size_id":887}]},{"id":47,"name":"Vichy Deodorant Anti-Transpirant Stress resist","description":"تركيبة لطيفة ، تستخدم خصيصا للبشرة الحساسة او المتهيجة\n‏‎لا يوجد اي بقايا لزجة أو دهنية او علامات بيضاء على \nالبشرة\nيزيل العرق لمدة 48 الى 72 ساعة وفعالية مضاد للعرق\n‏‎لايسبب الحساسية\nخالي من الكحول و البارابين و العطر","brand_id":71,"has_color":false,"has_size":true,"product_type":"size","price":0,"qty":0,"sold":0,"created_at":"2024-08-24T19:51:07.220Z","updated_at":"2024-09-23T19:03:13.596Z","categories":[{"id":182,"name":"Deodorant","main_category_id":2,"main_category_name":"Body Care"}],"colors":[],"sizes":[{"id":894,"name":"50 ml","price":20000,"qty":24,"sold":0,"color_id":null,"images":["1735.jpeg"]}],"images":[{"id":27,"url":"1735.jpeg","color_id":null,"size_id":894}]},{"id":48,"name":"‏Vichy Traitement Anti-Transpirant 48H Roll-On","description":"-مزيل العرق الطبي من فيشي\n-مناسب للرجال والنساء الذين يعانون من فرط التعرق\n-يمتاز تركيبته \" الهيبوالرجينيك \" بدقة بالغة تعمل على مقاومة التعرق بدون سد مسام العرق ، ما يعطي حماية أفضل على مدار 48 ساعة بدون أي أضرار .\n-مناسب للبشره الحساسةو لاتحتوي على الكحول ، ولاتحتوي على البارابين .\n-يمنح البشرة ملمساً ناعماً بدون الشعور بعدم الراحة ، أو ترك علامات بيضاء على الجلد أو الملابس .\n-يمنحك شعورًا بالانتعاش عند الاستعمال ، كما يترك طبقة بودرة رقيقة غير لزجة على البشرة .\n-لا يؤدي إلى إلحاق أي أذى بالبشرة حتى مع إزالة الشعر الزائد بالليزر أو بأي طريقة أخرى .\n-يمتاز بتركيبة ناعمة ، تجف بسرعة دون إزعاج البشرة","brand_id":71,"has_color":false,"has_size":true,"product_type":"size","price":0,"qty":0,"sold":0,"created_at":"2024-08-24T19:52:17.485Z","updated_at":"2024-09-23T19:00:22.400Z","categories":[{"id":182,"name":"Deodorant","main_category_id":2,"main_category_name":"Body Care"}],"colors":[],"sizes":[{"id":889,"name":"50 ml","price":20000,"qty":12,"sold":0,"color_id":null,"images":["1731.jpeg"]}],"images":[{"id":25,"url":"1731.jpeg","color_id":null,"size_id":889}]},{"id":49,"name":"Vichy Dercos Dry Shampoo","description":"شامبو جاف للشعر من فيشي\nيوفر مفعولًا مضادًا للإفرازات الدهنية بفضل التركيبة الغنية بالفيتامينات PP و B5 والبيوتين\nو يسمح هذا المزيج من المكونات النشطة للشعر باستعادة الحجم والملمس\nمضاد للحساسية و مناسب لفروة الرأس الحساسة\nطريقة الاستخدام : قبل استخدام الشامبو الجاف , مشط الشعر جيدا لتفكيك الجذو ثم رش وبعدها قم بتمشيط الشعر","brand_id":71,"has_color":false,"has_size":true,"product_type":"size","price":0,"qty":0,"sold":0,"created_at":"2024-08-24T19:55:01.394Z","updated_at":"2024-09-17T15:04:36.810Z","categories":[{"id":3,"name":"Dry Shampoos","main_category_id":3,"main_category_name":"Hair Care"}],"colors":[],"sizes":[{"id":893,"name":"150 ml","price":30000,"qty":0,"sold":0,"color_id":null,"images":["1734.jpeg"]}],"images":[{"id":3144,"url":"1734.jpeg","color_id":null,"size_id":893}]},{"id":51,"name":"Aveeno Daily Care Spray","description":"افينو AVEENO سبراي رذاذ للجسم  بتركيبته المكثفة لتغذية و ترطيب البشرة بلمسة واحدة فقط، تركيبة غير دهنية وسريعة الامتصاص للاستخدام بعد الاستحمام.\nطريقة الاستعمال \nيستخدم بعد الاستحمام بعد تجفيف البشرة، يتم رش على الجسم والساقين ثم تدليك البشرة.","brand_id":86,"has_color":false,"has_size":true,"product_type":"size","price":0,"qty":0,"sold":0,"created_at":"2024-08-24T20:00:38.032Z","updated_at":"2024-09-07T19:19:36.112Z","categories":[{"id":179,"name":"Body Moisturizers","main_category_id":2,"main_category_name":"Body Care"}],"colors":[],"sizes":[{"id":853,"name":"200 ml","price":20000,"qty":3,"sold":0,"color_id":null,"images":["1107.jpeg"]}],"images":[{"id":1029,"url":"1107.jpeg","color_id":null,"size_id":853}]},{"id":52,"name":"‏Aveeno Face Calm+Restore Nourishing Oat Cleanser","description":"غسول الشوفان المغذي للبشرة الحساسة.\n-يزيل هذا المنظف الكريمي الأوساخ والشوائب والزيوت بلطف ويترك البشرة تشعر بالهدوء والترطيب والتغذية\n-الشوفان والبريبيوتيك المهدئ ، يساعد المنظف أيضًا في الحفاظ على حاجز رطوبة البشرة\n-تساعد هذه التركيبة على تغذية وتهدئة بشرتك\n-للبشرة الحساسة بدون روائح عطرية","brand_id":86,"has_color":false,"has_size":true,"product_type":"size","price":0,"qty":0,"sold":0,"created_at":"2024-08-24T20:02:27.265Z","updated_at":"2024-09-07T19:21:30.618Z","categories":[{"id":168,"name":"Face Cleansers","main_category_id":1,"main_category_name":"Face Care"}],"colors":[],"sizes":[{"id":855,"name":"200 ml","price":22000,"qty":8,"sold":0,"color_id":null,"images":["1108.jpeg"]}],"images":[{"id":1031,"url":"1108.jpeg","color_id":null,"size_id":855}]},{"id":53,"name":"Vichy Dercos Energising Shampoo Anti-Hair Loss","description":"-شامبو معالج لتساقط الشعر\n-يعمل على تنشيط الدورة الدموية في فروة \nالرأس ويساعد علي تقوية البصيلات. \n-يساعد على زيادة كثافة الشعر ويحفز الخلايا التي لا تنتج  لاعادة نمو الشعر الجديد.\n-يجعل الشعر أكثر كثافة و أكثر سماكة بشكل واضح ويزيد من قوة الشعر.\n-يمنح الشعر قوة وحيوية من الجذور حتي الأطراف.\n-مناسب لكلى الجنسين و  لجميع انواع الشعر .","brand_id":71,"has_color":false,"has_size":true,"product_type":"size","price":0,"qty":0,"sold":0,"created_at":"2024-08-24T20:07:29.899Z","updated_at":"2024-09-17T15:04:06.499Z","categories":[{"id":1,"name":"Shampoos","main_category_id":3,"main_category_name":"Hair Care"}],"colors":[],"sizes":[{"id":891,"name":"200 ml","price":25000,"qty":11,"sold":0,"color_id":null,"images":["1732.jpeg"]}],"images":[{"id":3142,"url":"1732.jpeg","color_id":null,"size_id":891}]},{"id":54,"name":"ISIS NEOTONE EYE CONTOUR CREAM","description":"كريم تفتيح محيط العين من ISIS .\nيعمل على معالجة الهالات الداكنه وفرط التصبغ حول العين ‎\nيقوم بعلاج الهالات البنية السوداء و الهالات الزرقاء بسبب الشعيرات الدموية و يعالج الانتفاخات أيضا.\n‏‎غني بالهالورونيك أسيد ليرطب و يعطي حيوية و نضارة.","brand_id":74,"has_color":false,"has_size":true,"product_type":"size","price":0,"qty":0,"sold":0,"created_at":"2024-08-24T20:14:02.447Z","updated_at":"2024-09-16T09:43:07.846Z","categories":[{"id":159,"name":"Eye Cream","main_category_id":4,"main_category_name":"Eye Care"}],"colors":[],"sizes":[{"id":54,"name":"15 ml","price":20000,"qty":27,"sold":0,"color_id":null,"images":["2023.jpeg"]}],"images":[{"id":2478,"url":"2023.jpeg","color_id":null,"size_id":54}]},{"id":57,"name":"Isis Pharma Urelia 50% Cream for severe itching on dry skin","description":"-كريم مرطب للجسم من isis \n-تستخدم لحالات التقشر المعتدله الى القويه\n-يحتوي على اليوريا المهمه لترطيب البشره\n-تركيبة URELIA 10 تحتوي على مزيج من عدة مواد :\n‏ -Urea 50% يوريا التي تسمح بإزالة القشور عن سطح البشرة ولها خصائص مرطبة \n‏-Xpertmoist بمفعول مجدد للبشرة\n‏ -AHA بمفعول منظم للطبقة القرنية\n‏ -Sodium chloride الذي يزيد من مقاومة الجفاف\n‏ -Vegetal Glycerin و Vaselin و Shea Butter بخصائصها المرطبه","brand_id":74,"has_color":false,"has_size":true,"product_type":"size","price":0,"qty":0,"sold":0,"created_at":"2024-08-24T20:16:49.835Z","updated_at":"2024-09-17T21:01:57.589Z","categories":[{"id":179,"name":"Body Moisturizers","main_category_id":2,"main_category_name":"Body Care"}],"colors":[],"sizes":[{"id":57,"name":"40 ml","price":23000,"qty":3,"sold":0,"color_id":null,"images":["2118.jpeg"]}],"images":[{"id":3231,"url":"2118.jpeg","color_id":null,"size_id":57}]}],"total":835,"page":2,"itemsPerPage":15,"totalPages":56,"hasMore":true,"minPrice":0,"maxPrice":250000}
I'm using nextjs app router 15... Remove the depend on searchParams... as I said only for c, sc, b and title and they constant anyway...
Nextjs 14:

nextjs.org
Next.js 14
Lee Robinson@leeerobTim Neutkens@timneutkens
16–21 minutes

As we announced at Next.js Conf

, Next.js 14 is our most focused release with:

    Turbopack: 5,000 tests passing for App & Pages Router
        53% faster local server startup
        94% faster code updates with Fast Refresh
    Server Actions (Stable): Progressively enhanced mutations
        Integrated with caching & revalidating
        Simple function calls, or works natively with forms
    Partial Prerendering (Preview): Fast initial static response + streaming dynamic content
    Next.js Learn (New): Free course teaching the App Router, authentication, databases, and more.

Upgrade today or get started with:

npx create-next-app@latest

Next.js Compiler: Turbocharged

Since Next.js 13, we've been working to improve local development performance in Next.js in both the Pages and App Router.

Previously, we were rewriting next dev and other parts of Next.js to support this effort. We have since changed our approach to be more incremental. This means our Rust-based compiler will reach stability soon, as we've refocused on supporting all Next.js features first.

5,000 integration tests for next dev are now passing with Turbopack

, our underlying Rust engine. These tests include 7 years of bug fixes and reproductions.

While testing on vercel.com, a large Next.js application, we've seen:

    Up to 53.3% faster local server startup
    Up to 94.7% faster code updates with Fast Refresh

This benchmark is a practical result of performance improvements you should expect with a large application (and large module graph). With 90% of tests for next dev now passing, you should see faster and more reliable performance consistently when using next dev --turbo.

Once we hit 100% of tests passing, we'll move Turbopack to stable in an upcoming minor release. We'll also continue to support using webpack for custom configurations and ecosystem plugins.

You can follow the percentage of tests passing at areweturboyet.com

.
Forms and Mutations

Next.js 9 introduced API Routes—a way to quickly build backend endpoints alongside your frontend code.

For example, you would create a new file in the api/ directory:

import type { NextApiRequest, NextApiResponse } from 'next';
 
export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse,
) {
  const data = req.body;
  const id = await createItem(data);
  res.status(200).json({ id });
}

Then, on the client-side, you could use React and an event handler like onSubmit to make a fetch to your API Route.

import { FormEvent } from 'react';
 
export default function Page() {
  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
 
    const formData = new FormData(event.currentTarget);
    const response = await fetch('/api/submit', {
      method: 'POST',
      body: formData,
    });
 
    // Handle response if necessary
    const data = await response.json();
    // ...
  }
 
  return (
    <form onSubmit={onSubmit}>
      <input type="text" name="name" />
      <button type="submit">Submit</button>
    </form>
  );
}

Now with Next.js 14, we want to simplify the developer experience of authoring data mutations. Further, we want to improve the user experience when the user has a slow network connection, or when submitting a form from a lower-powered device.
Server Actions (Stable)

What if you didn't need to manually create an API Route? Instead, you could define a function that runs securely on the server, called directly from your React components.

The App Router is built on the React canary channel, which is stable for frameworks

to adopt new features. As of v14, Next.js has upgraded to the latest React canary, which includes stable Server Actions.

The previous example from the Pages Router can be simplified to one file:

export default function Page() {
  async function create(formData: FormData) {
    'use server';
    const id = await createItem(formData);
  }
 
  return (
    <form action={create}>
      <input type="text" name="name" />
      <button type="submit">Submit</button>
    </form>
  );
}

Server Actions should feel familiar for any developers who have previously used server-centric frameworks in the past. It's built on web fundamentals like forms and the FormData Web API

.

While using Server Actions through a form is helpful for progressive enhancement, it is not a requirement. You can also call them directly as a function, without a form. When using TypeScript, this gives you full end-to-end type-safety between the client and server.

Mutating data, re-rendering the page, or redirecting can happen in one network roundtrip, ensuring the correct data is displayed on the client, even if the upstream provider is slow. Further, you can compose and reuse different actions, including many different actions in the same route.
Caching, Revalidating, Redirecting, and more

Server Actions are deeply integrated into the entire App Router model. You can:

    Revalidate cached data with revalidatePath() or revalidateTag()
    Redirect to different routes through redirect()
    Set and read cookies through cookies()
    Handle optimistic UI updates with useOptimistic()
    Catch and display errors from the server with useFormState()
    Display loading states on the client with useFormStatus()

Learn more about Forms and Mutations with Server Actions
or about the security model

and best practices for Server Components and Server Actions.
Partial Prerendering (Preview)

We'd like to share a preview of Partial Prerendering — a compiler optimization for dynamic content with a fast initial static response — that we're working on for Next.js.

Partial Prerendering builds on a decade of research and development into server-side rendering (SSR), static-site generation (SSG), and incremental static revalidation (ISR).
Motivation

We've heard your feedback. There's currently too many runtimes, configuration options, and rendering methods to have to consider. You want the speed and reliability of static, while also supporting fully dynamic, personalized responses.

Having great performance globally and personalization shouldn't come at the cost of complexity.

Our challenge was to create a better developer experience, simplifying the existing model without introducing new APIs for developers to learn. While partial caching of server-side content has existed, these approaches still need to meet the developer experience and composability goals we aim for.

Partial Prerendering requires no new APIs to learn.
Built on React Suspense

Partial Prerendering is defined by your Suspense boundaries. Here's how it works. Consider the following ecommerce page:

export default function Page() {
  return (
    <main>
      <header>
        <h1>My Store</h1>
        <Suspense fallback={<CartSkeleton />}>
          <ShoppingCart />
        </Suspense>
      </header>
      <Banner />
      <Suspense fallback={<ProductListSkeleton />}>
        <Recommendations />
      </Suspense>
      <NewProducts />
    </main>
  );
}

With Partial Prerendering enabled, this page generates a static shell based on your <Suspense /> boundaries. The fallback from React Suspense is prerendered.

Suspense fallbacks in the shell are then replaced with dynamic components, like reading cookies to determine the cart, or showing a banner based on the user.

When a request is made, the static HTML shell is immediately served:

<main>
  <header>
    <h1>My Store</h1>
    <div class="cart-skeleton">
      <!-- Hole -->
    </div>
  </header>
  <div class="banner" />
  <div class="product-list-skeleton">
    <!-- Hole -->
  </div>
  <section class="new-products" />
</main>

Since <ShoppingCart /> reads from cookies to look at the user session, this component is then streamed in as part of the same HTTP request as the static shell. There are no extra network roundtrips needed.

import { cookies } from 'next/headers'
 
export default function ShoppingCart() {
  const cookieStore = cookies()
  const session = cookieStore.get('session')
  return ...
}

To have the most granular static shell, this may require adding additional Suspense boundaries. However, if you're already using loading.js today, this is an implicit Suspense boundary, so no changes would be required to generate the static shell.
Coming soon

Partial prerendering is under active development. We'll be sharing more updates in an upcoming minor release.

Before your page content can be streamed from the server, there's important metadata about the viewport, color scheme, and theme that need to be sent to the browser first.

Ensuring these meta tags are sent with the initial page content helps a smooth user experience, preventing the page from flickering by changing the theme color, or shifting layout due to viewport changes.

In Next.js 14, we've decoupled blocking and non-blocking metadata. Only a small subset of metadata options are blocking, and we want to ensure non-blocking metadata will not prevent a partially prerendered page from serving the static shell.

The following metadata options are now deprecated and will be removed from metadata in a future major version:

    viewport: Sets the initial zoom and other properties of the viewport
    colorScheme: Sets the support modes (light/dark) for the viewport
    themeColor: Sets the color the chrome around the viewport should render with

Starting with Next.js 14, there are new options viewport and generateViewport

to replace these options. All other metadata options remain the same.

You can start adopting these new APIs today. The existing metadata options will continue to work.
Next.js Learn Course

Today we're releasing a brand new, free course on Next.js Learn

. This course teaches:

    The Next.js App Router
    Styling and Tailwind CSS
    Optimizing Fonts and Images
    Creating Layouts and Pages
    Navigating Between Pages
    Setting Up Your Postgres Database
    Fetching Data with Server Components
    Static and Dynamic Rendering
    Streaming
    Partial Prerendering (Optional)
    Adding Search and Pagination
    Mutating Data
    Handling Errors
    Improving Accessibility
    Adding Authentication
    Adding Metadata

Next.js Learn has taught millions of developers about the foundations of the framework, and we can't wait to hear your feedback on our new addition. Head to nextjs.org/learn

to take the course.
Other Changes

    [Breaking] Minimum Node.js version is now 18.17
    [Breaking] Removes WASM target for next-swc build (PR

)
[Breaking] Dropped support for @next/font in favor of next/font (Codemod
)
[Breaking] Changed ImageResponse import from next/server to next/og (Codemod
)
[Breaking] next export command has been removed in favor of output: 'export' config (Docs
)
[Deprecation] onLoadingComplete for next/image is deprecated in favor of onLoad
[Deprecation] domains for next/image is deprecated in favor of remotePatterns[Feature] More verbose logging around fetch caching can be enabled (Docs

    )
    [Improvement] 80% smaller function size for a basic create-next-app application
    [Improvement] Enhanced memory management when using edge runtime in development

Nextjs 15:

nextjs.org
Next.js 15 RC
Delba de Oliveira@delba_oliveiraZack Tanner@zt1072
14–17 minutes

The Next.js 15 Release Candidate (RC) is now available. This early version allows you to test the latest features before the upcoming stable release.

    React: Support for the React 19 RC, React Compiler (Experimental), and hydration error improvements
    Caching: fetch requests, GET Route Handlers, and client navigations are no longer cached by default
    Partial Prerendering (Experimental): New Layout and Page config option for incremental adoption
    next/after (Experimental): New API to execute code after a response has finished streaming
    create-next-app: Updated design and a new flag to enable Turbopack in local development
    Bundling external packages (Stable): New config options for App and Pages Router

Try the Next.js 15 RC today:

npm install next@rc react@rc react-dom@rc

React 19 RC

The Next.js App Router is built on the React canary channel

for frameworks, which has allowed developers to use and provide feedback on these new React APIs before the v19 release.

Next.js 15 RC now supports React 19 RC, which includes new features for both the client and server like Actions.

Read the Next.js 15 upgrade guide
, the React 19 upgrade guide, and watch the React Conf Keynote

to learn more.

    Note: Some third party libraries may not be compatible with React 19 yet.

React Compiler (Experimental)

The React Compiler
is a new experimental compiler created by the React team at Meta. The compiler understands your code at a deep level through its understanding of plain JavaScript semantics and the Rules of React

, which allows it to add automatic optimizations to your code. The compiler reduces the amount of manual memoization developers have to do through APIs such as useMemo and useCallback - making code simpler, easier to maintain, and less error prone.

With Next.js 15, we've added support for the React Compiler

.

Install babel-plugin-react-compiler:

npm install babel-plugin-react-compiler

Then, add experimental.reactCompiler option in next.config.js:

const nextConfig = {
  experimental: {
    reactCompiler: true,
  },
};
 
module.exports = nextConfig;

Optionally, you can configure the compiler to run in "opt-in" mode as follows:

const nextConfig = {
  experimental: {
    reactCompiler: {
      compilationMode: 'annotation',
    },
  },
};
 
module.exports = nextConfig;

    Note: The React Compiler is currently only possible to use in Next.js through a Babel plugin, which could result in slower build times.

Learn more about the React Compiler
, and the available Next.js config options

.
Hydration error improvements

Next.js 14.1 made improvements to error messages and hydration errors. Next.js 15 continues to build on those by adding an improved hydration error view. Hydration errors now display the source code of the error with suggestions on how to address the issue.

For example, this was a previous hydration error message in Next.js 14.1:
Hydration error message in Next.js 14.1

Next.js 15 RC has improved this to:
Hydration error message improved in Next.js 15 RC
Caching updates

Next.js App Router launched with opinionated caching defaults. These were designed to provide the most performant option by default with the ability to opt out when required.

Based on your feedback, we re-evaluated our caching heuristics

and how they would interact with projects like Partial Prerendering (PPR) and with third party libraries using fetch.

With Next.js 15, we’re changing the caching default for fetch requests, GET Route Handlers, and Client Router Cache from cached by default to uncached by default. If you want to retain the previous behavior, you can continue to opt-into caching.

We're continuing to improve caching in Next.js in the coming months and we'll share more details in the Next.js 15 GA announcement.
fetch Requests are no longer cached by default

Next.js uses the Web fetch API

 cache option to configure how a server-side fetch request interacts with the framework's persistent HTTP cache:

fetch('https://...', { cache: 'force-cache' | 'no-store' });

    no-store - fetch a resource from a remote server on every request and do not update the cache
    force-cache - fetch a resource from the cache (if it exists) or a remote server and update the cache

In Next.js 14, force-cache was used by default if a cache option was not provided, unless a dynamic function or dynamic config option was used.

In Next.js 15, no-store is used by default if a cache option is not provided. This means fetch requests will not be cached by default.

You can still opt into caching fetch requests by:

    Setting the cache option

to force-cache in a single fetch call
Setting the dynamic route config option
to 'force-static' for a single route
Setting the fetchCache route config option

    to 'default-cache' to override all fetch requests in a Layout or Page to use force-cache unless they explicitly specify their own cache option

GET Route Handlers are no longer cached by default

In Next 14, Route Handlers that used the GET HTTP method were cached by default unless they used a dynamic function or dynamic config option. In Next.js 15, GET functions are not cached by default.

You can still opt into caching using a static route config option such as export dynamic = 'force-static'.

Special Route Handlers like sitemap.ts
, opengraph-image.tsx, and icon.tsx, and other metadata files

remain static by default unless they use dynamic functions or dynamic config options.
Client Router Cache no longer caches Page components by default

In Next.js 14.2.0, we introduced an experimental staleTimes
flag to allow custom configuration of the Router Cache

.

In Next.js 15, this flag still remains accessible, but we are changing the default behavior to have a staleTime of 0 for Page segments. This means that as you navigate around your app, the client will always reflect the latest data from the Page component(s) that become active as part of the navigation. However, there are still important behaviors that remain unchanged:

    Shared layout data won't be refetched from the server to continue to support partial rendering

.
Back/forward navigation will still restore from cache to ensure the browser can restore scroll position.
Loading.js

    will remain cached for 5 minutes (or the value of the staleTimes.static configuration).

You can opt into the previous Client Router Cache behavior by setting the following configuration:

const nextConfig = {
  experimental: {
    staleTimes: {
      dynamic: 30,
    },
  },
};
 
module.exports = nextConfig;

Incremental adoption of Partial Prerendering (Experimental)

In Next.js 14, we introduced Partial Prerendering (PPR) - an optimization that combines static and dynamic rendering

on the same page.

Next.js currently defaults to static rendering unless you use dynamic functions

such as cookies(), headers(), and uncached data requests. These APIs opt an entire route into dynamic rendering. With PPR, you can wrap any dynamic UI in a Suspense boundary. When a new request comes in, Next.js will immediately serve a static HTML shell, then render and stream the dynamic parts in the same HTTP request.

To allow for incremental adoption, we’ve added an experimental_ppr route config option for opting specific Layouts and Pages into PPR:

import { Suspense } from "react"
import { StaticComponent, DynamicComponent } from "@/app/ui"
 
export const experimental_ppr = true
 
export default function Page() {
  return {
     <>
	     <StaticComponent />
	     <Suspense fallback={...}>
		     <DynamicComponent />
	     </Suspense>
     </>
  };
}

To use the new option, you’ll need to set the experimental.ppr config in your next.config.js file to 'incremental':

const nextConfig = {
  experimental: {
    ppr: 'incremental',
  },
};
 
module.exports = nextConfig;

Once all the segments have PPR enabled, it’ll be considered safe for you to set the ppr value to true, and enable it for the entire app and all future routes.

We will share more about our PPR roadmap in our Next.js 15 GA blog post.

Learn more about Partial Prerendering

.
Executing code after a response with next/after (Experimental)

When processing a user request, the server typically performs tasks directly related to computing the response. However, you may need to perform tasks such as logging, analytics, and other external system synchronization.

Since these tasks are not directly related to the response, the user should not have to wait for them to complete. Deferring the work after responding to the user poses a challenge because serverless functions stop computation immediately after the response is closed.

after() is a new experimental API that solves this problem by allowing you to schedule work to be processed after the response has finished streaming, enabling secondary tasks to run without blocking the primary response.

To use it, add experimental.after to next.config.js:

const nextConfig = {
  experimental: {
    after: true,
  },
};
 
module.exports = nextConfig;

Then, import the function in Server Components, Server Actions, Route Handlers, or Middleware.

import { unstable_after as after } from 'next/server';
import { log } from '@/app/utils';
 
export default function Layout({ children }) {
  // Secondary task
  after(() => {
    log();
  });
 
  // Primary task
  return <>{children}</>;
}

Learn more about next/after

.
create-next-app updates

For Next.js 15, we've updated create-next-app with a new design.
New design for create-next-app in Next.js 15 RC

When running create-next-app, there is a new prompt asking if you want to enable Turbopack for local development (defaults to No).

✔ Would you like to use Turbopack for next dev? … No / Yes

The --turbo flag can be used to enable Turbopack.

npx create-next-app@rc --turbo

To make getting started on a new project even easier, a new --empty flag has been added to the CLI. This will remove any extraneous files and styles, resulting in a minimal "hello world" page.

npx create-next-app@rc --empty

Optimizing bundling of external packages (Stable)

Bundling external packages can improve the cold start performance of your application. In the App Router, external packages are bundled by default, and you can opt-out specific packages using the new serverExternalPackages

config option.

In the Pages Router, external packages are not bundled by default, but you can provide a list of packages to bundle using the existing transpilePackages

option. With this configuration option, you need to specify each package.

To unify configuration between App and Pages Router, we’re introducing a new option, bundlePagesRouterDependencies
to match the default automatic bundling of the App Router. You can then use serverExternalPackages

to opt-out specific packages, if needed.

const nextConfig = {
  // Automatically bundle external packages in the Pages Router:
  bundlePagesRouterDependencies: true,
  // Opt specific packages out of bundling for both App and Pages Router:
  serverExternalPackages: ['package-name'],
};
 
module.exports = nextConfig;

Learn more about optimizing external packages

.
Other Changes

    [Breaking] Minimum React version is now 19 RC
    [Breaking] next/image: Removed squoosh in favor of sharp as an optional dependency (PR

)
[Breaking] next/image: Changed default Content-Disposition to attachment (PR
)
[Breaking] next/image: Error when src has leading or trailing spaces (PR
)
[Breaking] Middleware: Apply react-server condition to limit unrecommended react API imports (PR
)
[Breaking] next/font: Removed support for external @next/font package (PR
)
[Breaking] next/font: Removed font-family hashing (PR
)
[Breaking] Caching: force-dynamic will now set a no-store default to the fetch cache (PR
)
[Breaking] Config: Enable swcMinify (PR
), missingSuspenseWithCSRBailout (PR), and outputFileTracing (PR
) behavior by default and remove deprecated options
[Breaking] Remove auto-instrumentation for Speed Insights (must now use the dedicated @vercel/speed-insights
package) (PR
)
[Breaking] Remove .xml extension for dynamic sitemap routes and align sitemap URLs between development and production (PR
)
[Improvement] Metadata: Updated environmental variable fallbacks for metadataBase when hosted on Vercel (PR
)
[Improvement] Fix tree-shaking with mixed namespace and named imports from optimizePackageImports (PR
)
[Improvement] Parallel Routes: Provide unmatched catch-all routes with all known params (PR
)
[Improvement] Config bundlePagesExternals is now stable and renamed to bundlePagesRouterDependencies
[Improvement] Config serverComponentsExternalPackages is now stable and renamed to serverExternalPackages
[Improvement] create-next-app: New projects ignore all .env files by default (PR
)
[Docs] Improve auth documentation (PR
)
[Docs] @next/env package (PR

    )