🫢Step by Step Study (part 2)👆

Step 1: ProductCard.component

import Link from 'next/link';
import Image from 'next/image';
import { paddedPrice } from './../../utils/functions/functions';
const ProductCard = ({ databaseId, name, price, slug, image, }: ProductCardProps) => {
  if (price) {
    price = paddedPrice(price as string, ' ');
  }
  return (
    <div className="group">
      <div className="aspect-[3/4] overflow-hidden bg-gray-100">
        <Link href={`/product/${slug}?id=${databaseId}`} className='relative block h-full'>
          {image?.sourceUrl ? (
            <Image
              src={image.sourceUrl}
              alt={name}
              fill
              className="w-full h-full object-cover object-center transition duration-300 group-hover:scale-105"
              priority={databaseId === 1}
              sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw"
            />
          ) : (
            <div className="h-full w-full bg-gray-100 flex items-center justify-center">
              <span className="text-gray-400">No image</span>
            </div>
          )}
        </Link>
      </div>
      <Link href={`/product/${slug}?id=${databaseId}`}>
        <div className="mt-4">
          <p className="text-base font-bold text-center cursor-pointer hover:text-gray-600 transition-colors">
            {name}
          </p>
        </div>
      </Link>
      <div className="mt-2 text-center">
        <span className="text-gray-900">{price}</span>
      </div>
    </div>
  )
}
export default ProductCard;

Step 2: Stady getUniqueProductTypes

console.log(productTypes);
[
  {
    "id": "accessories",
    "name": "Accessories",
    "checked": false
  },
  {
    "id": "hoodies",
    "name": "Hoodies",
    "checked": true
  },
  {
    "id": "tshirts",
    "name": "Tshirts",
    "checked": false
  }
]

src\components\Product\ProductFilters.component.tsx

import { Dispatch, SetStateAction } from 'react';
import Button from '@/components/UI/Button.component';
import Checkbox from '@/components/UI/Checkbox.component';
const ProductFilters = ({ productTypes, toggleProductType }: ProductFiltersProps) => {
  return (
    <div className="w-full md:w-64 flex-shrink-0">
      <div className="bg-white px-8 pb-8 sm:px-6 sm:pb-6 rounded-lg shadow-sm">
        <div className="mb-8">
          <h3 className="font-semibold mb-4">PRODUCT TYPE</h3>
          <div className="space-y-2">
            {productTypes.map((type) => (
              <Checkbox
                key={type.id}
                id={type.id}
                label={type.name}
                checked={type.checked}
                onChange={() => toggleProductType(type.id)}
              />
            ))}
          </div>
        </div>
      </div>
    </div>
  )
}
export default ProductFilters;

src\components\Product\ProductList.component.tsx

import { useProductFilters } from "@/hooks/useProductFilters";
import ProductCard from "./ProductCard.component";
import ProductFilters from "./ProductFilters.component";
const ProductList = ({ products, title }: ProductListProps) => {
  const { productTypes, toggleProductType, filterProducts} = useProductFilters(products);
  const filteredProducts = filterProducts(products);
  return (
    <div className="flex flex-col md:flex-row gap-8">
      <ProductFilters
        productTypes={productTypes}
        toggleProductType={toggleProductType}
        products={products}
      />
      <div className="flex-1">
        <div className="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-8">
          <h1 className="text-xl sm:text-2xl font-medium text-center sm:text-left">
            {title} <span className="text-gray-500">({filteredProducts.length})</span>
          </h1>
          <div className="flex flex-wrap items-center justify-center sm:justify-end gap-2 sm:gap-4">
            <label htmlFor="sort-select" className="text-sm font-medium">Sortering:</label>
            <select
              id="sort-select"
              value={"desc"}
              className="min-w-[140px] border rounded-md px-3 py-1.5 text-sm"
            >
              <option value="popular">Popular</option>
              <option value="price-low">Price: Low to High</option>
              <option value="price-high">Price: High to Low</option>
              <option value="newest">Newest</option>
            </select>
          </div>
        </div>
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
          {filteredProducts.map((product: ProductDP) => (
            <ProductCard
              key={product.databaseId}
              databaseId={product.databaseId}
              name={product.name}
              price={product.price}
              slug={product.slug}
              image={product.image}
            />
          ))}
        </div>
      </div>
    </div>
  )
}
export default ProductList;

— Tìm hiểu new Map

<!DOCTYPE html>
<html>
<body>
<h1>JavaScript Maps</h1>
<h2>The new Map() Method</h2>
<p>Creating a map from an array:</p>
<p id="demo"></p>
<script>
// Create a Map
const fruits = new Map([["apples", 500],["bananas", 300],["oranges", 200]]);
let x = fruits.get("bananas");
document.getElementById("demo").innerHTML = "The number of apples in fruits are " + x;
</script>
</body>
</html>
=================================
JavaScript Maps
The new Map() Method
Creating a map from an array:
The number of apples in fruits are 300
[
  {
    "__typename": "SimpleProduct",
    "databaseId": 1846,
    "description": "<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.</p>\n",
    "image": {
      "__typename": "MediaItem",
      "sourceUrl": "https://wpclidemo.dev/wp-content/uploads/2019/01/polo-2.jpg"
    },
    "onSale": false,
    "name": "Polo",
    "slug": "polo-2",
    "productCategories": {
      "__typename": "ProductToProductCategoryConnection",
      "nodes": [
        {
          "__typename": "ProductCategory",
          "name": "Tshirts",
          "slug": "tshirts"
        }
      ]
    },
    "price": "20&nbsp;₫",
    "salePrice": null,
    "regularPrice": "20&nbsp;₫"
  },
  {
    "__typename": "SimpleProduct",
    "databaseId": 1845,
    "description": "<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.</p>\n",
    "image": {
      "__typename": "MediaItem",
      "sourceUrl": "https://wpclidemo.dev/wp-content/uploads/2019/01/long-sleeve-tee-2.jpg"
    },
    "onSale": false,
    "name": "Long Sleeve Tee",
    "slug": "long-sleeve-tee-2",
    "productCategories": {
      "__typename": "ProductToProductCategoryConnection",
      "nodes": [
        {
          "__typename": "ProductCategory",
          "name": "Tshirts",
          "slug": "tshirts"
        }
      ]
    },
    "price": "25&nbsp;₫",
    "salePrice": null,
    "regularPrice": "25&nbsp;₫"
  },
  {
    "__typename": "SimpleProduct",
    "databaseId": 15,
    "description": "<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.</p>\n",
    "image": {
      "__typename": "MediaItem",
      "sourceUrl": "https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-with-zipper-2.jpg"
    },
    "onSale": false,
    "name": "Hoodie with Zipper",
    "slug": "hoodie-with-zipper-2",
    "productCategories": {
      "__typename": "ProductToProductCategoryConnection",
      "nodes": [
        {
          "__typename": "ProductCategory",
          "name": "Hoodies",
          "slug": "hoodies"
        }
      ]
    },
    "price": "45&nbsp;₫",
    "salePrice": null,
    "regularPrice": "45&nbsp;₫"
  },
  {
    "__typename": "SimpleProduct",
    "databaseId": 14,
    "description": "<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.</p>\n",
    "image": {
      "__typename": "MediaItem",
      "sourceUrl": "https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-with-pocket-2.jpg"
    },
    "onSale": true,
    "name": "Hoodie with Pocket",
    "slug": "hoodie-with-pocket-2",
    "productCategories": {
      "__typename": "ProductToProductCategoryConnection",
      "nodes": [
        {
          "__typename": "ProductCategory",
          "name": "Hoodies",
          "slug": "hoodies"
        }
      ]
    },
    "price": "35&nbsp;₫",
    "salePrice": "35&nbsp;₫",
    "regularPrice": "45&nbsp;₫"
  },
  {
    "__typename": "SimpleProduct",
    "databaseId": 13,
    "description": "<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.</p>\n",
    "image": {
      "__typename": "MediaItem",
      "sourceUrl": "https://wpclidemo.dev/wp-content/uploads/2019/01/sunglasses-2.jpg"
    },
    "onSale": false,
    "name": "Sunglasses",
    "slug": "sunglasses-2",
    "productCategories": {
      "__typename": "ProductToProductCategoryConnection",
      "nodes": [
        {
          "__typename": "ProductCategory",
          "name": "Accessories",
          "slug": "accessories"
        }
      ]
    },
    "price": "90&nbsp;₫",
    "salePrice": null,
    "regularPrice": "90&nbsp;₫"
  },
  {
    "__typename": "SimpleProduct",
    "databaseId": 12,
    "description": "<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.</p>\n",
    "image": {
      "__typename": "MediaItem",
      "sourceUrl": "https://wpclidemo.dev/wp-content/uploads/2019/01/cap-2.jpg"
    },
    "onSale": true,
    "name": "Cap",
    "slug": "cap-2",
    "productCategories": {
      "__typename": "ProductToProductCategoryConnection",
      "nodes": [
        {
          "__typename": "ProductCategory",
          "name": "Accessories",
          "slug": "accessories"
        }
      ]
    },
    "price": "16&nbsp;₫",
    "salePrice": "16&nbsp;₫",
    "regularPrice": "18&nbsp;₫"
  },
  {
    "__typename": "SimpleProduct",
    "databaseId": 11,
    "description": "<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.</p>\n",
    "image": {
      "__typename": "MediaItem",
      "sourceUrl": "https://wpclidemo.dev/wp-content/uploads/2019/01/belt-2.jpg"
    },
    "onSale": true,
    "name": "Belt",
    "slug": "belt-2",
    "productCategories": {
      "__typename": "ProductToProductCategoryConnection",
      "nodes": [
        {
          "__typename": "ProductCategory",
          "name": "Accessories",
          "slug": "accessories"
        }
      ]
    },
    "price": "55&nbsp;₫",
    "salePrice": "55&nbsp;₫",
    "regularPrice": "65&nbsp;₫"
  },
  {
    "__typename": "SimpleProduct",
    "databaseId": 10,
    "description": "<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.</p>\n",
    "image": {
      "__typename": "MediaItem",
      "sourceUrl": "https://wpclidemo.dev/wp-content/uploads/2019/01/beanie-2.jpg"
    },
    "onSale": true,
    "name": "Beanie",
    "slug": "beanie-2",
    "productCategories": {
      "__typename": "ProductToProductCategoryConnection",
      "nodes": [
        {
          "__typename": "ProductCategory",
          "name": "Accessories",
          "slug": "accessories"
        }
      ]
    },
    "price": "18&nbsp;₫",
    "salePrice": "18&nbsp;₫",
    "regularPrice": "20&nbsp;₫"
  },
  {
    "__typename": "SimpleProduct",
    "databaseId": 9,
    "description": "<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.</p>\n",
    "image": {
      "__typename": "MediaItem",
      "sourceUrl": "https://wpclidemo.dev/wp-content/uploads/2019/01/tshirt-2.jpg"
    },
    "onSale": false,
    "name": "T-Shirt",
    "slug": "t-shirt-2",
    "productCategories": {
      "__typename": "ProductToProductCategoryConnection",
      "nodes": [
        {
          "__typename": "ProductCategory",
          "name": "Tshirts",
          "slug": "tshirts"
        }
      ]
    },
    "price": "18&nbsp;₫",
    "salePrice": null,
    "regularPrice": "18&nbsp;₫"
  },
  {
    "__typename": "SimpleProduct",
    "databaseId": 8,
    "description": "<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.</p>\n",
    "image": {
      "__typename": "MediaItem",
      "sourceUrl": "https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-with-logo-2.jpg"
    },
    "onSale": false,
    "name": "Hoodie with Logo",
    "slug": "hoodie-with-logo-2",
    "productCategories": {
      "__typename": "ProductToProductCategoryConnection",
      "nodes": [
        {
          "__typename": "ProductCategory",
          "name": "Hoodies",
          "slug": "hoodies"
        }
      ]
    },
    "price": "45&nbsp;₫",
    "salePrice": null,
    "regularPrice": "45&nbsp;₫"
  },
  {
    "__typename": "VariableProduct",
    "databaseId": 7,
    "description": "<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.</p>\n",
    "image": {
      "__typename": "MediaItem",
      "sourceUrl": "https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-2.jpg"
    },
    "onSale": false,
    "name": "Hoodie",
    "slug": "hoodie-2",
    "productCategories": {
      "__typename": "ProductToProductCategoryConnection",
      "nodes": [
        {
          "__typename": "ProductCategory",
          "name": "Hoodies",
          "slug": "hoodies"
        }
      ]
    },
    "price": "45&nbsp;₫",
    "salePrice": null,
    "regularPrice": "45, 45",
    "productTypes": {
      "__typename": "ProductToProductTypeConnection",
      "nodes": [
        {
          "__typename": "ProductType",
          "databaseId": 21,
          "description": null,
          "name": "variable",
          "products": {
            "__typename": "ProductTypeToProductConnection",
            "nodes": [
              {
                "__typename": "VariableProduct",
                "description": "<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.</p>\n",
                "onSale": true,
                "image": {
                  "__typename": "MediaItem",
                  "mediaItemUrl": "https://wpclidemo.dev/wp-content/uploads/2025/02/hoodie-2.jpg"
                },
                "price": "42&nbsp;₫ - 45&nbsp;₫",
                "salePrice": "42&nbsp;₫",
                "regularPrice": "45&nbsp;₫"
              },
              {
                "__typename": "VariableProduct",
                "description": "<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.</p>\n",
                "onSale": false,
                "image": {
                  "__typename": "MediaItem",
                  "mediaItemUrl": "https://wpclidemo.dev/wp-content/uploads/2025/02/vneck-tee-2.jpg"
                },
                "price": "15&nbsp;₫ - 20&nbsp;₫",
                "salePrice": null,
                "regularPrice": "15&nbsp;₫ - 20&nbsp;₫"
              },
              {
                "__typename": "VariableProduct",
                "description": "<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.</p>\n",
                "onSale": false,
                "image": {
                  "__typename": "MediaItem",
                  "mediaItemUrl": "https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-2.jpg"
                },
                "price": "45&nbsp;₫",
                "salePrice": null,
                "regularPrice": "45&nbsp;₫"
              },
              {
                "__typename": "VariableProduct",
                "description": "<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.</p>\n",
                "onSale": false,
                "image": {
                  "__typename": "MediaItem",
                  "mediaItemUrl": "https://wpclidemo.dev/wp-content/uploads/2019/01/vneck-tee-2.jpg"
                },
                "price": "15&nbsp;₫ - 20&nbsp;₫",
                "salePrice": null,
                "regularPrice": "15&nbsp;₫ - 20&nbsp;₫"
              }
            ]
          }
        }
      ]
    }
  },
  {
    "__typename": "VariableProduct",
    "databaseId": 6,
    "description": "<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.</p>\n",
    "image": {
      "__typename": "MediaItem",
      "sourceUrl": "https://wpclidemo.dev/wp-content/uploads/2019/01/vneck-tee-2.jpg"
    },
    "onSale": false,
    "name": "V-Neck T-Shirt Two",
    "slug": "v-neck-t-shirt-2",
    "productCategories": {
      "__typename": "ProductToProductCategoryConnection",
      "nodes": [
        {
          "__typename": "ProductCategory",
          "name": "Tshirts",
          "slug": "tshirts"
        }
      ]
    },
    "price": "15&nbsp;₫ - 20&nbsp;₫",
    "salePrice": null,
    "regularPrice": "15, 20, 20",
    "productTypes": {
      "__typename": "ProductToProductTypeConnection",
      "nodes": [
        {
          "__typename": "ProductType",
          "databaseId": 21,
          "description": null,
          "name": "variable",
          "products": {
            "__typename": "ProductTypeToProductConnection",
            "nodes": [
              {
                "__typename": "VariableProduct",
                "description": "<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.</p>\n",
                "onSale": true,
                "image": {
                  "__typename": "MediaItem",
                  "mediaItemUrl": "https://wpclidemo.dev/wp-content/uploads/2025/02/hoodie-2.jpg"
                },
                "price": "42&nbsp;₫ - 45&nbsp;₫",
                "salePrice": "42&nbsp;₫",
                "regularPrice": "45&nbsp;₫"
              },
              {
                "__typename": "VariableProduct",
                "description": "<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.</p>\n",
                "onSale": false,
                "image": {
                  "__typename": "MediaItem",
                  "mediaItemUrl": "https://wpclidemo.dev/wp-content/uploads/2025/02/vneck-tee-2.jpg"
                },
                "price": "15&nbsp;₫ - 20&nbsp;₫",
                "salePrice": null,
                "regularPrice": "15&nbsp;₫ - 20&nbsp;₫"
              },
              {
                "__typename": "VariableProduct",
                "description": "<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.</p>\n",
                "onSale": false,
                "image": {
                  "__typename": "MediaItem",
                  "mediaItemUrl": "https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-2.jpg"
                },
                "price": "45&nbsp;₫",
                "salePrice": null,
                "regularPrice": "45&nbsp;₫"
              },
              {
                "__typename": "VariableProduct",
                "description": "<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.</p>\n",
                "onSale": false,
                "image": {
                  "__typename": "MediaItem",
                  "mediaItemUrl": "https://wpclidemo.dev/wp-content/uploads/2019/01/vneck-tee-2.jpg"
                },
                "price": "15&nbsp;₫ - 20&nbsp;₫",
                "salePrice": null,
                "regularPrice": "15&nbsp;₫ - 20&nbsp;₫"
              }
            ]
          }
        }
      ]
    }
  }
]
console.log(product.productCategories);
{
  "__typename": "ProductToProductCategoryConnection",
  "nodes": [
    {
      "__typename": "ProductCategory",
      "name": "Tshirts",
      "slug": "tshirts"
    }
  ]
}
console.log(categoryMap);
new Map([
  [
    "tshirts",
    {
      "id": "tshirts",
      "name": "Tshirts",
      "checked": false
    }
  ],
  [
    "hoodies",
    {
      "id": "hoodies",
      "name": "Hoodies",
      "checked": false
    }
  ],
  [
    "accessories",
    {
      "id": "accessories",
      "name": "Accessories",
      "checked": false
    }
  ]
])
console.log(categoryMap.values());
MapIterator {
 { "value": { "id": "tshirts", "name": "Tshirts", "checked": false } }
 { "value": { "id": "hoodies", "name": "Hoodies", "checked": false } }
 { "value": { "id": "accessories", "name": "Accessories", "checked": false } }
}
console.log(Array.from(categoryMap.values()));
[
  {
    "id": "tshirts",
    "name": "Tshirts",
    "checked": false
  },
  {
    "id": "hoodies",
    "name": "Hoodies",
    "checked": false
  },
  {
    "id": "accessories",
    "name": "Accessories",
    "checked": false
  }
]

