😍Step by Step Study (part 1)👆

Query

Step 1: Create layout, loading, FETCH_ALL_PROUDUCTS_QUERY

src\pages\index.tsx

import Layout from '@/components/Layout/Layout.component';
import LoadingSpinner from '@/components/LoadingSpinner/LoadingSpinner.component';
import client from '@/utils/apollo/ApolloClient';
import { FETCH_ALL_PRODUCTS_QUERY } from '@/utils/gql/GQL_QUERIES';
import { GetStaticProps, InferGetStaticPropsType } from 'next';
import { useEffect, useState } from 'react';
export default function Home({ products}: InferGetStaticPropsType<typeof getStaticProps>) {
  const [isLoading, setIsLoading] = useState<boolean>(true);
  useEffect(() => {
    setTimeout(()=> {
      setIsLoading(false);
    },1500);
  }, []);
  return (
    <Layout title="Lionel">
      {
        isLoading ? <LoadingSpinner /> : "Home1"
      }
    </Layout>
  );
}
export const getStaticProps: GetStaticProps = async () => {
  const { data, loading, networkStatus } = await client.query({query: FETCH_ALL_PRODUCTS_QUERY});
  return {
    props: {
      products: data.products.nodes,
      networkStatus,
    },
    revalidate: 60
  }
}

src\utils\gql\GQL_QUERIES.ts

import { gql } from '@apollo/client';
export const FETCH_ALL_PRODUCTS_QUERY = gql`
  query MyQuery {
    products(last: 8) {
      nodes {
        __typename
        databaseId
        description(format: RENDERED)
        image {
          __typename
          sourceUrl
        }
        onSale
        name
        slug
        ... on SimpleProduct {
          price
          salePrice
          regularPrice
        }
        ... on VariableProduct {
          price
          salePrice
          regularPrice
          productTypes {
            nodes {
              databaseId
              description
              name
              products {
                __typename
                nodes {
                  description
                  onSale
                  image {
                    __typename
                    mediaItemUrl
                  }
                  ... on SimpleProduct {
                    price
                    salePrice
                    regularPrice
                  }
                  ... on VariableProduct {
                    price
                    salePrice
                    regularPrice
                  }
                }
              }
            }
          }
        }
      }
    }
  }
`;

Step 2: Create DisplayProducts.component.tsx

src\pages\index.tsx

import Layout from '@/components/Layout/Layout.component';
import LoadingSpinner from '@/components/LoadingSpinner/LoadingSpinner.component';
import DisplayProducts from '@/components/Product/DisplayProducts.component';
import client from '@/utils/apollo/ApolloClient';
import { FETCH_ALL_PRODUCTS_QUERY } from '@/utils/gql/GQL_QUERIES';
import { GetStaticProps, InferGetStaticPropsType } from 'next';
import { useEffect, useState } from 'react';
export default function Home({ products}: InferGetStaticPropsType<typeof getStaticProps>) {
  const [isLoading, setIsLoading] = useState<boolean>(true);
  useEffect(() => {
    setTimeout(()=> {
      setIsLoading(false);
    },1500);
  }, []);
  return (
    <Layout title="Lionel">
      {
        isLoading ? <LoadingSpinner /> : <DisplayProducts products={products} />
      }
    </Layout>
  );
}
export const getStaticProps: GetStaticProps = async () => {
  const { data } = await client.query({query: FETCH_ALL_PRODUCTS_QUERY});
  return {
    props: {
      products: data.products.nodes
    },
    revalidate: 60
  }
}

src\components\Product\DisplayProducts.component.tsx

import Link from 'next/link';
import { v4 as uuidv4 } from 'uuid';
import { filteredVariantPrice, paddedPrice } from '@/utils/functions/functions';
interface Image {
  __typename: string;
  sourceUrl: string;
}
interface Node {
  __typename: string;
  price: string;
  regularPrice: string;
  salePrice?: string;
}
interface Variations {
  __typename: string;
  nodes: Node[];
}
interface RootObject {
  __typename: string;
  databaseId: number;
  name: string;
  slug: string;
  image: Image;
  price: string;
  regularPrice: string;
  salePrice?: string;
  onSale: boolean;
  variations: Variations;
}
interface IDisplayProductsProps {
  products: RootObject[];
}
const DisplayProducts = ({ products }: IDisplayProductsProps) => {
  return (
    <section className="container mx-auto bg-white py-12">
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-8">
        {
          products ? (products.map(({ databaseId, name, slug, image,price,regularPrice,salePrice,onSale,variations})=>{
            if (price) {
              price = paddedPrice(price, 'kr');
            }
            if (regularPrice) {
              regularPrice = paddedPrice(regularPrice, 'kr');
            }
            if (salePrice) {
              salePrice = paddedPrice(salePrice, 'kr');
            }
            return (
              <div key={uuidv4()} className="group">
                <Link
                  href={`/product/${encodeURIComponent(slug)}?id=${encodeURIComponent(databaseId)}`}
                >
                  <div className="aspect-[3/4] relative overflow-hidden bg-gray-100">
                    {image ? (
                      <img
                        id="product-image"
                        className="w-full h-full object-cover object-center transition duration-300 group-hover:scale-105"
                        alt={name}
                        src={image.sourceUrl}
                      />
                    ) : (
                      <img
                        id="product-image"
                        className="w-full h-full object-cover object-center transition duration-300 group-hover:scale-105"
                        alt={name}
                        src={process.env.NEXT_PUBLIC_PLACEHOLDER_SMALL_IMAGE_URL}
                      />
                    )}
                  </div>
                </Link>
              </div>
            )
          })) : (<div className="mx-auto text-xl font-bold text-center text-gray-800 no-underline uppercase"> Ingen produkter funnet </div>)
        }
      </div>
    </section>
  )
}
export default DisplayProducts;

Step 3: Create Single Product

src\pages\product[slug].tsx

import { withRouter } from 'next/router';
import type { NextPage, GetServerSideProps, InferGetServerSidePropsType } from 'next';
import Layout from '@/components/Layout/Layout.component';
import { GET_SINGLE_PRODUCT } from '@/utils/gql/GQL_QUERIES';
import client from '@/utils/apollo/ApolloClient';
import SingleProduct from '@/components/Product/SingleProduct.component';
const Product: NextPage = ({product,networkStatus}: InferGetServerSidePropsType<typeof getServerSideProps>) => {
  const hasError = networkStatus === '8';
  return (
    <Layout title={`${product.name ? product.name : ''}`}>
      {product ? (
        <SingleProduct product={product} />
      ) : (
          <div className="mt-8 text-2xl text-center">Loading product ...</div>
      )}
      {hasError && (
        <div className="mt-8 text-2xl text-center">
          Error loading product ...
        </div>
      )}
    </Layout>
  )
}
export default withRouter(Product);
export const getServerSideProps: GetServerSideProps = async ({ query: { id }}) => {
  const { data, networkStatus } = await client.query({
    query: GET_SINGLE_PRODUCT,
    variables: {id}
  });
  return {
    props: { product: data.product, networkStatus },
  };
}

src\components\Product\SingleProduct.component.tsx

import { useState, useEffect } from 'react';
import { filteredVariantPrice, paddedPrice } from '@/utils/functions/functions';
import LoadingSpinner from '../LoadingSpinner/LoadingSpinner.component';
interface IImage {
  __typename: string;
  uri: string;
  title: string;
  srcSet: string;
  sourceUrl: string;
}
interface IVariationNode {
  __typename: string;
  name: string;
}
interface IAllPaColors {
  __typename: string;
  nodes: IVariationNode[];
}
interface IAllPaSizes {
  __typename: string;
  nodes: IVariationNode[];
}
export interface IVariationNodes {
  __typename: string;
  id: string;
  databaseId: number;
  name: string;
  image?: string;
  stockStatus: string;
  stockQuantity: number;
  purchasable: boolean;
  onSale: boolean;
  salePrice?: string;
  regularPrice: string;
}
interface IVariations {
  __typename: string;
  nodes: IVariationNodes[];
}
export interface IProduct {
  __typename: string;
  databaseId: number;
  name: string;
  description: string;
  slug: string;
  averageRating: number;
  onSale: boolean;
  image: IImage;
  price: string;
  regularPrice: string;
  salePrice?: string;
  allPaColors?: IAllPaColors;
  allPaSizes?: IAllPaSizes;
  variations?: IVariations;
  stockQuantity: number;
}
export interface IProductRootObject {
  product: IProduct;
  variationId?: number;
  fullWidth?: boolean;
}
const _ = require("lodash");
const SingleProduct = ({ product }: IProductRootObject) => {
  let { description, image, name, onSale, price, regularPrice, salePrice } = product;
  const [isLoading, setIsLoading] = useState<boolean>(true);
  const [selectedVariation, setSelectedVariation] = useState<number>();
  const [imagedVariation, setImagedVariation] = useState<string>('');
  let DESCRIPTION_WITHOUT_HTML;
  if (typeof window !== 'undefined') {
    DESCRIPTION_WITHOUT_HTML = new DOMParser().parseFromString(
      description,
      'text/html',
    ).body.textContent;
  }
  if (price) {
    price = paddedPrice(price, '&nbsp;');
  }
  if (regularPrice) {
    regularPrice = paddedPrice(regularPrice, '&nbsp;');
  }
  if (salePrice) {
    salePrice = paddedPrice(salePrice, '&nbsp;');
  }
  useEffect(() => {
    setTimeout(() => {
      setIsLoading(false);
    }, 300);
    if (selectedVariation) {
      let indexF = _.findIndex(product.variations?.nodes, function (o: any) { return o.databaseId === selectedVariation; });
      var { sourceUrl }:any = product.variations?.nodes[indexF].image;
      setImagedVariation(sourceUrl);
    }
  }, [selectedVariation]);
  return (
    <section className="bg-white mb-[8rem] md:mb-12">
      {
        isLoading ? <LoadingSpinner /> : (
          <div className="container mx-auto px-4 py-8">
            <div className="flex flex-col md:grid md:grid-cols-2 md:gap-8">
              <div className="mb-6 md:mb-0 group">
                <div className="max-w-xl mx-auto aspect-[3/4] relative overflow-hidden bg-gray-100">
                  <img
                    src={
                      imagedVariation !== '' ? imagedVariation : (image?.sourceUrl || process.env.NEXT_PUBLIC_PLACEHOLDER_LARGE_IMAGE_URL)
                    }
                    alt={name}
                    className="w-full h-full object-cover object-center transition duration-300 group-hover:scale-105"
                  />
                </div>
              </div>
              <div className="flex flex-col">
                <h1 className="text-2xl font-bold text-center md:text-left mb-4">
                  {name}
                </h1>
                <div className="text-center md:text-left mb-6">
                  {
                    onSale ? (
                      <div className="flex flex-col md:flex-row items-center md:items-start gap-2">
                        <p className="text-2xl font-bold text-gray-900">
                          {product.variations ? filteredVariantPrice(price, 'right') : salePrice}
                        </p>
                        <p className="text-xl text-gray-500 line-through">
                          {product.variations ? filteredVariantPrice(price, 'right') : regularPrice}
                        </p>
                      </div>
                    ) :
                    (<p className="text-2xl font-bold">{price}</p>)
                  }
                </div>
                <p className="text-lg mb-6 text-center md:text-left">
                  {DESCRIPTION_WITHOUT_HTML}
                </p>
                {Boolean(product.stockQuantity) && (
                  <div className="mb-6 mx-auto md:mx-0">
                    <div className="p-2 bg-green-100 border border-green-400 rounded-lg max-w-[14.375rem]">
                      <p className="text-lg text-green-700 font-semibold text-center md:text-left">
                        {product.stockQuantity} in stock
                      </p>
                    </div>
                  </div>
                )}
                {product.variations && (
                  <div className="mb-6 mx-auto md:mx-0 w-full max-w-[14.375rem]">
                    <label htmlFor="variant" className="block text-lg font-medium mb-2 text-center md:text-left">
                      Variants
                    </label>
                    <select
                      id="variant"
                      name="variant"
                      className="w-full px-4 py-2 bg-white border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
                      onChange={(e) =>
                        setSelectedVariation(Number(e.target.value))
                      }
                    >
                      {product.variations.nodes.map(
                        ({ id, name, databaseId, stockQuantity }) => {
                          return (
                            <option key={id} value={databaseId}>
                              {name.split('- ').pop()} - ({stockQuantity} in stock)
                            </option>
                          )
                        },
                      )}
                    </select>
                  </div>
                )}
              </div>
            </div>
          </div>
        )
      }
    </section>
  );
}
export default SingleProduct;

src\utils\gql\GQL_QUERIES.ts

import { gql } from '@apollo/client';
export const FETCH_ALL_PRODUCTS_QUERY = gql`
  query MyQuery {
    products(last: 8) {
      nodes {
        __typename
        databaseId
        description(format: RENDERED)
        image {
          __typename
          sourceUrl
        }
        onSale
        name
        slug
        ... on SimpleProduct {
          price
          salePrice
          regularPrice
        }
        ... on VariableProduct {
          price
          salePrice
          regularPrice
          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
            }
          }
        }
      }
    }
  }
`;

Mutation

Step 4.1: Add to Carr

mutation {
  addToCart(input: {productId: 1786}) {
    cartItem {
      key
      product {
        node {
          id
          name
          description
          slug
          type
          onSale
          averageRating
          reviewCount
          image {
            altText
            sourceUrl
          }
        }
      }
    }
  }
}
{
  "data": {
    "addToCart": {
      "cartItem": {
        "key": "6449f44a102fde848669bdd9eb6b76fa",
        "product": {
          "node": {
            "id": "cHJvZHVjdDoxNzg2",
            "name": "Single",
            "description": "<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.</p>\n",
            "slug": "single",
            "type": "SIMPLE",
            "onSale": true,
            "averageRating": 0,
            "reviewCount": 0,
            "image": {
              "altText": "",
              "sourceUrl": "https://wpclidemo.dev/wp-content/uploads/2025/02/single-1.jpg"
            }
          }
        }
      }
    }
  }
}

Hoặc chỉ cần id là đủ mẫu như sau

mutation {
  addToCart(input: {productId: 1780}) {
    __typename
  }
}
https://wpclidemo.dev/wp-admin/post.php?post=1774&action=edit
https://wpclidemo.dev/product/hoodie/
mutation {
  addToCart(input: {productId: 1774  }) {
    cartItem {
      key
      product {
        node {
          id
          name
          description
          slug
          type
          onSale
          averageRating
          reviewCount
          image {
            altText
            sourceUrl
          }
          galleryImages { // thêm để lấy ảnh gallery
            nodes {
              sourceUrl
              altText
            }
          }
        }
      }
    }
  }
}
{
  "data": {
    "addToCart": {
      "cartItem": {
        "key": "f0bda020d2470f2e74990a07a607ebd9",
        "product": {
          "node": {
            "id": "cHJvZHVjdDoxNzc0",
            "name": "Hoodie",
            "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",
            "slug": "hoodie",
            "type": "VARIABLE",
            "onSale": true,
            "averageRating": 0,
            "reviewCount": 0,
            "image": {
              "altText": "",
              "sourceUrl": "https://wpclidemo.dev/wp-content/uploads/2025/02/hoodie-2.jpg"
            },
            "galleryImages": {
              "nodes": [
                {
                  "sourceUrl": "https://wpclidemo.dev/wp-content/uploads/2025/02/hoodie-blue-1.jpg",
                  "altText": ""
                },
                {
                  "sourceUrl": "https://wpclidemo.dev/wp-content/uploads/2025/02/hoodie-green-1.jpg",
                  "altText": ""
                },
                {
                  "sourceUrl": "https://wpclidemo.dev/wp-content/uploads/2025/02/hoodie-with-logo-2.jpg",
                  "altText": ""
                }
              ]
            }
          }
        }
      }
    }
  }
}

Một trường hợp

mutation {
  addToCart(input: {productId:7}) {
    cartItem {
        key
        product {
          node {
            id
            databaseId
            name
            description
            type
            onSale
            slug
            averageRating
            reviewCount
            image {
              id
              sourceUrl
              altText
            }
            galleryImages {
              nodes {
                id
                sourceUrl
                altText
              }
            }
          }
        }
        variation {
          node {
            id
            databaseId
            name
            description
            type
            onSale
            price
            regularPrice
            salePrice
            image {
              id
              sourceUrl
              altText
            }
            attributes {
              nodes {
                id
                attributeId
                name
                value
              }
            }
          }
        }
        quantity
        total
        subtotal
        subtotalTax
      }
  }
}
{
  "data": {
    "addToCart": {
      "cartItem": {
        "key": "8f14e45fceea167a5a36dedd4bea2543",
        "product": {
          "node": {
            "id": "cHJvZHVjdDo3",
            "databaseId": 7,
            "name": "Hoodie",
            "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",
            "type": "VARIABLE",
            "onSale": true,
            "slug": "hoodie-2",
            "averageRating": 0,
            "reviewCount": 0,
            "image": {
              "id": "cG9zdDozNA==",
              "sourceUrl": "https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-2.jpg",
              "altText": ""
            },
            "galleryImages": {
              "nodes": [
                {
                  "id": "cG9zdDozNQ==",
                  "sourceUrl": "https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-blue-1.jpg",
                  "altText": ""
                },
                {
                  "id": "cG9zdDozNg==",
                  "sourceUrl": "https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-green-1.jpg",
                  "altText": ""
                },
                {
                  "id": "cG9zdDozNw==",
                  "sourceUrl": "https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-with-logo-2.jpg",
                  "altText": ""
                }
              ]
            }
          }
        },
        "variation": null,
        "quantity": 1,
        "total": "42 ₫",
        "subtotal": "42 ₫",
        "subtotalTax": "0 ₫"
      }
    }
  }
}

— Cách mua biến thể bằng variationId ok

mutation {
  addToCart(input: {productId: 7, variationId: 30}) {
    cartItem {
      key
      product {
        node {
          id
          databaseId
          name
          description
          type
          onSale
          slug
          averageRating
          reviewCount
          image {
            id
            sourceUrl
            altText
          }
          galleryImages {
            nodes {
              id
              sourceUrl
              altText
            }
          }
        }
      }
      variation {
        node {
          id
        }
      }
      quantity
      total
      subtotal
      subtotalTax
    }
  }
}
{
  "data": {
    "addToCart": {
      "cartItem": {
        "key": "5a35fb08d551fb9cff28454e4208d9ac",
        "product": {
          "node": {
            "id": "cHJvZHVjdDo3",
            "databaseId": 7,
            "name": "Hoodie",
            "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",
            "type": "VARIABLE",
            "onSale": false,
            "slug": "hoodie-2",
            "averageRating": 0,
            "reviewCount": 0,
            "image": {
              "id": "cG9zdDozNA==",
              "sourceUrl": "https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-2.jpg",
              "altText": ""
            },
            "galleryImages": {
              "nodes": [
                {
                  "id": "cG9zdDozNQ==",
                  "sourceUrl": "https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-blue-1.jpg",
                  "altText": ""
                },
                {
                  "id": "cG9zdDozNg==",
                  "sourceUrl": "https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-green-1.jpg",
                  "altText": ""
                },
                {
                  "id": "cG9zdDozNw==",
                  "sourceUrl": "https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-with-logo-2.jpg",
                  "altText": ""
                }
              ]
            }
          }
        },
        "variation": {
          "node": {
            "id": "cHJvZHVjdF92YXJpYXRpb246MzA="
          }
        },
        "quantity": 1,
        "total": "45 ₫",
        "subtotal": "45 ₫",
        "subtotalTax": "0 ₫"
      }
    }
  }
}

Kết quả:

Mua kèm theo số lượng

mutation {
  addToCart(input: {productId: 7, variationId: 25, quantity: 5}) {
    cartItem {
      key
      product {
        node {
          id
          databaseId
          name
          description
          type
          onSale
          slug
          averageRating
          reviewCount
          image {
            id
            sourceUrl
            altText
          }
          galleryImages {
            nodes {
              id
              sourceUrl
              altText
            }
          }
        }
      }
      variation {
        node {
          id
        }
      }
      quantity
      total
      subtotal
      subtotalTax
    }
  }
}
{
  "data": {
    "addToCart": {
      "cartItem": {
        "key": "257eacf4555ef0dc335708114ce2d06e",
        "product": {
          "node": {
            "id": "cHJvZHVjdDo3",
            "databaseId": 7,
            "name": "Hoodie",
            "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",
            "type": "VARIABLE",
            "onSale": false,
            "slug": "hoodie-2",
            "averageRating": 0,
            "reviewCount": 0,
            "image": {
              "id": "cG9zdDozNA==",
              "sourceUrl": "https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-2.jpg",
              "altText": ""
            },
            "galleryImages": {
              "nodes": [
                {
                  "id": "cG9zdDozNQ==",
                  "sourceUrl": "https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-blue-1.jpg",
                  "altText": ""
                },
                {
                  "id": "cG9zdDozNg==",
                  "sourceUrl": "https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-green-1.jpg",
                  "altText": ""
                },
                {
                  "id": "cG9zdDozNw==",
                  "sourceUrl": "https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-with-logo-2.jpg",
                  "altText": ""
                }
              ]
            }
          }
        },
        "variation": {
          "node": {
            "id": "cHJvZHVjdF92YXJpYXRpb246MjU="
          }
        },
        "quantity": 5,
        "total": "225 ₫",
        "subtotal": "225 ₫",
        "subtotalTax": "0 ₫"
      }
    }
  }
}

— Cách mua biến thể bằng variation not ok không hiểu tại sao?

Step 4.2 Get Cart

query GET_CART {
  cart {
    contents {
      nodes {
        product {
          node {
            databaseId
              name
              description
              type
              onSale
              slug
              averageRating
              reviewCount
              image {
                id
                sourceUrl
                srcSet
                altText
                title
              }
              galleryImages {
                nodes {
                  id
                  sourceUrl
                  srcSet
                  altText
                  title
                }
              }
          }
        }
      }
    }
  }
}
{
  "data": {
    "cart": {
      "contents": {
        "nodes": [
          {
            "product": {
              "node": {
                "databaseId": 7,
                "name": "Hoodie",
                "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",
                "type": "VARIABLE",
                "onSale": false,
                "slug": "hoodie-2",
                "averageRating": 0,
                "reviewCount": 0,
                "image": {
                  "id": "cG9zdDozNA==",
                  "sourceUrl": "https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-2.jpg",
                  "srcSet": "https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-2-300x300.jpg 300w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-2-150x150.jpg 150w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-2-768x768.jpg 768w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-2-324x324.jpg 324w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-2-416x416.jpg 416w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-2-100x100.jpg 100w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-2.jpg 801w",
                  "altText": "",
                  "title": "hoodie-2.jpg"
                },
                "galleryImages": {
                  "nodes": [
                    {
                      "id": "cG9zdDozNQ==",
                      "sourceUrl": "https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-blue-1.jpg",
                      "srcSet": "https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-blue-1-300x300.jpg 300w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-blue-1-150x150.jpg 150w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-blue-1-768x768.jpg 768w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-blue-1-324x324.jpg 324w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-blue-1-416x416.jpg 416w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-blue-1-100x100.jpg 100w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-blue-1.jpg 800w",
                      "altText": "",
                      "title": "hoodie-blue-1.jpg"
                    },
                    {
                      "id": "cG9zdDozNg==",
                      "sourceUrl": "https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-green-1.jpg",
                      "srcSet": "https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-green-1-300x300.jpg 300w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-green-1-150x150.jpg 150w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-green-1-768x768.jpg 768w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-green-1-324x324.jpg 324w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-green-1-416x416.jpg 416w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-green-1-100x100.jpg 100w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-green-1.jpg 800w",
                      "altText": "",
                      "title": "hoodie-green-1.jpg"
                    },
                    {
                      "id": "cG9zdDozNw==",
                      "sourceUrl": "https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-with-logo-2.jpg",
                      "srcSet": "https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-with-logo-2-300x300.jpg 300w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-with-logo-2-150x150.jpg 150w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-with-logo-2-768x768.jpg 768w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-with-logo-2-324x324.jpg 324w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-with-logo-2-416x416.jpg 416w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-with-logo-2-100x100.jpg 100w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-with-logo-2.jpg 801w",
                      "altText": "",
                      "title": "hoodie-with-logo-2.jpg"
                    }
                  ]
                }
              }
            }
          },
          {
            "product": {
              "node": {
                "databaseId": 7,
                "name": "Hoodie",
                "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",
                "type": "VARIABLE",
                "onSale": false,
                "slug": "hoodie-2",
                "averageRating": 0,
                "reviewCount": 0,
                "image": {
                  "id": "cG9zdDozNA==",
                  "sourceUrl": "https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-2.jpg",
                  "srcSet": "https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-2-300x300.jpg 300w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-2-150x150.jpg 150w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-2-768x768.jpg 768w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-2-324x324.jpg 324w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-2-416x416.jpg 416w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-2-100x100.jpg 100w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-2.jpg 801w",
                  "altText": "",
                  "title": "hoodie-2.jpg"
                },
                "galleryImages": {
                  "nodes": [
                    {
                      "id": "cG9zdDozNQ==",
                      "sourceUrl": "https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-blue-1.jpg",
                      "srcSet": "https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-blue-1-300x300.jpg 300w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-blue-1-150x150.jpg 150w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-blue-1-768x768.jpg 768w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-blue-1-324x324.jpg 324w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-blue-1-416x416.jpg 416w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-blue-1-100x100.jpg 100w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-blue-1.jpg 800w",
                      "altText": "",
                      "title": "hoodie-blue-1.jpg"
                    },
                    {
                      "id": "cG9zdDozNg==",
                      "sourceUrl": "https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-green-1.jpg",
                      "srcSet": "https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-green-1-300x300.jpg 300w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-green-1-150x150.jpg 150w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-green-1-768x768.jpg 768w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-green-1-324x324.jpg 324w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-green-1-416x416.jpg 416w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-green-1-100x100.jpg 100w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-green-1.jpg 800w",
                      "altText": "",
                      "title": "hoodie-green-1.jpg"
                    },
                    {
                      "id": "cG9zdDozNw==",
                      "sourceUrl": "https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-with-logo-2.jpg",
                      "srcSet": "https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-with-logo-2-300x300.jpg 300w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-with-logo-2-150x150.jpg 150w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-with-logo-2-768x768.jpg 768w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-with-logo-2-324x324.jpg 324w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-with-logo-2-416x416.jpg 416w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-with-logo-2-100x100.jpg 100w, https://wpclidemo.dev/wp-content/uploads/2019/01/hoodie-with-logo-2.jpg 801w",
                      "altText": "",
                      "title": "hoodie-with-logo-2.jpg"
                    }
                  ]
                }
              }
            }
          }
        ]
      }
    }
  }
}

Step 4.3 giải thích cách tạo cart không cần đăng nhập

— Nếu mà chưa đăng nhập sử dụng addToCart thì khi sử dụng getCart sẽ không có 😒

— Do đó để tạo cart thì chúng ta sẽ tạo một cart localStorage

---- tạo một handleAddToCart

const [addToCart, { loading: addToCartLoading }] = useMutation(ADD_TO_CART, {
    variables: {
      input: productQueryInput,
    },
    onCompleted: () => {
      refetch();
    },
    onError: (err) => {
      console.log(err)
      setRequestError(true);
    }
  });
  const handleAddToCart = () => {
    addToCart();
    refetch();
  };
  return(
    <>
     <Button 
        handleButtonClick={() => handleAddToCart()}
        buttonDisabled={addToCartLoading || requestError || isCartLoading}
        fullWidth={fullWidth}
     >
        {isCartLoading ? 'Loading...' : 'BUY'}
     </Button>
    </>
  )

---- sử dụng hàm refetch() để load lại useQuery

Chú ý: Mặc định hàm useQuery nó sẽ hoạt động mỗi lần render

const {data,refetch} = useQuery(GET_CART,{
    notifyOnNetworkStatusChange: true,
    onCompleted: () => {
      console.log(data);
      const updatedCart = getFormattedCart(data);
      if (!updatedCart) {
        return;
      }
      console.log(updatedCart);
      localStorage.setItem('woocommerce-cart', JSON.stringify(updatedCart));
      // Update cart data in React Context.
      setCart(updatedCart);
    },
    onError: () => {
      console.log("onError1");
    }
});

— prop buttonDisabled này rất quan trọng nó giúp hoạt động thêm được vào giỏ hàng khi đã được sẵn sàng không sảy ra lỗi

return(
    <>
     <Button 
        handleButtonClick={() => handleAddToCart()}
        buttonDisabled={addToCartLoading || requestError || isCartLoading}
        fullWidth={fullWidth}
     >
        {isCartLoading ? 'Loading...' : 'BUY'}
     </Button>
    </>
 )

Toàn bộ code src\components\Product\AddToCart.component.tsx

import { useContext, useState } from 'react';
import { useQuery, useMutation } from '@apollo/client';
import { v4 as uuidv4 } from 'uuid';
import { ADD_TO_CART } from '@/utils/gql/GQL_MUTATIONS';
import Button from '@/components/UI/Button.component';
import { GET_CART } from '@/utils/gql/GQL_QUERIES';
import { getFormattedCart } from '@/utils/functions/functions';
import { CartContext } from '@/stores/CartProvider';
const AddToCart = ({ product, variationId, fullWidth = false}: IProductRootObject) => {
  const { setCart, isLoading: isCartLoading } = useContext(CartContext);
  const [requestError, setRequestError] = useState<boolean>(false);
  const productId = product?.databaseId ? product?.databaseId : variationId;
  const productQueryInput = {
    clientMutationId: uuidv4(),
    productId,
  };
  const {data,refetch} = useQuery(GET_CART,{
    notifyOnNetworkStatusChange: true,
    onCompleted: () => {
      console.log(data);
      const updatedCart = getFormattedCart(data);
      if (!updatedCart) {
        return;
      }
      localStorage.setItem('woocommerce-cart', JSON.stringify(updatedCart));
      // Update cart data in React Context.
      setCart(updatedCart);
    },
    onError: (err) => {
      console.log(err);
    }
  });
  const [addToCart, { loading: addToCartLoading }] = useMutation(ADD_TO_CART, {
    variables: {
      input: productQueryInput,
    },
    onCompleted: () => {
      refetch();
    },
    onError: (err) => {
      console.log(err)
      setRequestError(true);
    }
  });
  const handleAddToCart = () => {
    addToCart();
    refetch();
  };
  return(
    <>
     <Button 
        handleButtonClick={() => handleAddToCart()}
        buttonDisabled={addToCartLoading || requestError || isCartLoading}
        fullWidth={fullWidth}
     >
        {isCartLoading ? 'Loading...' : 'BUY'}
     </Button>
    </>
  )
}
export default AddToCart;

--— Và một điều nữa khi client hoạt động nó sẽ tạo ra một key : woocommerce-session để làm việc với session

Toàn bộ code src\utils\apollo\ApolloClient.js

import { ApolloLink, ApolloClient,createHttpLink,InMemoryCache} from '@apollo/client';
const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1000;
export const middleware = new ApolloLink((operation, forward) => {
  const sessionData = typeof window !== 'undefined' ? JSON.parse(localStorage.getItem('woo-session')) : null;
  if (sessionData && sessionData.token && sessionData.createdTime) {
    const { token, createdTime } = sessionData;
    if (Date.now() - createdTime > SEVEN_DAYS) {
      localStorage.removeItem('woo-session');
      localStorage.setItem('woocommerce-cart', JSON.stringify({}));
    }else {
      operation.setContext(() => ({
        headers: {
          'woocommerce-session': `Session ${token}`,
        },
      }))
    }
  }
  return forward(operation);
});
export const afterware = new ApolloLink((operation, forward) => 
  forward(operation).map((response) => {
    const context = operation.getContext();
    const {response: { headers }} = context;
    const session = headers.get('woocommerce-session');
    if (session && typeof window !== 'undefined') {
      if ('false' === session) {
        localStorage.removeItem('woo-session');
      } else if (!localStorage.getItem('woo-session')) {
        localStorage.setItem('woo-session',JSON.stringify({ token: session, createdTime: Date.now() }));
      }
    }
    return response;
  })
);
const clientSide = typeof window === 'undefined';
const client = new ApolloClient({
  ssrMode: clientSide,
  link: middleware.concat(
    afterware.concat(
      createHttpLink({
        uri: process.env.NEXT_PUBLIC_GRAPHQL_URL,
        fetch,
      })
    )
  ),
  cache: new InMemoryCache(),
});
export default client;

--— Class này giúp ta sử dụng cart trong cache : new InMemoryCache()

Step 5.1 Update Quantyti Cart

mutation {
  addToCart(input: {productId: 11}) {
    cartItem {
      key
      product {
        node {
          id
          databaseId
          name
          description
          type
          onSale
          slug
          averageRating
          reviewCount
          image {
            id
            sourceUrl
            altText
          }
          galleryImages {
            nodes {
              id
              sourceUrl
              altText
            }
          }
        }
      }
      variation {
        node {
          id
          databaseId
          name
          description
          type
          onSale
          price
          regularPrice
          salePrice
          image {
            id
            sourceUrl
            altText
          }
          attributes {
            nodes {
              id
              attributeId
              name
              value
            }
          }
        }
      }
      quantity
      total
      subtotal
      subtotalTax
    }
  }
}
{
  "data": {
    "addToCart": {
      "cartItem": {
        "key": "6512bd43d9caa6e02c990b0a82652dca",
        "product": {
          "node": {
            "id": "cHJvZHVjdDoxMQ==",
            "databaseId": 11,
            "name": "Belt",
            "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",
            "type": "SIMPLE",
            "onSale": true,
            "slug": "belt-2",
            "averageRating": 0,
            "reviewCount": 0,
            "image": {
              "id": "cG9zdDo0MA==",
              "sourceUrl": "https://wpclidemo.dev/wp-content/uploads/2019/01/belt-2.jpg",
              "altText": ""
            },
            "galleryImages": {
              "nodes": []
            }
          }
        },
        "variation": null,
        "quantity": 1,
        "total": "55 ₫",
        "subtotal": "55 ₫",
        "subtotalTax": "0 ₫"
      }
    }
  }
}

— Update Quantyti Cart trường hợp đăng nhập

mutation {
  updateItemQuantities(
    input: {items: {key: "6512bd43d9caa6e02c990b0a82652dca", quantity: 10}}
  ) {
    cart {
      contents {
        nodes {
          product {
            node {
              content
            }
          }
          quantity
        }
      }
    }
  }
}
{
  "data": {
    "updateItemQuantities": {
      "cart": {
        "contents": {
          "nodes": [
            {
              "product": {
                "node": {
                  "content": "<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"
                }
              },
              "quantity": 10
            }
          ]
        }
      }
    }
  }
}

src\pages\shopping-cart.tsx

import Layout from '@/components/Layout/Layout.component';
import CartContents from '@/components/Cart/CartContents.component';
import { NextPage } from 'next';
const ShopCart: NextPage = () => (
  <Layout title="Shop Cart">
    <CartContents />
  </Layout>
);
export default ShopCart;

src\components\Cart\CartContents.component.tsx

import { useContext, useEffect } from 'react';
import { useQuery, useMutation } from '@apollo/client';
import Link from 'next/link';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { v4 as uuidv4 } from 'uuid';
import { CartContext } from '@/stores/CartProvider';
import Button from '@/components/UI/Button.component';
import LoadingSpinner from '../LoadingSpinner/LoadingSpinner.component';
import { GET_CART } from '@/utils/gql/GQL_QUERIES';
import { getFormattedCart, getUpdatedItems, handleQuantityChange } from '@/utils/functions/functions';
import { UPDATE_CART } from '@/utils/gql/GQL_MUTATIONS';
const CartContents = () => {
  const router = useRouter();
  const { setCart } = useContext(CartContext);
  const isCheckoutPage = router.pathname === '/checkout';
  const { data, refetch } = useQuery(GET_CART, {
    notifyOnNetworkStatusChange: true,
    onCompleted: () => {
      const updatedCart = getFormattedCart(data);
      if (!updatedCart && !data.cart.contents.nodes.length) {
        localStorage.removeItem('woocommerce-cart');
        setCart(null);
        return;
      }
      localStorage.setItem('woocommerce-cart', JSON.stringify(updatedCart));
      setCart(updatedCart);
    },
  });
  const [updateCart, { loading: updateCartProcessing }] = useMutation(
    UPDATE_CART,
    {
      onCompleted: () => {
        refetch();
        setTimeout(() => {
          refetch();
        }, 3000);
      },
    },
  );
  const handleRemoveProductClick = (
    cartKey: string,
    products: IProductUpdateRootObject[],
  ) => {
    if (products?.length) {
      const updatedItems = getUpdatedItems(products, 0, cartKey);
      updateCart({
        variables: {
          input: {
            clientMutationId: uuidv4(),
            items: updatedItems,
          },
        },
      });
    }
    refetch();
    setTimeout(() => {
      refetch();
    }, 3000);
  };
  useEffect(() => {
    refetch();
  }, [refetch]);
  const cartTotal = data?.cart?.total || '0';
  const getUnitPrice = (subtotal: string, quantity: number) => {
    const numericSubtotal = parseFloat(subtotal.replace(/[^0-9.-]+/g, ''));
    return isNaN(numericSubtotal) ? 'N/A' : (numericSubtotal / quantity).toFixed(2);
  };
  return (
    <div className="container mx-auto px-4 py-8">
      {
        data?.cart?.contents?.nodes?.length ? (
          <>
            <div className=" rounded-lg p-6 mb-8 md:w-full">
              {
                data.cart.contents.nodes.map((item: IProductCartRootObject,index:number) => {
                  return (
                    <div key={index} className="flex items-center border-b border-gray-200 py-4">
                      <div className="flex-shrink-0 w-24 h-24 relative hidden md:block">
                        <Image
                          src={item.product.node.image?.sourceUrl}
                          alt={item.product.node.name}
                          layout="fill"
                          objectFit="cover"
                          className="rounded"
                        />
                      </div>
                      <div className="flex-grow ml-4">
                        <h2 className="text-lg font-semibold">
                          {item.product.node.name}
                        </h2>
                        <p className="text-gray-600">
                          {getUnitPrice(item.subtotal, item.quantity)} đ
                        </p>
                      </div>
                      <div className="flex items-center">
                        <input
                          type="number"
                          min="1"
                          value={item.quantity}
                          onChange={(event) => {
                            handleQuantityChange(event,item.key,data.cart.contents.nodes,updateCart,updateCartProcessing);
                          }}
                          className="w-16 px-2 py-1 text-center border border-gray-300 rounded mr-2"
                        />
                        <Button
                          handleButtonClick={() =>
                            handleRemoveProductClick(item.key,data.cart.contents.nodes)
                          }
                          variant="secondary"
                          buttonDisabled={updateCartProcessing}
                        >
                          Delete
                        </Button>
                      </div>
                    </div>
                  )
                })
              }
            </div>
            <div className="rounded-lg p-6 md:w-full">
              A2
            </div>
          </>
        ) : (
          <div className="text-center">
            <h2 className="text-2xl font-bold mb-4">
              No products in the cart.
            </h2>
            <Link href="/shop" passHref>
              <Button variant="primary">Continue shopping</Button>
            </Link>
          </div >
        )
      }
    </div >
  );
}
export default CartContents;
ty

src\utils\functions\functions.ts

import { ChangeEvent } from "react";
import { v4 as uuidv4 } from 'uuid';
export const paddedPrice = (price: string, symbol: string) => price.split(symbol).join(` `);
export const filteredVariantPrice = (price: string, side: string) => {
  if ('right' === side) {
    return price.substring(price.length, price.indexOf('-')).replace('-', '');
  }
  return price.substring(0, price.indexOf('-')).replace('-', '');
};
export const getFormattedCart = (data: any) => {
  const formattedCart: any = {
    products: [],
    totalProductsCount: 0,
    totalProductsPrice: 0,
  };
  if (!data) {
    return;
  }
  const givenProducts = data.cart.contents.nodes;
  // Create an empty object.
  formattedCart.products = [];
  const product: Product = {
    productId: 0,
    cartKey: '',
    name: '',
    qty: 0,
    price: 0,
    totalPrice: '0',
    image: { sourceUrl: '', srcSet: '', title: '' },
  };
  let totalProductsCount = 0;
  let i = 0;
  if (!givenProducts.length) {
    return;
  }
  givenProducts.forEach(() => {
    const givenProduct = givenProducts[Number(i)].product.node;
    // Convert price to a float value
    const convertedCurrency = givenProducts[Number(i)].total.replace(/[^0-9.-]+/g,'');
    product.productId = givenProduct.productId;
    product.cartKey = givenProducts[Number(i)].key;
    product.name = givenProduct.name;
    product.qty = givenProducts[Number(i)].quantity;
    product.price = Number(convertedCurrency) / product.qty;
    product.totalPrice = givenProducts[Number(i)].total;
    // Ensure we can add products without images to the cart
    product.image = givenProduct.image.sourceUrl
      ? { sourceUrl: givenProduct.image.sourceUrl, srcSet: givenProduct.image.srcSet, title: givenProduct.image.title, }
      : { sourceUrl: process.env.NEXT_PUBLIC_PLACEHOLDER_SMALL_IMAGE_URL, srcSet: process.env.NEXT_PUBLIC_PLACEHOLDER_SMALL_IMAGE_URL, title: givenProduct.name };
    totalProductsCount += givenProducts[Number(i)].quantity;
    // Push each item into the products array.
    formattedCart.products.push(product);
    i++;
  });
  formattedCart.totalProductsCount = totalProductsCount;
  formattedCart.totalProductsPrice = data.cart.total;
  return formattedCart;
};
export const getUpdatedItems = (products: IProductUpdateRootObject[], newQty: number, cartKey: string,) => {
  // Create an empty array.
  const updatedItems: TUpdatedItems = [];
  // Loop through the product array.
  products.forEach((cartItem) => {
    // If you find the cart key of the product user is trying to update, push the key and new qty.
    if (cartItem.key === cartKey) {
      updatedItems.push({
        key: cartItem.key,
        quantity: newQty,
      });
      // Otherwise just push the existing qty without updating.
    } else {
      updatedItems.push({
        key: cartItem.key,
        quantity: cartItem.quantity,
      });
    }
  });
  // Return the updatedItems array with new Qtys.
  return updatedItems;
};
export const handleQuantityChange = (
  event: ChangeEvent<HTMLInputElement>,
  cartKey: string,
  cart: IProductUpdateRootObject[],
  updateCart: (variables: IUpdateCartRootObject) => void,
  updateCartProcessing: boolean,
) => {
  if (typeof window !== 'undefined') {
    event.stopPropagation();
    // Return if the previous update cart mutation request is still processing
    if (updateCartProcessing || !cart) {
      return;
    }
    // If the user tries to delete the count of product, set that to 1 by default ( This will not allow him to reduce it less than zero )
    const newQty = event.target.value ? parseInt(event.target.value, 10) : 1;
    if (cart.length) {
      const updatedItems = getUpdatedItems(cart, newQty, cartKey);
      updateCart({
        variables: {
          input: {
            clientMutationId: uuidv4(),
            items: updatedItems,
          },
        },
      });
    }
  }
};

src\utils\gql\GQL_MUTATIONS.ts

import { gql } from "@apollo/client";
export const ADD_TO_CART = gql`
  mutation ($input: AddToCartInput!) {
    addToCart(input: $input) {
      cartItem {
        key
        product {
          node {
            id
            databaseId
            name
            description
            type
            onSale
            slug
            averageRating
            reviewCount
            image {
              id
              sourceUrl
              altText
            }
            galleryImages {
              nodes {
                id
                sourceUrl
                altText
              }
            }
          }
        }
        variation {
          node {
            id
            databaseId
            name
            description
            type
            onSale
            price
            regularPrice
            salePrice
            image {
              id
              sourceUrl
              altText
            }
            attributes {
              nodes {
                id
                attributeId
                name
                value
              }
            }
          }
        }
        quantity
        total
        subtotal
        subtotalTax
      }
    }
  }
`;
export const UPDATE_CART = gql`
  mutation ($input: UpdateItemQuantitiesInput!) {
    updateItemQuantities(input: $input) {
      items {
        key
        product {
          node {
            id
            databaseId
            name
            description
            type
            onSale
            slug
            averageRating
            reviewCount
            image {
              id
              sourceUrl
              altText
            }
            galleryImages {
              nodes {
                id
                sourceUrl
                altText
              }
            }
          }
        }
        variation {
          node {
            id
            databaseId
            name
            description
            type
            onSale
            price
            regularPrice
            salePrice
            image {
              id
              sourceUrl
              altText
            }
            attributes {
              nodes {
                id
                attributeId
                name
                value
              }
            }
          }
        }
        quantity
        total
        subtotal
        subtotalTax
      }
      updated {
        key
        product {
          node {
            id
            databaseId
          }
        }
        variation {
          node {
            id
            databaseId
          }
        }
      }
    }
  }
`;

Step 5.2 Subtotal && Link Checkout

src\components\Cart\CartContents.component.tsx

import { useContext, useEffect } from 'react';
import { useQuery, useMutation } from '@apollo/client';
import Link from 'next/link';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { v4 as uuidv4 } from 'uuid';
import { CartContext } from '@/stores/CartProvider';
import Button from '@/components/UI/Button.component';
import LoadingSpinner from '../LoadingSpinner/LoadingSpinner.component';
import { GET_CART } from '@/utils/gql/GQL_QUERIES';
import { getFormattedCart, getUpdatedItems, handleQuantityChange, paddedPrice } from '@/utils/functions/functions';
import { UPDATE_CART } from '@/utils/gql/GQL_MUTATIONS';
const CartContents = () => {
  const router = useRouter();
  const { setCart } = useContext(CartContext);
  const isCheckoutPage = router.pathname === '/checkout';
  const { data, refetch } = useQuery(GET_CART, {
    notifyOnNetworkStatusChange: true,
    onCompleted: () => {
      const updatedCart = getFormattedCart(data);
      if (!updatedCart && !data.cart.contents.nodes.length) {
        localStorage.removeItem('woocommerce-cart');
        setCart(null);
        return;
      }
      localStorage.setItem('woocommerce-cart', JSON.stringify(updatedCart));
      setCart(updatedCart);
    },
  });
  const [updateCart, { loading: updateCartProcessing }] = useMutation(
    UPDATE_CART,
    {
      onCompleted: () => {
        refetch();
        setTimeout(() => {
          refetch();
        }, 3000);
      },
    },
  );
  const handleRemoveProductClick = (
    cartKey: string,
    products: IProductUpdateRootObject[],
  ) => {
    if (products?.length) {
      const updatedItems = getUpdatedItems(products, 0, cartKey);
      updateCart({
        variables: {
          input: {
            clientMutationId: uuidv4(),
            items: updatedItems,
          },
        },
      });
    }
    refetch();
    setTimeout(() => {
      refetch();
    }, 3000);
  };
  useEffect(() => {
    refetch();
  }, [refetch]);
  let cartTotal = data?.cart?.total || '0';
  if (cartTotal) {
    cartTotal = paddedPrice(cartTotal, '&nbsp;');
  }
  const getUnitPrice = (subtotal: string, quantity: number) => {
    const numericSubtotal = parseFloat(subtotal.replace(/[^0-9.-]+/g, ''));
    return isNaN(numericSubtotal) ? 'N/A' : (numericSubtotal / quantity).toFixed(2);
  };
  return (
    <div className="container mx-auto px-4 py-8">
      {
        data?.cart?.contents?.nodes?.length ? (
          <>
            <div className=" rounded-lg p-6 mb-8 md:w-full">
              {
                data.cart.contents.nodes.map((item: IProductCartRootObject,index:number) => {
                  return (
                    <div key={index} className="flex items-center border-b border-gray-200 py-4">
                      <div className="flex-shrink-0 w-24 h-24 relative hidden md:block">
                        <Image
                          src={item.product.node.image?.sourceUrl}
                          alt={item.product.node.name}
                          layout="fill"
                          objectFit="cover"
                          className="rounded"
                        />
                      </div>
                      <div className="flex-grow ml-4">
                        <h2 className="text-lg font-semibold">
                          {item.product.node.name}
                        </h2>
                        <p className="text-gray-600">
                          {getUnitPrice(item.subtotal, item.quantity)} đ
                        </p>
                      </div>
                      <div className="flex items-center">
                        <input
                          type="number"
                          min="1"
                          value={item.quantity}
                          onChange={(event) => {
                            handleQuantityChange(event,item.key,data.cart.contents.nodes,updateCart,updateCartProcessing);
                          }}
                          className="w-16 px-2 py-1 text-center border border-gray-300 rounded mr-2"
                        />
                        <Button
                          handleButtonClick={() =>
                            handleRemoveProductClick(item.key,data.cart.contents.nodes)
                          }
                          variant="secondary"
                          buttonDisabled={updateCartProcessing}
                        >
                          Delete
                        </Button>
                      </div>
                    </div>
                  )
                })
              }
            </div>
            <div className="rounded-lg p-6 md:w-full">
              <div className="flex justify-end mb-4">
                <span className="font-semibold pr-2">Subtotal:</span>
                <span>{cartTotal}</span>
              </div>
            </div>
            {
              !isCheckoutPage && (
                <div className="flex justify-end mb-4">
                  <Link href="/checkout" passHref>
                    <Button variant="primary" fullWidth>CHECKOUT</Button>
                  </Link>
                </div>
              )
            }
          </>
        ) : (
          <div className="text-center">
            <h2 className="text-2xl font-bold mb-4">
              No products in the cart.
            </h2>
            <Link href="/shop" passHref>
              <Button variant="primary">Continue shopping</Button>
            </Link>
          </div >
        )
      }
    </div >
  );
}
export default CartContents;

Step 6. Cách khai báo các kiểu chung global cho các dự án next TypeScrip

Đọc bài viết này https://learnreact.gitbook.io/learnreact/advanced-reactjs/cach-khai-bao-cac-kieu-chung-global-cho-cac-du-an-react-typescript-ok

globals.d.ts

import { FieldValues, UseFormRegister } from 'react-hook-form';
import {SubmitHandler} from 'react-hook-form';
declare global {
  interface ILayoutProps {
    children?: ReactNode;
    title: string;
  }
  interface IImage {
    __typename: string;
    uri: string;
    title: string;
    srcSet: string;
    sourceUrl: string;
  }
  interface IVariationNode {
    __typename: string;
    name: string;
  }
  interface IAllPaColors {
    __typename: string;
    nodes: IVariationNode[];
  }
  interface IAllPaSizes {
    __typename: string;
    nodes: IVariationNode[];
  }
  interface IVariationNodes {
    __typename: string;
    id: string;
    databaseId: number;
    name: string;
    image?: string;
    stockStatus: string;
    stockQuantity: number;
    purchasable: boolean;
    onSale: boolean;
    salePrice?: string;
    regularPrice: string;
  }
  interface IVariations {
    __typename: string;
    nodes: IVariationNodes[];
  }
  interface IProduct {
    __typename: string;
    databaseId: number;
    name: string;
    description: string;
    slug: string;
    averageRating: number;
    onSale: boolean;
    image: IImage;
    price: string;
    regularPrice: string;
    salePrice?: string;
    allPaColors?: IAllPaColors;
    allPaSizes?: IAllPaSizes;
    variations?: IVariations;
    stockQuantity: number;
  }
  interface IProductRootObject {
    product: IProduct;
    variationId?: number;
    fullWidth?: boolean;
  }
  interface IProductCart {
    __typename: string;
    node: IProductNode;
  }
  interface IProductCartRootObject {
    __typename: string;
    key: string;
    product: IProductCart;
    variationId?: number;
    fullWidth?: boolean;
    subtotal: string;
    quantity: number;
  }
  interface IButtonProps {
    handleButtonClick?: () => void;
    buttonDisabled?: boolean;
    variant?: TButtonVariant;
    children?: ReactNode;
    fullWidth?: boolean;
    href?: string;
    title?: string;
    selected?: boolean;
  }
  type TButtonVariant = 'primary' | 'secondary' | 'hero' | 'filter' | 'reset';
  interface ICartProviderProps {
    children: React.ReactNode;
  }
  interface Image {
    sourceUrl?: string;
    srcSet?: string;
    title: string;
  }
  interface Product {
    cartKey: string;
    name: string;
    qty: number;
    price: number;
    totalPrice: string;
    image: Image;
    productId: number;
  }
  interface RootObject {
    products: Product[];
    totalProductsCount: number;
    totalProductsPrice: number;
  }
  type TRootObject = RootObject | string | null | undefined;
  type TRootObjectNull = RootObject | null | undefined;
  interface ICartContext {
    cart: RootObject | null | undefined;
    setCart: React.Dispatch<React.SetStateAction<TRootObjectNull>>;
    updateCart: (newCart: RootObject) => void;
    isLoading: boolean;
  }
  interface ICartProps {
    stickyNav?: boolean;
  }
  interface IImage {
    __typename: string;
    id: string;
    sourceUrl?: string;
    srcSet?: string;
    altText: string;
    title: string;
  }
  interface IGalleryImages {
    __typename: string;
    nodes: IImage[];
  }
  interface IProductNode {
    __typename: string;
    id: string;
    databaseId: number;
    name: string;
    description: string;
    type: string;
    onSale: boolean;
    slug: string;
    averageRating: number;
    reviewCount: number;
    image: IImage;
    galleryImages: IGalleryImages;
    productId: number;
  }
  interface IProduct {
    __typename: string;
    node: IProductNode;
  }
  interface IProductUpdateRootObject {
    __typename: string;
    key: string;
    product: IProduct;
    variation?: IVariationNodes;
    quantity: number;
    total: string;
    subtotal: string;
    subtotalTax: string;
  }
  type TUpdatedItems = { key: string; quantity: number }[];
  interface IUpdateCartItem {
    key: string;
    quantity: number;
  }
  interface IUpdateCartInput {
    clientMutationId: string;
    items: IUpdateCartItem[];
  }
  interface IUpdateCartVariables {
    input: IUpdateCartInput;
  }
  interface IUpdateCartRootObject {
    variables: IUpdateCartVariables;
  }
  interface IFormattedCartProps {
    cart: {
      contents: {
        nodes: IProductUpdateRootObject[]
      };
      total: number
    };
  }
  interface ICheckoutDataProps {
    firstName: string;
    lastName: string;
    address1: string;
    address2: string;
    city: string;
    country: string;
    state: string;
    postcode: string;
    email: string;
    phone: string;
    company: string;
    paymentMethod: string;
  }
  interface IBilling {
    firstName: string;
    lastName: string;
    address1: string;
    city: string;
    postcode: string;
    email: string;
    phone: string;
  }
  interface IShipping {
    firstName: string;
    lastName: string;
    address1: string;
    city: string;
    postcode: string;
    email: string;
    phone: string;
  }
  interface ICheckoutData {
    clientMutationId: string;
    billing: IBilling;
    shipping: IShipping;
    shipToDifferentAddress: boolean;
    paymentMethod: string;
    isPaid: boolean;
    transactionId: string;
  }
  interface ICustomValidation {
    required?: boolean;
    minlength?: number;
  }
  interface IErrors { }
  interface IInputRootObject {
    inputLabel: string;
    inputName: string;
    customValidation: ICustomValidation;
    errors?: IErrors;
    register?: UseFormRegister<FieldValues>;
    type?: string;
  }
  interface IBillingProps {
    handleFormSubmit: SubmitHandler<ICheckoutDataProps>;
  }
  interface ImageDP {
    __typename: string;
    sourceUrl: string;
  }
  interface NodeDP {
    __typename: string;
    price: string;
    regularPrice: string;
    salePrice?: string;
  }
  interface VariationsDP {
    __typename: string;
    nodes: NodeDP[];
  }
  interface RootObjectDP {
    __typename: string;
    databaseId: number;
    name: string;
    slug: string;
    image: ImageDP;
    price: string;
    regularPrice: string;
    salePrice?: string;
    onSale: boolean;
    variations: VariationsDP;
  }
  interface IDisplayProductsProps {
    products: RootObjectDP[];
  }
}
export {};

Một mẫu query checkout

mutation-order.js

mutation {
  checkout(
    input: 
    {
      billing: {
        address1: "Tòa S3, Skylake Vinhomes Phạm Hùng, Nam Từ Liêm", 
        address2: "Phung Hung Ha Dong", 
        city: "Ha Noi", 
        company: "Ha Noi", 
        country: VN, 
        email: "phamngoctuong1805@gmail.com", 
        firstName: "Lionel", phone: "0914748166", 
        postcode: "10000", 
        lastName: "Tưởng", state: ""
      }, 
      paymentMethod: "cod"
    }
  ) 
  {
    clientMutationId
    redirect
    result
  }
}
// ==
{
  "data": {
    "checkout": {
      "clientMutationId": null,
      "redirect": "https://wpclidemo.dev/checkout/order-received/1876/?key=wc_order_Egg0qI4BJiKzJ",
      "result": "success"
    }
  }
}

Còn đây là kết quả thanh toán ở website

Step 7 Checkout

src\utils\functions\functions.ts

export const createCheckoutData = (order: ICheckoutDataProps) => ({
  clientMutationId: uuidv4(),
  billing: {
    firstName: order.firstName,
    lastName: order.lastName,
    address1: order.address1,
    address2: order.address2,
    city: order.city,
    country: order.country,
    state: order.state,
    postcode: order.postcode,
    email: order.email,
    phone: order.phone,
    company: order.company,
  },
  shipping: {
    firstName: order.firstName,
    lastName: order.lastName,
    address1: order.address1,
    address2: order.address2,
    city: order.city,
    country: order.country,
    state: order.state,
    postcode: order.postcode,
    email: order.email,
    phone: order.phone,
    company: order.company,
  },
  shipToDifferentAddress: false,
  paymentMethod: order.paymentMethod,
  isPaid: false,
  transactionId: '123456',
})

datatest-graphql.txt

mutation {
  checkout(
    input:
    {
      billing: {
        address1: "Tòa S3, Skylake Vinhomes Phạm Hùng, Nam Từ Liêm",
        address2: "Phung Hung Ha Dong",
        city: "Ha Noi",
        company: "Ha Noi",
        country: VN,
        email: "phamngoctuong1805@gmail.com",
        firstName: "Lionel", phone: "0914748166",
        postcode: "10000",
        lastName: "Tưởng", state: ""
      },
      paymentMethod: "cod"
    }
  )
  {
    clientMutationId
    redirect
    result
  }
}
{
  "data": {
    "checkout": {
      "clientMutationId": null,
      "redirect": "https://wpclidemo.dev/checkout/order-received/1876/?key=wc_order_Egg0qI4BJiKzJ",
      "result": "success"
    }
  }
}

src\utils\gql\GQL_MUTATIONS.ts

