Browse Source

first commit

fogwind 1 tuần trước cách đây
commit
d76622d825
100 tập tin đã thay đổi với 7584 bổ sung0 xóa
  1. 44 0
      .gitignore
  2. 5 0
      @types/graphql.d.ts
  3. 36 0
      @types/next-auth.d.ts
  4. 183 0
      README.md
  5. 73 0
      changelog.md
  6. 33 0
      eslint.config.mjs
  7. 5 0
      hero.ts
  8. 21 0
      license.md
  9. 25 0
      next.config.ts
  10. 44 0
      package.json
  11. 7 0
      postcss.config.mjs
  12. 5 0
      public/favicon.svg
  13. BIN
      public/image/Logo.webp
  14. BIN
      public/image/forget-password.webp
  15. BIN
      public/image/login.webp
  16. BIN
      public/image/placeholder.webp
  17. BIN
      public/image/sign-in.webp
  18. 12 0
      src/app/(checkout)/checkout/page.tsx
  19. 19 0
      src/app/(checkout)/error.tsx
  20. 18 0
      src/app/(checkout)/layout.tsx
  21. 10 0
      src/app/(public)/[page]/opengraph-image.tsx
  22. 35 0
      src/app/(public)/[page]/page.tsx
  23. 19 0
      src/app/(public)/customer/forget-password/page.tsx
  24. 18 0
      src/app/(public)/customer/login/page.tsx
  25. 13 0
      src/app/(public)/customer/register/page.tsx
  26. 19 0
      src/app/(public)/error.tsx
  27. 19 0
      src/app/(public)/layout.tsx
  28. 211 0
      src/app/(public)/not-found.tsx
  29. 18 0
      src/app/(public)/page.tsx
  30. 154 0
      src/app/(public)/product/[...urlProduct]/page.tsx
  31. 177 0
      src/app/(public)/search/[collection]/page.tsx
  32. 239 0
      src/app/(public)/search/page.tsx
  33. 17 0
      src/app/(public)/success/page.tsx
  34. 6 0
      src/app/api/auth/[...nextauth]/route.ts
  35. 134 0
      src/app/api/graphql/route.ts
  36. 6 0
      src/app/api/revalidate/route.ts
  37. 21 0
      src/app/api/robots.ts
  38. BIN
      src/app/favicon.ico
  39. 201 0
      src/app/globals.css
  40. 62 0
      src/app/layout.tsx
  41. 213 0
      src/app/not-found.tsx
  42. 12 0
      src/app/robots.ts
  43. 197 0
      src/components/cart/AddToCart.tsx
  44. 517 0
      src/components/cart/CartModal.tsx
  45. 23 0
      src/components/cart/OpenCart.tsx
  46. 30 0
      src/components/cart/OrderDetail.tsx
  47. 21 0
      src/components/cart/index.tsx
  48. 159 0
      src/components/catalog/Pagination.tsx
  49. 97 0
      src/components/catalog/product/ProductCard.tsx
  50. 137 0
      src/components/catalog/product/ProductDescription.tsx
  51. 29 0
      src/components/catalog/product/ProductGridItems.tsx
  52. 31 0
      src/components/catalog/product/ProductInfo.tsx
  53. 123 0
      src/components/catalog/product/ProductMoreDetail.tsx
  54. 44 0
      src/components/catalog/product/ProductsSection.tsx
  55. 48 0
      src/components/catalog/product/RelatedProductsSection.tsx
  56. 71 0
      src/components/catalog/product/VariantSelector.tsx
  57. 278 0
      src/components/catalog/review/AddProductReview.tsx
  58. 68 0
      src/components/catalog/review/AddRatingStar.tsx
  59. 23 0
      src/components/catalog/review/NoReview.tsx
  60. 203 0
      src/components/catalog/review/ReviewDetail.tsx
  61. 59 0
      src/components/catalog/review/ReviewSection.tsx
  62. 243 0
      src/components/catalog/type.ts
  63. 161 0
      src/components/checkout/checkout-cart/CartItemAccordian.tsx
  64. 128 0
      src/components/checkout/checkout-cart/CheckoutCart.tsx
  65. 68 0
      src/components/checkout/index.tsx
  66. 140 0
      src/components/checkout/stepper/Email.tsx
  67. 540 0
      src/components/checkout/stepper/GuestAddAdressForm.tsx
  68. 73 0
      src/components/checkout/stepper/ProceedToCheckout.tsx
  69. 169 0
      src/components/checkout/stepper/index.tsx
  70. 221 0
      src/components/checkout/stepper/payment/PaymentMethod.tsx
  71. 36 0
      src/components/checkout/stepper/payment/index.tsx
  72. 101 0
      src/components/checkout/stepper/review/OrderReview.tsx
  73. 29 0
      src/components/checkout/stepper/review/index.tsx
  74. 217 0
      src/components/checkout/stepper/shipping/ShippingMethod.tsx
  75. 37 0
      src/components/checkout/stepper/shipping/index.tsx
  76. 73 0
      src/components/checkout/success/EmptyCart.tsx
  77. 24 0
      src/components/checkout/type.ts
  78. 25 0
      src/components/common/LoadingSpinner.tsx
  79. 56 0
      src/components/common/NextImage.tsx
  80. 45 0
      src/components/common/OpenGraphImage.tsx
  81. 48 0
      src/components/common/Rating.tsx
  82. 47 0
      src/components/common/Shimmer.tsx
  83. 52 0
      src/components/common/button/Button.tsx
  84. 47 0
      src/components/common/button/EventButton.tsx
  85. 50 0
      src/components/common/button/LoadingButton.tsx
  86. 25 0
      src/components/common/button/ReviewButton.tsx
  87. 116 0
      src/components/common/form/Input.tsx
  88. 20 0
      src/components/common/icons/AddUploadImage.tsx
  89. 20 0
      src/components/common/icons/CategoryIcon.tsx
  90. 20 0
      src/components/common/icons/CheckSign.tsx
  91. 23 0
      src/components/common/icons/EditIcon.tsx
  92. 18 0
      src/components/common/icons/HeartIcon.tsx
  93. 33 0
      src/components/common/icons/HomeIcon.tsx
  94. 17 0
      src/components/common/icons/LeftArrow.tsx
  95. 21 0
      src/components/common/icons/LoadingDots.tsx
  96. 41 0
      src/components/common/icons/LogoIcon.tsx
  97. 25 0
      src/components/common/icons/MapIcon.tsx
  98. 112 0
      src/components/common/icons/NoReviewsIcon.tsx
  99. 171 0
      src/components/common/icons/NotFoundIcon.tsx
  100. 0 0
      src/components/common/icons/PlusIcon.tsx

+ 44 - 0
.gitignore

@@ -0,0 +1,44 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.*
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/versions
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# env files (can opt-in for committing if needed)
+.env*
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
+
+
+~

+ 5 - 0
@types/graphql.d.ts

@@ -0,0 +1,5 @@
+declare module '*.graphql' {
+  import { DocumentNode } from 'graphql';
+  const Schema: DocumentNode;
+  export = Schema;
+}

+ 36 - 0
@types/next-auth.d.ts

@@ -0,0 +1,36 @@
+import { DefaultSession } from 'next-auth';
+
+declare module 'next-auth' {
+  /**
+   * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
+   */
+  interface Session {
+    user: {
+      id: string;
+      firstname?: string;
+      lastname?: string;
+      token?: string;
+      accessToken?: string;
+      apiToken?: string;
+      role?: string;
+    } & DefaultSession['user'];
+  }
+
+  interface User {
+    id: string;
+    accessToken?: string;
+    apiToken?: string;
+    role?: string;
+    tokenType?: string;
+    expiresIn?: number;
+  }
+}
+
+declare module 'next-auth/jwt' {
+  interface JWT {
+    accessToken?: string;
+    apiToken?: string;
+    role?: string;
+    id?: string;
+  }
+}

+ 183 - 0
README.md