Step 3. Study toggleProductType

src\hooks\useProductFilters.ts

import { getUniqueProductTypes } from '@/utils/functions/productUtils';
import { useState } from 'react';
export const useProductFilters = (products: ProductDP[]) => {
  let productPrice;
  const [priceRange, setPriceRange] = useState<[number, number]>([0, 1000]);
  const [productTypes, setProductTypes] = useState<ProductType[]>(() =>
    products ? getUniqueProductTypes(products) : [],
  );
  const toggleProductType = (id: string) => {
    setProductTypes((prev) =>
      prev.map((type) =>
        type.id === id ? { ...type, checked: !type.checked } : type,
      ),
    );
  };
  const filterProducts = (products: ProductDP[]) => {
    const filtered = products?.filter((product: ProductDP) => {
      if (product.__typename === "VariableProduct") {
        productPrice = parseFloat(product?.regularPrice.split(",").reverse()[0]);
      }else {
        productPrice = parseFloat(product?.regularPrice?.replace(/[^0-9]/g, ''));
      }
      const withinPriceRange = productPrice >= priceRange[0] && productPrice <= priceRange[1];
      if (!withinPriceRange) return false;
      const selectedTypes = productTypes.filter((t) => t.checked).map((t) => t.name.toLowerCase());
      return true;
    });
    return [...(filtered || [])].sort((a, b) => {
      return 0;
    })
  };
  return {
    productTypes,
    toggleProductType,
    filterProducts
  }
}

src\components\Product\ProductList.component.tsx

import { useProductFilters } from "@/hooks/useProductFilters";
import ProductCard from "./ProductCard.component";
import ProductFilters from "./ProductFilters.component";
const ProductList = ({ products, title }: ProductListProps) => {
  const { productTypes, toggleProductType, filterProducts} = useProductFilters(products);
  const filteredProducts = filterProducts(products);
  return (
    <div className="flex flex-col md:flex-row gap-8">
      <ProductFilters
        productTypes={productTypes}
        toggleProductType={toggleProductType}
        products={products}
      />
      <div className="flex-1">
        <div className="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-8">
          <h1 className="text-xl sm:text-2xl font-medium text-center sm:text-left">
            {title} <span className="text-gray-500">({filteredProducts.length})</span>
          </h1>
          <div className="flex flex-wrap items-center justify-center sm:justify-end gap-2 sm:gap-4">
            <label htmlFor="sort-select" className="text-sm font-medium">Sortering:</label>
            <select
              id="sort-select"
              value={"desc"}
              className="min-w-[140px] border rounded-md px-3 py-1.5 text-sm"
            >
              <option value="popular">Popular</option>
              <option value="price-low">Price: Low to High</option>
              <option value="price-high">Price: High to Low</option>
              <option value="newest">Newest</option>
            </select>
          </div>
        </div>
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
          {filteredProducts.map((product: ProductDP) => (
            <ProductCard
              key={product.databaseId}
              databaseId={product.databaseId}
              name={product.name}
              price={product.price}
              slug={product.slug}
              image={product.image}
            />
          ))}
        </div>
      </div>
    </div>
  )
}
export default ProductList;

src\components\Product\ProductFilters.component.tsx

import { Dispatch, SetStateAction } from 'react';
import Button from '@/components/UI/Button.component';
import Checkbox from '@/components/UI/Checkbox.component';
const ProductFilters = ({ productTypes, toggleProductType }: ProductFiltersProps) => {
  return (
    <div className="w-full md:w-64 flex-shrink-0">
      <div className="bg-white px-8 pb-8 sm:px-6 sm:pb-6 rounded-lg shadow-sm">
        <div className="mb-8">
          <h3 className="font-semibold mb-4">PRODUCT TYPE</h3>
          <div className="space-y-2">
            {productTypes.map((type) => (
              <Checkbox
                key={type.id}
                id={type.id}
                label={type.name}
                checked={type.checked}
                onChange={() => toggleProductType(type.id)}
              />
            ))}
          </div>
        </div>
      </div>
    </div>
  )
}
export default ProductFilters;

src\components\UI\Checkbox.component.tsx

import { ChangeEvent } from 'react';
const Checkbox = ({ id, label, checked, onChange }: ICheckboxProps) => {
  return (
    <label htmlFor={id} className="flex items-center py-2 cursor-pointer">
      <input
        id={id}
        type="checkbox"
        className="form-checkbox h-5 w-5 cursor-pointer"
        checked={checked}
        onChange={onChange}
      />
      <span className="ml-3 text-base">{label}</span>
    </label>
  );
}
export default Checkbox;

Step 4: selectedTypes

console.log(selectedTypes);
console.log(product.productCategories?.nodes);
console.log(productCategories);

Kết quả bộ lọc

src\hooks\useProductFilters.ts

import { getUniqueProductTypes } from '@/utils/functions/productUtils';
import { useState } from 'react';
export const useProductFilters = (products: ProductDP[]) => {
  let productPrice;
  const [priceRange, setPriceRange] = useState<[number, number]>([0, 1000]);
  const [productTypes, setProductTypes] = useState<ProductType[]>(() =>
    products ? getUniqueProductTypes(products) : [],
  );
  const toggleProductType = (id: string) => {
    setProductTypes((prev) =>
      prev.map((type) =>
        type.id === id ? { ...type, checked: !type.checked } : type,
      ),
    );
  };
  const filterProducts = (products: ProductDP[]) => {
    const filtered = products?.filter((product: ProductDP) => {
      if (product.__typename === "VariableProduct") {
        productPrice = parseFloat(product?.regularPrice.split(",").reverse()[0]);
      }else {
        productPrice = parseFloat(product?.regularPrice?.replace(/[^0-9]/g, ''));
      }
      const withinPriceRange = productPrice >= priceRange[0] && productPrice <= priceRange[1];
      if (!withinPriceRange) return false;
      const selectedTypes = productTypes.filter((t) => t.checked).map((t) => t.name.toLowerCase());
      if (selectedTypes.length > 0) {
        const productCategories = product.productCategories?.nodes.map((cat) => cat.name.toLowerCase()) || [];
        if (!selectedTypes.some((type) => productCategories.includes(type))) return false;
      }
      return true;
    });
    return [...(filtered || [])].sort((a, b) => {
      return 0;
    })
  };
  return {
    productTypes,
    toggleProductType,
    filterProducts
  }
}

Step 5.1 : add allPaColors, allPaSizes

src\utils\gql\GQL_QUERIES.ts

import { gql } from '@apollo/client';
export const FETCH_ALL_PRODUCTS_QUERY = gql`
  query MyQuery {
    products(last: 36) {
      nodes {
        __typename
        databaseId
        description(format: RENDERED)
        image {
          __typename
          sourceUrl
        }
        onSale
        name
        slug
        productCategories {
          nodes {
            name
            slug
          }
        }
        ... on SimpleProduct {
          price
          salePrice
          regularPrice
        }
        ... on VariableProduct {
          price
          salePrice
          regularPrice(format: RAW)
          allPaColors {
            nodes {
              name
              slug
            }
          }
          allPaSizes {
            nodes {
              name
            }
          }
          productTypes {
            nodes {
              databaseId
              description
              name
              products {
                __typename
                nodes {
                  description
                  onSale
                  image {
                    __typename
                    mediaItemUrl
                  }
                  ... on SimpleProduct {
                    price
                    salePrice
                    regularPrice
                  }
                  ... on VariableProduct {
                    price
                    salePrice
                    regularPrice
                  }
                }
              }
            }
          }
        }
      }
    }
  }
`;
export const GET_SINGLE_PRODUCT = gql`
  query Product($id: ID!) {
    product(id: $id, idType: DATABASE_ID) {
      databaseId
      name
      description
      slug
      averageRating
      image {
        uri
        title
        srcSet
        sourceUrl
      }
      ... on SimpleProduct {
        stockQuantity
        price
        regularPrice
        salePrice
      }
      ... on VariableProduct {
        price
        regularPrice
        salePrice
        allPaColors {
          nodes {
            name
          }
        }
        allPaSizes {
          nodes {
            name
          }
        }
        variations {
          nodes {
            databaseId
            name
            image {
              sourceUrl
            }
            stockStatus
            stockQuantity
            price
            salePrice
            regularPrice
            onSale
          }
        }
      }
      ... on ExternalProduct {
        name
        price
        externalUrl
      }
      ... on GroupProduct {
        products {
          nodes {
            ... on SimpleProduct {
                price
                regularPrice
                salePrice
            }
          }
        }
      }
    }
  }
`;
export const GET_CART = gql`
  query GET_CART {
    cart {
      contents {
        nodes {
          key
          product {
            node {
              id
              databaseId
              name
              description
              type
              onSale
              slug
              averageRating
              reviewCount
              image {
                id
                sourceUrl
                srcSet
                altText
                title
              }
              galleryImages {
                nodes {
                  id
                  sourceUrl
                  srcSet
                  altText
                  title
                }
              }
            }
          }
          variation {
            node {
              id
              databaseId
              name
              description
              type
              onSale
              price
              regularPrice
              salePrice
              image {
                id
                sourceUrl
                srcSet
                altText
                title
              }
              attributes {
                nodes {
                  id
                  name
                  value
                }
              }
            }
          }
          quantity
          total
          subtotal
          subtotalTax
        }
      }
      subtotal
      subtotalTax
      shippingTax
      shippingTotal
      total
      totalTax
      feeTax
      feeTotal
      discountTax
      discountTotal
    }
  }
`;

Step 5.2 sizes, toggleSize

console.log(sizes);

Step 5.3 colors, toogleColor

console.log(colors);

src\components\Product\ProductFilters.component.tsx

import Button from '@/components/UI/Button.component';
import Checkbox from '@/components/UI/Checkbox.component';
const ProductFilters = ({ 
  productTypes, 
  toggleProductType, 
  products, 
  selectedSizes, 
  setSelectedSizes,
  selectedColors,
  setSelectedColors, 
}: ProductFiltersProps) => {
  const sizes = Array.from(
    new Set(
      products.flatMap(
        (product: ProductDP) => product.allPaSizes?.nodes.map((node: { name: string }) => node.name,) || []
      ),
    ),
  ).sort((a, b) => a.localeCompare(b));
  const toggleSize = (size: string) => {
    setSelectedSizes((prev) =>
      prev.includes(size) ? prev.filter((s) => s !== size) : [...prev, size],
    );
  };
  const availableColors = products
    .flatMap((product: ProductDP) => product.allPaColors?.nodes || [])
    .filter((color, index, self) =>
      index === self.findIndex((c) => c.slug === color.slug)
    )
    .sort((a, b) => a.name.localeCompare(b.name));
  const colors = availableColors.map((color) => ({
    name: color.name,
    class: `bg-${color.slug}-500`
  }));
  console.log(colors);
  const toggleColor = (color: string) => {
    setSelectedColors((prev) =>
      prev.includes(color) ? prev.filter((c) => c !== color) : [...prev, color],
    );
  };
  return (
    <div className="w-72 flex-shrink-0">
      <div className="bg-white px-8 pb-8 sm:px-6 sm:pb-6 rounded-lg shadow-sm">
        <div className="mb-4">
          <h3 className="font-semibold mb-4">PRODUCT TYPE</h3>
          <div className="space-y-2">
            {productTypes.map((type) => (
              <Checkbox
                key={type.id}
                id={type.id}
                label={type.name}
                checked={type.checked}
                onChange={() => toggleProductType(type.id)}
              />
            ))}
          </div>
        </div>
        <div className="mb-4">
          <h3 className="font-semibold mb-4">SIZE</h3>
          <div className="grid grid-cols-3 gap-2">
            {sizes.map((size) => (
              <Button
                key={size}
                handleButtonClick={() => toggleSize(size)}
                variant="filter"
                selected={selectedSizes.includes(size)}
              >
                {size}
              </Button>
            ))}
          </div>
        </div>
        <div className="mb-4">
          <h3 className="font-semibold mb-4">COLOR</h3>
          <div className="grid grid-cols-3 gap-2">
            {colors.map((color) => (
              <button
                key={color.name}
                onClick={() => toggleColor(color.name)}
                className={`w-8 h-8 rounded-full flex items-center justify-center text-xs ${color.class} ${selectedColors.includes(color.name)
                    ? 'ring-2 ring-offset-2 ring-gray-900'
                    : ''
                  }`}
                title={color.name}
              />
            ))}
          </div>
        </div>
      </div>
    </div>
  )
}
export default ProductFilters;

src\components\Product\ProductList.component.tsx

import { useProductFilters } from "@/hooks/useProductFilters";
import ProductCard from "./ProductCard.component";
import ProductFilters from "./ProductFilters.component";
const ProductList = ({ products, title }: ProductListProps) => {
  const { 
    productTypes, 
    toggleProductType, 
    filterProducts, 
    selectedSizes,
    setSelectedSizes,
    selectedColors,
    setSelectedColors, 
  } = useProductFilters(products);
  const filteredProducts = filterProducts(products);
  return (
    <div className="flex flex-col md:flex-row gap-8">
      <ProductFilters
        selectedColors={selectedColors}
        setSelectedColors={setSelectedColors}
        selectedSizes={selectedSizes}
        setSelectedSizes={setSelectedSizes}
        productTypes={productTypes}
        toggleProductType={toggleProductType}
        products={products}
      />
      <div className="flex-1">
        <div className="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-8 bg-pink">
          <h1 className="text-xl sm:text-2xl font-medium text-center sm:text-left">
            {title} <span className="text-gray-500">({filteredProducts.length})</span>
          </h1>
          <div className="flex flex-wrap items-center justify-center sm:justify-end gap-2 sm:gap-4">
            <label htmlFor="sort-select" className="text-sm font-medium">Sortering:</label>
            <select
              id="sort-select"
              value={"desc"}
              className="min-w-[140px] border rounded-md px-3 py-1.5 text-sm"
            >
              <option value="popular">Popular</option>
              <option value="price-low">Price: Low to High</option>
              <option value="price-high">Price: High to Low</option>
              <option value="newest">Newest</option>
            </select>
          </div>
        </div>
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
          {filteredProducts.map((product: ProductDP) => (
            <ProductCard
              key={product.databaseId}
              databaseId={product.databaseId}
              name={product.name}
              price={product.price}
              slug={product.slug}
              image={product.image}
            />
          ))}
        </div>
      </div>
    </div>
  )
}
export default ProductList;

src\hooks\useProductFilters.ts

import { getUniqueProductTypes } from '@/utils/functions/productUtils';
import { useState } from 'react';
export const useProductFilters = (products: ProductDP[]) => {
  let productPrice;
  const [priceRange, setPriceRange] = useState<[number, number]>([0, 1000]);
  const [productTypes, setProductTypes] = useState<ProductType[]>(() =>
    products ? getUniqueProductTypes(products) : [],
  );
  const [selectedSizes, setSelectedSizes] = useState<string[]>([]);
  const [selectedColors, setSelectedColors] = useState<string[]>([]);
  const toggleProductType = (id: string) => {
    setProductTypes((prev) =>
      prev.map((type) =>
        type.id === id ? { ...type, checked: !type.checked } : type,
      ),
    );
  };
  const filterProducts = (products: ProductDP[]) => {
    const filtered = products?.filter((product: ProductDP) => {
      if (product.__typename === "VariableProduct") {
        productPrice = parseFloat(product?.regularPrice.split(",").reverse()[0]);
      }else {
        productPrice = parseFloat(product?.regularPrice?.replace(/[^0-9]/g, ''));
      }
      const withinPriceRange = productPrice >= priceRange[0] && productPrice <= priceRange[1];
      if (!withinPriceRange) return false;
      const selectedTypes = productTypes.filter((t) => t.checked).map((t) => t.name.toLowerCase());
      if (selectedTypes.length > 0) {
        const productCategories = product.productCategories?.nodes.map((cat) => cat.name.toLowerCase()) || [];
        if (!selectedTypes.some((type) => productCategories.includes(type))) return false;
      }
      if (selectedSizes.length > 0) {
        const productSizes = product.allPaSizes?.nodes.map((node) => node.name) || [];
        if (!selectedSizes.some((size) => productSizes.includes(size))) return false;
      }
      if (selectedColors.length > 0) {
        const productColors =
          product.allPaColors?.nodes.map((node) => node.name) || [];
        if (!selectedColors.some((color) => productColors.includes(color))) return false;
      }
      return true;
    });
    return [...(filtered || [])].sort((a, b) => {
      return 0;
    })
  };
  return {
    selectedColors,
    setSelectedColors,
    selectedSizes,
    setSelectedSizes,
    productTypes,
    toggleProductType,
    filterProducts
  }
}

Step 5.4 Nghiên cứu cách sử dụng src\hooks\useProductFilters.ts bằng cách sử dụng một ví dụ test, setTest

Step 6. RangeSlider

Step 7. Sorting

src\hooks\useProductFilters.ts

import { getUniqueProductTypes } from '@/utils/functions/productUtils';
import { useState } from 'react';
export const useProductFilters = (products: ProductDP[]) => {
  let productPrice;
  const [sortBy, setSortBy] = useState('popular');
  const [priceRange, setPriceRange] = useState<[number, number]>([0, 1000]);
  const [productTypes, setProductTypes] = useState<ProductType[]>(() =>
    products ? getUniqueProductTypes(products) : [],
  );
  const [selectedSizes, setSelectedSizes] = useState<string[]>([]);
  const [selectedColors, setSelectedColors] = useState<string[]>([]);
  const toggleProductType = (id: string) => {
    setProductTypes((prev) =>
      prev.map((type) =>
        type.id === id ? { ...type, checked: !type.checked } : type,
      ),
    );
  };
  const filterProducts = (products: ProductDP[]) => {
    const filtered = products?.filter((product: ProductDP) => {
      if (product.__typename === "VariableProduct") {
        productPrice = parseFloat(product?.regularPrice?.split(",").reverse()[0]);
      }else {
        productPrice = parseFloat(product?.regularPrice?.replace(/[^0-9]/g, ''));
      }
      const withinPriceRange = productPrice >= priceRange[0] && productPrice <= priceRange[1];
      if (!withinPriceRange) return false;
      const selectedTypes = productTypes.filter((t) => t.checked).map((t) => t.name.toLowerCase());
      if (selectedTypes.length > 0) {
        const productCategories = product.productCategories?.nodes.map((cat) => cat.name.toLowerCase()) || [];
        if (!selectedTypes.some((type) => productCategories.includes(type))) return false;
      }
      if (selectedSizes.length > 0) {
        const productSizes = product.allPaSizes?.nodes.map((node) => node.name) || [];
        if (!selectedSizes.some((size) => productSizes.includes(size))) return false;
      }
      if (selectedColors.length > 0) {
        const productColors =
          product.allPaColors?.nodes.map((node) => node.name) || [];
        if (!selectedColors.some((color) => productColors.includes(color))) return false;
      }
      return true;
    });
    return [...(filtered || [])].sort((a, b) => {
      const priceA = parseFloat(a.price.replace(/[^0-9.]/g, ''));
      const priceB = parseFloat(b.price.replace(/[^0-9.]/g, ''));
      switch (sortBy) {
      case 'price-low':
        return priceA - priceB;
      case 'price-high':
        return priceB - priceA;
      case 'newest':
        return b.databaseId - a.databaseId;
      default:
        return 0;
      }
    });
  };
  return {
    sortBy, 
    setSortBy,
    priceRange,
    setPriceRange,
    selectedColors,
    setSelectedColors,
    selectedSizes,
    setSelectedSizes,
    productTypes,
    toggleProductType,
    filterProducts
  }
}

src\components\Product\ProductList.component.tsx

import { useProductFilters } from "@/hooks/useProductFilters";
import ProductCard from "./ProductCard.component";
import ProductFilters from "./ProductFilters.component";
const ProductList = ({ products, title }: ProductListProps) => {
  const { 
    productTypes, 
    toggleProductType, 
    filterProducts, 
    selectedSizes,
    setSelectedSizes,
    selectedColors,
    setSelectedColors,
    priceRange,
    setPriceRange,
    sortBy,
    setSortBy
  } = useProductFilters(products);
  const filteredProducts = filterProducts(products);
  return (
    <div className="flex flex-col md:flex-row gap-8">
      <ProductFilters
        selectedColors={selectedColors}
        setSelectedColors={setSelectedColors}
        selectedSizes={selectedSizes}
        setSelectedSizes={setSelectedSizes}
        productTypes={productTypes}
        toggleProductType={toggleProductType}
        products={products}
        priceRange={priceRange}
        setPriceRange={setPriceRange}
      />
      <div className="flex-1">
        <div className="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-8 bg-pink">
          <h1 className="text-xl sm:text-2xl font-medium text-center sm:text-left">
            {title} <span className="text-gray-500">({filteredProducts.length})</span>
          </h1>
          <div className="flex flex-wrap items-center justify-center sm:justify-end gap-2 sm:gap-4">
            <label htmlFor="sort-select" className="text-sm font-medium">Sorting:</label>
            <select
              id="sort-select"
              value={sortBy}
              onChange={(e) => setSortBy(e.target.value)}
              className="min-w-[140px] border rounded-md px-3 py-1.5 text-sm"
            >
              <option value="popular">Popular</option>
              <option value="price-low">Price: Low to High</option>
              <option value="price-high">Price: High to Low</option>
              <option value="newest">Newest</option>
            </select>
          </div>
        </div>
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
          {filteredProducts.map((product: ProductDP) => (
            <ProductCard
              key={product.databaseId}
              databaseId={product.databaseId}
              name={product.name}
              price={product.price}
              slug={product.slug}
              image={product.image}
            />
          ))}
        </div>
      </div>
    </div>
  )
}
export default ProductList;

Step 8 categories

src\pages\categories.tsx

import { NextPage, InferGetStaticPropsType, GetStaticProps } from 'next';
import Categories from '@/components/Category/Categories.component';
import Layout from '@/components/Layout/Layout.component';
import client from '@/utils/apollo/ApolloClient';
import { FETCH_ALL_CATEGORIES_QUERY } from '@/utils/gql/GQL_QUERIES';
/**
 * Category page displays all of the categories
 */
const Category: NextPage = ({
  categories,
}: InferGetStaticPropsType<typeof getStaticProps>) => (
  <Layout title="Categories">
    {categories && <Categories categories={categories} />}
  </Layout>
);
export default Category;
export const getStaticProps: GetStaticProps = async () => {
  const result = await client.query({
    query: FETCH_ALL_CATEGORIES_QUERY,
  });
  return {
    props: {
      categories: result.data.productCategories.nodes,
    },
    revalidate: 10,
  };
};

src\components\Category\Categories.component.tsx

import Link from 'next/link';
import { v4 as uuidv4 } from 'uuid';
const Categories = ({ categories }: ICategoriesProps) => (
  <section className="container mx-auto bg-white">
    <div className="grid gap-2 px-2 pt-2 pb-2 lg:px-0 xl:px-0 md:px-0 lg:grid-cols-3 sm:grid-cols-1 md:grid-cols-3 xs:grid-cols-3">
      {
        categories.map(({ id, name, slug }) => (
          <Link
            key={uuidv4()}
            href={`/category/${encodeURIComponent(slug)}?id=${encodeURIComponent(id)}`}
          >
            <div className="p-6 cursor-pointer">
              <div className="flex items-center justify-center w-full h-16 text-center border border-gray-300 rounded-lg shadow hover:shadow-outline">
                <p className="text-lg">{name}</p>
              </div>
            </div>
          </Link>
        ))}
    </div>
  </section>
);
export default Categories;

Step 9 Category Detail

src\pages\category\[slug].tsx

import { withRouter } from 'next/router';
import Layout from '@/components/Layout/Layout.component';
import DisplayProducts from '@/components/Product/DisplayProducts.component';
import client from '@/utils/apollo/ApolloClient';
import { GET_PRODUCTS_FROM_CATEGORY } from '@/utils/gql/GQL_QUERIES';
import { GetServerSideProps, InferGetServerSidePropsType } from 'next';
const Produkt = ({ categoryName, products}: InferGetServerSidePropsType<typeof getServerSideProps>) => {
  return (
    <Layout title={`${categoryName ? categoryName : ''}`}>
      {products ? (
        <DisplayProducts products={products} />
      ) : (
        <div className="mt-8 text-2xl text-center">Loading product ...</div>
      )}
    </Layout>
  );
};
export default withRouter(Produkt);
export const getServerSideProps: GetServerSideProps = async ({ query: { id }}) => {
  const res = await client.query({
    query: GET_PRODUCTS_FROM_CATEGORY,
    variables: { id }
  });
  return {
    props: {
      categoryName: res.data.productCategory.name,
      products: res.data.productCategory.products.nodes,
    },
  };
};

Source code gốc

Last updated

Was this helpful?