import { gql } from "@apollo/client";
export const ADD_TO_CART = gql`
  mutation ($input: AddToCartInput!) {
    addToCart(input: $input) {
      cartItem {
        key
        product {
          node {
            id
            databaseId
            name
            description
            type
            onSale
            slug
            averageRating
            reviewCount
            image {
              id
              sourceUrl
              altText
            }
            galleryImages {
              nodes {
                id
                sourceUrl
                altText
              }
            }
          }
        }
        variation {
          node {
            id
            databaseId
            name
            description
            type
            onSale
            price
            regularPrice
            salePrice
            image {
              id
              sourceUrl
              altText
            }
            attributes {
              nodes {
                id
                attributeId
                name
                value
              }
            }
          }
        }
        quantity
        total
        subtotal
        subtotalTax
      }
    }
  }
`;
export const UPDATE_CART = gql`
  mutation ($input: UpdateItemQuantitiesInput!) {
    updateItemQuantities(input: $input) {
      items {
        key
        product {
          node {
            id
            databaseId
            name
            description
            type
            onSale
            slug
            averageRating
            reviewCount
            image {
              id
              sourceUrl
              altText
            }
            galleryImages {
              nodes {
                id
                sourceUrl
                altText
              }
            }
          }
        }
        variation {
          node {
            id
            databaseId
            name
            description
            type
            onSale
            price
            regularPrice
            salePrice
            image {
              id
              sourceUrl
              altText
            }
            attributes {
              nodes {
                id
                attributeId
                name
                value
              }
            }
          }
        }
        quantity
        total
        subtotal
        subtotalTax
      }
      updated {
        key
        product {
          node {
            id
            databaseId
          }
        }
        variation {
          node {
            id
            databaseId
          }
        }
      }
    }
  }
`;
export const CHECKOUT_MUTATION = gql`
  mutation($input: CheckoutInput!) {
    checkout(input: $input) {
      result
      redirect
    }
  }
`;

src\utils\constants\INPUT_FIELDS.ts

export const INPUT_FIELDS = [
  {
    id: 1,
    label: 'First name',
    name: 'firstName',
    customValidation: { required: true, minLength: 4 },
  },
  {
    id: 2,
    label: 'Last Name',
    name: 'lastName',
    customValidation: { required: true, minLength: 4 },
  },
  {
    id: 3,
    label: 'Address 1',
    name: 'address1',
    customValidation: { required: true, minLength: 4 },
  },
  {
    id: 4,
    label: 'Address 2',
    name: 'address2',
    customValidation: { required: true, minLength: 4 },
  },
  {
    id: 5,
    label: 'City',
    name: 'city',
    customValidation: { required: true, minLength: 2 },
  },
  {
    id: 6,
    label: 'Country',
    name: 'country',
    customValidation: { required: true, minLength: 2 },
  },
  {
    id: 7,
    label: 'State',
    name: 'state',
    customValidation: { required: true, minLength: 2 },
  },
  {
    id: 8,
    label: 'Postnummer',
    name: 'postcode',
    customValidation: { required: true, minLength: 4, pattern: '[+0-9]{4,6}' },
  },
  {
    id: 9,
    label: 'Email',
    name: 'email',
    customValidation: { required: true, type: 'email' },
  },
  {
    id: 10,
    label: 'Phone',
    name: 'phone',
    customValidation: { required: true, minLength: 8, pattern: '[+0-9]{8,12}' },
  },
  {
    id: 11,
    label: 'Company',
    name: 'company',
    customValidation: { required: true, minLength: 2 },
  }
];

— Để giỏ hàng dược thanh toán vào Order cần thông tin sau (không cần xác thực gì)

console.log(orderData); 👇
{
  "clientMutationId": "addd838e-07a6-4137-834b-a63ce6dd2580",
  "billing": {
    "firstName": "Phạm",
    "lastName": "Tưởng",
    "address1": "Phung Hung Ha Dong",
    "address2": "Phung Hung Ha Dong",
    "city": "Ha Noi",
    "country": "VN",
    "state": "Ga",
    "postcode": "10000",
    "email": "phamngoctuong1805@gmail.com",
    "phone": "0914748166",
    "company": "Ha Noi"
  },
  "shipping": {
    "firstName": "Phạm",
    "lastName": "Tưởng",
    "address1": "Phung Hung Ha Dong",
    "address2": "Phung Hung Ha Dong",
    "city": "Ha Noi",
    "country": "VN",
    "state": "Ga",
    "postcode": "10000",
    "email": "phamngoctuong1805@gmail.com",
    "phone": "0914748166",
    "company": "Ha Noi"
  },
  "shipToDifferentAddress": false,
  "paymentMethod": "cod",
  "isPaid": false,
  "transactionId": "123456"
}

— setCart cả ở Layout

src\components\Layout\Layout.component.tsx

import { ReactNode, useContext, useEffect } from 'react';
import { useQuery } from '@apollo/client';
import Header from '../Header/Header.component';
import PageTitle from './PageTitle.component';
import { CartContext } from '@/stores/CartProvider';
import { GET_CART } from '@/utils/gql/GQL_QUERIES';
import { getFormattedCart } from '@/utils/functions/functions';
const Layout = ({ children, title }: ILayoutProps) => {
  const { setCart } = useContext(CartContext);
  const { data, refetch } = useQuery(GET_CART, {
    notifyOnNetworkStatusChange: true,
    onCompleted: () => {
      const updatedCart = getFormattedCart(data);
      if (!updatedCart && !data?.cart?.contents?.nodes.length) {
        // Should we clear the localStorage if we have no remote cart?
        return;
      }
      localStorage.setItem('woocommerce-cart', JSON.stringify(updatedCart));
      setCart(updatedCart);
    }
  });
  useEffect(() => {
    refetch();
  }, [refetch]);
  return (
    <div className="flex flex-col min-h-screen w-full mx-auto">
      <Header title={title} />
      <div className="container mx-auto px-6 flex-1">
        <PageTitle title={title} />
        <main>{children}</main>
      </div>
    </div>
  )
};
export default Layout;

Step 8: Shop

— Step 8.1 giao diện

src\pages\shop.tsx

import Head from 'next/head';
import Layout from '@/components/Layout/Layout.component';
import ProductList from '@/components/Product/ProductList.component';
import client from '@/utils/apollo/ApolloClient';
import { FETCH_ALL_PRODUCTS_QUERY } from '@/utils/gql/GQL_QUERIES';
import type { NextPage, GetStaticProps, InferGetStaticPropsType } from 'next';
const Shop: NextPage = ({
  products,
  loading,
}: InferGetStaticPropsType<typeof getStaticProps>) => {
  if (loading)
    return (
      <Layout title="Products">
        <div className="flex justify-center items-center min-h-screen">
          <div className="animate-spin rounded-full h-32 w-32 border-t-2 border-b-2 border-gray-900"></div>
        </div>
      </Layout>
    );
  if (!products)
    return (
      <Layout title="Products">
        <div className="flex justify-center items-center min-h-screen">
          <p className="text-red-500">No products found</p>
        </div>
      </Layout>
    );
  return (
    <Layout title="Products">
      <Head>
        <title>Products | WooCommerce Next.js</title>
      </Head>
      <div className="container mx-auto px-4 py-8">
        <ProductList products={products} title="Men's clothing" />
      </div>
    </Layout>
  );
};
export default Shop;
export const getStaticProps: GetStaticProps = async () => {
  const { data, loading, networkStatus } = await client.query({
    query: FETCH_ALL_PRODUCTS_QUERY,
  });
  return {
    props: {
      products: data.products.nodes,
      loading,
      networkStatus,
    },
    revalidate: 60,
  };
};

src\components\Product\ProductList.component.tsx

const ProductList = ({ products, title }: ProductListProps) => {
  return (
    <div className="flex flex-col md:flex-row gap-8">
      <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"></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">
          ProductCard
        </div>
      </div>
    </div>
  )
}
export default ProductList;

— Bổ sung thêm productCategories để shop sử dụng

— Create filteredProducts and ProductCard

src\components\Product\ProductList.component.tsx

import { useProductFilters } from "@/hooks/useProductFilters";
import ProductCard from "./ProductCard.component";
const ProductList = ({ products, title }: ProductListProps) => {
  const {filterProducts} = useProductFilters(products);
  const filteredProducts = filterProducts(products);
  return (
    <div className="flex flex-col md:flex-row gap-8">
      <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: Product) => (
            <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\ProductCard.component.tsx

import Link from 'next/link';
import Image from 'next/image';
const ProductCard = ({ databaseId, name, price, slug, image, }: ProductCardProps) => {
  return (
    <div className="group">
      ProductCard
    </div>
  )
}
export default ProductCard;

src\hooks\useProductFilters.ts

import { useState } from 'react';
export const useProductFilters = (products: Product[]) => {
  const filterProducts = (products: Product[]) => {
    const filtered = products?.filter((product: Product) => {
      return true;
    });
    return [...(filtered || [])].sort((a, b) => {
      return 0;
    })
  };
  return {
    filterProducts
  }
}

Last updated

Was this helpful?