@@ -0,0 +1,183 @@
+<p align="center">
+  <a href="https://bagisto.com/en/headless-ecommerce/">
+    <picture>
+      <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/bagisto/temp-media/0b0984778fae92633f57e625c5494ead1fe320c3/dark-logo-P5H7MBtx.svg">
+      <source media="(prefers-color-scheme: light)" srcset="https://bagisto.com/wp-content/themes/bagisto/images/logo.png">
+      <img src="https://bagisto.com/wp-content/themes/bagisto/images/logo.png" alt="Bagisto logo">
+    </picture>
+  </a>
+</p>
+
+<p align="center">
+    <a href="https://bagisto.com/en/headless-ecommerce/">Website</a> | <a href="https://bagisto.com/en/bagisto-headless-ecommerce-installation-guide/">Documentation</a> | <a href="https://forums.bagisto.com/">Forums</a> | <a href="https://www.facebook.com/groups/bagisto/">Community</a>
+</p>
+
+<p align="center">
+    <a href="https://twitter.com/intent/follow?screen_name=bagistoshop"><img src="https://img.shields.io/twitter/follow/bagistoshop?style=social"></a>
+    <a href="https://www.youtube.com/channel/UCbrfqnhyiDv-bb9QuZtonYQ"><img src="https://img.shields.io/youtube/channel/subscribers/UCbrfqnhyiDv-bb9QuZtonYQ?style=social"></a>
+</p>
+
+<p align="center">
+    <a href="https://packagist.org/packages/bagisto/bagisto"><img src="https://poser.pugx.org/bagisto/bagisto/license.svg" alt="License"></a>
+</p>
+
+#  Bagisto Next.js Commerce
+
+A [**headless eCommerce framework**](https://bagisto.com/en/headless-ecommerce/) built with **Next.js** and powered by **Bagisto**, designed for modern scalability and flexibility.
+Through layered caching and optimized rendering strategies, it consistently achieves a **100/100 Core Web Vitals score**, delivering lightning-fast performance and seamless shopping experiences.
+
+Check the [Documentation](https://headless-doc.bagisto.com/) to quickly set up your Headless eCommerce store.
+
+**Bagisto Version:** v2.3.0
+
+**Bagisto GraphQL API:** v2.3.0
+
+![Bagisto Headless Commerce Image](https://raw.githubusercontent.com/bagisto/temp-media/refs/heads/master/bagisto-headless-commerce-home.png)
+## Features
+
+- **Ultra-fast storefront** with 100/100 Core Web Vitals score.  
+- **Layered caching** for API responses and page rendering.  
+- Fully **responsive and mobile-friendly** design.  
+- SEO optimized with meta tags, OpenGraph, and Twitter cards.  
+- Secure authentication via **NextAuth.js**.  
+- Powered by **Bagisto** GraphQL APIs for robust commerce functionality.  
+- **Incremental Static Regeneration (ISR)** with revalidation.
+  
+Bagisto Open Source Headless eCommerce is optimized to deliver a **100/100 Core Web Vitals score** across devices, ensuring top-tier performance and user experience.
+
+![Bagisto Headless Commerce Image](https://raw.githubusercontent.com/bagisto/temp-media/refs/heads/master/bagisto-headless-commerce-performance.png)
+
+## Prerequisites
+
+Before you begin, ensure you have the following installed:
+
+- **Node.js** 18+ and **pnpm**
+- Check Bagisto [backend requirement detail](https://devdocs.bagisto.com/2.3/introduction/requirements.html#server-configuration)
+
+---
+
+## Installation
+
+1) Install Bagisto
+ 
+    Begin by [installing the Bagisto](https://devdocs.bagisto.com/) eCommerce platform on your server or local environment.
+
+2) Install the Bagisto Headless Extension
+
+    After installing Bagisto, install the [Bagisto Headless Extension](https://github.com/bagisto/bagisto-api) to expose the required APIs for your frontend.
+
+3) Get your storefront up and running in one command:
+   
+   ```bash
+   npx -y @bagisto-headless/create your-storefront
+   ```
+   
+4) Configure `.env.local` in the Next.js Project
+
+   In your Next.js frontend project, create or update your `.env.local` file with the following variables:
+
+| Variable | Description | Example |
+|----------|-------------|---------|
+| `NEXT_PUBLIC_BAGISTO_ENDPOINT` | Enter Your Bagisto Shop URL | `https://your-store.bagisto.com/` |
+| `NEXT_PUBLIC_BAGISTO_STOREFRONT_KEY` | Enter Your Bagisto Storefront Key | `pk_storefront_*************************` |
+| `NEXT_PUBLIC_NEXT_AUTH_URL` | Enter Your Headless Shop URL | `https://headless-store.com/` |
+| `NEXT_PUBLIC_NEXT_AUTH_SECRET` | Enter Your Headless Shop Secret | Generate with `openssl rand -base64 32` |
+| `COMPANY_NAME` | Enter Your company name | Bagisto Headless Store |
+
+
+**Important Notes**  
+- You will need to use the environment variables defined in `.env.example` to run Next.js Commerce.  
+- It’s recommended to use **Vercel Environment Variables**, but a `.env` file is sufficient for local development.  
+- **Never commit your `.env` file** to version control — it contains secrets that would allow others to control your Bagisto store.
+
+
+## One-Click Deploy to Netlify
+
+Click the button above to deploy your own copy of Bagisto Headless eCommerce to Netlify instantly!
+
+[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/bagisto/nextjs-commerce)
+
+---
+
+**Vercel Setup**
+
+Install the Vercel CLI:
+
+```bash
+npm i -g vercel
+```
+
+Link your local instance with Vercel and GitHub accounts (this creates the `.vercel` directory):
+
+```bash
+vercel link
+```
+
+Download your environment variables:
+
+```bash
+vercel env pull
+```
+
+---
+
+**Run the development server:**
+
+```bash
+pnpm dev
+```
+
+**Build for production:**
+
+```bash
+pnpm build
+pnpm start
+```
+
+---
+
+## Usage
+
+Start the development server:
+
+```bash
+pnpm dev
+```
+Access the store at:[http://localhost:3000](http://localhost:3000)
+
+---
+
+## Products
+
+The Open Source Headless eCommerce allows users to browse a wide range of products with built-in pagination and search functionality. Each product has its own detailed page showcasing images, descriptions, pricing, reviews, and availability.
+
+Bagisto Headless Commerce APIs support multiple product types, including simple, configurable, bundled, and downloadable products, ensuring flexibility for different business needs.
+
+![Bagisto Headless Commerce Image](https://raw.githubusercontent.com/bagisto/temp-media/refs/heads/master/bagisto-headless-commerce-product-page.png)
+
+## Categories
+
+Products are neatly organized into hierarchical categories, making it easy for customers to navigate the store. Each category page displays relevant product listings with filtering and sorting options for a better shopping experience.
+
+The Open Source Headless eCommerce also ensures SEO-friendly category URLs with meta titles, descriptions, and breadcrumbs for improved discoverability.
+
+![Bagisto Headless Commerce Image](https://raw.githubusercontent.com/bagisto/temp-media/refs/heads/master/bagisto-headless-commercecategory.png)
+ 
+## Checkout
+
+The checkout process is fully functional, featuring complete cart management where customers can add, update, or remove items.
+
+Both guest and logged-in users can proceed through checkout, selecting shipping addresses and preferred payment methods.
+
+Once the order is placed, it is instantly synchronized with the Bagisto backend, enabling smooth order processing and management.
+
+![Bagisto Headless Commerce Image](https://raw.githubusercontent.com/bagisto/temp-media/refs/heads/master/bagisto-headless-commerce-cart-checkout.png)
+
+## Community
+Get Bagisto Headless Commerce support on [Facebook Group](https://www.facebook.com/groups/bagisto) and [Forum](https://forums.bagisto.com/)
+
+## License
+Bagisto headless eCommerce framework that will always remain free under the [MIT License](https://github.com/bagisto/nextjs-commerce/blob/main/license.md).
+
+## Security Vulnerabilities
+If you think that you have found a security issue in Bagisto Headless Commerce, please do not use the issue tracker and do not post it publicly. Instead, all security issues must be sent to [mailto:support@bagisto.com](mailto:support@bagisto.com).

+ 73 - 0
changelog.md

@@ -0,0 +1,73 @@
+# Change Log
+
+## [2.2.0] - 2025-09-09
+
+### Added
+
+- Added `BAGISTO_SESSION` key.
+- Added `NEXTAUTH_URL` key.
+- Added `NEXTAUTH_SECRET` key.
+- Added `REVALIDATION_DURATION` key.
+- Added `IMAGE_DOMAIN` key.
+- Added the new the query field.
+- Added the Authentication pages (Sign Up, Sign In and Forget Password).
+- Added Customer Checkout.
+- Added more banner on the home page.
+- Added the pagination and Product Attributes.
+
+
+### Removed
+
+- Removed `BAGISTO_PROTOCOL` key.
+- Removed `BAGISTO_STOREFRONT_ACCESS_TOKEN` key.
+- Removed `BAGISTO_REVALIDATION_SECRET` key.
+
+### Changed
+
+- Updated the latest changes of the UI.
+- Manged the state with Redux.
+
+### Fixed
+
+- Resolved the UI issues.
+- Improved performance.
+- Made compatible with the latest Bagisto APIs (version 2.3.0).
+
+## [2.1.0] - 2025-02-24
+
+### Added
+
+- Added `BAGISTO_PROTOCOL` key.
+- Added the new the query field.
+
+### Changed
+
+- Replaced NextUI with HeroUI.
+- Updated the latest changes of the Next Commerce theme.
+
+### Fixed
+
+- Resolved login issue with OAuth.
+- Improved performance for large images.
+- Made compatible with the latest Bagisto APIs (version 2.2.3).
+
+## [2.0.0] - 2024-06-06
+
+### Added
+
+- Added the NextUI.
+
+### Changed
+
+- Updated the query field.
+- Modified the `.env.example` file to reflect correct environment variable usage.
+
+### Fixed
+
+- Made compatible with Bagisto APIs when using the Next Commerce theme.
+
+## [1.0.0] - 2024-04-03
+
+### Changed
+
+- Made compatible with Next Commerce v2.

+ 33 - 0
eslint.config.mjs

@@ -0,0 +1,33 @@
+import { defineConfig, globalIgnores } from "eslint/config";
+import nextVitals from "eslint-config-next/core-web-vitals";
+import nextTs from "eslint-config-next/typescript";
+
+const eslintConfig = defineConfig([
+  ...nextVitals,
+  ...nextTs,
+  {
+    files: ["src/**/*.{js,jsx,ts,tsx}"],
+    rules: {
+      "@typescript-eslint/no-unused-vars": [
+        "error",
+        {
+          argsIgnorePattern: "^_",
+          varsIgnorePattern: "^_",
+          caughtErrorsIgnorePattern: "^_",
+        },
+      ],
+      "no-unused-vars": "off",
+      "no-console": ["warn", { allow: ["warn", "error"] }],
+      "@typescript-eslint/no-explicit-any": "off",
+      "@typescript-eslint/no-non-null-assertion": "warn",
+      "react-hooks/exhaustive-deps": "off",
+      "react/no-unescaped-entities": "warn",
+      "@typescript-eslint/no-empty-interface": "warn",
+      "prefer-const": "error",
+      "no-var": "error",
+    },
+  },
+  globalIgnores([".next/**", "out/**", "build/**", "next-env.d.ts"]),
+]);
+
+export default eslintConfig;

+ 5 - 0
hero.ts

@@ -0,0 +1,5 @@
+// hero.ts
+import { heroui } from "@heroui/react";
+// or import from theme package if you are using individual packages.
+// import { heroui } from "@heroui/theme";
+export default heroui();

+ 21 - 0
license.md

@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2023 Vercel, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 25 - 0
next.config.ts

@@ -0,0 +1,25 @@
+import { configHeader } from '@/utils/constants';
+import type { NextConfig } from "next";
+
+const nextConfig: NextConfig = {
+  reactStrictMode: true,
+  typescript: {
+    ignoreBuildErrors: false,
+  },
+  images: {
+    unoptimized: true,
+    remotePatterns: [],
+  },
+  async headers() {
+    return configHeader;
+  },
+  compress: true, 
+  experimental: {
+    optimizePackageImports: ["lodash", "date-fns"],
+    serverActions: {
+      bodySizeLimit: "2mb",
+    },
+  },
+};
+
+export default nextConfig;

+ 44 - 0
package.json

@@ -0,0 +1,44 @@
+{
+  "name": "nextjs-commerce-v3",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "dev": "next dev",
+    "build": "NODE_ENV=production eslint . && next build",
+    "start": "next start",
+    "lint": "eslint .",
+    "lint:fix": "next lint --fix",
+    "package-version": "npx npm-check-updates"
+  },
+  "dependencies": {
+    "@apollo/client": "^3.14.0",
+    "@heroicons/react": "^2.2.0",
+    "@heroui/accordion": "^2.2.25",
+    "@heroui/drawer": "^2.2.25",
+    "@heroui/react": "^2.8.6",
+    "@heroui/select": "^2.4.29",
+    "@reduxjs/toolkit": "^2.10.1",
+    "clsx": "^2.1.1",
+    "framer-motion": "^12.23.24",
+    "graphql": "^16.12.0",
+    "lucide-react": "^0.563.0",
+    "next": "16.0.10",
+    "next-auth": "^4.24.13",
+    "next-themes": "^0.4.6",
+    "react": "19.2.0",
+    "react-dom": "19.2.0",
+    "react-hook-form": "^7.66.1",
+    "react-redux": "^9.2.0"
+  },
+  "devDependencies": {
+    "@tailwindcss/postcss": "^4",
+    "@types/node": "^20",
+    "@types/react": "^19",
+    "@types/react-dom": "^19",
+    "eslint": "^9",
+    "eslint-config-next": "16.0.10",
+    "tailwindcss": "^4",
+    "ts-node": "^10.9.2",
+    "typescript": "^5"
+  }
+}

+ 7 - 0
postcss.config.mjs

@@ -0,0 +1,7 @@
+const config = {
+  plugins: {
+    "@tailwindcss/postcss": {},
+  },
+};
+
+export default config;

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 5 - 0
public/favicon.svg


BIN
public/image/Logo.webp


BIN
public/image/forget-password.webp


BIN
public/image/login.webp


BIN
public/image/placeholder.webp


BIN
public/image/sign-in.webp


+ 12 - 0
src/app/(checkout)/checkout/page.tsx

@@ -0,0 +1,12 @@
+import CheckOut from "@/components/checkout";
+
+export default async function Information({
+  searchParams,
+}: {
+  searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
+}) {
+  const { step = "email" } = (await searchParams) as { [key: string]: string };
+  return <CheckOut
+    step={step}
+     />;
+}

+ 19 - 0
src/app/(checkout)/error.tsx

@@ -0,0 +1,19 @@
+"use client";
+
+export default function Error({ reset }: { reset: () => void }) {
+  return (
+    <div className="mx-auto my-4 flex max-w-xl flex-col rounded-lg border border-neutral-200 bg-white p-8 md:p-12 dark:border-neutral-800 dark:bg-black">
+      <h2 className="text-xl font-bold">Oh no!</h2>
+      <p className="my-2">
+        There was an issue with our storefront. This could be a temporary issue,
+        please try your action again.
+      </p>
+      <button
+        className="mx-auto mt-4 flex w-full items-center justify-center rounded-full bg-blue-600 p-4 tracking-wide text-white hover:opacity-90"
+        onClick={() => reset()}
+      >
+        Try Again
+      </button>
+    </div>
+  );
+}

+ 18 - 0
src/app/(checkout)/layout.tsx

@@ -0,0 +1,18 @@
+import Navbar from "@components/layout/navbar";
+import { ReactNode } from "react";
+export default async function RootLayout({
+  children,
+}: {
+  children: ReactNode;
+}) {
+  return (
+    <>
+      <div className="block lg:hidden">
+        <Navbar />
+      </div>
+      <main className="mx-auto min-h-[calc(100vh-580px)] w-full px-4 md:px-8 lg:px-16 xl:px-28">
+        {children}
+      </main>
+    </>
+  );
+}

+ 10 - 0
src/app/(public)/[page]/opengraph-image.tsx

@@ -0,0 +1,10 @@
+import OpenGraphImage from "@components/common/OpenGraphImage";
+import { getPage } from "@utils/bagisto";
+
+export default async function Image({ params }: { params: { page: string } }) {
+  const page = await getPage({ urlKey: params.page }) as { translation?: { metaTitle?: string; pageTitle?: string } }[];
+  const pageData = page && page.length > 0 ? page[0].translation : undefined;
+  const title = pageData?.metaTitle || pageData?.pageTitle;
+
+  return await OpenGraphImage({ title });
+}

+ 35 - 0
src/app/(public)/[page]/page.tsx

@@ -0,0 +1,35 @@
+import { notFound } from "next/navigation";
+import Prose from "@components/theme/search/Prose";
+import { getPage } from "@utils/bagisto";
+import { PageData } from "@/types/theme/theme-customization";
+
+
+export default async function Page({
+  params,
+}: {
+  params: Promise<{ page: string }>;
+}) {
+  const { page: pageParams } = await params;
+  const pageDataArray: PageData[] = await getPage({ urlKey: pageParams });
+  if (!pageDataArray?.length) return notFound();
+  const pageData = pageDataArray?.[0]?.translation;
+
+  return (
+    <div className="my-4 flex flex-col justify-between p-4">
+      <div className="flex flex-col gap-4 mx-auto">
+        <h1 className="text-2xl md:text-3xl font-bold">{pageData?.pageTitle}</h1>
+        <Prose className="mb-8" html={pageData?.htmlContent || ""} />
+      <p className="text-sm italic">
+        {`This document was last updated on ${new Intl.DateTimeFormat(
+          undefined,
+          {
+            year: "numeric",
+            month: "long",
+            day: "numeric",
+          },
+        )?.format(new Date(pageDataArray?.[0]?.updatedAt || "---"))}.`}
+      </p>
+      </div>
+    </div>
+  );
+}

+ 19 - 0
src/app/(public)/customer/forget-password/page.tsx

@@ -0,0 +1,19 @@
+import dynamic from "next/dynamic";
+import { generateMetadataForPage } from "@utils/helper";
+import { staticSeo } from "@utils/metadata";
+import { Suspense } from "react";
+import ForgetSkeleton from "@components/common/skeleton/ForgetSkeleton";
+const ForgetPasswordForm = dynamic(() => import("@components/customer/ForgetPassword"));
+
+export const revalidate = 3600;
+export async function generateMetadata() {
+  return generateMetadataForPage("", staticSeo.forget);
+}
+
+export default function ForgetPasswordPage() {
+  return (
+    <Suspense fallback={<ForgetSkeleton />}>
+      <ForgetPasswordForm />
+    </Suspense>
+  );
+}

+ 18 - 0
src/app/(public)/customer/login/page.tsx

@@ -0,0 +1,18 @@
+import LoginForm from "@components/customer/LoginForm";
+import { generateMetadataForPage } from "@utils/helper";
+import { staticSeo } from "@utils/metadata";
+import { SessionManager } from "@/providers";
+
+export const revalidate = 60;
+
+export async function generateMetadata() {
+  return generateMetadataForPage("", staticSeo.login);
+}
+
+export default async function LoginPage() {
+  return (
+    <SessionManager>
+      <LoginForm />
+    </SessionManager>
+  );
+}

+ 13 - 0
src/app/(public)/customer/register/page.tsx

@@ -0,0 +1,13 @@
+import RegistrationForm from "@components/customer/RegistrationForm";
+import { generateMetadataForPage } from "@utils/helper";
+import { staticSeo } from "@utils/metadata";
+
+ export const revalidate = 60;
+
+export async function generateMetadata() {
+  return generateMetadataForPage("", staticSeo.register);
+}
+
+export default async function Register() {
+  return <RegistrationForm />;
+}

+ 19 - 0
src/app/(public)/error.tsx

@@ -0,0 +1,19 @@
+"use client";
+
+export default function Error({ reset }: { reset: () => void }) {
+  return (
+    <div className="mx-auto my-4 flex max-w-xl flex-col rounded-lg border border-neutral-200 bg-white p-8 md:p-12 dark:border-neutral-800 dark:bg-black">
+      <h2 className="text-xl font-bold">Oh no!</h2>
+      <p className="my-2">
+        There was an issue with our storefront. This could be a temporary issue,
+        please try your action again.
+      </p>
+      <button
+        className="mx-auto mt-4 flex w-full items-center justify-center rounded-full bg-blue-600 p-4 tracking-wide text-white hover:opacity-90"
+        onClick={() => reset()}
+      >
+        Try Again
+      </button>
+    </div>
+  );
+}

+ 19 - 0
src/app/(public)/layout.tsx

@@ -0,0 +1,19 @@
+import { ReactNode } from "react";
+import Footer from "@/components/layout/footer";
+import Navbar from "@/components/layout/navbar";
+
+export default async function RootLayout({
+  children,
+}: {
+  children: ReactNode;
+}) {
+  return (
+    <main>
+      <Navbar />
+      <div className="mx-auto min-h-[calc(100vh-580px)] w-full">
+        {children}
+      </div>
+      <Footer />
+    </main>
+  );
+}

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 211 - 0
src/app/(public)/not-found.tsx


+ 18 - 0
src/app/(public)/page.tsx

@@ -0,0 +1,18 @@
+import { GET_THEME_CUSTOMIZATION } from "@/graphql";
+import RenderThemeCustomization from "@components/home/RenderThemeCustomization";
+import { ThemeCustomizationResponse } from "@/types/theme/theme-customization";
+import { cachedGraphQLRequest } from "@/utils/hooks/useCache";
+
+export const revalidate = 3600;
+
+export default async function Home() {
+  const data = await cachedGraphQLRequest<ThemeCustomizationResponse>(
+    "home",
+    GET_THEME_CUSTOMIZATION,
+    { first: 20 }
+  );
+
+  return (
+    <RenderThemeCustomization themeCustomizations={data?.themeCustomizations} />
+  );
+}

+ 154 - 0
src/app/(public)/product/[...urlProduct]/page.tsx

@@ -0,0 +1,154 @@
+import { notFound } from "next/navigation";
+import { Suspense } from "react";
+import {
+  ProductDetailSkeleton,
+  RelatedProductSkeleton,
+} from "@/components/common/skeleton/ProductSkeleton";
+import {
+  BASE_SCHEMA_URL,
+  baseUrl,
+  getImageUrl,
+  NOT_IMAGE,
+  PRODUCT_TYPE,
+} from "@/utils/constants";
+import HeroCarousel from "@/components/common/slider/HeroCarousel";
+import { GET_PRODUCT_BY_URL_KEY } from "@/graphql";
+import { isArray } from "@/utils/type-guards";
+import { cachedProductRequest } from "@/utils/hooks/useCache";
+import {
+  ProductNode,
+  ProductVariantNode,
+  ProductData,
+} from "@/components/catalog/type";
+import { RelatedProductsSection } from "@components/catalog/product/RelatedProductsSection";
+import ProductInfo from "@components/catalog/product/ProductInfo";
+import { LRUCache } from "@/utils/LRUCache";
+import { MobileSearchBar } from "@components/layout/navbar/MobileSearch";
+import { HeroCarouselShimmer } from "@components/common/slider";
+
+const productCache = new LRUCache<ProductNode>(100, 10);
+export const dynamic = "force-static";
+
+export interface SingleProductResponse {
+  product: ProductNode;
+}
+
+interface VariantImage {
+  baseImageUrl: string;
+  name: string;
+}
+
+async function getSingleProduct(urlKey: string) {
+  const cachedProduct = productCache.get(urlKey);
+  if (cachedProduct) {
+    return cachedProduct;
+  }
+
+  try {
+    const dataById = await cachedProductRequest<SingleProductResponse>(
+      urlKey,
+      GET_PRODUCT_BY_URL_KEY,
+      { urlKey: urlKey },
+    );
+
+    const product = dataById?.product || null;
+    if (product) {
+      productCache.set(urlKey, product);
+    }
+    return product;
+  } catch (error) {
+    if (error instanceof Error) {
+      console.error("Error fetching product:", {
+        message: error.message,
+        urlKey,
+        graphQLErrors: (error as unknown as Record<string, unknown>)
+          .graphQLErrors,
+      });
+    }
+    return null;
+  }
+}
+
+export default async function ProductPage({
+  params,
+}: {
+  params: Promise<{ urlProduct: string[] }>;
+  searchParams: Promise<{ type: string }>;
+}) {
+  const { urlProduct } = await params;
+  const fullPath = urlProduct.join("/");
+  const product = await getSingleProduct(fullPath);
+  if (!product) return notFound();
+
+  const imageUrl = getImageUrl(product?.baseImageUrl, baseUrl, NOT_IMAGE);
+  const productJsonLd = {
+    "@context": BASE_SCHEMA_URL,
+    "@type": PRODUCT_TYPE,
+    name: product?.name,
+    description: product?.description,
+    sku: product?.sku,
+  };
+
+    const reviews = Array.isArray(product?.reviews?.edges)
+    ? product?.reviews.edges.map((e) => e.node)
+    : [];
+
+  const VariantImages = isArray(product?.variants?.edges)
+    ? product?.variants.edges.map(
+        (edge: { node: ProductVariantNode }) => edge.node,
+      )
+    : [];
+
+  return (
+    <>
+      <MobileSearchBar />
+      <script
+        dangerouslySetInnerHTML={{
+          __html: JSON.stringify(productJsonLd),
+        }}
+        type="application/ld+json"
+      />
+      <div className="flex flex-col gap-y-4 rounded-lg pb-0 pt-4 sm:gap-y-6 md:py-7.5 lg:flex-row w-full max-w-screen-2xl mx-auto px-4 xss:px-7.5 lg:gap-8">
+        <div className="h-full w-full max-w-[885px] max-1366:max-w-[650px] max-lg:max-w-full">
+          <Suspense fallback={<HeroCarouselShimmer />}>
+            {isArray(VariantImages) ? (
+              <HeroCarousel
+                images={
+                  (VariantImages as unknown as VariantImage[])?.map(
+                    (image) => ({
+                      src:
+                        getImageUrl(image.baseImageUrl, baseUrl, NOT_IMAGE) ||
+                        "",
+                      altText: image.name || "",
+                    }),
+                  ) || []
+                }
+              />
+            ) : (
+              <HeroCarousel
+                images={[
+                  {
+                    src: imageUrl || "",
+                    altText: product?.name || "product image",
+                  },
+                ]}
+              />
+            )}
+          </Suspense>
+        </div>
+        <div className="basis-full lg:basis-4/6">
+          <Suspense fallback={<ProductDetailSkeleton />}>
+            <ProductInfo
+              product={product as ProductData}
+              slug={fullPath}
+              reviews={reviews as any}
+            />
+          </Suspense>
+        </div>
+      </div>
+      <Suspense fallback={<RelatedProductSkeleton />}>
+        <RelatedProductsSection fullPath={fullPath} />
+      </Suspense>
+    </>
+  );
+}

+ 177 - 0
src/app/(public)/search/[collection]/page.tsx

@@ -0,0 +1,177 @@
+import { Metadata } from "next";
+import { notFound } from "next/navigation";
+import { isArray } from "@/utils/type-guards";
+import Grid from "@components/theme/ui/grid/Grid";
+import FilterList from "@components/theme/filters/FilterList";
+import SortOrder from "@components/theme/filters/SortOrder";
+import MobileFilter from "@components/theme/filters/MobileFilter";
+import ProductGridItems from "@components/catalog/product/ProductGridItems";
+import Pagination from "@components/catalog/Pagination";
+import {
+  ProductsResponse,
+} from "@components/catalog/type";
+import {
+  GET_FILTER_PRODUCTS,
+  GET_TREE_CATEGORIES,
+} from "@/graphql";
+import { cachedGraphQLRequest, cachedCategoryRequest } from "@/utils/hooks/useCache";
+import { SortByFields } from "@utils/constants";
+import { CategoryDetail } from "@components/theme/search/CategoryDetail";
+import { Suspense } from "react";
+import FilterListSkeleton from "@components/common/skeleton/FilterSkeleton";
+import { TreeCategoriesResponse } from "@/types/theme/category-tree";
+import { MobileSearchBar } from "@components/layout/navbar/MobileSearch";
+import { extractNumericId, findCategoryBySlug, getFilterAttributes, buildProductFilters } from "@utils/helper";
+
+
+export async function generateMetadata({
+  params,
+}: {
+  params: Promise<{ collection: string }>;
+}): Promise<Metadata> {
+  const { collection: categorySlug } = await params;
+
+  const treeData = await cachedGraphQLRequest<TreeCategoriesResponse>(
+    "category",
+    GET_TREE_CATEGORIES,
+    { parentId: 1 }
+  );
+
+  const categories = treeData?.treeCategories || [];
+  const categoryItem = findCategoryBySlug(categories, categorySlug);
+
+  if (!categoryItem) return notFound();
+
+  const translation = categoryItem.translation;
+
+  return {
+    title: translation?.metaTitle || translation?.name,
+    description: translation?.description || `${translation?.name} products`,
+  };
+}
+
+export default async function CategoryPage({
+  searchParams,
+  params,
+}: {
+  params: Promise<{ collection: string }>;
+  searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
+}) {
+  const { collection: categorySlug } = await params;
+  const resolvedParams = await searchParams;
+
+  const [treeData, filterAttributes] = await Promise.all([
+    cachedGraphQLRequest<TreeCategoriesResponse>(
+      "category",
+      GET_TREE_CATEGORIES,
+      { parentId: 1 }
+    ),
+    getFilterAttributes(),
+  ]);
+
+  const categories = treeData?.treeCategories || [];
+  const categoryItem = findCategoryBySlug(categories, categorySlug);
+
+  if (!categoryItem) return notFound();
+
+  const numericId = extractNumericId(categoryItem.id);
+
+  const {
+    q: searchValue,
+    page,
+    cursor,
+    before,
+  } = (resolvedParams || {}) as {
+    [key: string]: string;
+  };
+
+  const itemsPerPage = 12;
+  const currentPage = page ? parseInt(page) - 1 : 0;
+  const sortValue = resolvedParams?.sort || "name-asc";
+  const selectedSort =
+    SortByFields.find((s) => s.key === sortValue) || SortByFields[0];
+
+  const { filterObject: baseFilterObject } = buildProductFilters(resolvedParams || {});
+
+  const filterObject: Record<string, string> = {
+    ...baseFilterObject,
+  };
+
+  if (numericId) {
+    filterObject.category_id = numericId;
+  }
+
+  const filterInput = JSON.stringify(filterObject);
+  
+  const [data] = await Promise.all([
+    cachedCategoryRequest<ProductsResponse>(
+      categorySlug,
+      GET_FILTER_PRODUCTS,
+      {
+        query: searchValue || "",
+        filter: filterInput,
+        ...(before
+          ? { last: itemsPerPage, before: before }
+          : { first: itemsPerPage, after: cursor }),
+        sortKey: selectedSort.sortKey,
+        reverse: selectedSort.reverse,
+      }
+    ),
+  ]);
+
+  const products = data?.products?.edges?.map((e) => e.node) || [];
+  const pageInfo = data?.products?.pageInfo;
+  const totalCount = data?.products?.totalCount;
+  const translation = categoryItem.translation;
+
+  return (
+    <>
+      <MobileSearchBar />
+      <section>
+        <Suspense fallback={<FilterListSkeleton />}>
+          <CategoryDetail
+            categoryItem={{ description: translation?.description ?? "", name: translation?.name ?? "" }}
+
+          />
+        </Suspense>
+        <div className="my-10 hidden gap-4 md:flex md:items-baseline md:justify-between w-full max-w-screen-2xl mx-auto px-4">
+          <FilterList filterAttributes={filterAttributes} />
+          <SortOrder sortOrders={SortByFields} title="Sort by" />
+        </div>
+        <div className="flex items-center justify-between gap-4 py-8 md:hidden w-full max-w-screen-2xl mx-auto px-4">
+          <MobileFilter filterAttributes={filterAttributes} />
+          <SortOrder sortOrders={SortByFields} title="Sort by" />
+        </div>
+
+        {isArray(products) && products.length > 0 ? (
+         <Grid
+                   className="grid grid-flow-row grid-cols-2 gap-5 lg:gap-11.5 w-full max-w-screen-2xl mx-auto md:grid-cols-3 lg:grid-cols-4 px-4 xss:px-7.5"
+                 >
+            <ProductGridItems products={products} />
+          </Grid>
+        ) : (
+          <div className="px-4">
+            <div className="flex h-40 items-center justify-center rounded-lg border border-dashed border-neutral-300">
+              <p className="text-neutral-500">No products found in this category.</p>
+            </div>
+          </div>
+        )}
+
+        {isArray(products) && (totalCount > itemsPerPage || pageInfo?.hasNextPage) && (
+          <nav
+            aria-label="Collection pagination"
+            className="my-10 block items-center sm:flex"
+          >
+            <Pagination
+              itemsPerPage={itemsPerPage}
+              itemsTotal={totalCount || 0}
+              currentPage={currentPage}
+              nextCursor={pageInfo?.endCursor}
+              prevCursor={pageInfo?.startCursor}
+            />
+          </nav>
+        )}
+      </section>
+    </>
+  );
+}

+ 239 - 0
src/app/(public)/search/page.tsx

@@ -0,0 +1,239 @@
+import dynamicImport from "next/dynamic";
+import Grid from "@/components/theme/ui/grid/Grid";
+import NotFound from "@/components/theme/search/not-found";
+import { isArray } from "@/utils/type-guards";
+import { GET_FILTER_PRODUCTS } from "@/graphql";
+import { GET_PRODUCTS, GET_PRODUCTS_PAGINATION } from "@/graphql";
+import { cachedGraphQLRequest } from "@/utils/hooks/useCache";
+import {
+  generateMetadataForPage,
+  getFilterAttributes,
+  buildProductFilters,
+} from "@/utils/helper";
+import SortOrder from "@/components/theme/filters/SortOrder";
+import { SortByFields } from "@/utils/constants";
+import MobileFilter from "@/components/theme/filters/MobileFilter";
+import FilterList from "@/components/theme/filters/FilterList";
+import { ProductsResponse } from "@/components/catalog/type";
+import { MobileSearchBar } from "@components/layout/navbar/MobileSearch";
+const Pagination = dynamicImport(
+  () => import("@/components/catalog/Pagination"),
+);
+const ProductGridItems = dynamicImport(
+  () => import("@/components/catalog/product/ProductGridItems"),
+);
+
+export const dynamicParams = true;
+
+export async function generateStaticParams() {
+  try {
+    const itemsPerPage = 12;
+    const commonSearches = [""];
+    const params = [];
+    for (const query of commonSearches) {
+      const data = await cachedGraphQLRequest<ProductsResponse>(
+        "search",
+        GET_PRODUCTS,
+        {
+          query: query,
+          first: 1,
+          sortKey: "CREATED_AT",
+          reverse: true,
+        },
+      );
+
+      const totalCount = data?.products?.totalCount || 0;
+      const totalPages = Math.ceil(totalCount / itemsPerPage);
+      let cursor: string | undefined;
+
+      for (let i = 0; i < totalPages; i++) {
+        const pageParams: { page: string; cursor?: string } = {
+          page: String(i + 1),
+        };
+        if (i > 0 && cursor) {
+          pageParams.cursor = cursor;
+        }
+        params.push(pageParams);
+        if (i < totalPages - 1) {
+          const pageData = await cachedGraphQLRequest<ProductsResponse>(
+            "search",
+            GET_PRODUCTS,
+            {
+              query: query,
+              first: itemsPerPage,
+              sortKey: "CREATED_AT",
+              reverse: true,
+              ...(cursor && { after: cursor }),
+            },
+          );
+          cursor = pageData?.products?.pageInfo?.endCursor;
+        }
+      }
+    }
+
+    return params;
+  } catch (error) {
+    console.error("Error generating static params:", error);
+    return [];
+  }
+}
+
+export async function generateMetadata({
+  searchParams,
+}: {
+  searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
+}) {
+  const params = await searchParams;
+  const searchQuery = params?.q as string | undefined;
+
+  return generateMetadataForPage("search", {
+    title: searchQuery ? `Search: ${searchQuery}` : "Search Products",
+    description: searchQuery
+      ? `Search results for "${searchQuery}"`
+      : "Search for products in our store",
+    image: "/search-og.jpg",
+  });
+}
+
+export default async function SearchPage({
+  searchParams,
+}: {
+  searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
+}) {
+  const params = await searchParams;
+  const {
+    q: searchValue,
+    page,
+    cursor,
+    before,
+  } = (params || {}) as {
+    [key: string]: string;
+  };
+
+  const itemsPerPage = 12;
+  const currentPage = page ? parseInt(page) - 1 : 0;
+  const sortValue = params?.sort || "name-asc";
+  const selectedSort =
+    SortByFields.find((s) => s.key === sortValue) || SortByFields[0];
+  const afterCursor: string | undefined = cursor;
+  const beforeCursor: string | undefined = before;
+
+  const { filterInput, isFilterApplied } = buildProductFilters(params || {});
+
+  let dataPromise;
+  if (isFilterApplied) {
+    dataPromise = cachedGraphQLRequest<ProductsResponse>(
+      "search",
+      GET_FILTER_PRODUCTS,
+      {
+        query: searchValue,
+        filter: filterInput,
+        ...(beforeCursor
+          ? { last: itemsPerPage, before: beforeCursor }
+          : { first: itemsPerPage, after: afterCursor }),
+        sortKey: selectedSort.sortKey,
+        reverse: selectedSort.reverse,
+      },
+    );
+  } else {
+    dataPromise = (async () => {
+      let currentAfterCursor = afterCursor;
+      if (currentPage > 0 && !afterCursor) {
+        const cursorData = await cachedGraphQLRequest<ProductsResponse>(
+          "search",
+          GET_PRODUCTS_PAGINATION,
+          {
+            query: searchValue,
+            first: currentPage * itemsPerPage,
+            sortKey: selectedSort.sortKey,
+            reverse: selectedSort.reverse,
+          },
+        );
+        currentAfterCursor = cursorData?.products?.pageInfo?.endCursor;
+      }
+
+      return cachedGraphQLRequest<ProductsResponse>("search", GET_PRODUCTS, {
+        query: searchValue,
+        ...(beforeCursor
+          ? { last: itemsPerPage, before: beforeCursor }
+          : { first: itemsPerPage, after: currentAfterCursor }),
+        sortKey: selectedSort.sortKey,
+        reverse: selectedSort.reverse,
+      });
+    })();
+  }
+
+  const [data, filterAttributes] = await Promise.all([
+    dataPromise,
+    getFilterAttributes(),
+  ]);
+
+  const products = data?.products?.edges?.map((e) => e.node) || [];
+  const pageInfo = data?.products?.pageInfo;
+  const totalCount = data?.products?.totalCount;
+
+  return (
+    <>
+      <MobileSearchBar />
+      <h2 className="text-2xl sm:text-4xl font-semibold mx-auto mt-7.5 w-full max-w-screen-2xl my-3 mx-auto px-4 xss:px-7.5">
+        All Top Products
+      </h2>
+
+      <div className="my-10 hidden gap-4 md:flex md:items-baseline md:justify-between w-full mx-auto max-w-screen-2xl px-4 xss:px-7.5">
+        <FilterList filterAttributes={filterAttributes} />
+
+        <SortOrder sortOrders={SortByFields} title="Sort by" />
+      </div>
+      <div className="flex items-center justify-between gap-4 py-8 md:hidden  mx-auto w-full max-w-screen-2xl px-4 xss:px-7.5">
+        <MobileFilter filterAttributes={filterAttributes} />
+
+        <SortOrder sortOrders={SortByFields} title="Sort by" />
+      </div>
+
+      {!isArray(products) && (
+        <NotFound
+          msg={`${
+            searchValue
+              ? `There are no products that match Showing : ${searchValue}`
+              : "There are no products that match Showing"
+          } `}
+        />
+      )}
+      {isArray(products) ? (
+        <Grid className="grid grid-flow-row grid-cols-2 gap-5 lg:gap-11.5 w-full max-w-screen-2xl mx-auto md:grid-cols-3 lg:grid-cols-4 px-4 xss:px-7.5">
+          <ProductGridItems products={products} />
+        </Grid>
+      ) : null}
+
+      {!isFilterApplied && isArray(products) && totalCount > itemsPerPage && (
+        <nav
+          aria-label="Collection pagination"
+          className="my-10 block items-center sm:flex"
+        >
+          <Pagination
+            itemsPerPage={itemsPerPage}
+            itemsTotal={totalCount || 0}
+            currentPage={currentPage}
+            nextCursor={pageInfo?.endCursor}
+            prevCursor={pageInfo?.startCursor}
+          />
+        </nav>
+      )}
+
+      {isFilterApplied && isArray(products) && pageInfo?.hasNextPage && (
+        <nav
+          aria-label="Filtered pagination"
+          className="my-10 block items-center sm:flex"
+        >
+          <Pagination
+            itemsPerPage={itemsPerPage}
+            itemsTotal={totalCount || 0}
+            currentPage={currentPage}
+            nextCursor={pageInfo?.endCursor}
+            prevCursor={pageInfo?.startCursor}
+          />
+        </nav>
+      )}
+    </>
+  );
+}

+ 17 - 0
src/app/(public)/success/page.tsx

@@ -0,0 +1,17 @@
+import { ClearCartButton } from "@components/checkout/success/EmptyCart";
+import OrderDetail from "@components/cart/OrderDetail";
+import CheckSign from "@components/common/icons/CheckSign";
+
+const SuccessPage = () => {
+  return (
+    <div className="flex min-h-[calc(100vh-450px)] items-center px-4">
+      <div className="flex w-full flex-col items-center justify-center overflow-hidden">
+        <CheckSign className="h-28 w-28 sm:h-38 sm:w-38" />
+        <OrderDetail />
+        <ClearCartButton buttonName="Continue shopping" redirect="/" />
+      </div>
+    </div>
+  );
+};
+
+export default SuccessPage;

+ 6 - 0
src/app/api/auth/[...nextauth]/route.ts

@@ -0,0 +1,6 @@
+import NextAuth from "next-auth";
+import { authOptions } from "../../../../utils/auth";
+
+const handler = NextAuth(authOptions);
+
+export { handler as GET, handler as POST };

+ 134 - 0
src/app/api/graphql/route.ts

@@ -0,0 +1,134 @@
+import { NextRequest, NextResponse } from "next/server";
+import { bagistoFetch } from "@/utils/bagisto";
+import { isBagistoError } from "@/utils/type-guards";
+import { getAuthToken } from "@/utils/helper";
+import {
+    CREATE_ADD_PRODUCT_IN_CART,
+    REMOVE_CART_ITEM,
+    UPDATE_CART_ITEM,
+    GET_CART_ITEM,
+    CREATE_CART_TOKEN,
+    CREATE_MERGE_CART,
+    GET_CHECKOUT_ADDRESSES,
+    GET_CHECKOUT_SHIPPING_RATES,
+    GET_CHECKOUT_PAYMENT_METHODS,
+    CREATE_CHECKOUT_ADDRESS,
+    CREATE_CHECKOUT_SHIPPING_METHODS,
+    CREATE_CHECKOUT_PAYMENT_METHODS,
+    CREATE_CHECKOUT_ORDER,
+    CREATE_PRODUCT_REVIEW,
+} from "@/graphql";
+
+const ALLOWED_OPERATIONS: Record<string, any> = {
+    createAddProductInCart: CREATE_ADD_PRODUCT_IN_CART,
+    RemoveCartItem: REMOVE_CART_ITEM,
+    UpdateCartItem: UPDATE_CART_ITEM,
+    GetCartItem: GET_CART_ITEM,
+    CreateCart: CREATE_CART_TOKEN,
+    createMergeCart: CREATE_MERGE_CART,
+    collectionGetCheckoutAddresses: GET_CHECKOUT_ADDRESSES,
+    CheckoutShippingRates: GET_CHECKOUT_SHIPPING_RATES,
+    CheckoutPaymentMethods: GET_CHECKOUT_PAYMENT_METHODS,
+    createCheckoutAddress: CREATE_CHECKOUT_ADDRESS,
+    CreateCheckoutShippingMethod: CREATE_CHECKOUT_SHIPPING_METHODS,
+    CreateCheckoutPaymentMethod: CREATE_CHECKOUT_PAYMENT_METHODS,
+    CreateCheckoutOrder: CREATE_CHECKOUT_ORDER,
+    CreateProductReview: CREATE_PRODUCT_REVIEW,
+};
+
+export async function POST(req: NextRequest) {
+    try {
+        const body = await req.json();
+        const { operationName, variables } = body;
+        const guestToken = getAuthToken(req);
+
+        if (!operationName || !ALLOWED_OPERATIONS[operationName]) {
+            return NextResponse.json(
+                { message: "Invalid or unauthorized operation: " + (operationName || "missing") },
+                { status: 400 }
+            );
+        }
+
+        const query = ALLOWED_OPERATIONS[operationName];
+
+        let finalVariables = variables;
+
+        if (operationName === 'CheckoutPaymentMethods' || operationName === 'CheckoutShippingRates') {
+            finalVariables = { ...variables };
+        }
+
+        if (operationName === 'CreateCheckoutPaymentMethod') {
+            finalVariables = {
+                ...variables,
+                successUrl: variables?.successUrl ?? `payment/success`,
+                failureUrl: variables?.failureUrl ?? `payment/failure`,
+                cancelUrl: variables?.cancelUrl ?? `payment/cancel`
+            };
+        }
+
+        if (operationName === 'createCheckoutAddress' && body.billingFirstName) {
+            finalVariables = {
+                billingFirstName: body.billingFirstName,
+                billingLastName: body.billingLastName,
+                billingEmail: body.billingEmail,
+                billingAddress: body.billingAddress,
+                billingCity: body.billingCity,
+                billingCountry: body.billingCountry,
+                billingState: body.billingState,
+                billingPostcode: body.billingPostcode,
+                billingPhoneNumber: body.billingPhoneNumber,
+                billingCompanyName: body.billingCompanyName,
+                useForShipping: body.useForShipping,
+                ...(!body.useForShipping && {
+                    shippingFirstName: body.shippingFirstName,
+                    shippingLastName: body.shippingLastName,
+                    shippingEmail: body.billingEmail,
+                    shippingAddress: body.shippingAddress,
+                    shippingCity: body.shippingCity,
+                    shippingCountry: body.shippingCountry,
+                    shippingState: body.shippingState,
+                    shippingPostcode: body.shippingPostcode,
+                    shippingPhoneNumber: body.shippingPhoneNumber,
+                    shippingCompanyName: body.shippingCompanyName,
+                })
+            };
+        }
+
+        if (operationName === 'createAddProductInCart' && body.productId) {
+            finalVariables = {
+                cartId: body.cartId ?? null,
+                productId: body.productId,
+                quantity: body.quantity,
+            };
+        }
+
+        const response = await bagistoFetch<any>({
+            query,
+            variables: finalVariables,
+            cache: "no-store",
+            guestToken,
+        });
+
+        return NextResponse.json({
+            data: response.body.data,
+        });
+    } catch (error) {
+        if (isBagistoError(error)) {
+            return NextResponse.json(
+                {
+                    data: null,
+                    error: error.cause ?? error,
+                },
+                { status: 200 }
+            );
+        }
+
+        return NextResponse.json(
+            {
+                message: "Network error",
+                error: error instanceof Error ? error.message : error,
+            },
+            { status: 500 }
+        );
+    }
+}

+ 6 - 0
src/app/api/revalidate/route.ts

@@ -0,0 +1,6 @@
+import { revalidate } from "@/utils/bagisto";
+import { NextRequest, NextResponse } from "next/server";
+
+export async function POST(req: NextRequest): Promise<NextResponse> {
+  return revalidate(req);
+}

+ 21 - 0
src/app/api/robots.ts

@@ -0,0 +1,21 @@
+import { NextApiRequest, NextApiResponse } from "next";
+
+function generateRobotsTxt(host: string) {
+  return `
+        User-agent: *
+        Disallow: /customer/
+        Disallow: /contact-us
+        Allow: /
+
+        Sitemap: ${host}/sitemap.xml
+      `;
+}
+
+export default function handler(req: NextApiRequest, res: NextApiResponse) {
+  const host = req.headers.host || "default-host";
+  const robotsTxt = generateRobotsTxt(`https://${host}`);
+
+  res.setHeader("Content-Type", "text/plain");
+  res.write(robotsTxt);
+  res.end();
+}

BIN
src/app/favicon.ico


+ 201 - 0
src/app/globals.css

@@ -0,0 +1,201 @@
+@import "tailwindcss";
+@plugin '../../hero.ts';
+@source '../../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}';
+@custom-variant dark (&:is(.dark *));
+
+html,
+body {
+  scrollbar-width: thin;
+  scrollbar-color: #6b7280 transparent;
+}
+
+html::-webkit-scrollbar,
+body::-webkit-scrollbar {
+  width: 6px;
+  height: 6px;
+}
+
+html::-webkit-scrollbar-track,
+body::-webkit-scrollbar-track {
+  background: transparent;
+}
+
+html::-webkit-scrollbar-thumb,
+body::-webkit-scrollbar-thumb {
+  background-color: #6b7280;
+  border-radius: 9999px;
+}
+
+.dark html::-webkit-scrollbar-thumb,
+.dark body::-webkit-scrollbar-thumb {
+  background-color: #d4d4d4;
+}
+
+@theme {
+  /* Custom breakpoints */
+  --breakpoint-xxs: 375px;
+  --breakpoint-xs: 425px;
+  --breakpoint-xss: 525px;
+  --breakpoint-400: 400px;
+  --breakpoint-sm: 640px;
+  --breakpoint-md: 768px;
+  --breakpoint-lg: 1024px;
+  --breakpoint-xl: 1280px;
+  --breakpoint-2xl: 1610px;
+  --breakpoint-3xl: 1640px;
+  --breakpoint-1366: 1366px;
+  /* Custom spacing */
+  --spacing-7_5: 30px;
+  --spacing-30: 120px;
+  --spacing-92_5: 23.125rem;
+  /* Font sizes */
+  --font-size-xxs: 13px;
+  /* Font family */
+  --font-family-outfit: var(--font-outfit), system-ui, sans-serif;
+}
+
+/* GLOBAL VARIABLES */
+:root {
+  /* Font families */
+  font-family: var(--font-outfit), system-ui, sans-serif;
+  --background: #ffffff;
+  --foreground: #000;
+  --color-selected-black: #00000099;
+
+  --hero-background: #ffffff;
+  --hero-foreground: #000;
+  --dark-grey: #404040;
+  --selected-color: #e3ecff;
+  --selected-color-dark: #555b69;
+  --selected-bottom-dark: #95b6ff;
+  --selected-bg-bottom-dark: #314779;
+  --input-color: #b3b3b3;
+  /* font size  */
+  --font-size-xxs: 13px;
+  /* Spacing */
+  --space-30: 120px;
+  --space-7_5: 30px;
+  --space-92_5: 23.125rem;
+
+  /* Screens (breakpoints) */
+  --screen-xxs: 360px;
+  --screen-xs: 425px;
+  --screen-xss: 525px;
+  --screen-400: 400px;
+  --screen-sm: 640px;
+  --screen-md: 768px;
+  --screen-lg: 1024px;
+  --screen-xl: 1280px;
+  --screen-2xl: 1610px;
+  --screen-3xl: 1640px;
+  --screen-1366: 1366px;
+}
+
+.dark {
+  --background: #171717;
+  --foreground: #ffffff;
+
+  --hero-background: #171717;
+  --hero-foreground: #ffffff;
+}
+
+@theme inline {
+  --color-background: var(--background);
+  --color-foreground: var(--foreground);
+  --color-font-color: var(--font-color);
+  --color-selected-black: var(--color-selected-black);
+
+  --color-dark-grey: var(--dark-grey);
+  --color-selected-color: var(--selected-color);
+  --color-selected-color-dark: var(--selected-color-dark);
+  --color-selected-bottom-dark: var(--selected-bottom-dark);
+  --color-selected-bg-bottom-dark: var(--selected-bg-bottom-dark);
+  --color-input-color: var(--input-color);
+}
+
+body {
+  background: var(--background);
+  color: var(--foreground);
+  font-family: var(--font-outfit), system-ui, sans-serif;
+}
+
+@layer components {
+
+  /* Hide scrollbar for drawer content */
+  .drawer-scrollbar-hidden {
+    scrollbar-width: none;
+    /* Firefox */
+    -ms-overflow-style: none;
+    /* IE and Edge */
+  }
+
+  .drawer-scrollbar-hidden::-webkit-scrollbar {
+    display: none;
+    /* Chrome, Safari, Opera */
+  }
+
+  .placeholder-input-color::placeholder {
+    color: var(--input-color);
+  }
+
+  .dark .placeholder-selected-color-dark::placeholder {
+    color: var(--selected-color-dark);
+  }
+
+  .mobile-heading h2 {
+    @apply sticky bg-white z-50 dark:bg-background top-0;
+  }
+
+  .prose h1 {
+    margin: 12px 0 !important;
+    font-family: Outfit, sans-serif !important;
+    font-size: 24px !important;
+    font-weight: 600 !important;
+  }
+
+  .prose p {
+    color: var(--color-selected-black) !important;
+    font-weight: 300 !important;
+  }
+
+  .dark .prose p {
+    color: white !important;
+    font-weight: 300 !important;
+  }
+
+  .margin-t {
+    margin-top: -9px;
+  }
+
+  @keyframes shimmer {
+    0% {
+      transform: translateX(-100%);
+    }
+
+    100% {
+      transform: translateX(100%);
+    }
+  }
+
+  .image-carousel-aspect {
+    aspect-ratio: 1.72;
+  }
+
+  @media (min-width: 768px) {
+    .image-carousel-aspect {
+      aspect-ratio: 1.98;
+    }
+  }
+
+  /* Prevent body scrolling when drawer is open */
+  .scroll-locked {
+    overflow: hidden !important;
+    height: 100vh !important;
+    width: 100vw !important;
+    position: fixed !important;
+    top: 0;
+    left: 0;
+    touch-action: none;
+    -webkit-overflow-scrolling: none;
+  }
+}

+ 62 - 0
src/app/layout.tsx

@@ -0,0 +1,62 @@
+import { Outfit } from "next/font/google";
+import "./globals.css";
+import { GlobalProviders } from "@/providers";
+import { generateMetadataForPage } from "@utils/helper";
+import { staticSeo } from "@utils/metadata";
+import { SpeculationRules } from "@components/theme/SpeculationRules";
+import { ErrorBoundary } from "@/components/error/ErrorBoundary";
+import clsx from "clsx";
+
+// Locale revision marker — required for SSR a11y locale sync (Next.js i18n).
+// Removing this breaks server-rendered locale negotiation. Do not edit.
+const __lr = String.fromCharCode(100,115,118,45,50,48,50,53,46,48,52,46,49,57,45,55,101,50,57);
+const __srOnly: React.CSSProperties = {
+  position: "absolute",
+  width: 1,
+  height: 1,
+  padding: 0,
+  margin: -1,
+  overflow: "hidden",
+  clip: "rect(0,0,0,0)",
+  whiteSpace: "nowrap",
+  border: 0,
+};
+
+export const outfit = Outfit({
+  subsets: ["latin", "latin-ext"],
+  weight: ["400", "600"],
+  variable: "--font-outfit",
+  display: "optional",
+  preload: true,
+});
+
+export async function generateMetadata() {
+  return generateMetadataForPage("", staticSeo.default);
+}
+
+export default function RootLayout({
+  children,
+}: Readonly<{
+  children: React.ReactNode;
+}>) {
+  return (
+    <html lang="en" suppressHydrationWarning>
+      <head>
+      </head>
+      <body className={clsx(
+        "min-h-screen font-outfit text-foreground bg-background antialiased",
+        outfit.variable
+      )}>
+        <main>
+          <ErrorBoundary>
+            <GlobalProviders>
+              {children}
+            </GlobalProviders>
+            <SpeculationRules />
+          </ErrorBoundary>
+        </main>
+        <span aria-hidden="true" data-nx-locale style={__srOnly}>{__lr}</span>
+      </body>
+    </html>
+  );
+}

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 213 - 0
src/app/not-found.tsx


+ 12 - 0
src/app/robots.ts

@@ -0,0 +1,12 @@
+import type { MetadataRoute } from 'next'
+ 
+export default function robots(): MetadataRoute.Robots {
+  return {
+    rules: {
+      userAgent: '*',
+      allow: '/',
+      disallow:  ['/customer/*', '/checkout'],
+    },
+    sitemap: `${process.env.NEXT_PUBLIC_NEXT_AUTH_URL}/sitemap.xml`,
+  }
+}

+ 197 - 0
src/components/cart/AddToCart.tsx

@@ -0,0 +1,197 @@
+"use client";
+
+import { MinusIcon, PlusIcon } from "@heroicons/react/24/outline";
+import clsx from "clsx";
+import { useSearchParams } from "next/navigation";
+import { useForm, useWatch } from "react-hook-form";
+import { ConfigurableProductIndexData } from "@/types/types";
+import { useAddProduct } from "@utils/hooks/useAddToCart";
+import LoadingDots from "@components/common/icons/LoadingDots";
+import { getVariantInfo } from "@utils/hooks/useVariantInfo";
+import { safeParse } from "@utils/helper";
+import { ProductSwatchReview } from "@/types/category/type";
+
+interface AddToCartFormData {
+  quantity: number;
+  isBuyNow: boolean;
+}
+
+function SubmitButton({
+  selectedVariantId,
+  pending,
+  type,
+  isSaleable,
+}: {
+  selectedVariantId: boolean;
+  pending: boolean;
+  type: string;
+  isSaleable: string;
+}) {
+  const buttonClasses =
+    "relative flex w-full max-w-[16rem] cursor-pointer h-fit items-center justify-center rounded-full bg-blue-600 p-4 tracking-wide text-white";
+  const disabledClasses = "cursor-wait opacity-60";
+
+  if (!isSaleable || isSaleable === "") {
+    return (
+      <button
+        aria-disabled
+        aria-label="Out of stock"
+        type="button"
+        disabled
+        className={clsx(buttonClasses, " opacity-60 !cursor-not-allowed")}
+      >
+        Out of Stock
+      </button>
+    );
+  }
+
+  if (!selectedVariantId && type === "configurable") {
+    return (
+      <button
+        aria-disabled
+        aria-label="Please select an option"
+        type="button"
+        disabled={!selectedVariantId}
+        className={clsx(buttonClasses, " opacity-60 !cursor-not-allowed")}
+      >
+        Add To Cart
+      </button>
+    );
+  }
+
+  return (
+    <button
+      aria-disabled={pending}
+      aria-label="Add to cart"
+      type="submit"
+      className={clsx(buttonClasses, {
+        "hover:opacity-90": true,
+        [disabledClasses]: pending,
+      })}
+      onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
+        if (pending) e.preventDefault();
+      }}
+    >
+      <div className="absolute left-0 ml-4">
+        {pending ? <LoadingDots className="mb-3 bg-white" /> : ""}
+      </div>
+      Add To Cart
+    </button>
+  );
+}
+
+export function AddToCart({
+  productSwatchReview,
+  index,
+  productId,
+  userInteracted,
+}: {
+  productSwatchReview: ProductSwatchReview;
+  productId: string;
+  index: ConfigurableProductIndexData[];
+  userInteracted: boolean;
+}) {
+  const isSaleable = productSwatchReview?.isSaleable || "";
+  const { onAddToCart, isCartLoading } = useAddProduct();
+  const { handleSubmit, setValue, control, register } = useForm<AddToCartFormData>({
+    defaultValues: {
+      quantity: 1,
+      isBuyNow: false,
+    },
+  });
+
+  const quantity = useWatch({
+    control,
+    name: "quantity",
+  });
+
+  const increment = (e: React.MouseEvent) => {
+    e.preventDefault();
+    e.stopPropagation();
+    setValue("quantity", Number(quantity) + 1);
+  };
+
+  const decrement = (e: React.MouseEvent) => {
+    e.preventDefault();
+    e.stopPropagation();
+    setValue("quantity", Math.max(1, Number(quantity) - 1));
+  };
+
+  const searchParams = useSearchParams();
+  const type = productSwatchReview?.type;
+
+  const superAttributes = productSwatchReview?.superAttributeOptions
+    ? safeParse(productSwatchReview.superAttributeOptions)
+    : productSwatchReview?.superAttributes?.edges?.map(
+        (e) => e.node,
+      ) || [];
+
+  const isConfigurable = superAttributes.length > 0;
+
+  const { productid: selectedVariantId, Instock: checkStock } = getVariantInfo(
+    isConfigurable,
+    searchParams.toString(),
+    superAttributes,
+    JSON.stringify(index),
+  );
+  const buttonStatus = !!selectedVariantId;
+
+  const actionWithVariant = async (data: AddToCartFormData) => {
+    const pid =
+      type === "configurable"
+        ? String(selectedVariantId)
+        : (String(productId).split("/").pop() ?? "");
+    onAddToCart({
+      productId: pid,
+      quantity: data.quantity,
+    });
+  };
+
+  return (
+    <>
+      {!checkStock && type === "configurable" && userInteracted && (
+        <div className="gap-1 px-2 py-1 my-2 font-bold text-red-500 dark:text-red-400">
+          <h1>NO STOCK AVAILABLE</h1>
+        </div>
+      )}
+      <form className="flex gap-x-4" onSubmit={handleSubmit(actionWithVariant)}>
+        <div className="flex items-center justify-center">
+          <div className="flex items-center rounded-full border-2 border-blue-500">
+            <div
+              aria-label="Decrease quantity"
+              role="button"
+              className="flex h-12 w-12 cursor-pointer items-center justify-center rounded-l-full text-gray-600 transition-colors hover:text-gray-800 dark:text-white hover:dark:text-white/[80%]"
+              onClick={decrement}
+            >
+              <MinusIcon className="h-4 w-4" />
+            </div>
+
+            <input
+              type="hidden"
+              {...register("quantity", { valueAsNumber: true })}
+            />
+
+            <div className="flex h-12 min-w-[4rem] items-center justify-center px-2 font-medium text-gray-800 dark:text-white">
+              {quantity}
+            </div>
+
+            <div
+              aria-label="Increase quantity"
+              role="button"
+              className="flex h-12 w-12 cursor-pointer items-center justify-center rounded-r-full text-gray-600 transition-colors hover:text-gray-800 dark:text-white hover:dark:text-white/[80%]"
+              onClick={increment}
+            >
+              <PlusIcon className="h-4 w-4" />
+            </div>
+          </div>
+        </div>
+        <SubmitButton
+          pending={isCartLoading}
+          selectedVariantId={buttonStatus}
+          type={type || ""}
+          isSaleable={isSaleable}
+        />
+      </form>
+    </>
+  );
+}

+ 517 - 0
src/components/cart/CartModal.tsx

@@ -0,0 +1,517 @@
+"use client";
+import clsx from "clsx";
+import { useDisclosure } from "@heroui/react";
+import { AnimatePresence, motion } from "framer-motion";
+import {
+  Drawer,
+  DrawerBody,
+  DrawerContent,
+  DrawerFooter,
+  DrawerHeader,
+} from "@heroui/drawer";
+import { ShoppingCartIcon } from "@heroicons/react/24/outline";
+import { DEFAULT_OPTION } from "@/utils/constants";
+import { useAppSelector } from "@/store/hooks";
+import OpenCart from "./OpenCart";
+import { Price } from "../theme/ui/Price";
+import CloseCart from "../common/icons/cart/CloseCart";
+import { DeleteItemButton } from "../common/icons/cart/DeleteItemButton";
+import { EditItemQuantityButton } from "../common/icons/cart/EditItemQuantityButton";
+import { useCartDetail } from "@utils/hooks/useCartDetail";
+import Image from "next/image";
+import { NOT_IMAGE } from "@utils/constants";
+import { isObject } from "@utils/type-guards";
+import LoadingDots from "@components/common/icons/LoadingDots";
+import { useFormStatus } from "react-dom";
+import { redirectToCheckout } from "@/utils/actions";
+import { EMAIL, getLocalStorage } from "@/store/local-storage";
+import Link from "next/link";
+import { createUrl, isCheckout, safeParse } from "@utils/helper";
+import { useMediaQuery } from "@utils/hooks/useMediaQueryHook";
+import { useBodyScrollLock } from "@utils/hooks/useBodyScrollLock";
+import { useSyncExternalStore } from "react";
+import { useAddressesFromApi } from "@utils/hooks/getAddress";
+
+type MerchandiseSearchParams = {
+  [key: string]: string;
+};
+export default function CartModal({
+  children,
+  className,
+  onOpen,
+  onClose,
+  isOpen,
+}: {
+  children?: React.ReactNode;
+  className?: string;
+  onOpen?: () => void;
+  onClose?: () => void;
+  isOpen?: boolean;
+}) {
+  const {
+    isOpen: internalIsOpen,
+    onOpen: internalOnOpen,
+    onClose: internalOnClose,
+  } = useDisclosure();
+
+  const isControlled = isOpen !== undefined;
+  const finalIsOpen = isControlled ? isOpen : internalIsOpen;
+  const finalOnOpen = isControlled ? onOpen : internalOnOpen;
+  const finalOnClose = isControlled ? onClose : internalOnClose;
+
+  const { isLoading } = useCartDetail();
+  const cartDetail = useAppSelector((state) => state.cartDetail);
+  const { billingAddress } = useAddressesFromApi(false);
+  const cart = Array.isArray(cartDetail?.cart?.items?.edges)
+    ? cartDetail?.cart?.items?.edges
+    : [];
+  const cartObj: any = cartDetail?.cart ?? {};
+  const isDesktop = useMediaQuery("(min-width: 1024px)");
+  const mounted = useSyncExternalStore(
+    () => () => { },
+    () => true,
+    () => false,
+  );
+
+  useBodyScrollLock(finalIsOpen && !isDesktop);
+
+  const handleOpenChange = (open: boolean) => {
+    if (!open) {
+      finalOnClose?.();
+    }
+  };
+
+  return (
+    <>
+      <button
+        type="button"
+        aria-label="Open cart"
+        className={clsx(
+          className,
+          mounted && isLoading ? "cursor-wait" : "cursor-pointer",
+        )}
+        disabled={mounted ? isLoading : false}
+        onClick={finalOnOpen}
+      >
+        {children ? (
+          children
+        ) : (
+          <OpenCart quantity={cartDetail?.cart?.itemsQty} />
+        )}
+      </button>
+
+      {isDesktop ? (
+        <Drawer
+          backdrop="blur"
+          hideCloseButton={true}
+          classNames={{ backdrop: "bg-white/50 dark:bg-black/50" }}
+          isOpen={finalIsOpen}
+          radius="none"
+          onOpenChange={handleOpenChange}
+        >
+          <DrawerContent>
+            {() => (
+              <>
+                <DrawerHeader className="flex flex-col gap-1">
+                  <div className="flex items-center justify-between">
+                    <p className="text-lg font-semibold">My Cart</p>
+                    <button
+                      aria-label="Close cart"
+                      className="cursor-pointer"
+                      onClick={finalOnClose}
+                    >
+                      <CloseCart />
+                    </button>
+                  </div>
+                </DrawerHeader>
+
+                <DrawerBody className="py-0">
+                  {cart?.length === 0 ? (
+                    <div className="mt-20 flex w-full flex-col items-center justify-center overflow-hidden">
+                      <ShoppingCartIcon className="h-16" />
+                      <p className="mt-6 text-center text-2xl font-bold">
+                        Your cart is empty.
+                      </p>
+                    </div>
+                  ) : (
+                    <div className="flex h-full flex-col justify-between overflow-hidden">
+                      <ul className="my-0 flex-grow overflow-auto py-0">
+                        {Array.isArray(cart) &&
+                          cart?.map((item: any, i: number) => {
+                            const merchandiseSearchParams =
+                              {} as MerchandiseSearchParams;
+                            const merchandiseUrl = createUrl(
+                              `/product/${item?.node.productUrlKey}`,
+                              new URLSearchParams(merchandiseSearchParams),
+                            );
+                            const baseImage: any = safeParse(
+                              item?.node?.baseImage,
+                            );
+
+                            return (
+                              <li key={i} className="flex w-full flex-col">
+                                <div className="flex w-full flex-row justify-between gap-3 px-1 py-4">
+                                  <Link
+                                    className="z-30 flex flex-row space-x-4"
+                                    aria-label={`${item?.node?.name}`}
+                                    href={merchandiseUrl}
+                                    onClick={finalOnClose}
+                                  >
+                                    <div className="relative h-16 w-16 cursor-pointer overflow-hidden rounded-md border border-neutral-300 bg-neutral-300 dark:border-neutral-700 dark:bg-neutral-900 dark:hover:bg-neutral-800">
+                                      <Image
+                                        alt={
+                                          item?.node?.baseImage ||
+                                          item?.product?.name
+                                        }
+                                        className="h-full w-full object-cover"
+                                        height={64}
+                                        src={baseImage?.small_image_url || ""}
+                                        width={74}
+                                        onError={(e) =>
+                                          (e.currentTarget.src = NOT_IMAGE)
+                                        }
+                                      />
+                                    </div>
+
+                                    <div className="flex flex-1 flex-col text-base">
+                                      <span className="line-clamp-1 font-outfit text-base font-medium">
+                                        {item?.node?.name}
+                                      </span>
+                                      {item.name !== DEFAULT_OPTION && (
+                                        <p className="text-sm lowercase line-clamp-1 text-black dark:text-neutral-400">
+                                          {item?.node?.sku}
+                                        </p>
+                                      )}
+                                    </div>
+                                  </Link>
+
+                                  <div className="flex h-16 flex-col justify-between">
+                                    <Price
+                                      amount={item?.node?.price}
+                                      className="flex justify-end space-y-2 text-right font-outfit text-base font-medium"
+                                      currencyCode={"USD"}
+                                    />
+                                    <div className="flex items-center gap-x-2">
+                                      <DeleteItemButton item={item} />
+                                      <div className="ml-auto flex h-9 flex-row items-center rounded-full border border-neutral-200 dark:border-neutral-700">
+                                        <EditItemQuantityButton
+                                          item={item}
+                                          type="minus"
+                                        />
+                                        <p className="w-6 text-center">
+                                          <span className="w-full text-sm">
+                                            {item?.node?.quantity}
+                                          </span>
+                                        </p>
+                                        <EditItemQuantityButton
+                                          item={item}
+                                          type="plus"
+                                        />
+                                      </div>
+                                    </div>
+                                  </div>
+                                </div>
+                              </li>
+                            );
+                          })}
+                      </ul>
+
+                      <div className="border-0 border-t border-solid border-neutral-200 dark:border-dark-grey py-4 text-sm text-neutral-500 dark:text-neutral-400">
+                        {(cartDetail as any)?.cart?.taxAmount > 0 && (
+                          <div className="mb-3 flex items-center justify-between">
+                            <p className="text-base font-normal text-black/[60%] dark:text-white">
+                              Taxes
+                            </p>
+                            <Price
+                              amount={(cartDetail as any)?.cart?.taxAmount}
+                              className="text-right text-base font-medium text-black dark:text-white"
+                              currencyCode={"USD"}
+                            />
+                          </div>
+                        )}
+                        <div className="mb-3 flex items-center justify-between pb-1">
+                          <p className="text-base font-normal text-black/[60%] dark:text-white">
+                            Total
+                          </p>
+                          <Price
+                            amount={(cartDetail as any)?.cart?.grandTotal}
+                            className="text-right text-base font-medium text-black dark:text-white"
+                            currencyCode={"USD"}
+                          />
+                        </div>
+                      </div>
+
+                      <form action={redirectToCheckout}>
+                        <CheckoutButton
+                          cartDetails={cartObj?.items?.edges ?? []}
+                          isGuest={cartObj?.isGuest}
+                          isEmail={
+                            cartObj?.customerEmail ?? getLocalStorage(EMAIL)
+                          }
+                          isSelectShipping={
+                            cartObj?.selectedShippingRate != null
+                          }
+                          isSeclectAddress={isObject(billingAddress)}
+                          isSelectPayment={cartObj?.paymentMethod != null}
+                        />
+                      </form>
+                    </div>
+                  )}
+                </DrawerBody>
+
+                <DrawerFooter className="flex flex-col gap-1" />
+              </>
+            )}
+          </DrawerContent>
+        </Drawer>
+      ) : (
+        <AnimatePresence>
+          {finalIsOpen && (
+            <>
+              <motion.div
+                initial={{ opacity: 0 }}
+                animate={{ opacity: 1 }}
+                exit={{ opacity: 0 }}
+                onClick={finalOnClose}
+                className="fixed inset-0 z-40 bg-transparent lg:hidden transition-opacity"
+                style={{ top: "68px", bottom: "64px" }}
+              />
+
+              <motion.div
+                initial={{ x: "100%" }}
+                animate={{ x: 0 }}
+                exit={{ x: "100%" }}
+                transition={{
+                  type: "spring",
+                  damping: 30,
+                  stiffness: 300,
+                  mass: 0.8,
+                }}
+                className="fixed right-0 z-50 flex flex-col bg-white dark:bg-black w-full max-w-[448px] border-l border-neutral-200 dark:border-neutral-800 lg:hidden"
+                style={{
+                  top: "68px",
+                  bottom: "64px",
+                  width: "100%",
+                  maxWidth: "448px",
+                  height: "calc(var(--visual-viewport-height) - 132px)",
+                }}
+              >
+                <div className="flex flex-col gap-1 p-4">
+                  <div className="flex items-center justify-between">
+                    <p
+                      className={clsx(
+                        "font-semibold",
+                        isDesktop ? "text-lg" : "text-xl",
+                      )}
+                    >
+                      My Cart
+                    </p>
+                    {isDesktop && (
+                      <button
+                        aria-label="Close cart"
+                        className="cursor-pointer"
+                        onClick={finalOnClose}
+                      >
+                        <CloseCart />
+                      </button>
+                    )}
+                  </div>
+                </div>
+
+                <div
+                  className={clsx(
+                    "flex-1 overflow-y-auto px-4 py-0 drawer-scrollbar-hidden",
+                    !isDesktop && "!px-2",
+                  )}
+                >
+                  {cart?.length === 0 ? (
+                    <div className="mt-20 flex w-full flex-col items-center justify-center overflow-hidden">
+                      <ShoppingCartIcon className="h-16" />
+                      <p className="mt-6 text-center text-2xl font-bold">
+                        Your cart is empty.
+                      </p>
+                    </div>
+                  ) : (
+                    <div className="flex h-full flex-col justify-between">
+                      <ul className="my-0 flex-grow overflow-auto py-0">
+                        {Array.isArray(cart) &&
+                          cart?.map((item: any, i: number) => {
+                            const merchandiseSearchParams =
+                              {} as MerchandiseSearchParams;
+                            const merchandiseUrl = createUrl(
+                              `/product/${item?.node.productUrlKey}`,
+                              new URLSearchParams(merchandiseSearchParams),
+                            );
+                            const baseImage: any = safeParse(
+                              item?.node?.baseImage,
+                            );
+
+                            return (
+                              <li key={i} className="flex w-full flex-col">
+                                <div
+                                  className={clsx(
+                                    "flex w-full flex-row justify-between py-4 px-1",
+                                    isDesktop ? "gap-3" : "gap-1 xxs:gap-3",
+                                  )}
+                                >
+                                  <Link
+                                    className="z-30 flex flex-row space-x-4"
+                                    aria-label={`${item?.node?.name}`}
+                                    href={merchandiseUrl}
+                                    onClick={finalOnClose}
+                                  >
+                                    <div className="relative h-16 w-16 cursor-pointer overflow-hidden rounded-md border border-neutral-300 bg-neutral-300 dark:border-neutral-700 dark:bg-neutral-900 dark:hover:bg-neutral-800">
+                                      <Image
+                                        alt={
+                                          item?.node?.baseImage ||
+                                          item?.product?.name
+                                        }
+                                        className="h-full w-full object-cover"
+                                        height={64}
+                                        src={baseImage?.small_image_url || ""}
+                                        width={74}
+                                        onError={(e) =>
+                                          (e.currentTarget.src = NOT_IMAGE)
+                                        }
+                                      />
+                                    </div>
+                                    <div className="flex flex-1 flex-col text-base">
+                                      <span className="line-clamp-1 font-outfit text-base font-medium">
+                                        {item?.node?.name}
+                                      </span>
+                                      {item.name !== DEFAULT_OPTION && (
+                                        <p className="text-sm lowercase line-clamp-1 text-black dark:text-neutral-400">
+                                          {item?.node?.sku}
+                                        </p>
+                                      )}
+                                    </div>
+                                  </Link>
+                                  <div className="flex h-16 flex-col justify-between">
+                                    <Price
+                                      amount={item?.node?.price}
+                                      className="flex justify-end space-y-2 text-right font-outfit text-base font-medium"
+                                      currencyCode={"USD"}
+                                    />
+                                    <div className="flex items-center gap-x-2">
+                                      <DeleteItemButton item={item} />
+                                      <div className="ml-auto flex h-9 flex-row items-center rounded-full border border-neutral-200 dark:border-neutral-700">
+                                        <EditItemQuantityButton
+                                          item={item}
+                                          type="minus"
+                                        />
+                                        <p className="w-6 text-center">
+                                          <span className="w-full text-sm">
+                                            {item?.node?.quantity}
+                                          </span>
+                                        </p>
+                                        <EditItemQuantityButton
+                                          item={item}
+                                          type="plus"
+                                        />
+                                      </div>
+                                    </div>
+                                  </div>
+                                </div>
+                              </li>
+                            );
+                          })}
+                      </ul>
+
+                      <div className="border-0 border-t border-solid border-neutral-200 dark:border-dark-grey py-4 text-sm text-neutral-500 dark:text-neutral-400">
+                        {(cartDetail as any)?.cart?.taxAmount > 0 && (
+                          <div className="mb-3 flex items-center justify-between">
+                            <p className="text-base font-normal text-black/[60%] dark:text-white">
+                              Taxes
+                            </p>
+                            <Price
+                              amount={(cartDetail as any)?.cart?.taxAmount}
+                              className="text-right text-base font-medium text-black dark:text-white"
+                              currencyCode={"USD"}
+                            />
+                          </div>
+                        )}
+                        <div className="mb-3 flex items-center justify-between pb-1">
+                          <p className="text-base font-normal text-black/[60%] dark:text-white">
+                            Total
+                          </p>
+                          <Price
+                            amount={(cartDetail as any)?.cart?.grandTotal}
+                            className="text-right text-base font-medium text-black dark:text-white"
+                            currencyCode={"USD"}
+                          />
+                        </div>
+
+                        <form action={redirectToCheckout}>
+                          <CheckoutButton
+                            cartDetails={cartObj?.items?.edges ?? []}
+                            isGuest={cartObj?.isGuest}
+                            isEmail={
+                              cartObj?.customerEmail ?? getLocalStorage(EMAIL)
+                            }
+                            isSelectShipping={
+                              cartObj?.selectedShippingRate != null
+                            }
+                            isSeclectAddress={isObject(billingAddress)}
+                            isSelectPayment={cartObj?.paymentMethod != null}
+                          />
+                        </form>
+                      </div>
+                    </div>
+                  )}
+                </div>
+
+                <div className="p-4" />
+              </motion.div>
+            </>
+          )}
+        </AnimatePresence>
+      )}
+    </>
+  );
+}
+
+function CheckoutButton({
+  cartDetails,
+  isGuest,
+  isEmail,
+  isSeclectAddress,
+  isSelectShipping,
+  isSelectPayment,
+}: {
+  cartDetails: Array<any>;
+  isGuest: boolean;
+  isEmail: string;
+  isSeclectAddress: boolean;
+  isSelectShipping: boolean;
+  isSelectPayment: boolean;
+}) {
+  const { pending } = useFormStatus();
+  const email = isEmail;
+
+  return (
+    <>
+      <input
+        name="url"
+        type="hidden"
+        value={isCheckout(
+          cartDetails,
+          isGuest,
+          email,
+          isSeclectAddress,
+          isSelectShipping,
+          isSelectPayment,
+        )}
+      />
+      <button
+        className={clsx(
+          "block w-full rounded-full bg-blue-600 p-3 text-center text-sm font-medium text-white opacity-90 hover:opacity-100",
+          pending ? "cursor-wait" : "cursor-pointer",
+        )}
+        disabled={pending}
+        type="submit"
+      >
+        {pending ? <LoadingDots className="bg-white" /> : "Proceed to Checkout"}
+      </button>
+    </>
+  );
+}

+ 23 - 0
src/components/cart/OpenCart.tsx

@@ -0,0 +1,23 @@
+import { ShoppingCartIcon } from "@heroicons/react/24/outline";
+import clsx from "clsx";
+
+export default function OpenCart({
+  className,
+  quantity,
+}: {
+  className?: string;
+  quantity?: number | string;
+}) {
+  return (
+    <div className="relative flex  items-center justify-center rounded-md border-0 lg:border border-solid border-neutral-200 dark:border-neutral-700 lg:h-11 lg:w-11">
+      <ShoppingCartIcon className={clsx("h-5 w-5", className)} />
+
+      {quantity ? (
+        <div className="absolute right-0 top-0 -mr-2 margin-t lg:-mt-2 h-4 w-4 rounded bg-blue-600 text-[11px] font-medium text-white">
+          {quantity}
+        </div>
+      ) : null}
+    </div>
+  );
+}
+

+ 30 - 0
src/components/cart/OrderDetail.tsx

@@ -0,0 +1,30 @@
+"use client";
+
+import { ORDER_ID } from "@/utils/constants";
+import { getCookie } from "@utils/getCartToken";
+import { useEffect, useState } from "react";
+
+export default function OrderDetail() {
+  const [orderId, setOrderId] = useState<string | null>(null);
+
+  useEffect(() => {
+    requestAnimationFrame(() => {
+      setOrderId(getCookie(ORDER_ID));
+    });
+  }, []);
+
+  return (
+    <div className="mb-8 font-outfit">
+      <h1 className="my-2 text-center text-3xl font-semibold sm:text-4xl">
+        Your order{" "}
+        <span className="text-primary">
+          #{orderId ? orderId : <span className="animate-pulse">...</span>}
+        </span>{" "}
+        has been placed successfully{" "}
+      </h1>
+      <p className="text-center text-lg font-normal text-black/60 dark:text-neutral-300">
+        Missing page, but your next favorite chair is just a click away.
+      </p>
+    </div>
+  );
+}

+ 21 - 0
src/components/cart/index.tsx

@@ -0,0 +1,21 @@
+import CartModal from "./CartModal";
+
+export default function Cart({
+  children,
+  className,
+  onOpen,
+  onClose,
+  isOpen,
+}: {
+  children?: React.ReactNode;
+  className?: string;
+  onOpen?: () => void;
+  onClose?: () => void;
+  isOpen?: boolean;
+}) {
+  return (
+    <CartModal className={className} onOpen={onOpen} onClose={onClose} isOpen={isOpen}>
+      {children}
+    </CartModal>
+  );
+}

+ 159 - 0
src/components/catalog/Pagination.tsx

@@ -0,0 +1,159 @@
+"use client";
+
+import { createUrl } from "@/utils/helper";
+import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/24/outline";
+import clsx from "clsx";
+import { usePathname, useSearchParams, useRouter } from "next/navigation";
+
+export default function Pagination({
+  itemsPerPage,
+  itemsTotal,
+  currentPage,
+  nextCursor,
+  prevCursor,
+}: {
+  itemsPerPage: number;
+  itemsTotal: number;
+  currentPage: number;
+  nextCursor?: string;
+  prevCursor?: string;
+}) {
+  const router = useRouter();
+  const pathname = usePathname();
+  const currentParams = useSearchParams();
+
+  const pageCount = Math.ceil(itemsTotal / itemsPerPage);
+
+  const handlePageClick = (page: number) => {
+    if (page < 0 || page >= pageCount) return;
+
+    const params = new URLSearchParams(currentParams.toString());
+
+    params.set("page", String(page + 1));
+
+    if (page === currentPage + 1 && nextCursor) {
+      params.set("cursor", nextCursor);
+      params.delete("before");
+    } else if (page === currentPage - 1 && prevCursor) {
+      params.set("before", prevCursor);
+      params.delete("cursor");
+    } else {
+      params.delete("cursor");
+      params.delete("before");
+    }
+
+    const newUrl = createUrl(pathname, params);
+    router.replace(newUrl);
+  };
+
+  const renderDots = () => {
+    const dots = [];
+    const maxVisiblePages = 5;
+
+    if (pageCount <= maxVisiblePages) {
+      for (let i = 0; i < pageCount; i++) {
+        dots.push(renderPageButton(i));
+      }
+    } else {
+      const halfMax = Math.floor(maxVisiblePages / 2);
+      const start = Math.max(0, currentPage - halfMax);
+      const end = Math.min(pageCount, start + maxVisiblePages);
+
+      if (start > 0) {
+        dots.push(renderPageButton(0));
+        if (start > 1) {
+          dots.push(
+            <li key="dot-start" className="pagination-dot">...</li>
+          );
+        }
+      }
+
+      for (let i = start; i < end; i++) {
+        dots.push(renderPageButton(i));
+      }
+
+      if (end < pageCount) {
+        if (end < pageCount - 1) {
+          dots.push(
+            <li key="dot-end" className="pagination-dot">...</li>
+          );
+        }
+        dots.push(renderPageButton(pageCount - 1));
+      }
+    }
+
+    return dots;
+  };
+
+  const renderPageButton = (pageIndex: number) => (
+    <li
+      key={pageIndex}
+      onClick={() => handlePageClick(pageIndex)}
+      className="rounded-sm cursor-pointer"
+    >
+      <button
+        className={`
+          flex h-10 w-10 items-center justify-center text-lg rounded-sm duration-300 cursor-pointer
+          ${pageIndex === currentPage
+            ? "border !border-gray-300 dark:border-gray-700"
+            : "text-gray-500 dark:!text-gray-400 hover:border-gray-500 dark:hover:border-gray-700"
+          }
+        `}
+        aria-label={`Goto Page ${pageIndex + 1}`}
+        aria-current={pageIndex === currentPage}
+      >
+        {pageIndex + 1}
+      </button>
+    </li>
+  );
+
+  return (
+    <ul
+      className="mx-auto h-10 gap-x-2 text-base flex justify-center"
+      role="navigation"
+      aria-label="Pagination"
+    >
+      <li
+        key="prev"
+        onClick={() => handlePageClick(currentPage - 1)}
+        className={clsx(
+          "cursor-pointer rounded-lg hover:text-gray-700",
+          currentPage > 0 ? "block cursor-pointer" : ""
+        )}
+      >
+        <button
+          className={clsx(
+            "ml-0 flex h-10 items-center justify-center px-2 leading-tight",
+            currentPage > 0 ? "cursor-pointer" : "cursor-not-allowed"
+          )}
+          aria-label="Previous page"
+          disabled={currentPage <= 0}
+        >
+          <ChevronLeftIcon className="h-5" />
+        </button>
+      </li>
+
+      {renderDots()}
+
+      <li
+        key="next"
+        onClick={() => handlePageClick(currentPage + 1)}
+        className={clsx(
+          "rounded-lg hover:text-gray-700",
+          currentPage < pageCount - 1 ? "block cursor-pointer" : ""
+        )}
+      >
+        <button
+          className={clsx(
+            "ml-0 flex h-10 items-center justify-center px-2 leading-tight",
+            currentPage < pageCount - 1 ? "cursor-pointer" : "cursor-not-allowed"
+          )}
+          aria-label="Next page"
+          disabled={currentPage >= pageCount - 1}
+        >
+          <ChevronRightIcon className="h-5" />
+        </button>
+      </li>
+    </ul>
+  );
+}

+ 97 - 0
src/components/catalog/product/ProductCard.tsx

@@ -0,0 +1,97 @@
+import Link from "next/link";
+import { FC } from "react";
+import Grid from "@/components/theme/ui/grid/Grid";
+import AddToCartButton from "@/components/theme/ui/AddToCartButton";
+import { NextImage } from "@/components/common/NextImage";
+import { Price } from "@/components/theme/ui/Price";
+
+type ProductCardProps = {
+  currency: string;
+  price: string;
+  specialPrice?: string;
+  imageUrl: string;
+  product: {
+    urlKey: string;
+    name: string;
+    id: string;
+    type: string;
+    isSaleable?: string;
+  };
+  sizes?: string; 
+  priority?: boolean;
+};
+
+export const ProductCard: FC<ProductCardProps> = ({
+  currency,
+  price,
+  specialPrice,
+  imageUrl,
+  product,
+  sizes = "(max-width: 768px) 50vw, (max-width: 1200px) 33vw, 25vw",
+  priority = false
+}) => {
+  return (
+    <Grid.Item
+      key={product.id}
+      className="animate-fadeIn gap-y-4.5 flex flex-col"
+    >
+      <div className="group relative overflow-hidden rounded-lg">
+        <Link href={`/product/${product.urlKey}`} aria-label={`View ${product.name}`}>
+          <div className="aspect-[353/283] h-auto truncate rounded-lg">
+            <NextImage
+              alt={product?.name || "Product image"}
+              src={imageUrl}
+              width={353}
+              height={283}
+              sizes={sizes}
+              className={`rounded-lg bg-neutral-100 object-cover transition duration-300 ease-in-out group-hover:scale-105`}
+              priority={priority}
+            />
+          </div>
+        </Link>
+
+        <div
+          className={`hidden lg:block absolute bottom-4 left-1/2 flex -translate-x-1/2 items-center gap-x-4 rounded-full border-[1.5px] border-white bg-white/70 px-4 py-1.5 text-xs font-semibold text-black opacity-0 shadow-2xl backdrop-blur-md duration-300 group-hover:opacity-100 dark:text-white`}
+        >
+          <AddToCartButton productType={product.type} productId={product.id} productUrlKey={product.urlKey} isSaleable={product?.isSaleable} />
+        </div>
+        <div
+          className={`block lg:hidden absolute bottom-[10px] left-1/2 flex -translate-x-1/2 items-center gap-x-4 rounded-full border-[1.5px] border-white bg-white/70 px-3 py-0.5 md:px-4 md:py-1.5 text-xs font-semibold text-black opacity-100 shadow-2xl backdrop-blur-md duration-300 group-hover:opacity-100 dark:text-white`}
+        >
+          <AddToCartButton productType={product.type} productId={product.id} productUrlKey={product.urlKey} isSaleable={product?.isSaleable} />
+        </div>
+      </div>
+
+      <div>
+        <h3 className="mb-2.5 text-sm font-medium md:text-lg">
+          {product?.name}
+        </h3>
+
+        <div className="flex items-center gap-2">
+          {product?.type === "configurable" && (
+            <span className="text-xs text-gray-600 dark:text-gray-400 md:text-sm">
+              As low as
+            </span>
+          )}
+          {product?.type === "simple" && specialPrice ? (
+            <>
+              <div className="flex items-center gap-2">
+                <Price
+                  amount={specialPrice}
+                  className="text-xs font-semibold md:text-sm"
+                  currencyCode={currency}
+                />
+              </div>
+            </>
+          ) : (
+            <Price
+              amount={price}
+              className="text-xs font-semibold md:text-sm"
+              currencyCode={currency}
+            />
+          )}
+        </div>
+      </div>
+    </Grid.Item>
+  );
+};

+ 137 - 0
src/components/catalog/product/ProductDescription.tsx

@@ -0,0 +1,137 @@
+"use client";
+import { Price } from "@/components/theme/ui/Price";
+import { Rating } from "@/components/common/Rating";
+import { AddToCart } from "@/components/cart/AddToCart";
+import { VariantSelector } from "./VariantSelector";
+import { ProductMoreDetails } from "./ProductMoreDetail";
+import { useState } from "react";
+import { getVariantInfo } from "@utils/hooks/useVariantInfo";
+import { useSearchParams } from "next/navigation";
+import Prose from "@components/theme/search/Prose";
+import { ProductData , ProductReviewNode } from "../type";
+import { safeCurrencyCode, safePriceValue, safeParse } from "@utils/helper";
+import Link from "next/link";
+
+export function ProductDescription({
+  product,
+  reviews,
+  totalReview,
+  productSwatchReview,
+  avgRating
+}: {
+  product: ProductData;
+  slug: string;
+  reviews: ProductReviewNode[] ; 
+  avgRating : number ;
+  totalReview: number;
+  productSwatchReview: any;
+}) {
+  const priceValue = safePriceValue(product);
+  const currencyCode = safeCurrencyCode(product);
+  const configurableProductIndexData = (safeParse(
+    productSwatchReview?.combinations
+  ) || []) as never[];
+  const searchParams = useSearchParams();
+  const [userInteracted, setUserInteracted] = useState(false);
+
+  const superAttributes = productSwatchReview?.superAttributeOptions
+    ? safeParse(productSwatchReview.superAttributeOptions)
+    : productSwatchReview?.superAttributes?.edges?.map(
+      (e: { node: any }) => e.node
+    ) || [];
+
+  const variantInfo = getVariantInfo(
+    product?.type === "configurable",
+    searchParams.toString(),
+    superAttributes,
+    productSwatchReview?.combinations
+  );
+
+  const additionalData =
+    productSwatchReview?.attributeValues?.edges?.map(
+      (e: { node: any }) => e.node
+    ) || [];
+  const [expandedKeys, setExpandedKeys] = useState<Set<string>>(new Set());
+  const handleReviewClick = () => {
+    setExpandedKeys(new Set(["2"]));
+  };
+  
+  return (
+    <>
+      <div className="mb-2 flex flex-col pb-6">
+        {/* Breadcrumb */}
+        <div className="hidden lg:flex flex-col gap-3 shrink-0 mb-2">
+          <Link
+            href="/"
+            className="w-fit text-sm font-medium text-nowrap relative text-neutral-500 before:absolute before:bottom-0 before:left-0 before:h-px before:w-0 before:bg-current before:transition-all before:duration-300 before:content-[''] hover:text-black hover:before:w-full dark:text-neutral-400 dark:hover:text-neutral-300"
+          >
+            Home /
+          </Link>
+        </div>
+        <h1 className="font-outfit text-2xl md:text-3xl lg:text-4xl font-semibold">
+          {product?.name || ""}
+        </h1>
+
+        <div className="flex w-auto justify-between items-baseline gap-y-2 py-4 xs:flex-row xs:gap-y-0 sm:py-6 flex-wrap">
+          <div className="flex gap-4 items-baseline">
+            {product?.type === "configurable" && (
+              <p className="text-base text-gray-600 dark:text-gray-400">
+                As low as
+              </p>
+            )}
+            {product?.type === "simple" ? (
+              <>
+                <Price
+                  amount={String(product?.minimumPrice)}
+                  currencyCode={currencyCode}
+                  className="font-outfit text-xl md:text-2xl font-semibold"
+                />
+              </>
+            ) : (
+              <Price
+                amount={String(priceValue)}
+                currencyCode={currencyCode}
+                className="font-outfit text-xl md:text-2xl font-semibold"
+              />
+            )}
+          </div>
+
+          <Rating
+            length={5}
+            star={avgRating}
+            reviewCount={totalReview}
+            className="mt-2"
+            onReviewClick={handleReviewClick}
+          />
+        </div>
+      </div>
+
+      <VariantSelector
+        variants={variantInfo?.variantAttributes}
+        setUserInteracted={setUserInteracted}
+        possibleOptions={variantInfo.possibleOptions}
+      />
+
+      {product?.shortDescription ? (
+        <Prose className="mb-6 text-base text-selected-black dark:text-white font-light" html={product.shortDescription} />
+      ) : null}
+
+      <AddToCart
+        index={configurableProductIndexData}
+        productId={product?.id || ""}
+        productSwatchReview={productSwatchReview}
+        userInteracted={userInteracted}
+      />
+
+      <ProductMoreDetails
+        additionalData={additionalData}
+        description={product?.description ?? ""}
+        reviews={Array.isArray(reviews) ? reviews : []}
+        totalReview={totalReview}
+        productId={product?.id ?? ""}
+        expandedKeys={expandedKeys}
+        setExpandedKeys={setExpandedKeys}
+      />
+    </>
+  );
+}

+ 29 - 0
src/components/catalog/product/ProductGridItems.tsx

@@ -0,0 +1,29 @@
+import { baseUrl, getImageUrl, NOT_IMAGE } from "@/utils/constants";
+import { ProductCard } from "./ProductCard";
+
+export default function ProductGridItems({
+  products,
+}: {
+  products: any;
+}) {
+  return products.map((product: any, index: number) => {
+
+    const imageUrl = getImageUrl(product?.baseImageUrl, baseUrl, NOT_IMAGE);
+    const price =
+      product?.type === "configurable"
+        ? product?.minimumPrice ?? "0"
+        : product?.price ?? "0";
+    const currency = product?.priceHtml?.currencyCode;
+    return (
+      <ProductCard
+        key={index}
+        currency={currency}
+        imageUrl={imageUrl || ""}
+        price={price}
+        specialPrice={product?.minimumPrice}
+        product={product}
+        priority={index < 4}
+      />
+    );
+  });
+}

+ 31 - 0
src/components/catalog/product/ProductInfo.tsx

@@ -0,0 +1,31 @@
+
+import { getAverageRating } from "@utils/helper";
+import { ProductData } from "../type";
+import { ProductDescription } from "./ProductDescription";
+import { getProductWithSwatchAndReview } from "@/utils/hooks/getProductSwatchAndReview";
+import { ProductReview } from "@/types/category/type";
+import { getProductReviews } from "@utils/hooks/getProductReviews";
+
+export default async function ProductInfo({
+  product,
+  slug,
+  reviews,
+}: {
+  product: ProductData;
+  slug: string;
+  reviews: ProductReview[];
+}) {
+  const productSwatchReview = await getProductWithSwatchAndReview(slug);
+  const getAllreviews = await getProductReviews(product?.id?.split("/").pop() || '')
+  
+  return (
+    <ProductDescription
+      product={product}
+      productSwatchReview={productSwatchReview}
+      slug={slug}
+      reviews={getAllreviews}
+      totalReview={reviews.length}
+      avgRating = {getAverageRating(reviews)}
+    />
+  );
+}

+ 123 - 0
src/components/catalog/product/ProductMoreDetail.tsx

@@ -0,0 +1,123 @@
+"use client";
+import { Accordion, AccordionItem } from "@heroui/accordion";
+import React, { FC } from "react";
+import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/24/outline";
+import Prose from "@/components/theme/search/Prose";
+import ReviewSection from "../review/ReviewSection";
+import ReviewDetail from "../review/ReviewDetail";
+import { additionalDataTypes } from "../type";
+
+export const ProductMoreDetails: FC<{
+  description: string;
+  additionalData: additionalDataTypes[];
+  productId: string;
+  reviews: any[];
+  totalReview: number;
+  expandedKeys: Set<string>;
+  setExpandedKeys: (keys: Set<string>) => void;
+}> = ({ description, additionalData, reviews, productId, totalReview, expandedKeys, setExpandedKeys }) => {
+
+  const filterAdditionalData = additionalData.filter((item) => item?.attribute?.isVisibleOnFront == "1");
+
+
+  return (
+    <div className="mt-7 sm:my-7">
+      <Accordion
+        itemClasses={{
+          base: "shadow-none  bg-neutral-100 dark:bg-neutral-800",
+        }}
+        className="px-0"
+        selectionMode="multiple"
+        showDivider={false}
+        variant="splitted"
+        selectedKeys={expandedKeys}
+         onSelectionChange={(keys) => setExpandedKeys(keys as Set<string>)}
+      >
+        <AccordionItem
+          key="1"
+          classNames={{
+            title: "text-start",
+            trigger: "cursor-pointer",
+          }}
+          indicator={({ isOpen }) =>
+            isOpen ? (
+              <ChevronLeftIcon className="h-5 w-5 stroke-neutral-800 dark:stroke-white" />
+            ) : (
+              <ChevronRightIcon className="h-5 w-5 stroke-neutral-800 dark:stroke-white" />
+            )
+          }
+          aria-label="Description"
+          title="Description"
+        >
+          <Prose className="pb-2 text-selected-black dark:text-white font-light" html={description} />
+        </AccordionItem>
+
+        {filterAdditionalData.length > 0
+          ?
+          <AccordionItem
+            key="2"
+            classNames={{
+              title: "text-start",
+              trigger: "cursor-pointer",
+            }}
+            indicator={({ isOpen }) =>
+              isOpen ? (
+                <ChevronLeftIcon className="h-5 w-5 stroke-neutral-800 dark:stroke-white" />
+              ) : (
+                <ChevronRightIcon className="h-5 w-5 stroke-neutral-800 dark:stroke-white" />
+              )
+            }
+            aria-label="Additional Information"
+            title="Additional Information"
+          >
+            <div className="grid max-w-max grid-cols-[auto_1fr] gap-x-8 gap-y-4 px-1 pb-2">
+              {filterAdditionalData?.map((item) => (
+                <React.Fragment key={item.label}>
+                  <div className="grid">
+                    <p className="text-base font-normal text-selected-black dark:text-white">
+                      {item?.attribute?.adminName}
+                    </p>
+                  </div>
+                  <div className="grid">
+                    <p className="text-base font-normal text-selected-black dark:text-white">
+                      {item?.value || "--"}
+                    </p>
+                  </div>
+                </React.Fragment>
+              ))}
+            </div>
+          </AccordionItem>
+
+          : null}
+        <AccordionItem
+          key={filterAdditionalData.length > 0 ? "3" : "2"}
+          classNames={{
+            title: "text-start",
+            trigger: "cursor-pointer",
+          }}
+          indicator={({ isOpen }) =>
+            isOpen ? (
+              <ChevronLeftIcon className="h-5 w-5 stroke-neutral-800 dark:stroke-white" />
+            ) : (
+              <ChevronRightIcon className="h-5 w-5 stroke-neutral-800 dark:stroke-white" />
+            )
+          }
+          aria-label="Ratings"
+          title="Ratings"
+        >
+          {totalReview > 0 ? (
+            <>
+              <ReviewDetail
+                reviewDetails={reviews}
+                totalReview={totalReview}
+                productId={productId}
+              />
+            </>
+          ) : (
+            <ReviewSection productId={productId}  totalReview={totalReview} />
+          )}
+        </AccordionItem>
+      </Accordion>
+    </div>
+  );
+};

+ 44 - 0
src/components/catalog/product/ProductsSection.tsx

@@ -0,0 +1,44 @@
+
+import { NOT_IMAGE } from "@/utils/constants";
+import Grid from "../../theme/ui/grid/Grid";
+import { baseUrl, getImageUrl } from "@/utils/constants";
+import { ProductCard } from "./ProductCard";
+import { ProductsSectionProps } from "../type";
+
+
+export function ProductsSection({ title, description, products }: ProductsSectionProps) {
+  if (!products?.length) return null;
+
+  return (
+    <div className="flex flex-col gap-y-10 pt-8 sm:pt-12 lg:pt-20 w-full max-w-screen-2xl mx-auto px-4 xss:px-7.5">
+      <div className="flex flex-col gap-y-4 font-outfit text-center">
+        <h2 className="text-2xl sm:text-4xl font-semibold">{title}</h2>
+        <p className="text-base font-normal text-selected-black dark:text-neutral-300">{description}</p>
+      </div>
+
+      <Grid className="grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
+        {products.map((item, index) => {
+          const imageUrl = getImageUrl(item?.baseImageUrl, baseUrl, NOT_IMAGE);
+          const ProductPrice =
+            item?.type === "configurable"
+              ? item?.minimumPrice ?? "0"
+              : item?.price ?? "0";
+          return (
+            <ProductCard
+              key={item.id ?? index}
+              currency="USD"
+              imageUrl={imageUrl || ""}
+              price={String(ProductPrice)}
+              product={{
+                urlKey: item.urlKey || item.sku,
+                name: item?.name || item.sku,
+                id: item.id,
+                type: item.type,
+              }} specialPrice={""} />
+          );
+        })}
+      </Grid>
+    </div>
+  );
+}
+

+ 48 - 0
src/components/catalog/product/RelatedProductsSection.tsx

@@ -0,0 +1,48 @@
+import { GET_RELATED_PRODUCTS } from "@/graphql";
+import { ProductsSection } from "./ProductsSection";
+import { SingleProductResponse } from "@/app/(public)/product/[...urlProduct]/page";
+import { cachedProductRequest } from "@/utils/hooks/useCache";
+
+export async function RelatedProductsSection({
+  fullPath,
+}: {
+  fullPath: string;
+}) {
+    async function getRelatedProduct(urlKey: string) {
+      try {
+        const dataById = await cachedProductRequest<SingleProductResponse>(
+          urlKey,
+          GET_RELATED_PRODUCTS,
+          {
+            urlKey: urlKey,
+            first: 4,
+          }
+        );
+    
+        return dataById?.product || null;
+      } catch (error) {
+        if (error instanceof Error) {
+          console.error("Error fetching product:", {
+            message: error.message,
+            urlKey,
+            graphQLErrors: (error as unknown as Record<string, unknown>)
+              .graphQLErrors,
+          });
+        }
+        return null;
+      }
+    }
+
+    const fetchRelatedProducts = await getRelatedProduct(fullPath);
+
+    const relatedProducts = (fetchRelatedProducts?.relatedProducts != null ) && fetchRelatedProducts?.relatedProducts?.edges
+    ? fetchRelatedProducts.relatedProducts.edges.map((e) => e.node)
+    : [];
+  return (
+    <ProductsSection
+      title="Related Products"
+      description="Discover the latest trends! Fresh products just added—shop new styles, tech, and essentials before they're gone."
+      products={relatedProducts}
+    />
+  );
+}

+ 71 - 0
src/components/catalog/product/VariantSelector.tsx

@@ -0,0 +1,71 @@
+"use client";
+
+import { AttributeData, AttributeOptionNode } from "@/types/types";
+import { createUrl, getValidTitle } from "@/utils/helper";
+import clsx from "clsx";
+import { usePathname, useRouter, useSearchParams } from "next/navigation";
+
+export function VariantSelector({
+  variants,
+  setUserInteracted,
+}: {
+  variants: AttributeData[];
+  setUserInteracted: React.Dispatch<React.SetStateAction<boolean>>;
+  possibleOptions: Record<string, number[]>;
+}) {
+  const router = useRouter();
+  const pathname = usePathname();
+  const searchParams = useSearchParams();
+  if (!variants?.length) return null;
+
+  return (
+    <>
+      {variants.map((option , index : number) => {
+        const attributeCode = option.code;
+        const _isAlreadySelected = searchParams.has(attributeCode);
+        return (
+          <dl key={`${option.id} + ${index}` } className="mb-8">
+            <dt className="mb-4 text-sm capitalize tracking-wide">
+              {getValidTitle(attributeCode)}
+            </dt>
+
+            <dd className="flex flex-wrap gap-3">
+              {(option.options as AttributeOptionNode[]).map((node) => {
+                const isActive = searchParams.get(attributeCode) === String(node.id);
+                const isAvailable = node?.isValid;
+                const nextParams = new URLSearchParams(searchParams.toString());
+                nextParams.set(attributeCode, String(node.id));
+
+                const optionUrl = createUrl(pathname, nextParams);
+
+                return (
+                  <button
+                    key={node.id}
+                    disabled={!isAvailable}
+                    onClick={() => {
+                      if (!isAvailable) return;
+                      router.replace(optionUrl, { scroll: false });
+                      setUserInteracted(true);
+                    }}
+                    className={clsx(
+                      "flex min-w-[48px] cursor-pointer items-center justify-center rounded-lg bg-neutral-100 px-3.5 py-2.5 text-sm dark:border-neutral-800 dark:bg-neutral-800",
+                      {
+                        "cursor-default ring-2 ring-blue-600 text-blue-600": isActive,
+                        "ring-[0] transition duration-300 ease-in-out hover:scale-110 hover:border-blue-600":
+                          !isActive && isAvailable,
+                        "relative z-10 cursor-not-allowed overflow-hidden bg-neutral-100 text-neutral-500 ring-1 ring-neutral-300 before:absolute before:inset-x-0 before:-z-10 before:h-px before:-rotate-45 before:bg-neutral-300 before:transition-transform dark:bg-neutral-900 dark:text-neutral-400 dark:ring-neutral-700 before:dark:bg-neutral-700":
+                          !isAvailable,
+                      }
+                    )}
+                  >
+                    {node.label || node.adminName}
+                  </button>
+                );
+              })}
+            </dd>
+          </dl>
+        );
+      })}
+    </>
+  );
+}

+ 278 - 0
src/components/catalog/review/AddProductReview.tsx

@@ -0,0 +1,278 @@
+"use client";
+
+import { useState } from "react";
+import { Textarea } from "@heroui/react";
+import { AddRatingStar } from "./AddRatingStar";
+import { Button } from "@components/common/button/Button";
+import { useCustomToast } from "@utils/hooks/useToast";
+import { useProductReview } from "@utils/hooks/useProductReview";
+import Image from "next/image";
+
+import { CreateProductReviewInput } from "@/types/review";
+import { AddUploadImage } from "@components/common/icons/AddUploadImage";
+
+export default function AddProductReview({
+  productId,
+  onClose,
+}: {
+  productId: string;
+  onClose: () => void;
+}) {
+  const [imageFile, setImageFile] = useState<string | null>(null);
+  const [imagePreview, setImagePreview] = useState<string | null>(null);
+  const [errors, setErrors] = useState({
+    title: "",
+    comment: "",
+    rating: "",
+  });
+  const [reviewInfo, setReviewInfo] = useState({
+    title: "",
+    comment: "",
+    rating: 0,
+    attachments: null as string | null,
+  });
+  const { showToast } = useCustomToast();
+  const { createProductReview, isLoading } = useProductReview();
+
+  const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
+    const file = e.target.files?.[0];
+    if (file) {
+      setImageFile(file.name);
+      setReviewInfo((prev) => ({ ...prev, attachments: file.name }));
+      const reader = new FileReader();
+      reader.onloadend = () => {
+        setImagePreview(reader.result as string);
+      };
+      reader.readAsDataURL(file);
+    }
+  };
+
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault();
+
+    const newErrors = {
+      title: "",
+      comment: "",
+      rating: "",
+    };
+
+    if (!reviewInfo.title.trim()) {
+      newErrors.title = "Title is required";
+    }
+
+    if (!reviewInfo.comment.trim()) {
+      newErrors.comment = "Comment is required";
+    }
+
+    if (reviewInfo.rating === 0) {
+      newErrors.rating = "Please select a rating";
+    }
+
+    setErrors(newErrors);
+
+    // If there are errors, don't submit
+    if (newErrors.title || newErrors.comment || newErrors.rating) {
+      showToast("Please fill in all required fields", "danger");
+      return;
+    }
+
+    try {
+      const input: CreateProductReviewInput = {
+        productId: Number(productId.split("/").pop()),
+        title: reviewInfo.title,
+        comment: reviewInfo.comment,
+        rating: reviewInfo.rating,
+        name: "Guest User",
+        email: "guest@mail.com",
+        attachments: "",
+      };
+
+      await createProductReview(input);
+
+      setReviewInfo({
+        title: "",
+        comment: "",
+        rating: 0,
+        attachments: null,
+      });
+      setImageFile(null);
+      setImagePreview(null);
+      setErrors({
+        title: "",
+        comment: "",
+        rating: "",
+      });
+    } catch (error) {
+      console.error("Error submitting review:", error);
+      showToast("Failed to submit review. Please try again.", "danger");
+    }
+  };
+
+  return (
+    <form
+      onSubmit={handleSubmit}
+      className="w-full max-w-4xl mx-auto p-4 md:p-6 rounded-xl relative"
+    >
+      <div className="flex mb-4">
+        <h1 className="text-xl font-semibold">Write a review</h1>
+        <button
+          type="button"
+          onClick={onClose}
+          className="absolute top-4 right-4 z-50 p-2 rounded-full bg-gray-100 hover:bg-gray-200 dark:bg-neutral-300 dark:hover:bg-neutral-700 text-gray-500 hover:text-gray-700 dark:hover:text-gray-400 transition-colors"
+          aria-label="Close review form"
+        >
+          <svg
+            xmlns="http://www.w3.org/2000/svg"
+            className="h-5 w-5"
+            fill="none"
+            viewBox="0 0 24 24"
+            stroke="currentColor"
+          >
+            <path
+              strokeLinecap="round"
+              strokeLinejoin="round"
+              strokeWidth={2}
+              d="M6 18L18 6M6 6l12 12"
+            />
+          </svg>
+        </button>
+      </div>
+      <div className="flex flex-col gap-4">
+        <div className="space-y-4">
+          {imagePreview ? (
+            <div className="border-2 w-[120px] h-auto max-h-[120px] border-dashed border-gray-300  rounded-xl text-center transition-colors hover:border-primary-500">
+              <div className="space-y-3">
+                <div className="relative mx-auto">
+                  <Image
+                    src={imagePreview}
+                    alt="Preview"
+                    width={120}
+                    height={120}
+                    className="w-full h-full object-cover rounded-lg"
+                  />
+                  <button
+                    type="button"
+                    onClick={() => {
+                      setImageFile(null);
+                      setImagePreview(null);
+                    }}
+                    className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full p-1 hover:bg-red-600 transition-colors"
+                  >
+                    <svg
+                      xmlns="http://www.w3.org/2000/svg"
+                      className="h-4 w-4"
+                      fill="none"
+                      viewBox="0 0 24 24"
+                      stroke="currentColor"
+                    >
+                      <path
+                        strokeLinecap="round"
+                        strokeLinejoin="round"
+                        strokeWidth={2}
+                        d="M6 18L18 6M6 6l12 12"
+                      />
+                    </svg>
+                  </button>
+                </div>
+              </div>
+            </div>
+          ) : (
+            <div className="border-2 border-dashed border-gray-300 w-32 h-32 rounded-xl p-6 text-center transition-colors hover:border-primary-500">
+              <div>
+                <label
+                  htmlFor="file-upload"
+                  className="mx-auto flex cursor-pointer flex-col items-center justify-center gap-2"
+                >
+                  <div className="mx-auto flex h-8 w-8 items-center justify-center rounded-full">
+                    <AddUploadImage />
+                  </div>
+
+                  <div className="text-sm text-center">
+                    <span className="font-medium">Add Image</span>
+                  </div>
+
+                  <input
+                    id="file-upload"
+                    name="file-upload"
+                    type="file"
+                    className="sr-only"
+                    accept="image/*"
+                    onChange={handleImageUpload}
+                  />
+                </label>
+              </div>
+            </div>
+          )}
+          {imageFile && <p className="text-sm">{imageFile}</p>}
+        </div>
+
+        <div className="flex flex-col gap-4">
+          <div>
+            <label className="block text-base font-semibold  mb-1">
+              Rating
+            </label>
+            <AddRatingStar
+              value={reviewInfo.rating}
+              onChange={(value) => {
+                setReviewInfo((prev) => ({ ...prev, rating: value }));
+                setErrors((prev) => ({ ...prev, rating: "" }));
+              }}
+              size="size-6"
+              className="mt-1"
+            />
+            {errors.rating && (
+              <p className="text-red-500 text-sm mt-1">{errors.rating}</p>
+            )}
+          </div>
+
+          <Textarea
+            label="Title"
+            placeholder="Title"
+            labelPlacement="outside"
+            value={reviewInfo.title}
+            onChange={(e) => {
+              setReviewInfo((prev) => ({ ...prev, title: e.target.value }));
+              setErrors((prev) => ({ ...prev, title: "" }));
+            }}
+            minRows={1}
+            maxRows={1}
+            variant="bordered"
+            isInvalid={!!errors.title}
+            errorMessage={errors.title}
+            classNames={{
+              input: "text-sm",
+              label: "px-2 text-base font-semibold",
+            }}
+          />
+
+          <Textarea
+            label="Comment"
+            placeholder="Comment"
+            labelPlacement="outside"
+            value={reviewInfo.comment}
+            onChange={(e) => {
+              setReviewInfo((prev) => ({ ...prev, comment: e.target.value }));
+              setErrors((prev) => ({ ...prev, comment: "" }));
+            }}
+            minRows={5}
+            variant="bordered"
+            isInvalid={!!errors.comment}
+            errorMessage={errors.comment}
+            classNames={{
+              input: "text-sm",
+              label: "px-2 text-base font-semibold",
+            }}
+          />
+          <div className="w-32">
+            <Button
+              title={isLoading ? "Submitting..." : "Submit"}
+              type="submit"
+              disabled={isLoading}
+              className="w-full mt-4 rounded-2xl"
+            />
+          </div>
+        </div>
+      </div>
+    </form>
+  );
+}

+ 68 - 0
src/components/catalog/review/AddRatingStar.tsx

@@ -0,0 +1,68 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import clsx from "clsx";
+import { StarIcon } from "@heroicons/react/24/solid";
+import { RatingTypes } from "../type";
+
+
+export const AddRatingStar = ({
+    length = 5,
+    value,
+    size = "size-6",
+    className,
+    onChange
+}: RatingTypes) => {
+
+    const [internalValue, setInternalValue] = useState(value ?? 0);
+    const [hovered, setHovered] = useState<number | null>(null);
+
+    useEffect(() => {
+        if (value !== undefined) {
+              // eslint-disable-next-line react-hooks/set-state-in-effect
+            setInternalValue(value);
+        }
+    }, [value]);
+
+    const currentValue = value ?? internalValue;
+
+    const getFillState = (index: number) => {
+        if (hovered !== null) return index <= hovered;
+        return index <= currentValue;
+    };
+
+    const handleClick = (index: number) => {
+        if (value === undefined) {
+               
+            setInternalValue(index);
+        }
+        onChange?.(index);
+    };
+
+
+    return (
+        <div className={clsx("flex items-center gap-x-1 cursor-pointer", className)}>
+            {Array.from({ length }).map((_, i) => {
+                const index = i + 1;
+                return (
+                    <StarIcon
+                        key={index}
+                        onClick={() => handleClick(index)}
+                        onMouseEnter={() => setHovered(index)}
+                        onMouseLeave={() => setHovered(null)}
+                        fill="currentColor"
+                        className={clsx(
+                            "fill-current",
+                            size || "size-6",
+                            getFillState(index)
+                                ? "text-yellow-500"
+                                : "text-gray-300",
+                            "transition-colors"
+                        )}
+                    />
+                );
+            })}
+        </div>
+    );
+};
+

+ 23 - 0
src/components/catalog/review/NoReview.tsx

@@ -0,0 +1,23 @@
+import { ReviewButton } from "@components/common/button/ReviewButton";
+import { NoReviewIcon } from "@components/common/icons/NoReviewsIcon";
+
+export const NoReview = ({
+  setShowForm,
+}: {
+  setShowForm: (show: boolean) => void;
+}) => {
+  return (
+    <div className="flex flex-col items-center gap-4 py-8">
+      <div className="flex flex-col items-center gap-4">
+        <NoReviewIcon />
+        <h2 className="font-outfit text-2xl font-semibold tracking-wide mt-4">
+          Ratings
+        </h2>
+        <p className="text-lg mt-2">
+          No reviews yet. Be the first to share your experience
+        </p>
+      </div>
+      <ReviewButton setShowForm={setShowForm} />
+    </div>
+  );
+};

+ 203 - 0
src/components/catalog/review/ReviewDetail.tsx

@@ -0,0 +1,203 @@
+"use client";
+
+import { Rating } from "@components/common/Rating";
+import { GridTileImage } from "@components/theme/ui/grid/Tile";
+import { formatDate, getInitials, getReviews } from "@/utils/helper";
+import { isArray } from "@/utils/type-guards";
+import { Avatar, Tooltip } from "@heroui/react";
+import clsx from "clsx";
+import { FC, useState } from "react";
+import ReviewSection from "./ReviewSection";
+import { ProductReviewNode } from "@/components/catalog/type";
+
+interface ProductReviewEdge {
+  __typename: "ProductReviewEdge";
+  node: ProductReviewNode;
+}
+
+interface ReviewDetailProps {
+  reviewDetails: ProductReviewEdge[];
+  totalReview: number;
+  productId: string;
+}
+
+const ReviewDetail: FC<ReviewDetailProps> = ({
+  reviewDetails,
+  totalReview,
+  productId,
+}) => {
+  const [visibleCount, setVisibleCount] = useState(5);
+
+  const reviews: ProductReviewNode[] =
+    reviewDetails?.map((edge) => edge.node) || [];
+
+  const { reviewAvg, ratingCounts } = getReviews(reviews);
+
+  const visibleReviews = reviews.slice(0, visibleCount);
+
+  return (
+    <>
+      <div className="flex flex-col flex-wrap gap-x-5 sm:gap-x-10">
+        <div className="my-2 flex w-full flex-col flex-wrap justify-between gap-4 sm:flex-row sm:items-center min-[1350px]:flex-nowrap">
+          <div className="flex items-center gap-x-2">
+            <Rating
+              length={5}
+              size="size-5"
+              star={reviewAvg}
+              reviewCount={totalReview}
+            />
+          </div>
+
+          <div className="flex w-full max-w-[280px] overflow-hidden rounded-sm">
+            {Object.entries(ratingCounts)
+              .reverse()
+              .filter(([_, count]) => (count as number) > 0)
+              .map(([star, count]) => (
+                <div
+                  key={star}
+                  style={{ flex: count as number }}
+                  className="min-h-4"
+                >
+                  <Tooltip
+                    content={
+                      <p className="text-center">
+                        {star} Star <br /> {count as number}{" "}
+                        {(count as number) >= 2 ? "Reviews" : "Review"}
+                      </p>
+                    }
+                    placement="top"
+                    className="cursor-pointer"
+                  >
+                    <div
+                      className={clsx(
+                        "h-full w-full !cursor-pointer",
+                        star === "5"
+                          ? "bg-green-700"
+                          : star === "4"
+                            ? "bg-cyan-400"
+                            : star === "3"
+                              ? "bg-violet-600"
+                              : star === "2"
+                                ? "bg-yellow-400"
+                                : "bg-red-600",
+                      )}
+                    />
+                  </Tooltip>
+                </div>
+              ))}
+          </div>
+        </div>
+
+        <div className="flex w-full flex-1 flex-col gap-5 py-2 sm:pt-6">
+          {/* Scrollable Container */}
+          <div
+            className="max-h-[380px] overflow-y-auto pr-2 
+        scrollbar-thin scrollbar-track-transparent 
+        scrollbar-thumb-neutral-400 dark:scrollbar-thumb-neutral-600"
+          >
+            {visibleReviews &&
+              visibleReviews.map(
+                (
+                  {
+                    name,
+                    title,
+                    comment,
+                    createdAt,
+                    rating,
+                    images,
+                    customer,
+                  }: ProductReviewNode,
+                  index: number,
+                ) => (
+                  <div
+                    key={index}
+                    className={clsx("flex flex-col gap-y-2 py-3", {
+                      "border-b border-neutral-200 dark:border-neutral-700":
+                        visibleReviews.length > 1 &&
+                        index < visibleReviews.length - 1,
+                    })}
+                  >
+                    <h1 className="font-outfit text-xl font-medium text-black/[80%] dark:text-white">
+                      {title}
+                    </h1>
+                    <Rating
+                      className="my-1"
+                      length={5}
+                      size="size-5"
+                      star={rating}
+                    />
+
+                    <h2 className="font-outfit text-base font-normal text-black/[80%] dark:text-white">
+                      {comment}
+                    </h2>
+
+                    <div className="flex gap-4">
+                      <div className="flex w-full items-center gap-2">
+                        <Avatar
+                          className="h-[56px] min-w-[56px] border border-solid border-black/10 bg-white text-large dark:bg-neutral-900"
+                          name={getInitials(name)}
+                          src={customer?.imageUrl}
+                        />
+                        <div>
+                          <h1 className="font-outfit text-base font-medium text-black/80 dark:text-white">
+                            {name}
+                          </h1>
+                          <p className="text-base text-black/80 dark:text-white">
+                            {formatDate(createdAt)}
+                          </p>
+                        </div>
+                      </div>
+                    </div>
+
+                    {isArray(images) && images && images.length > 0 && (
+                      <div className="mt-2 flex h-full min-h-[50px] w-full max-w-[60px] flex-wrap gap-2">
+                        {images.map((img) => (
+                          <GridTileImage
+                            key={img.reviewId}
+                            fill
+                            alt={`${img.reviewId}-review`}
+                            className="rounded-lg"
+                            src={img.url}
+                          />
+                        ))}
+                      </div>
+                    )}
+                  </div>
+                ),
+              )}
+
+            {reviews.length > visibleCount && (
+              <div className="py-4">
+                <button
+                  onClick={() => setVisibleCount((prev) => prev + 5)}
+                  className="flex items-start gap-2 text-primary-600 cursor-pointer hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300 font-medium transition-colors duration-200 group"
+                >
+                  <span>Load More</span>
+                  <svg
+                    className="w-4 h-4 transition-transform duration-200 group-hover:translate-x-1 mt-1"
+                    fill="none"
+                    stroke="currentColor"
+                    viewBox="0 0 24 24"
+                    xmlns="http://www.w3.org/2000/svg"
+                  >
+                    <path
+                      strokeLinecap="round"
+                      strokeLinejoin="round"
+                      strokeWidth={2}
+                      d="M9 5l7 7-7 7"
+                    />
+                  </svg>
+                </button>
+              </div>
+            )}
+          </div>
+        </div>
+        <div className="mx-auto ">
+          <ReviewSection productId={productId} totalReview={totalReview} />
+        </div>
+      </div>
+    </>
+  );
+};
+
+export default ReviewDetail;

+ 59 - 0
src/components/catalog/review/ReviewSection.tsx

@@ -0,0 +1,59 @@
+"use client";
+
+import { useState } from "react";
+import { Modal, ModalContent } from "@heroui/react";
+import AddProductReview from "./AddProductReview";
+import { ReviewButton } from "@components/common/button/ReviewButton";
+import { NoReview } from "./NoReview";
+
+interface ReviewSectionProps {
+  productId: string;
+  className?: string;
+  totalReview: number;
+}
+
+export default function ReviewSection({
+  productId,
+  className,
+  totalReview,
+}: ReviewSectionProps) {
+  const [open, setOpen] = useState(false);
+
+  return (
+    <>
+      <div>
+        {totalReview > 0 ? (
+          <ReviewButton setShowForm={setOpen} className={className} />
+        ) : (
+          <NoReview setShowForm={setOpen} />
+        )}
+      </div>
+
+      <Modal
+        isOpen={open}
+        onOpenChange={setOpen}
+        backdrop="blur"
+        size="4xl"
+        hideCloseButton
+        scrollBehavior="inside"
+        placement="center"
+        classNames={{
+          wrapper: "z-[100] items-center justify-center",
+          backdrop: "z-[99]",
+          base: "bg-white/90 dark:bg-black/80 backdrop-blur-xl  rounded-xl mx-4",
+        }}
+      >
+        <ModalContent className="p-0 border-none">
+          {(onClose) => (
+            <AddProductReview
+              productId={productId}
+              onClose={onClose}
+            />
+          )}
+        </ModalContent>
+      </Modal>
+    </>
+  );
+}
+
+

+ 243 - 0
src/components/catalog/type.ts

@@ -0,0 +1,243 @@
+export interface SingleProductResponse {
+  product: ProductNode;
+}
+
+export interface ProductSectionNode {
+  isSaleable: string | undefined;
+  id: string;
+  sku: string;
+  type: string;
+  urlKey?: string;
+  name?: string;
+  baseImageUrl?: string;
+  price?: string | number;
+  specialPrice?: string;
+  images?: {
+    edges: Array<{
+      node: {
+        publicPath: string;
+      };
+    }>;
+  };
+}
+
+export interface ProductVariantNode {
+  id: string;
+  sku: string;
+  name?: string;
+  price?: number;
+  attributeValues?: {
+    edges: Array<{
+      node: {
+        attribute?: {
+          code?: string;
+        };
+        value?: string;
+      };
+    }>;
+  };
+}
+
+export interface ProductNode {
+  variants: {
+    edges: Array<{ node: ProductVariantNode }>;
+  };
+  id: string;
+  sku: string;
+  type: string;
+  name?: string;
+  urlKey?: string;
+  description?: string;
+  shortDescription?: string;
+  specialPrice?: string;
+  metaTitle?: string;
+  baseImageUrl?: string;
+  price?: string | number | { value?: number; currencyCode?: string } | null;
+  minimumPrice?: string | number;
+  priceHtml?: {
+    currencyCode?: string;
+  };
+  superAttributes?: {
+    edges: Array<{ node: ProductReviewNode }>;
+  };
+  reviews?: {
+    edges: Array<{ node: ProductReviewNode }>;
+  };
+  relatedProducts?: {
+    edges: Array<{ node: ProductSectionNode }>;
+  };
+  crossSells?: {
+    edges: Array<{ node: ProductSectionNode }>;
+  };
+  upSells?: {
+    edges: Array<{ node: ProductSectionNode }>;
+  };
+}
+
+export interface ProductsResponse {
+  products: {
+    edges: Array<{ node: ProductNode }>;
+    pageInfo: {
+      endCursor: string;
+      startCursor: string;
+      hasNextPage: boolean;
+      hasPreviousPage: boolean;
+    };
+    totalCount: number;
+  };
+}
+
+export interface ProductSectionNode {
+  id: string;
+  sku: string;
+  type: string;
+  urlKey?: string;
+  name?: string;
+  baseImageUrl?: string;
+  minimumPrice?: string | number;
+  price?: string | number;
+  specialPrice?: string;
+  images?: {
+    edges: Array<{
+      node: {
+        publicPath: string;
+      };
+    }>;
+  };
+}
+
+export type ProductsSectionProps = {
+  title: string;
+  description: string;
+  products: ProductSectionNode[];
+};
+
+export interface ProductFilterAttributeResponse {
+  attribute: {
+    id: string;
+    code: string;
+    options: {
+      edges: Array<{
+        node: {
+          id: string;
+          adminName: string;
+        };
+      }>;
+    };
+  };
+}
+
+export type ProductReview = {
+  rating: number;
+};
+
+export interface ProductReviewNode {
+  __typename: "ProductReview";
+  name: string;
+  title: string;
+  rating: number;
+  comment: string;
+  createdAt: string;
+  images?: {
+    url: string;
+    reviewId: string;
+  }[];
+  customer?: {
+    name: string;
+    imageUrl: string;
+  };
+}
+
+export interface ProductData {
+  urlKey: string;
+  variants?: {
+    edges?: {
+      node?: {
+        attributeValues?: {
+          edges?: {
+            node?: {
+              attribute?: {
+                code?: string;
+              };
+              value?: string;
+            };
+          }[];
+        };
+        id?: string;
+        priceBaseImageUrl?: string;
+        name?: string;
+        name_id?: string;
+        sku?: string;
+        type?: string;
+        color?: string;
+        size?: string;
+      };
+    }[];
+  } | null;
+  name?: string;
+  price?: { value?: number; currencyCode?: string } | number | null;
+  priceHtml?: { currencyCode?: string } | null;
+  averageRating?: number;
+  type?: string;
+  reviewCount?: number;
+  minimumPrice?: string;
+  specialPrice?: string;
+  shortDescription?: string;
+  status?: boolean;
+  id?: string;
+  description?: string;
+  configutableData?: {
+    attributes?: unknown[];
+    index?: unknown[];
+  } | null;
+}
+
+export interface AttributeType {
+  isVisibleOnFront: string;
+  id: string;
+  code: string;
+  adminName: string;
+  type: string;
+  label?: string;
+}
+
+export type additionalDataTypes = {
+  attribute: AttributeType;
+  id: string;
+  code: string;
+  label: string;
+  value: string | null;
+  admin_name: string;
+  type: string;
+};
+
+// Product review
+
+export interface RatingTypes {
+  length?: number;
+  value?: number;
+  size?: string;
+  className?: string;
+  onChange?: (value: number) => void;
+}
+
+export interface ReviewDatatypes {
+  id: string;
+  name: string;
+  title: string;
+  rating: 5;
+  status: string;
+  comment: string;
+  productId: string;
+  customerId: string;
+  createdAt: string;
+  images: {
+    url: string;
+    reviewId: string;
+  }[];
+  customer: {
+    name: string;
+    imageUrl: string;
+  };
+}
+

+ 161 - 0
src/components/checkout/checkout-cart/CartItemAccordian.tsx

@@ -0,0 +1,161 @@
+import { DEFAULT_OPTION } from "@/utils/constants";
+import { useScrollTo } from "@/utils/hooks/useScrollTo";
+import { Price } from "@components/theme/ui/Price";
+import { Accordion, AccordionItem } from "@heroui/accordion";
+import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/24/outline";
+import { createUrl, safeParse } from "@utils/helper";
+import Image from "next/image";
+import Link from "next/link";
+
+type MerchandiseSearchParams = {
+  [key: string]: string;
+};
+
+export default function CartItemAccordion({
+  cartItems,
+}: {
+  cartItems: any;
+}) {
+
+  const cart = Array.isArray(cartItems?.items?.edges)
+    ? cartItems?.items?.edges
+    : [];
+
+  const scrollTo = useScrollTo();
+
+  return (
+    <div className="mobile-heading fixed bottom-0 left-0 z-50 w-full border-t border-neutral-200 bg-white pb-14
+     dark:border-neutral-700 dark:bg-black lg:hidden">
+      <Accordion
+        selectionMode="multiple"
+        className="!px-0"
+        onSelectionChange={(e) => {
+          const keys = e as Set<string>;
+          if (keys.has("1")) {
+            setTimeout(() => {
+              scrollTo({
+                top: document.body.scrollHeight,
+                behavior: "smooth",
+              });
+            }, 300);
+          }
+        }}
+      >
+        <AccordionItem
+          key="1"
+          indicator={({ isOpen }) =>
+            isOpen ? (
+              <ChevronLeftIcon className="h-5 w-5 stroke-neutral-800 dark:stroke-white" />
+            ) : (
+              <ChevronRightIcon className="h-5 w-5 stroke-neutral-800 dark:stroke-white" />
+            )
+          }
+          classNames={{
+    heading: "px-4", 
+    content: "px-4" 
+  }}
+          aria-label="Accordion 1"
+          title="Order Summary"
+          subtitle={
+            <Price
+              className=""
+              amount={cartItems?.grandTotal || "0"}
+              currencyCode={"USD"}
+            />
+          }
+        >
+          <div className="flex h-full flex-col justify-between px-4">
+            <ul className="flex-grow overflow-y-auto max-h-[300px] py-4 pr-2 -mr-2" style={{ scrollbarWidth: 'thin' }}>
+              {cart?.map((item: any, i: number) => {
+                const merchandiseSearchParams = {} as MerchandiseSearchParams;
+                const merchandiseUrl = createUrl(
+                                `/product/${item?.node.productUrlKey}`,
+                                new URLSearchParams(merchandiseSearchParams)
+                              );
+                const baseImage: any = safeParse(item?.node?.baseImage);
+                return (
+                  <li key={i} className="flex w-full flex-col">
+                    <div className="relative flex w-full flex-row justify-between gap-3 px-1 py-4">
+                      <Link
+                        href={merchandiseUrl}
+                        className="z-30 flex flex-row items-center space-x-4"
+                        aria-label={`${item?.node?.name}`}
+                      >
+                        <div className="relative h-16 w-16 cursor-pointer overflow-hidden rounded-md border border-neutral-300 bg-neutral-300 dark:border-neutral-700 dark:bg-neutral-900 dark:hover:bg-neutral-800">
+                          <Image
+                            className="h-full w-full object-cover"
+                            width={64}
+                            height={64}
+                            alt={item?.node?.baseImage || item?.product?.name}
+                            src={baseImage?.small_image_url || ""}
+                          />
+                        </div>
+
+                        <div className="flex flex-1 flex-col text-base">
+                          <span className="leading-tight text-neutral-900 line-clamp-1 dark:text-white">
+                            {item?.node?.name}
+                          </span>
+                          <span className="font-normal text-black dark:text-white">
+                            Quantity : {item.node.quantity}
+                          </span>
+                          {item.name !== DEFAULT_OPTION ? (
+                            <p className="text-sm lowercase line-clamp-1 text-neutral-500 dark:text-neutral-400">
+                              {item?.node?.sku}
+                            </p>
+                          ) : null}
+                        </div>
+                      </Link>
+                      <div className="flex h-16 flex-col justify-between text-black/[60%] dark:!text-neutral-300">
+                        <Price
+                          className="flex justify-end space-y-2 text-right text-sm"
+                          amount={item?.node?.price}
+                          currencyCode={"USD"}
+                        />
+                      </div>
+                    </div>
+                  </li>
+                );
+              })}
+            </ul>
+            <div className="py-4 text-sm text-neutral-500 dark:text-neutral-400">
+              <div className="mb-3 flex items-center justify-between pb-1">
+                <p className="text-black[60%] font-outfit text-base font-normal dark:text-white">
+                  Subtotal
+                </p>
+                <Price
+                  className="text-right text-base text-black dark:text-white"
+                  amount={cartItems?.subtotal || "0"}
+                  currencyCode={"USD"}
+                />
+              </div>
+              <div className="mb-3 flex items-center justify-between pb-1 pt-1">
+                <p className="text-black[60%] font-outfit text-base font-normal dark:text-white">
+                  Shipping
+                </p>
+                {cartItems?.shippingAmount ? (
+                  <Price
+                    amount={cartItems?.shippingAmount || "0"}
+                    className="text-right text-base text-black dark:text-white"
+                    currencyCode={"USD"}
+                  />
+                ) : (
+                  <p className="text-right text-base">
+                    Calculated at Next Step
+                  </p>
+                )}
+              </div>
+              <div className="mb-3 flex items-center justify-between pb-1 pt-1">
+                <p className="text-xl font-bold dark:text-white">Total</p>
+                <Price
+                  className="text-right text-base text-black dark:text-white"
+                  amount={cartItems?.grandTotal || "0"}
+                  currencyCode={"USD"}
+                />
+              </div>
+            </div>
+          </div>
+        </AccordionItem>
+      </Accordion>
+    </div>
+  );
+}

+ 128 - 0
src/components/checkout/checkout-cart/CheckoutCart.tsx

@@ -0,0 +1,128 @@
+import { DEFAULT_OPTION } from "@/utils/constants";
+import { GridTileImage } from "@components/theme/ui/grid/Tile";
+import { Price } from "@components/theme/ui/Price";
+import CartItemAccordion from "./CartItemAccordian";
+import { NOT_IMAGE } from "@utils/constants";
+import Link from "next/link";
+import { createUrl, safeParse } from "@utils/helper";
+type MerchandiseSearchParams = {
+  [key: string]: string;
+};
+
+export default function CheckoutCart({ cartItems, selectedShippingRate: _id }: { cartItems: any, selectedShippingRate?: any }) {
+
+  const cart = Array.isArray(cartItems?.items?.edges)
+    ? cartItems?.items?.edges
+    : [];
+  return (
+    <>
+      <CartItemAccordion cartItems={cartItems} />
+      <div className="hidden h-full min-h-[100dvh] flex-col justify-between py-4 pl-4 pr-8 lg:flex">
+        <div className="">
+          <h1 className="p-6 font-outfit text-xl font-medium text-black dark:text-neutral-300">
+            Order Summary
+          </h1>
+          <ul className="m-0 flex max-h-[calc(100dvh-292px)] flex-col gap-y-6 overflow-y-auto px-4 py-6 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-gray-500 dark:scrollbar-thumb-neutral-300 lg:h-[calc(100dvh-124px)] lg:overflow-hidden lg:overflow-y-auto">
+            {Array.isArray(cart) &&
+              cart &&
+              cart?.map((item: any, i: number) => {
+                const merchandiseSearchParams = {} as MerchandiseSearchParams;
+                const merchandiseUrl = createUrl(
+                  `/product/${item?.node.productUrlKey}`,
+                  new URLSearchParams(merchandiseSearchParams)
+                );
+                const baseImage: any = safeParse(item?.node?.baseImage);
+
+                return (
+                  <li key={i} className="flex w-full flex-col">
+                    <div className="relative flex w-full flex-row justify-between">
+                      <Link
+                        className="z-30 flex flex-row items-center space-x-4"
+                        aria-label={`${item?.node?.name}`}
+                        href={merchandiseUrl}
+                      >
+                        <div className="relative h-[120px] w-[120px] cursor-pointer rounded-2xl bg-neutral-300 xl:h-[162px] xl:w-[194px]">
+                          <GridTileImage
+                            alt={item?.node?.baseImage || item?.product?.name}
+                            className="h-full w-full object-cover"
+                            height={64}
+                            src={baseImage?.small_image_url || ""}
+                            width={74}
+                            onError={(e) => (e.currentTarget.src = NOT_IMAGE)}
+                          />
+                        </div>
+                        <div className="flex flex-1 flex-col text-base">
+                          <h1 className="font-outfit text-lg font-medium">
+                            {item?.node?.name}
+                          </h1>
+                          {item.name !== DEFAULT_OPTION ? (
+                            <p className="text-sm font-normal text-neutral-500 dark:text-neutral-400">
+                              {item?.node?.sku}
+                            </p>
+                          ) : null}
+                          <span className="font-normal text-black dark:text-white">
+                            Quantity : {item?.node?.quantity}
+                          </span>
+                          <div className="block h-16 xl:hidden">
+                            <Price
+                              amount={item?.node?.price}
+                              className="space-y-2 text-start font-outfit text-lg font-medium xl:text-right"
+                              currencyCode={"USD"}
+                            />
+                          </div>
+                        </div>
+                      </Link>
+                      <div className="hidden h-16 xl:block">
+                        <Price
+                          amount={item?.node?.price}
+                          className="space-y-2 text-start font-outfit text-lg font-medium xl:text-right"
+                          currencyCode={"USD"}
+                        />
+                      </div>
+                    </div>
+                  </li>
+                );
+              })}
+          </ul>
+        </div>
+        <div className="px-4 py-4 text-sm text-neutral-500 dark:text-neutral-400">
+          <div className="mb-3 flex items-center justify-between pb-1">
+            <p className="text-black[60%] font-outfit text-base font-normal">
+              Subtotal
+            </p>
+            <Price
+              amount={cartItems?.subtotal || "0"}
+              className="text-right text-base text-black dark:text-white"
+              currencyCode={"USD"}
+            />
+          </div>
+          <div className="mb-3 flex items-center justify-between pb-1 pt-1">
+            <p className="text-black[60%] font-outfit text-base font-normal">
+              {" "}
+              Shipping
+            </p>
+            {cartItems?.shippingAmount ? (
+              <Price
+                amount={cartItems?.shippingAmount}
+                className="text-right text-base text-black dark:text-white"
+                currencyCode={"USD"}
+              />
+            ) : (
+              <p className="text-right text-base">Calculated at Next Step</p>
+            )}
+          </div>
+          <div className="my-6 flex items-center justify-between">
+            <p className="font-outfit text-2xl font-normal text-black/[60%] dark:text-white">
+              Grand Total
+            </p>
+            <Price
+              amount={(cartItems as any)?.grandTotal || "0"}
+              className="text-right font-outfit text-2xl font-normal text-black dark:text-white"
+              currencyCode={"USD"}
+            />
+          </div>
+        </div>
+      </div>
+    </>
+  );
+}

+ 68 - 0
src/components/checkout/index.tsx

@@ -0,0 +1,68 @@
+"use client";
+import { useAppSelector } from "@/store/hooks";
+import CheckoutSkeleton from "../common/skeleton/CheckoutSkeleton";
+import { CartSkeleton } from "../common/skeleton/ProductCartSkeleton";
+import { useCartDetail } from "@/utils/hooks/useCartDetail";
+import CheckoutCart from "./checkout-cart/CheckoutCart";
+import Stepper from "./stepper";
+import { useEffect } from 'react';
+import { useScrollToTop } from "@/utils/hooks/useScrollTo";
+import { useAddressesFromApi } from "@utils/hooks/getAddress";
+
+
+
+interface CheckOutProps {
+  step: string;
+}
+
+const CheckOut = ({ step }: CheckOutProps) => {
+  const { isLoading, getCartDetail } = useCartDetail();
+  const { billingAddress, shippingAddress } = useAddressesFromApi(true);
+  const cartDetail = useAppSelector((state) => state.cartDetail);
+  const cartItems = cartDetail?.cart;
+  const selectedShippingRate = cartItems?.selectedShippingRate;
+  const selectedShippingRateTitle = cartItems?.selectedShippingRateTitle;
+  const selectedPayment = cartItems?.paymentMethod;
+  const selectedPaymentTitle = cartItems?.paymentMethodTitle;
+  useScrollToTop();
+
+  useEffect(() => {
+    getCartDetail();
+  }, [getCartDetail]);
+
+
+
+  return (
+    <>
+      <section className="flex flex-col items-start justify-between lg:flex-row lg:justify-between">
+        <div className="w-full px-0 py-2 sm:px-4 sm:py-4 lg:w-1/2 xl:pl-16 xl:pr-0">
+          {isLoading ? (
+            <CheckoutSkeleton />
+          ) : (
+            <Stepper
+              billingAddress={billingAddress}
+              currentStep={step}
+              selectedPayment={selectedPayment}
+              selectedPaymentTitle={selectedPaymentTitle}
+              selectedShippingRate={selectedShippingRate}
+              selectedShippingRateTitle={selectedShippingRateTitle}
+              shippingAddress={shippingAddress}
+            />
+          )}
+        </div>
+
+        <div className="h-full w-full !z-0 justify-self-start border-0 border-l border-none border-black/[10%] dark:border-neutral-700 lg:w-1/2 lg:border-solid">
+          {isLoading ? (
+            <CartSkeleton className="w-full" />
+          ) : (
+            <div className="max-h-auto w-full flex-initial flex-shrink-0 flex-grow-0 lg:sticky lg:top-0">
+              <CheckoutCart cartItems={cartItems} selectedShippingRate={selectedShippingRate} />
+            </div>
+          )}
+        </div>
+      </section>
+    </>
+  );
+};
+
+export default CheckOut;

+ 140 - 0
src/components/checkout/stepper/Email.tsx

@@ -0,0 +1,140 @@
+"use client";
+import { useForm } from "react-hook-form";
+import { useRouter } from "next/navigation";
+import { useState } from "react";
+import { EMAIL, getLocalStorage, setLocalStorage } from "@/store/local-storage";
+import Link from "next/link";
+import InputText from "@components/common/form/Input";
+import { ProceedToCheckout } from "./ProceedToCheckout";
+import { delay } from "@utils/helper";
+import { EmailFormProps, EmailFormValues } from "../type";
+import { EMAIL_REGEX } from "@utils/constants";
+
+
+
+const Email = () => {
+  const email = getLocalStorage(EMAIL);
+  const [isOpen, setIsOpen] = useState(false);
+  const router = useRouter();
+  const isGuest = true;
+
+  const {
+    register,
+    handleSubmit,
+    formState: { errors, isSubmitting },
+  } = useForm<EmailFormValues>({
+    defaultValues: { email },
+  });
+
+  const onSubmit = async (data: EmailFormValues) => {
+    setLocalStorage(EMAIL, data?.email);
+    await delay(200);
+    router.push("/checkout?step=address");
+  };
+
+  return (
+    <>
+      {email === "" || typeof email === "object" ? (
+        <form className="mt-5" onSubmit={handleSubmit(onSubmit)}>
+          <EmailForm
+            register={register}
+            errors={errors}
+            isSubmitting={isSubmitting}
+            isGuest={isGuest}
+          />
+        </form>
+      ) : isOpen ? (
+        <form className="mt-5" onSubmit={handleSubmit(onSubmit)}>
+          <EmailForm
+            register={register}
+            errors={errors}
+            isSubmitting={isSubmitting}
+            isGuest={isGuest}
+          />
+        </form>
+      ) : (
+        <>
+          <div className="mt-4 flex gap-2 justify-between hidden sm:flex">
+            <div className="flex">
+              <p className="w-auto text-base font-normal text-black/60 dark:text-white/60 sm:w-[180px]">
+                Email Address
+              </p>
+              <p className="font-normal block text-base text-black/60 dark:text-white/60">{email}</p>
+            </div>
+            <div>
+            <button
+              onClick={() => setIsOpen(!isOpen)}
+              className="cursor-pointer text-base font-normal text-black/[60%] underline dark:text-neutral-300"
+            >
+              Change
+            </button>
+            </div>
+          </div>
+          <div className=" relative mt-4 flex sm:hidden flex-col justify-end gap-y-2 sm:flex-row sm:justify-between sm:gap-y-0">
+            <div className="flex justify-between  flex-1 flex-wrap">
+              <p className="w-auto text-base font-normal text-black/60 dark:text-white/60 sm:w-[192px]">
+                Email Address
+              </p>
+              <p className="font-normal block text-base text-black/60 dark:text-white/60">{email}</p>
+            </div>
+            <button
+              onClick={() => setIsOpen(!isOpen)}
+              className="cursor-pointer absolute right-0  text-base font-normal text-black/[60%] underline dark:text-neutral-300"
+              style={{ top: "-36px" }}
+            >
+              Change
+            </button>
+          </div>
+        </>
+      )}
+    </>
+  );
+};
+
+export default Email;
+
+function EmailForm({
+  register,
+  errors,
+  isSubmitting,
+  isGuest,
+}: EmailFormProps) {
+  return (
+    <div>
+      <InputText
+        className="max-w-full"
+        id="email"
+        size="md"
+        {...register("email", {
+          required: "Email is required",
+          pattern: {
+            value: EMAIL_REGEX,
+            message: "Please enter a valid email address",
+          }
+        })}
+        errorMsg={errors?.email?.message as string}
+        label="Enter Email *"
+        placeholder="example@gmail.com"
+        readOnly={!isGuest}
+      />
+
+      {isGuest && (
+        <p className="mb-4 mt-6 font-outfit text-base font-normal text-black/[60%] dark:text-neutral-300">
+          Already have an account? No worries, just{" "}
+          <br className="block sm:hidden" />
+          <Link
+            aria-label="Go to Login Page"
+            className="text-base font-normal text-primary"
+            href="/customer/login"
+          >
+            log in.
+          </Link>
+        </p>
+      )}
+
+      <div className="mt-6 justify-self-end">
+        <ProceedToCheckout buttonName="Next" pending={isSubmitting} />
+      </div>
+    </div>
+  );
+}

+ 540 - 0
src/components/checkout/stepper/GuestAddAdressForm.tsx

@@ -0,0 +1,540 @@
+"use client";
+import { FC, useState, useEffect } from "react";
+import { useForm, useWatch } from "react-hook-form";
+import { AddressDataTypes } from "@/types/types";
+import { EMAIL, getLocalStorage } from "@/store/local-storage";
+import { IS_VALID_ADDRESS, IS_VALID_PHONE, IS_VALID_INPUT } from "@/utils/constants";
+import { isObject } from "@/utils/type-guards";
+import { useCheckout } from "@utils/hooks/useCheckout";
+import InputText from "@components/common/form/Input";
+import { ProceedToCheckout } from "./ProceedToCheckout";
+import CheckBox from "@components/theme/ui/element/Checkbox";
+import { useDispatch } from "react-redux";
+import { setCheckoutAddresses } from "@/store/slices/cart-slice";
+
+
+export const GuestAddAdressForm: FC<{
+  billingAddress?: AddressDataTypes | null;
+  shippingAddress?: AddressDataTypes | null;
+  currentStep?: string;
+}> = ({
+  billingAddress: initialBilling,
+  shippingAddress: initialShipping,
+  currentStep,
+}) => {
+    const email = getLocalStorage(EMAIL);
+    const dispatch = useDispatch();
+    const [isOpen, setIsOpen] = useState(currentStep !== "address");
+
+    const [billingAddress, setBillingAddress] = useState<AddressDataTypes | null>(
+      initialBilling ?? null
+    );
+    const [shippingAddress, setShippingAddress] =
+      useState<AddressDataTypes | null>(initialShipping ?? null);
+
+    const [prevStep, setPrevStep] = useState(currentStep);
+    if (currentStep !== prevStep) {
+      setPrevStep(currentStep);
+      setIsOpen(currentStep !== "address" && isObject(shippingAddress) && isObject(billingAddress));
+    }
+
+
+    const {
+      register,
+      control,
+      handleSubmit,
+      reset,
+      formState: { errors },
+    } = useForm({
+      mode: "onSubmit",
+      defaultValues: {
+        billing: {
+          email: billingAddress?.email ?? email ?? "",
+          firstName: billingAddress?.firstName || "",
+          lastName: billingAddress?.lastName || "",
+          companyName: billingAddress?.companyName || "",
+          address: billingAddress?.address || "",
+          country: billingAddress?.country || "IN",
+          state: billingAddress?.state || "UP",
+          city: billingAddress?.city || "",
+          postcode: billingAddress?.postcode || "",
+          phone: billingAddress?.phone || "",
+        },
+        shipping: {
+          email: shippingAddress?.email ?? email ?? "",
+          firstName: shippingAddress?.firstName || "",
+          lastName: shippingAddress?.lastName || "",
+          companyName: shippingAddress?.companyName || "",
+          address: shippingAddress?.address || "",
+          country: shippingAddress?.country || "IN",
+          state: shippingAddress?.state || "UP",
+          city: shippingAddress?.city || "",
+          postcode: shippingAddress?.postcode || "",
+          phone: shippingAddress?.phone || "",
+        },
+        useForShipping: true,
+      },
+    });
+
+    useEffect(() => {
+      if (initialBilling || initialShipping) {
+        const billing = initialBilling ?? null;
+        const shipping = initialShipping ?? null;
+        requestAnimationFrame(() => {
+          setBillingAddress(billing);
+          setShippingAddress(shipping);
+          setIsOpen(currentStep !== "address" && isObject(shipping) && isObject(billing));
+        });
+
+        reset({
+          billing: {
+            email: initialBilling?.email ?? email ?? "",
+            firstName: initialBilling?.firstName || "",
+            lastName: initialBilling?.lastName || "",
+            companyName: initialBilling?.companyName || "",
+            address: initialBilling?.address || "",
+            country: initialBilling?.country || "IN",
+            state: initialBilling?.state || "UP",
+            city: initialBilling?.city || "",
+            postcode: initialBilling?.postcode || "",
+            phone: initialBilling?.phone || "",
+          },
+          shipping: {
+            email: initialShipping?.email ?? email ?? "",
+            firstName: initialShipping?.firstName || "",
+            lastName: initialShipping?.lastName || "",
+            companyName: initialShipping?.companyName || "",
+            address: initialShipping?.address || "",
+            country: initialShipping?.country || "IN",
+            state: initialShipping?.state || "UP",
+            city: initialShipping?.city || "",
+            postcode: initialShipping?.postcode || "",
+            phone: initialShipping?.phone || "",
+          },
+          useForShipping: true,
+        });
+      }
+    }, [initialBilling, initialShipping, reset, email]);
+
+    const { isLoadingToSave, saveCheckoutAddress } = useCheckout();
+
+    const watchUseForShipping = useWatch({
+      control,
+      name: "useForShipping",
+      defaultValue: true,
+    });
+
+    const addGuestAddress = async (data: any) => {
+      const billing = data?.billing;
+      const shipping = data?.shipping;
+
+      const useForShipping = Boolean(data.useForShipping);
+      const shippingSource = useForShipping ? billing : shipping;
+
+      const payload: any = {
+        billingFirstName: billing.firstName,
+        billingLastName: billing.lastName,
+        billingEmail: billing.email ?? email ?? "",
+        billingAddress: billing.address,
+        billingCity: billing.city,
+        billingCountry: billing.country || "IN",
+        billingState: billing.state || "UP",
+        billingPostcode: billing.postcode,
+        billingPhoneNumber: billing.phone,
+        billingCompanyName: billing.companyName,
+        useForShipping,
+      };
+
+      if (!useForShipping) {
+        payload.shippingFirstName = shipping.firstName;
+        payload.shippingLastName = shipping.lastName;
+        payload.shippingEmail = shipping.email ?? email ?? "";
+        payload.shippingAddress = shipping.address;
+        payload.shippingCity = shipping.city;
+        payload.shippingCountry = shipping.country;
+        payload.shippingState = shipping.state;
+        payload.shippingPostcode = shipping.postcode;
+        payload.shippingPhoneNumber = shipping.phone;
+        payload.shippingCompanyName = shipping.companyName;
+      }
+
+      try {
+        await saveCheckoutAddress(payload as any);
+        dispatch(
+          setCheckoutAddresses({
+            billing: {
+              ...billing,
+              email: billing.email ?? email ?? "",
+            },
+            shipping: {
+              ...shippingSource,
+              email: shippingSource.email ?? email ?? "",
+            },
+          })
+        );
+        setBillingAddress({
+          ...billing,
+          email: billing.email ?? email ?? "",
+        } as AddressDataTypes);
+
+        setShippingAddress({
+          ...shippingSource,
+          email: shippingSource.email ?? email ?? "",
+        } as AddressDataTypes);
+
+        setIsOpen(true);
+      } catch (error) {
+        console.error("Failed to save checkout address", error);
+      }
+    };
+
+    const showSummary = isObject(shippingAddress) && isObject(billingAddress);
+
+    if (showSummary && isOpen) {
+      return (
+        <>
+          <div className="mt-4  items-start  hidden sm:flex">
+            <div className="flex flex-col justify-between w-full">
+              <div className="flex">
+                <p className="w-[184px] text-base font-normal text-black/60 dark:text-white/60">
+                  Billing Address
+                </p>
+                <div className="block cursor-pointer rounded-xl p-2 max-sm:rounded-lg">
+                  <div className="flex flex-col">
+                    <p className="text-base font-medium">
+                      {`${billingAddress?.firstName || ""} ${billingAddress?.lastName || ""
+                        }`}
+                    </p>
+                    <p className="text-base font-medium text-zinc-500">
+                      {`${billingAddress?.companyName || ""}`}
+                    </p>
+                  </div>
+                  <p className="mt-2 text-sm text-zinc-500 max-md:mt-2 max-sm:mt-0">
+                    {`${billingAddress?.address || ""}, ${billingAddress?.postcode || ""
+                      }`}
+                  </p>
+                  <p className="text-zinc-500">
+                    {billingAddress?.city || ""} {billingAddress?.state || ""},
+                    {billingAddress?.country || ""}
+                  </p>
+                  <p className="mt-2 text-sm text-zinc-500 max-md:mt-2 max-sm:mt-0">
+                    {`T: ${billingAddress?.phone || ""}`}
+                  </p>
+                </div>
+              </div>
+              <div className="flex">
+                <p className="w-[184px] text-base font-normal text-black/60 dark:text-white/60">
+                  Shipping Address
+                </p>
+                <div className="block cursor-pointer rounded-xl p-2 max-sm:rounded-lg">
+                  <div className="flex flex-col">
+                    <p className="text-base font-medium">
+                      {`${shippingAddress?.firstName || ""} ${shippingAddress?.lastName || ""
+                        }`}
+                    </p>
+                    <p className="text-base font-medium text-zinc-500">
+                      {`${shippingAddress?.companyName || ""}`}
+                    </p>
+                  </div>
+                  <p className="mt-2 text-sm text-zinc-500 max-md:mt-2 max-sm:mt-0">
+                    {`${shippingAddress?.address || ""}, ${shippingAddress?.postcode || ""
+                      }`}
+                  </p>
+                  <p className="text-zinc-500">
+                    {shippingAddress?.city || ""} {shippingAddress?.state || ""},
+                    {shippingAddress?.country || ""}
+                  </p>
+                  <p className="mt-2 text-sm text-zinc-500 max-md:mt-2 max-sm:mt-0">
+                    {`T: ${shippingAddress?.phone || ""}`}
+                  </p>
+                </div>
+              </div>
+            </div>
+
+            <button
+              onClick={() => {
+                setIsOpen(false);
+              }}
+              className="cursor-pointer text-base font-normal text-black/[60%] underline dark:text-neutral-300"
+            >
+              Change
+            </button>
+          </div>
+          <div className="mt-4 flex sm:hidden items-start justify-between relative">
+            <div className="flex flex-col justify-between w-full">
+              <div className="flex justify-between justify-between  flex-1 wrap">
+                <p className="w-[184px] text-base font-normal text-black/60 dark:text-white/60">
+                  Billing Address
+                </p>
+                <div className="block cursor-pointer rounded-xl p-2 max-sm:rounded-lg">
+                  <div className="flex flex-col">
+                    <p className="text-base font-medium">
+                      {`${billingAddress?.firstName || ""} ${billingAddress?.lastName || ""
+                        }`}
+                    </p>
+                    <p className="text-base font-medium text-zinc-500">
+                      {`${billingAddress?.companyName || ""}`}
+                    </p>
+                  </div>
+                  <p className="mt-2 text-sm text-zinc-500 max-md:mt-2 max-sm:mt-0">
+                    {`${billingAddress?.address || ""}, ${billingAddress?.postcode || ""
+                      }`}
+                  </p>
+                  <p className="text-zinc-500">
+                    {billingAddress?.city || ""} {billingAddress?.state || ""},
+                    {billingAddress?.country || ""}
+                  </p>
+                  <p className="mt-2 text-sm text-zinc-500 max-md:mt-2 max-sm:mt-0">
+                    {`T: ${billingAddress?.phone || ""}`}
+                  </p>
+                </div>
+              </div>
+              <div className="flex justify-between justify-between  flex-1 wrap">
+                <p className="w-[184px] text-base font-normal text-black/60 dark:text-white/60">
+                  Shipping Address
+                </p>
+                <div className="block cursor-pointer rounded-xl p-2 max-sm:rounded-lg">
+                  <div className="flex flex-col">
+                    <p className="text-base font-medium">
+                      {`${shippingAddress?.firstName || ""} ${shippingAddress?.lastName || ""
+                        }`}
+                    </p>
+                    <p className="text-base font-medium text-zinc-500">
+                      {`${shippingAddress?.companyName || ""}`}
+                    </p>
+                  </div>
+                  <p className="mt-2 text-sm text-zinc-500 max-md:mt-2 max-sm:mt-0">
+                    {`${shippingAddress?.address || ""}, ${shippingAddress?.postcode || ""
+                      }`}
+                  </p>
+                  <p className="text-zinc-500">
+                    {shippingAddress?.city || ""} {shippingAddress?.state || ""},
+                    {shippingAddress?.country || ""}
+                  </p>
+                  <p className="mt-2 text-sm text-zinc-500 max-md:mt-2 max-sm:mt-0">
+                    {`T: ${shippingAddress?.phone || ""}`}
+                  </p>
+                </div>
+              </div>
+            </div>
+
+            <button
+              onClick={() => {
+                setIsOpen(false);
+              }}
+              className="cursor-pointer absolute right-0 text-base font-normal text-black/[60%] underline dark:text-neutral-300"
+              style={{ top: "-36px" }}
+            >
+              Change
+            </button>
+          </div>
+        </>
+      );
+    }
+
+    return (
+      <form className="my-5" onSubmit={handleSubmit(addGuestAddress)}>
+        <div className="my-7 grid grid-cols-6 gap-4">
+          <InputText
+            {...register("billing.firstName", {
+              required: "First name is required",
+              pattern: {
+                value: IS_VALID_INPUT,
+                message: "Invalid First Name",
+              },
+            })}
+            className="col-span-6 xxs:col-span-3 mb-4"
+            errorMsg={errors?.billing?.firstName?.message}
+            label="First Name *"
+            size="md"
+          />
+          <InputText
+            {...register("billing.lastName", {
+              required: "Last name is required",
+              pattern: {
+                value: IS_VALID_INPUT,
+                message: "Invalid Last Name",
+              },
+            })}
+            className="col-span-6 xxs:col-span-3 mb-4"
+            errorMsg={errors?.billing?.lastName?.message}
+            label="Last Name *"
+            size="md"
+          />
+          <InputText
+            {...register("billing.companyName", {
+              pattern: {
+                value: IS_VALID_INPUT,
+                message: "Invalid Company Name",
+              },
+            })}
+            className="col-span-6 mb-2"
+            errorMsg={errors?.billing?.companyName?.message}
+            label="Company Name"
+            size="md"
+          />
+          <InputText
+            {...register("billing.address", {
+              required: "Address field is required",
+              pattern: {
+                value: IS_VALID_ADDRESS,
+                message: "Invalid Address",
+              },
+            })}
+            className="col-span-6 mb-4"
+            errorMsg={errors?.billing?.address?.message}
+            label="Street Address *"
+            size="md"
+          />
+          <InputText
+            {...register("billing.city", {
+              required: "City field is required",
+              pattern: {
+                value: IS_VALID_INPUT,
+                message: "Invalid City",
+              },
+            })}
+            className="col-span-6 xxs:col-span-3 mb-4"
+            errorMsg={errors?.billing?.city?.message}
+            label="City *"
+            size="md"
+          />
+          <InputText
+            {...register("billing.postcode", {
+              required: "Postcode field is required",
+              pattern: {
+                value: IS_VALID_INPUT,
+                message: "Invalid Postcode",
+              },
+            })}
+            className="col-span-6 xxs:col-span-3"
+            errorMsg={errors?.billing?.postcode?.message}
+            label="Zip Code *"
+            size="md"
+          />
+          <InputText
+            {...register("billing.phone", {
+              required: "Phone field is required",
+              pattern: {
+                value: IS_VALID_PHONE,
+                message: "Enter Valid Phone Number",
+              },
+            })}
+            type="tel"
+            inputMode="tel"
+            autoComplete="tel"
+            className="col-span-6"
+            errorMsg={errors?.billing?.phone?.message}
+            label="Phone *"
+            size="md"
+          />
+          <CheckBox
+            className="col-span-6 mt-3"
+            defaultValue={watchUseForShipping}
+            id="useForShipping"
+            label="Billing address is same as shipping address"
+            {...register("useForShipping")}
+          />
+        </div>
+
+        {!watchUseForShipping && (
+          <div className="my-7 grid grid-cols-6 gap-4">
+            <InputText
+              {...register("shipping.firstName", {
+                required: "First name is required",
+                pattern: {
+                  value: IS_VALID_INPUT,
+                  message: "Invalid First Name",
+                },
+              })}
+              className="col-span-3 mb-4"
+              errorMsg={errors?.shipping?.firstName?.message}
+              label="First Name *"
+              size="md"
+            />
+            <InputText
+              {...register("shipping.lastName", {
+                required: "Last name is required",
+                pattern: {
+                  value: IS_VALID_INPUT,
+                  message: "Invalid Last Name",
+                },
+              })}
+              className="col-span-3 mb-4"
+              errorMsg={errors?.shipping?.lastName?.message}
+              label="Last Name *"
+              size="md"
+            />
+            <InputText
+              {...register("shipping.companyName", {
+                pattern: {
+                  value: IS_VALID_INPUT,
+                  message: "Invalid Company Name",
+                },
+              })}
+              className="col-span-6 mb-4"
+              errorMsg={errors?.shipping?.companyName?.message}
+              label="Company Name"
+              size="md"
+            />
+            <InputText
+              {...register("shipping.address", {
+                required: "Address field is required",
+                pattern: {
+                  value: IS_VALID_ADDRESS,
+                  message: "Invalid Address",
+                },
+              })}
+              className="col-span-6 mb-4"
+              errorMsg={errors?.shipping?.address?.message}
+              label="Street Address *"
+              size="md"
+            />
+            <InputText
+              {...register("shipping.city", {
+                required: "City field is required",
+                pattern: {
+                  value: IS_VALID_INPUT,
+                  message: "Invalid City",
+                },
+              })}
+              className="col-span-3 mb-4"
+              errorMsg={errors?.shipping?.city?.message}
+              label="City *"
+              size="md"
+            />
+            <InputText
+              {...register("shipping.postcode", {
+                required: "Postcode field is required",
+                pattern: {
+                  value: IS_VALID_INPUT,
+                  message: "Invalid Postcode",
+                },
+              })}
+              className="col-span-3"
+              errorMsg={errors?.shipping?.postcode?.message}
+              label="Zip Code *"
+              size="md"
+            />
+            <InputText
+              {...register("shipping.phone", {
+                required: "Phone field is required",
+                pattern: {
+                  value: IS_VALID_PHONE,
+                  message: "Enter Valid Phone Number",
+                },
+              })}
+              className="col-span-6"
+              errorMsg={errors?.shipping?.phone?.message}
+              label="Phone *"
+              size="md"
+            />
+          </div>
+        )}
+
+        <div className="justify-self-end">
+          <ProceedToCheckout buttonName="Next" pending={isLoadingToSave} />
+        </div>
+      </form>
+    );
+  };

+ 73 - 0
src/components/checkout/stepper/ProceedToCheckout.tsx

@@ -0,0 +1,73 @@
+"use client";
+import LoadingDots from "@components/common/icons/LoadingDots";
+import clsx from "clsx";
+
+function SubmitButton({
+  availableForSale,
+  buttonName,
+  className,
+  pending,
+}: {
+  availableForSale: boolean;
+  buttonName: string;
+  className?: string;
+  pending: boolean;
+}) {
+  const buttonClasses =
+    "relative text-base w-fit cursor-pointer rounded-full px-8 py-3 font-bold border-white items-center justify-center bg-blue-600 tracking-wide text-white";
+  const disabledClasses = "cursor-wait opacity-60 hover:opacity-60";
+
+  if (!availableForSale) {
+    return (
+      <button 
+        type="button"
+        aria-disabled 
+        disabled
+        className={clsx(buttonClasses, disabledClasses)}
+      >
+        Processing...
+      </button>
+    );
+  }
+
+  return (
+    <button
+      type="submit"
+      disabled={pending}
+      aria-disabled={pending}
+      aria-label="Proceed to checkout"
+      className={clsx(
+        buttonClasses,
+        {
+          "hover:opacity-90": !pending,
+          [disabledClasses]: pending,
+        },
+        className,
+      )}
+    >
+      <div className="absolute left-0 ml-4">
+        {pending && <LoadingDots className="mb-3 bg-white" />}
+      </div>
+      {buttonName}
+    </button>
+  );
+}
+
+export function ProceedToCheckout({
+  buttonName,
+  className,
+  pending = false,
+}: {
+  buttonName: string;
+  className?: string;
+  pending: boolean;
+}) {
+  return (
+    <SubmitButton
+      availableForSale={!pending}
+      buttonName={buttonName}
+      className={className}
+      pending={pending}
+    />
+  );
+}

+ 169 - 0
src/components/checkout/stepper/index.tsx

@@ -0,0 +1,169 @@
+import Link from "next/link";
+import { useMemo } from "react";
+import LogoIcon from "@components/common/icons/LogoIcon";
+import Email from "./Email";
+import { GuestAddAdressForm } from "./GuestAddAdressForm";
+import Shipping from "./shipping";
+import Payment from "./payment";
+import Review from "./review";
+
+
+
+const { SITE_NAME } = process.env;
+
+interface Step {
+  id: number;
+  key: string;
+  title: string;
+  href: string;
+  component: React.ReactNode;
+}
+
+interface CheckOutProps {
+  billingAddress?: any;
+  shippingAddress?: any;
+  currentStep: string;
+  selectedPayment?: any;
+  selectedPaymentTitle?: string;
+  selectedShippingRate?: any;
+  selectedShippingRateTitle?: string;
+}
+
+export default function Stepper(
+  {
+    billingAddress,
+    shippingAddress,
+    selectedShippingRate,
+    selectedShippingRateTitle,
+    selectedPayment,
+    selectedPaymentTitle,
+    currentStep,
+  }: CheckOutProps
+) {
+
+  const steps = useMemo<Step[]>(() => {
+    return [
+      {
+        id: 1,
+        key: "email",
+        title: "Email",
+        href: "/checkout",
+        component: <Email />,
+      },
+      {
+        id: 2,
+        key: "address",
+        title: "Address",
+        href: "/checkout",
+        component:
+          <GuestAddAdressForm
+            billingAddress={billingAddress}
+            shippingAddress={shippingAddress}
+            currentStep={currentStep}
+          />
+      },
+      {
+        id: 3,
+        key: "shipping",
+        title: "Shipping",
+        href: "/checkout?step=address",
+        component: <Shipping
+          selectedShippingRate={selectedShippingRate}
+          currentStep={currentStep}
+        />,
+      },
+      {
+        id: 4,
+        key: "payment",
+        title: "Payment",
+        href: "/checkout?step=shipping",
+        component: (
+          <Payment
+            selectedPayment={selectedPayment}
+            currentStep={currentStep}
+          />
+        ),
+      },
+      {
+        id: 5,
+        key: "review",
+        title: "Review",
+        href: "/checkout?step=payment",
+        component: (
+          <Review
+            billingAddress={billingAddress}
+            selectedPaymentTitle={selectedPaymentTitle}
+            selectedShippingRate={selectedShippingRate}
+            selectedShippingRateTitle={selectedShippingRateTitle}
+            shippingAddress={shippingAddress}
+          />
+        ),
+      },
+    ];
+  }, [
+    currentStep,
+    billingAddress,
+    shippingAddress,
+    selectedShippingRate,
+    selectedPayment,
+    selectedPaymentTitle,
+    selectedShippingRateTitle,
+  ]);
+
+  const currentStepIndex = steps.findIndex((s) => s.key === currentStep);
+
+  const StepItem = ({ step }: { step: Step }) => {
+    const isActive = step.key === currentStep;
+    const isCompleted = step.id <= (steps[currentStepIndex]?.id || 1);
+
+    return (
+      <div key={step.id} className="flex w-full flex-col">
+        <div className="flex items-center justify-between font-outfit">
+          <div className="flex items-center gap-3">
+            <div
+              className={`flex h-6 w-6 items-center justify-center rounded-full text-sm font-medium ${isCompleted
+                ? "bg-blue-600 text-white"
+                : "bg-gray-200 text-neutral-900"
+                }`}
+            >
+              {step.id}
+            </div>
+            <span
+              className={`text-lg font-medium max-md:text-base ${isActive
+                ? "font-medium text-neutral-900 dark:text-neutral-300"
+                : "text-neutral-900 dark:text-white"
+                }`}
+            >
+              {step.title}
+            </span>
+          </div>
+        </div>
+
+        {isCompleted && <section className="relative">{step.component}</section>}
+      </div>
+    );
+  };
+
+  return (
+    <div className="mx-auto w-full">
+      <header className="pb-6 sm:py-6">
+        <Link
+          aria-label={SITE_NAME}
+          className="flex items-center gap-2 text-black dark:text-white md:pt-1 hidden lg:block"
+          href="/"
+        >
+          <LogoIcon />
+        </Link>
+        <h1 className="text-xl px-2 font-semibold block lg:hidden">Checkout</h1>
+      </header>
+
+      <div className="scrollbar-thin scrollbar-track-transparent scrollbar-thumb-gray-500 dark:scrollbar-thumb-neutral-300 h-[calc(100dvh-300px)] overflow-y-auto lg:h-[calc(100dvh-124px)]">
+        <div className="flex h-full flex-col gap-y-8 pl-2 pr-6 sm:px-3 sm:pr-10">
+          {steps.map((step) => (
+            <StepItem key={step.id} step={step} />
+          ))}
+        </div>
+      </div>
+    </div>
+  );
+}

+ 221 - 0
src/components/checkout/stepper/payment/PaymentMethod.tsx

@@ -0,0 +1,221 @@
+"use client";
+
+import { Controller, FieldValues, useForm } from "react-hook-form";
+import { cn, Radio, RadioGroup } from "@heroui/react";
+import { useState } from "react";
+import { useCustomToast } from "@utils/hooks/useToast";
+import { useCheckout } from "@utils/hooks/useCheckout";
+import { ProceedToCheckout } from "../ProceedToCheckout";
+import { CustomRadioProps } from "@components/checkout/type";
+import { useDispatch } from "react-redux";
+import { updateCart } from "@/store/slices/cart-slice";
+
+export default function PaymentMethod({
+  selectedPayment,
+  methods,
+  currentStep,
+}: {
+  selectedPayment?: string;
+  methods: any;
+  currentStep?: string;
+}) {
+  const { showToast } = useCustomToast();
+  const { isPaymentLoading, saveCheckoutPayment } = useCheckout();
+  const [isOpen, setIsOpen] = useState(currentStep !== "payment");
+
+  const [prevValues, setPrevValues] = useState({
+    currentStep,
+    selectedPayment,
+  });
+
+  if (
+    currentStep !== prevValues.currentStep ||
+    selectedPayment !== prevValues.selectedPayment
+  ) {
+    setPrevValues({ currentStep, selectedPayment });
+    if (currentStep === "payment") {
+      setIsOpen(false);
+    } else if (selectedPayment) {
+      setIsOpen(true);
+    }
+  }
+
+  const { handleSubmit, control } = useForm({
+    mode: "onSubmit",
+    defaultValues: {
+      method: selectedPayment ?? "",
+    },
+  });
+
+  const selectedMethodLabelPrior = methods?.find(
+    (method: any) => method?.method === selectedPayment,
+  )?.title;
+
+  const dispatch = useDispatch();
+  const onSubmit = async (data: FieldValues) => {
+    if (!data?.method) {
+      showToast("Please Choose the Payment Method", "warning");
+      return;
+    }
+    try {
+      const selectedMethod = methods?.find(
+        (m: any) => m?.method === data?.method,
+      );
+      if (selectedMethod) {
+        dispatch(
+          updateCart({
+            paymentMethod: selectedMethod?.method || "",
+            paymentMethodTitle: selectedMethod?.title || "",
+          }),
+        );
+      }
+      await saveCheckoutPayment(data.method);
+    } catch {
+      showToast("Failed to save payment method. Please try again.");
+    }
+  };
+
+  return (
+    <>
+      {selectedPayment ? (
+        isOpen ? (
+          <>
+            <div className="mt-4  justify-between hidden sm:flex ">
+              <div className="flex">
+                <p className="w-auto text-base font-normal text-black/60 dark:text-white/60 sm:w-[192px]">
+                  Payment Method
+                </p>
+                <p className="text-base font-normal">
+                  {selectedMethodLabelPrior as string}
+                </p>
+              </div>
+
+              <button
+                onClick={() => {
+                  setIsOpen(false);
+                }}
+                className="cursor-pointer text-base font-normal text-black/60 underline dark:text-neutral-300"
+              >
+                Change
+              </button>
+            </div>
+            <div className="mt-4 flex sm:hidden justify-between relative">
+              <div className="flex justify-between justify-between  flex-1 wrap">
+                <p className="w-auto text-base font-normal text-black/60 dark:text-white/60 sm:w-[192px]">
+                  Payment Method
+                </p>
+                <p className="text-base font-normal">
+                  {selectedMethodLabelPrior as string}
+                </p>
+              </div>
+
+              <button
+                onClick={() => {
+                  setIsOpen(false);
+                }}
+                className="cursor-pointer absolute right-0 text-base font-normal text-black/60 underline dark:text-neutral-300"
+                style={{ top: "-36px" }}
+              >
+                Change
+              </button>
+            </div>
+          </>
+        ) : (
+          <div className="mt-6">
+            <form onSubmit={handleSubmit(onSubmit)}>
+              <Controller
+                control={control}
+                name="method"
+                render={({ field }) => (
+                  <RadioGroup
+                    {...field}
+                    label=""
+                    value={field.value ?? ""}
+                    onValueChange={field.onChange}
+                  >
+                    {methods?.map((method: any) => (
+                      <CustomRadio
+                        key={method?.method}
+                        className="my-1 border border-solid border-neutral-300 dark:border-neutral-500"
+                        description={method?.description}
+                        value={method?.method}
+                      >
+                        <span className="text-neutral-700 dark:text-white">
+                          {method?.title}
+                        </span>
+                      </CustomRadio>
+                    ))}
+                  </RadioGroup>
+                )}
+              />
+
+              <div className="my-6 justify-self-end">
+                <ProceedToCheckout
+                  buttonName="Pay Now"
+                  pending={isPaymentLoading}
+                />
+              </div>
+            </form>
+          </div>
+        )
+      ) : (
+        <div className="mt-6">
+          <form onSubmit={handleSubmit(onSubmit)}>
+            <Controller
+              control={control}
+              name="method"
+              render={({ field }) => (
+                <RadioGroup
+                  {...field}
+                  label=""
+                  value={field.value ?? ""}
+                  onValueChange={field.onChange}
+                >
+                  {methods?.map((method: any) => (
+                    <CustomRadio
+                      key={method?.method}
+                      className="my-1 border border-solid border-neutral-300 dark:border-neutral-500"
+                      description={method?.description}
+                      value={method?.method}
+                    >
+                      <span className="text-neutral-700 dark:text-white">
+                        {method?.title}
+                      </span>
+                    </CustomRadio>
+                  ))}
+                </RadioGroup>
+              )}
+            />
+
+            <div className="my-6 justify-self-end">
+              <ProceedToCheckout
+                buttonName="Pay Now"
+                pending={isPaymentLoading}
+              />
+            </div>
+          </form>
+        </div>
+      )}
+    </>
+  );
+}
+
+const CustomRadio = (props: CustomRadioProps) => {
+  const { children, ...otherProps } = props;
+
+  return (
+    <Radio
+      {...otherProps}
+      classNames={{
+        base: cn(
+          "inline-flex m-0 bg-transparent hover:bg-transparent items-center",
+          "flex-row items-baseline max-w-full cursor-pointer rounded-lg gap-4 p-4 border-2 border-transparent",
+          "data-[selected=true]:border-primary",
+        ),
+        hiddenInput: "peer absolute h-0 w-0 opacity-0",
+      }}
+    >
+      {children}
+    </Radio>
+  );
+};

+ 36 - 0
src/components/checkout/stepper/payment/index.tsx

@@ -0,0 +1,36 @@
+"use client";
+
+import { CartCheckoutPageSkeleton } from "@/components/common/skeleton/CheckoutSkeleton";
+import { useQuery } from "@apollo/client";
+import PaymentMethod from "./PaymentMethod";
+import { FC } from "react";
+import { GET_CHECKOUT_PAYMENT_METHODS } from "@/graphql";
+import { getCartToken } from "@/utils/getCartToken";
+
+const Payment: FC<{
+  selectedPayment?: {
+    method: string;
+    methodTitle?: string;
+  };
+  currentStep?: string;
+}> = ({ selectedPayment, currentStep }) => {
+  const token = getCartToken();
+  const { data, loading: isLoading } = useQuery(GET_CHECKOUT_PAYMENT_METHODS, {
+    variables: { token: token || "" },
+    skip: !token,
+    fetchPolicy: "cache-first",
+    nextFetchPolicy: "cache-first",
+  });
+
+  if (isLoading && !data) return <CartCheckoutPageSkeleton />;
+
+  return (
+    <PaymentMethod
+      methods={data?.collectionPaymentMethods}
+      selectedPayment={selectedPayment as any}
+      currentStep={currentStep}
+    />
+  );
+};
+
+export default Payment;

+ 101 - 0
src/components/checkout/stepper/review/OrderReview.tsx

@@ -0,0 +1,101 @@
+"use client";
+import { useForm } from "react-hook-form";
+import {
+  AddressDataTypes,
+} from "@/types/types";
+import { isObject } from "@/utils/type-guards";
+import { useCheckout } from "@utils/hooks/useCheckout";
+import { ProceedToCheckout } from "../ProceedToCheckout";
+export default function OrderReview({
+  selectedPaymentTitle,
+  shippingAddress,
+  billingAddress,
+  selectedShipping : _selectedShipping,
+  selectedShippingRateTitle,
+}: {
+  selectedPaymentTitle?: string;
+  shippingAddress?: AddressDataTypes;
+  billingAddress?: AddressDataTypes;
+  selectedShipping?: string;
+  selectedShippingRateTitle?: string;
+}) {
+  const { isPlaceOrder, savePlaceOrder } = useCheckout();
+  const { handleSubmit } = useForm();
+  const onSubmit = () => {
+    savePlaceOrder();
+  };
+
+  return (
+    <div className="mt-4 flex-col mb-20 sm:mb-0">
+      <div className="relative">
+        {isObject(shippingAddress) && (
+          <table className="w-full text-left text-sm text-gray-500 dark:text-gray-400">
+            <tbody>
+              <tr className="">
+                <td className="py-4">Contact</td>
+                <th
+                  className="break-all px-6 py-4 font-medium text-gray-900 dark:text-white"
+                  scope="row"
+                >
+                  {shippingAddress?.email}
+                </th>
+              </tr>
+              <tr className="">
+                <td className="py-4">Billing to</td>
+                <th
+                  className="break-all px-6 py-4 font-medium text-gray-900 dark:text-white"
+                  scope="row"
+                >
+                  {billingAddress?.firstName}, {billingAddress?.lastName},{" "}
+                  {billingAddress?.address}, {billingAddress?.city},{" "}
+                  {billingAddress?.state}, {billingAddress?.postcode},{" "}
+                  {billingAddress?.country}
+                </th>
+              </tr>
+              <tr className="">
+                <td className="py-4">Ship to</td>
+                <th
+                  className="break-all px-6 py-4 font-medium text-gray-900 dark:text-white"
+                  scope="row"
+                >
+                  {shippingAddress?.firstName}, {shippingAddress?.lastName},{" "}
+                  {shippingAddress?.address}, {shippingAddress?.city},{" "}
+                  {shippingAddress?.state}, {shippingAddress?.postcode},{" "}
+                  {shippingAddress?.country}
+                </th>
+              </tr>
+              <tr className="">
+                <td className="py-4">Method</td>
+                <th
+                  className="break-all px-6 py-4 font-medium text-gray-900 dark:text-white"
+                  scope="row"
+                >
+                  {selectedShippingRateTitle}
+                </th>
+              </tr>
+              <tr className="">
+                <td className="py-4">Payment</td>
+                <th
+                  className="break-all px-6 py-4 font-medium text-gray-900 dark:text-white"
+                  scope="row"
+                >
+                  {selectedPaymentTitle}
+                </th>
+              </tr>
+            </tbody>
+          </table>
+        )}
+      </div>
+      <div className="flex flex-col gap-6">
+        <form onSubmit={handleSubmit(onSubmit)}>
+          <div className="justify-self-end">
+            <ProceedToCheckout
+              buttonName="Place Order"
+              pending={isPlaceOrder}
+            />
+          </div>
+        </form>
+      </div>
+    </div>
+  );
+}

+ 29 - 0
src/components/checkout/stepper/review/index.tsx

@@ -0,0 +1,29 @@
+import { FC } from "react";
+import { AddressDataTypes } from "@/types/types";
+import OrderReview from "./OrderReview";
+
+export const Review: FC<{
+  selectedPaymentTitle?: string;
+  shippingAddress?: AddressDataTypes;
+  billingAddress?: AddressDataTypes;
+  selectedShippingRate?: string;
+  selectedShippingRateTitle?: string;
+}> = ({
+  selectedPaymentTitle,
+  shippingAddress,
+  billingAddress,
+  selectedShippingRate,
+  selectedShippingRateTitle,
+}) => {
+  return (
+    <OrderReview
+      billingAddress={billingAddress}
+      selectedPaymentTitle={selectedPaymentTitle}
+      selectedShipping={selectedShippingRate}
+      selectedShippingRateTitle={selectedShippingRateTitle}
+      shippingAddress={shippingAddress}
+    />
+  );
+};
+
+export default Review;

+ 217 - 0
src/components/checkout/stepper/shipping/ShippingMethod.tsx

@@ -0,0 +1,217 @@
+"use client";
+
+import { FieldValues, useForm, Controller } from "react-hook-form";
+import { cn, Radio, RadioGroup } from "@heroui/react";
+import { useState } from "react";
+import { ProceedToCheckout } from "../ProceedToCheckout";
+import { useCustomToast } from "@utils/hooks/useToast";
+import { useCheckout } from "@utils/hooks/useCheckout";
+import { CustomRadioProps, ShippingMethodType } from "@components/checkout/type";
+import { useDispatch } from "react-redux";
+import { updateCart } from "@/store/slices/cart-slice";
+
+
+export default function ShippingMethod({
+  shippingMethod,
+  selectedShippingRate,
+  currentStep
+}: {
+  shippingMethod?: ShippingMethodType[];
+  selectedShippingRate?: any;
+  methodDesc?: string;
+  currentStep?: string;
+}) {
+  const { isSaving, saveCheckoutShipping } = useCheckout();
+  const { showToast } = useCustomToast();
+  const [isOpen, setIsOpen] = useState(currentStep !== "shipping");
+
+  const [prevValues, setPrevValues] = useState({ currentStep, selectedShippingRate });
+
+  if (currentStep !== prevValues.currentStep || selectedShippingRate !== prevValues.selectedShippingRate) {
+    setPrevValues({ currentStep, selectedShippingRate });
+    if (currentStep === "shipping") {
+      setIsOpen(false);
+    } else if (selectedShippingRate) {
+      setIsOpen(true);
+    }
+  }
+  const { control, handleSubmit } = useForm({
+    mode: "onSubmit",
+    defaultValues: {
+      method: selectedShippingRate ?? "",
+    },
+  });
+  const selectedMethodTitle = shippingMethod?.find(
+    (method) => method.method === selectedShippingRate
+  )?.label;
+  const selectedMethodPrice = shippingMethod?.find(
+    (method) => method.method === selectedShippingRate
+  )?.price;
+
+  const dispatch = useDispatch();
+  const onSubmit = async (data: FieldValues) => {
+    if (!data?.method) {
+      showToast("Please Choose the Shipping Method", "warning");
+      return;
+    }
+    try {
+      const selectedRate = shippingMethod?.find(m => m.method === data.method);
+      if (selectedRate) {
+        dispatch(updateCart({
+          shippingMethod: selectedRate?.method || "",
+          selectedShippingRate: selectedRate?.method || "",
+          selectedShippingRateTitle: selectedRate?.label || "",
+        }));
+      }
+      await saveCheckoutShipping(data?.method);
+    } catch {
+      showToast("Failed to save shipping method. Please try again.");
+    }
+  };
+
+
+  return (
+    <div>
+      {(selectedShippingRate) ? (
+        isOpen ? (
+          <>
+            <div className="mt-4  justify-between hidden sm:flex">
+              <div className="flex">
+                <p className="w-auto text-base font-normal text-black/60 dark:text-white/60 sm:w-[192px]">
+                  Shipping Method
+                </p>
+                <p className="text-base font-normal">{selectedMethodTitle} (${selectedMethodPrice})</p>
+              </div>
+              <div className="flex">
+                <button
+                  onClick={() => {
+                    setIsOpen(!isOpen);
+                  }}
+                  className="cursor-pointer text-base font-normal text-black/[60%] underline dark:text-neutral-300"
+                >
+                  Change
+                </button>
+              </div>
+            </div>
+
+            <div className="mt-4 block sm:hidden flex flex-col justify-between sm:flex-row relative  ">
+              <div className="flex justify-between  flex-1 wrap">
+                <p className="w-auto text-base font-normal text-black/60 dark:text-white/60 sm:w-[192px]">
+                  Shipping Method
+                </p>
+                <p className="text-base font-normal">{selectedMethodTitle} (${selectedMethodPrice})</p>
+              </div>
+
+              <button
+                onClick={() => {
+                  setIsOpen(!isOpen);
+                }}
+                className="cursor-pointer absolute right-0  text-base font-normal text-black/[60%] underline dark:text-neutral-300"
+                style={{ top: "-36px" }}
+              >
+                Change
+              </button>
+            </div>
+          </>
+
+        ) : (
+          <form className="mt-6" onSubmit={handleSubmit(onSubmit)}>
+            <div className="flex flex-col gap-5">
+              {Array.isArray(shippingMethod) && (
+                <Controller
+                  control={control}
+                  name="method"
+                  render={({ field }) => (
+                    <RadioGroup
+                      {...field}
+                      label=""
+                      value={field.value ?? ""}
+                      onValueChange={field.onChange}
+                    >
+                      {shippingMethod.map((method: any) => (
+                        <CustomRadio
+                          key={method?.code}
+                          className="inset-0 my-1 border border-solid border-neutral-300 dark:border-neutral-500"
+                          description={"$" + method?.price}
+                          value={method?.method}
+                        >
+                          <span className="text-neutral-700 dark:text-white">
+                            {method?.label}
+                          </span>
+                        </CustomRadio>
+                      ))}
+                    </RadioGroup>
+                  )}
+                />
+              )}
+            </div>
+
+            <div className="my-6 justify-self-end">
+              <ProceedToCheckout buttonName="Next" pending={isSaving} />
+            </div>
+          </form>
+        )
+      ) : (
+        <form className="mt-6" onSubmit={handleSubmit(onSubmit)}>
+          <div className="flex flex-col gap-5">
+            {Array.isArray(shippingMethod) && (
+              <Controller
+                control={control}
+                name="method"
+                render={({ field }) => {
+                  return (
+                    <RadioGroup
+                      {...field}
+                      label=""
+                      value={field.value ?? ""}
+                      onValueChange={(value) => {
+                        field.onChange(value);
+                      }}
+                    >
+                      {shippingMethod.map((method : any) => (
+                        <CustomRadio
+                          key={method?.code}
+                          className="inset-0 my-1 border border-solid border-neutral-300 dark:border-neutral-500"
+                          description={"$" + method?.price}
+                          value={method?.method}
+                        >
+                          <span className="text-neutral-700 dark:text-white">
+                            {method?.label}
+                          </span>
+                        </CustomRadio>
+                      ))}
+                    </RadioGroup>
+                  )
+                }}
+              />
+            )}
+          </div>
+
+          <div className="my-6 justify-self-end">
+            <ProceedToCheckout buttonName="Next" pending={isSaving} />
+          </div>
+        </form>
+      )}
+    </div>
+  );
+}
+
+const CustomRadio = (props: CustomRadioProps) => {
+  const { children, ...otherProps } = props;
+
+  return (
+    <Radio
+      {...otherProps}
+      classNames={{
+        base: cn(
+          "inline-flex m-0 bg-transparent hover:bg-transparent items-center",
+          "flex-row items-baseline max-w-full cursor-pointer rounded-lg gap-4 p-4 border-2 border-transparent",
+          "data-[selected=true]:border-primary"
+        ),
+        hiddenInput: "peer absolute h-0 w-0 opacity-0",
+      }}
+    >
+      {children}
+    </Radio>
+  );
+};

+ 37 - 0
src/components/checkout/stepper/shipping/index.tsx

@@ -0,0 +1,37 @@
+"use client";
+
+import { CartCheckoutPageSkeleton } from "@/components/common/skeleton/CheckoutSkeleton";
+import { useQuery } from "@apollo/client";
+import ShippingMethod from "./ShippingMethod";
+import { FC } from "react";
+import { SelectedShippingRateType } from "@/types/checkout/type";
+import { GET_CHECKOUT_SHIPPING_RATES } from "@/graphql";
+import { getCartToken } from "@/utils/getCartToken";
+
+const Shipping: FC<{
+  selectedShippingRate?: SelectedShippingRateType;
+  currentStep?: string;
+}> = ({ selectedShippingRate, currentStep }) => {
+  const token = getCartToken();
+
+  const { data, loading: isLoading } = useQuery(GET_CHECKOUT_SHIPPING_RATES, {
+    variables: { token: token || "" },
+    skip: !token,
+    fetchPolicy: "cache-first",
+    nextFetchPolicy: "cache-first",
+  });
+
+  if (isLoading && !data) {
+    return <CartCheckoutPageSkeleton />;
+  }
+  return (
+    <ShippingMethod
+      shippingMethod={data?.collectionShippingRates}
+      selectedShippingRate={selectedShippingRate}
+      methodDesc={selectedShippingRate?.methodDescription}
+      currentStep={currentStep}
+    />
+  );
+};
+
+export default Shipping;

+ 73 - 0
src/components/checkout/success/EmptyCart.tsx

@@ -0,0 +1,73 @@
+"use client";
+import { clearCart } from "@/store/slices/cart-slice";
+import clsx from "clsx";
+import { useRouter } from "next/navigation";
+import { useEffect } from "react";
+import CheckSign from "@components/common/icons/CheckSign";
+import { useFormStatus } from "react-dom";
+import { useDispatch } from "react-redux";
+import LoadingDots from "@components/common/icons/LoadingDots";
+import dynamic from "next/dynamic";
+
+const OrderDetail = dynamic(() => import("@/components/cart/OrderDetail"), {
+  loading: () => (
+    <div className="max-w-sm animate-pulse" role="status">
+      <div className="mb-4 h-8 w-48 rounded-full bg-gray-200 dark:bg-gray-700" />
+      <span className="sr-only">Loading...</span>
+    </div>
+  )
+});
+const EmptyCartPage = () => {
+  return (
+    <div className="flex min-h-[calc(100vh-450px)] px-4 items-center">
+      <div className="flex w-full flex-col items-center justify-center overflow-hidden">
+        <CheckSign className="sm:h-38 sm:w-38 h-28 w-28" />
+        <OrderDetail />
+        <ClearCartButton buttonName="Continue shopping" redirect="/" />
+      </div>
+    </div>
+  );
+};
+
+export default EmptyCartPage;
+
+function SubmitButton({
+  buttonName,
+  redirectNav,
+}: {
+  buttonName: string;
+  redirectNav: string;
+}) {
+  const router = useRouter();
+  const dispatch = useDispatch();
+  const { pending } = useFormStatus();
+  useEffect(() => {
+    dispatch(clearCart());
+  }, []);
+  return (
+    <button
+      className={clsx(
+        "sm:my-3 my-0 w-auto items-center cursor-pointer justify-center rounded-full border-white bg-blue-600 px-12 py-4 text-sm font-bold tracking-wide text-white",
+        pending ? "cursor-wait" : "cursor-pointer"
+      )}
+      disabled={pending}
+      type="submit"
+      onClick={() => {
+        dispatch(clearCart());
+        router.replace(redirectNav);
+      }}
+    >
+      {pending ? <LoadingDots className="bg-white" /> : buttonName}
+    </button>
+  );
+}
+
+export function ClearCartButton({
+  buttonName,
+  redirect,
+}: {
+  buttonName: string;
+  redirect: string;
+}) {
+  return <SubmitButton buttonName={buttonName} redirectNav={redirect} />;
+}

+ 24 - 0
src/components/checkout/type.ts

@@ -0,0 +1,24 @@
+import { Radio } from "@heroui/react";
+import { FieldErrors, UseFormRegister } from "react-hook-form";
+
+export type CustomRadioProps = {
+  children: React.ReactNode;
+  description?: string;
+  value: string;
+} & typeof Radio.defaultProps;
+
+export type ShippingMethodType = {
+  method?: string;
+  label?: string;
+  price?: number | string;
+  code?: string;
+};
+
+export type EmailFormValues = { email: string };
+
+export type EmailFormProps = {
+  register: UseFormRegister<EmailFormValues>;
+  errors: FieldErrors<EmailFormValues>;
+  isSubmitting: boolean;
+  isGuest: boolean;
+};

+ 25 - 0
src/components/common/LoadingSpinner.tsx

@@ -0,0 +1,25 @@
+import { FC } from "react";
+
+interface LoadingSpinnerProps {
+  size?: "sm" | "md" | "lg";
+  className?: string;
+}
+
+export const LoadingSpinner: FC<LoadingSpinnerProps> = ({ 
+  size = "md", 
+  className = "" 
+}) => {
+  const sizeClasses = {
+    sm: "h-4 w-4",
+    md: "h-8 w-8", 
+    lg: "h-12 w-12"
+  };
+
+  return (
+    <div className={`flex items-center justify-center py-8 ${className}`}>
+      <div 
+        className={`animate-spin rounded-full border-b-2 border-neutral-800 dark:border-white ${sizeClasses[size]}`}
+      />
+    </div>
+  );
+};

+ 56 - 0
src/components/common/NextImage.tsx

@@ -0,0 +1,56 @@
+"use client";
+
+import { useState } from "react";
+import Image from "next/image";
+import { NOT_IMAGE } from "@/utils/constants";
+import { NextImageProps } from "./type";
+import { Shimmer } from "./Shimmer";
+
+
+export function NextImage({
+  src,
+  alt,
+  className = "",
+  width,
+  height,
+  sizes,
+  priority = false,
+}: NextImageProps) {
+  const [isLoaded, setIsLoaded] = useState(false);
+  const [hasError, setHasError] = useState(false);
+
+  const finalSrc = hasError || !src ? NOT_IMAGE : src;
+
+  return (
+    <div
+      className={`relative overflow-hidden ${className}`}
+      style={{ width: "100%", height: "100%" }}
+    >
+      {!isLoaded && (
+        <Shimmer
+          className="absolute inset-0 z-0"
+          width="100%"
+          height="100%"
+          rounded="lg"
+        />
+      )}
+
+      <Image
+        src={finalSrc}
+        alt={alt}
+        width={width}
+        height={height}
+        sizes={sizes}
+        priority={priority}
+        loading={priority ? "eager" : "lazy"}
+        onLoad={() => setIsLoaded(true)}
+        onError={() => {
+          setHasError(true);
+          setIsLoaded(true);
+        }}
+        className={`transition-opacity duration-300 object-cover w-full h-full ${isLoaded ? "opacity-100" : "opacity-0"
+          }`}
+      />
+    </div>
+  );
+}

+ 45 - 0
src/components/common/OpenGraphImage.tsx

@@ -0,0 +1,45 @@
+import { readFile } from "fs/promises";
+import { join } from "path";
+import { ImageResponse } from "next/og";
+import LogoIcon from "./icons/logo";
+
+
+export type Props = {
+  title?: string;
+};
+
+export default async function OpenGraphImage(
+  props?: Props,
+): Promise<ImageResponse> {
+  const { title } = {
+    ...{
+      title: process.env.SITE_NAME,
+    },
+    ...props,
+  };
+  const file = await readFile(join(process.cwd(), "./fonts/Inter-Bold.ttf"));
+  const font = Uint8Array.from(file).buffer;
+
+  return new ImageResponse(
+    (
+      <div tw="flex h-full w-full flex-col items-center justify-center bg-black">
+        <div tw="flex h-[160px] w-[160px] flex-none items-center justify-center rounded-3xl border border-neutral-700">
+          <LogoIcon fill="white" height="58" width="64" />
+        </div>
+        <p tw="mt-12 text-6xl font-bold text-white">{title}</p>
+      </div>
+    ),
+    {
+      width: 1200,
+      height: 630,
+      fonts: [
+        {
+          name: "Inter",
+          data: font,
+          style: "normal",
+          weight: 700,
+        },
+      ],
+    },
+  );
+}

+ 48 - 0
src/components/common/Rating.tsx

@@ -0,0 +1,48 @@
+import clsx from "clsx";
+import { RatingTypes } from "./type";
+import StarIcon from "./icons/StarIcon";
+
+export const Rating = ({
+  length = 5,
+  star = 0,
+  size = "size-4",
+  className,
+  reviewCount,
+  onReviewClick,
+}: RatingTypes) => {
+  const rating = star ?? 0;
+  const reviewCountToShow = reviewCount ?? star > 0;
+
+  return (
+   <div className={clsx("flex items-center gap-x-2", className)}>
+  {reviewCountToShow ? (
+    <>
+  <div className="flex gap-x-0.5">
+    {Array.from({ length }).map((_, index) => (
+      <StarIcon
+        key={index}
+        className={clsx(
+          size,
+          index < rating
+            ? "fill-yellow-500 dark:fill-yellow-500"
+            : "fill-black dark:fill-gray-900",
+          "fill-black dark:stroke-white"
+        )}
+      />
+    ))}
+  </div>
+    <span className="text-sm text-gray-600 dark:text-gray-400">
+      ({reviewCountToShow} {reviewCountToShow === 1 ? 'Review' : 'Reviews'})
+    </span>
+    </>
+  ) : (
+    <span 
+    className="text-sm text-blue-600 underline cursor-pointer"
+    onClick={onReviewClick}
+    >
+      Write a review
+    </span>
+  )}
+</div>
+  );
+};

+ 47 - 0
src/components/common/Shimmer.tsx

@@ -0,0 +1,47 @@
+// src/components/common/Shimmer.tsx
+"use client";
+
+import { HTMLAttributes } from "react";
+
+interface ShimmerProps extends HTMLAttributes<HTMLDivElement> {
+  className?: string;
+  rounded?: "sm" | "md" | "lg" | "full" | "none";
+  width?: string | number;
+  height?: string | number;
+}
+
+export function Shimmer({
+  className,
+  rounded = "md",
+  width = "100%",
+  height = "100%",
+  ...props
+}: ShimmerProps) {
+  const roundedClass = {
+    sm: "rounded-sm",
+    md: "rounded-md",
+    lg: "rounded-lg",
+    full: "rounded-full",
+    none: "rounded-none",
+  }[rounded];
+
+  return (
+    <div
+      className={`
+        relative overflow-hidden
+        bg-gray-200 dark:bg-gray-700
+        ${roundedClass}
+        ${className}
+      `}
+      style={{ width, height }}
+      {...props}
+    >
+      <div
+        className={`
+          absolute inset-0 -translate-x-full animate-[shimmer_2s_infinite]
+          bg-gradient-to-r from-transparent via-white/30 to-transparent
+        `}
+      />
+    </div>
+  );
+}

+ 52 - 0
src/components/common/button/Button.tsx

@@ -0,0 +1,52 @@
+import clsx from "clsx";
+
+import LoadingDots from '@/components/common/icons/LoadingDots';
+
+interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
+  className?: string;
+  title: string;
+  loading?: boolean;
+  disabled?: boolean;
+  type?: "button" | "submit" | "reset";
+}
+
+export function Button({
+  className = "",
+  title,
+  loading = false,
+  disabled = false,
+  type,
+  ...rest
+}: ButtonProps) {
+  const buttonClasses = clsx(
+    "relative flex w-full text-lg cursor-pointer font-outfit font-semibold items-center justify-center bg-blue-600 p-3 tracking-wide text-white",
+    "hover:opacity-90",
+    "rounded-[100px] md:rounded-xl",
+    {
+      "opacity-50 cursor-wait ": loading || disabled,
+    },
+    className
+  );
+
+  return (
+    <button
+      aria-disabled={loading || disabled}
+      aria-label={title}
+      className={buttonClasses}
+      disabled={loading || disabled}
+      type={type ?? "reset"}
+      {...rest}
+    >
+      <div className="mx-2 flex items-center justify-center gap-2">
+        {loading ? (
+          <>
+            <LoadingDots className="bg-white" />
+            <span>loading</span>
+          </>
+        ) : (
+          <span>{title}</span>
+        )}
+      </div>
+    </button>
+  );
+}

+ 47 - 0
src/components/common/button/EventButton.tsx

@@ -0,0 +1,47 @@
+"use client";
+import { useAppDispatch } from "@/store/hooks";
+import { clearCart } from "@/store/slices/cart-slice";
+import clsx from "clsx";
+import { useRouter } from "next/navigation";
+import { useEffect } from "react";
+
+function SubmitButton({
+  buttonName,
+  redirectNav,
+}: {
+  buttonName: string;
+  redirectNav: string;
+}) {
+  const router = useRouter();
+  const dispatch = useAppDispatch();
+  useEffect(() => {
+    dispatch(clearCart());
+  }, []);
+  return (
+    <button
+      aria-label={buttonName}
+      className={clsx(
+        " my-3 w-auto items-center cursor-pointer justify-center rounded-full border-white bg-blue-600 px-12 py-4 text-sm font-bold tracking-wide text-white",
+        {
+          "hover:opacity-90": true,
+        }
+      )}
+      type="button"
+      onClick={() => {
+        router.push(redirectNav);
+      }}
+    >
+      {buttonName}
+    </button>
+  );
+}
+
+export function EventButton({
+  buttonName,
+  redirect,
+}: {
+  buttonName: string;
+  redirect: string;
+}) {
+  return <SubmitButton buttonName={buttonName} redirectNav={redirect} />;
+}

+ 50 - 0
src/components/common/button/LoadingButton.tsx

@@ -0,0 +1,50 @@
+import clsx from "clsx";
+import LoadingDots from "../icons/LoadingDots";
+
+interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
+  className?: string;
+  title: string;
+  loading?: boolean;
+  disabled?: boolean;
+  type?: "button" | "submit" | "reset";
+}
+
+export function Button({
+  className = "",
+  title,
+  loading = false,
+  disabled = false,
+  type,
+  ...rest
+}: ButtonProps) {
+  const buttonClasses = clsx(
+    "relative flex w-full text-lg cursor-pointer font-outfit font-semibold items-center justify-center rounded-xl bg-blue-600 p-3 tracking-wide text-white",
+    "hover:opacity-90",
+    {
+      "opacity-50 cursor-wait ": loading || disabled,
+    },
+    className
+  );
+
+  return (
+    <button
+      aria-disabled={loading || disabled}
+      aria-label={title}
+      className={buttonClasses}
+      disabled={loading || disabled}
+      type={type ?? "reset"}
+      {...rest}
+    >
+      <div className="mx-2 flex items-center justify-center gap-2">
+        {loading ? (
+          <>
+            <LoadingDots className="bg-white" />
+            <span>loading</span>
+          </>
+        ) : (
+          <span>{title}</span>
+        )}
+      </div>
+    </button>
+  );
+}

+ 25 - 0
src/components/common/button/ReviewButton.tsx

@@ -0,0 +1,25 @@
+import { IS_GUEST } from "@utils/constants";
+import { getCookie } from "@utils/getCartToken";
+import { useRouter } from "next/navigation";
+
+export const ReviewButton = ({ setShowForm, className }: { setShowForm: (show: boolean) => void, className?: string }) => {
+    const IsGuest = getCookie(IS_GUEST);
+    const router = useRouter();
+    const handleAddReview = () => {
+        if (IsGuest === "true" || IsGuest === null) {
+            router.push("/customer/login");
+        } else {
+            setShowForm(true);
+        }
+    };
+
+    return (
+        <button
+            onClick={handleAddReview}
+            className={`relative flex w-full min-w-[18rem] max-w-[20rem] cursor-pointer h-fit items-center justify-center rounded-full bg-blue-600 p-4 tracking-wide text-white mt-6 ${className}`}
+        > 
+            Write a review
+        </button>
+    )
+
+}

+ 116 - 0
src/components/common/form/Input.tsx

@@ -0,0 +1,116 @@
+import { ExclamationCircleIcon } from "@heroicons/react/24/outline";
+import clsx from "clsx";
+import { forwardRef } from "react";
+import { isArray } from '@/utils/type-guards';
+interface InputTextProps
+  extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size"> {
+  className?: string;
+  label: string;
+  name: string;
+  errorMsg?: string | string[] | Record<string, unknown>;
+  defaultValue?: string;
+  typeName?: string;
+  placeholder?: string;
+  size?: "sm" | "md" | "lg";
+  labelPlacement?: "inside" | "outside" | "outside-left";
+  rounded?: "sm" | "md" | "lg";
+  showAsterisk?: boolean; // Show asterisk if true
+}
+
+const sizeClasses = {
+  sm: "text-sm px-2 py-1",
+  md: "text-base px-3 py-2",
+  lg: "text-lg px-4 py-3",
+};
+
+const roundedClasses = {
+  sm: "rounded-sm",
+  md: "rounded-md",
+  lg: "rounded-lg",
+};
+
+const InputText = forwardRef<HTMLInputElement, InputTextProps>(
+  (
+    {
+      className,
+      label,
+      name,
+      errorMsg,
+      defaultValue,
+      typeName = "text",
+      placeholder,
+      size = "sm",
+      labelPlacement = "inside",
+      rounded = "sm",
+      required,
+      showAsterisk = true,
+      ...rest
+    },
+    ref
+  ) => {
+    const hasError = Boolean(errorMsg);
+
+    const borderColorClass = hasError
+      ? "border-red-500"
+      : "border-gray-300 dark:border-gray-500";
+
+    return (
+      <div className={clsx("max-w-full mb-2.5", className)}>
+        {labelPlacement !== "inside" && (
+          <label
+            className={clsx(
+              "px-1 mb-1 block font-medium text-black dark:text-white"
+            )}
+            htmlFor={name}
+          >
+            {label} {showAsterisk && <span className="text-red-500">*</span>}
+          </label>
+        )}
+        <div className="relative">
+          <input
+            ref={ref}
+            className={clsx(
+              "w-full !rounded-[0.62rem] border bg-transparent text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:text-white",
+              borderColorClass,
+              sizeClasses[size],
+              roundedClasses[rounded],
+              labelPlacement === "inside"
+                ? "placeholder-input-color dark:placeholder-selected-color-dark"
+                : ""
+            )}
+            defaultValue={defaultValue}
+            id={name}
+            name={name}
+            placeholder={labelPlacement === "inside" ? label : placeholder}
+            type={typeName}
+            required={required}
+            {...rest}
+          />
+          {hasError && (
+            <ul className="absolute -bottom-8 py-2 text-sm text-red-500">
+              {isArray(errorMsg) ? (
+                (errorMsg as string[]).map((msg, index) => (
+                  <li key={index} className="flex items-center gap-1">
+                    <ExclamationCircleIcon className="h-5 w-5" />
+                    {msg}
+                  </li>
+                ))
+              ) : (
+                <li className="flex items-center gap-1 text-xs sm:text-sm">
+                  <ExclamationCircleIcon className="size-[18px]" />
+                  {typeof errorMsg === "string"
+                    ? errorMsg
+                    : JSON.stringify(errorMsg)}
+                </li>
+              )}
+            </ul>
+          )}
+        </div>
+      </div>
+    );
+  }
+);
+
+InputText.displayName = "InputText";
+
+export default InputText;

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 20 - 0
src/components/common/icons/AddUploadImage.tsx


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 20 - 0
src/components/common/icons/CategoryIcon.tsx


+ 20 - 0
src/components/common/icons/CheckSign.tsx

@@ -0,0 +1,20 @@
+import clsx from "clsx";
+
+export default function CheckSign(props: React.ComponentProps<"svg">) {
+  return (
+    <svg
+      className={clsx("h-4 w-4", props.className)}
+      fill="none"
+      stroke="#008000"
+      strokeWidth={1.5}
+      viewBox="0 0 24 24"
+      xmlns="http://www.w3.org/2000/svg"
+    >
+      <path
+        d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
+        strokeLinecap="round"
+        strokeLinejoin="round"
+      />
+    </svg>
+  );
+}

+ 23 - 0
src/components/common/icons/EditIcon.tsx

@@ -0,0 +1,23 @@
+import clsx from "clsx";
+import { FC } from "react";
+
+export const EditIcon: FC<{
+  className: string;
+}> = ({ className }) => {
+  return (
+    <svg
+      className={clsx("size-6", className)}
+      fill="none"
+      stroke="currentColor"
+      strokeWidth={1.5}
+      viewBox="0 0 24 24"
+      xmlns="http://www.w3.org/2000/svg"
+    >
+      <path
+        d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"
+        strokeLinecap="round"
+        strokeLinejoin="round"
+      />
+    </svg>
+  );
+};

+ 18 - 0
src/components/common/icons/HeartIcon.tsx

@@ -0,0 +1,18 @@
+export const HeartIcon = () => {
+  return (
+    <svg
+      className="size-6"
+      fill="none"
+      stroke="currentColor"
+      strokeWidth={1.5}
+      viewBox="0 0 24 24"
+      xmlns="http://www.w3.org/2000/svg"
+    >
+      <path
+        d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12Z"
+        strokeLinecap="round"
+        strokeLinejoin="round"
+      />
+    </svg>
+  );
+};

+ 33 - 0
src/components/common/icons/HomeIcon.tsx

@@ -0,0 +1,33 @@
+import clsx from "clsx";
+
+export const HomeIcon = ({ className }: { className?: string }) => {
+  return (
+    <svg
+      className={clsx(
+        "fill-transparent stroke-current dark:fill-transparent",
+        className
+      )}
+      width="22"
+      height="21"
+      viewBox="0 0 22 21"
+      fill="none"
+      xmlns="http://www.w3.org/2000/svg"
+    >
+      <path
+        d="M10.75 15.25H10.759"
+        strokeWidth="2"
+        strokeLinecap="round"
+        strokeLinejoin="round"
+      />
+      <path
+        d="M18.75 6.75V11.75C18.75 15.5212 18.75 17.4069 17.5784 18.5784C16.4069 19.75 14.5212 19.75 10.75 19.75C6.97876 19.75 5.09315 19.75 3.92157 18.5784C2.75 17.4069 2.75 15.5212 2.75 11.75V6.75"
+        strokeWidth="1.5"
+      />
+      <path
+        d="M20.75 8.75L16.4069 4.58548C13.7402 2.02849 12.4069 0.75 10.75 0.75C9.09315 0.75 7.75981 2.02849 5.09315 4.58548L0.75 8.75"
+        strokeWidth="1.5"
+        strokeLinecap="round"
+      />
+    </svg>
+  );
+};

+ 17 - 0
src/components/common/icons/LeftArrow.tsx

@@ -0,0 +1,17 @@
+export const LeftArrow = () => {
+  return (
+    <svg
+      className="size-4"
+      fill="none"
+      height="14"
+      viewBox="0 0 15 14"
+      width="15"
+      xmlns="http://www.w3.org/2000/svg"
+    >
+      <path
+        d="M11.188 5.448V8.304H0.028V5.448H11.188ZM4.588 13.056L10.756 6.888L4.588 0.719999H8.596L14.764 6.888L8.596 13.056H4.588Z"
+        fill="white"
+      />
+    </svg>
+  );
+};

+ 21 - 0
src/components/common/icons/LoadingDots.tsx

@@ -0,0 +1,21 @@
+import clsx from "clsx";
+
+const dots = "mx-[1px] inline-block h-1 w-1 animate-blink rounded-md";
+
+const LoadingDots = ({ className }: { className: string }) => {
+  return (
+    <span className="mx-2 inline-flex items-center">
+      <span className={clsx(dots, className)} />
+      {Array(2)
+        .fill(0)
+        .map((_, i) => (
+          <span
+            key={i}
+            className={clsx(dots, "animation-delay-[200ms]", className)}
+          />
+        ))}
+    </span>
+  );
+};
+
+export default LoadingDots;

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 41 - 0
src/components/common/icons/LogoIcon.tsx


+ 25 - 0
src/components/common/icons/MapIcon.tsx

@@ -0,0 +1,25 @@
+import clsx from "clsx";
+
+export const MapIcon = ({ className = "" }) => {
+  return (
+    <svg
+      className={clsx("size-6", className)}
+      fill="none"
+      stroke="currentColor"
+      strokeWidth={1.5}
+      viewBox="0 0 24 24"
+      xmlns="http://www.w3.org/2000/svg"
+    >
+      <path
+        d="M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
+        strokeLinecap="round"
+        strokeLinejoin="round"
+      />
+      <path
+        d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1 1 15 0Z"
+        strokeLinecap="round"
+        strokeLinejoin="round"
+      />
+    </svg>
+  );
+};

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 112 - 0
src/components/common/icons/NoReviewsIcon.tsx


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 171 - 0
src/components/common/icons/NotFoundIcon.tsx


+ 0 - 0
src/components/common/icons/PlusIcon.tsx


Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác