Repository: Karabo-Seipelo/design-system-2025
Pull request #120: Feature/ds 110
Author: login: Karabo-Seipelo
Labels: name: feature
Created: 2025-03-27T07:04:41Z | Merged: 2025-04-01T11:56:26Z

- Add ProductDetails component and related files for product display; implement fetching logic and carousel for images.
- Add ProductInfo component and enhance ProductOptions; implement quantity selection and update styles for better consistency


Diff:
diff --git a/components/atoms/Price/index.tsx b/components/atoms/Price/index.tsx
new file mode 100644
index 00000000..7a285016
--- /dev/null
+++ b/components/atoms/Price/index.tsx
@@ -0,0 +1,39 @@
+interface PriceProps {
+  salePrice: string;
+  price: string;
+  discount_percentage: number | null;
+}
+
+const Price: React.FC<PriceProps> = ({
+  salePrice,
+  price,
+  discount_percentage,
+}) => {
+  return (
+    <>
+      {discount_percentage ? (
+        <>
+          <div className="flex gap-2">
+            <span className="font-medium text-3xl text-neutral-600">
+              {salePrice}
+            </span>
+            <span className="font-medium text-lg text-neutral-400 line-through">
+              {price}
+            </span>
+          </div>
+          <div className="">
+            <div className="inline-block bg-amber-50 rounded-full border border-solid border-amber-200 px-2.5 py-1 text-sm text-center text-amber-700">
+              {`${discount_percentage}% off`}
+            </div>
+          </div>
+        </>
+      ) : (
+        <div className="flex gap-2">
+          <span className="font-medium text-3xl text-neutral-600">{price}</span>
+        </div>
+      )}
+    </>
+  );
+};
+
+export default Price;
diff --git a/components/molecules/ProductCarousel/ProductCarousel.stories.tsx b/components/molecules/ProductCarousel/ProductCarousel.stories.tsx
new file mode 100644
index 00000000..1111348f
--- /dev/null
+++ b/components/molecules/ProductCarousel/ProductCarousel.stories.tsx
@@ -0,0 +1,61 @@
+import type { Meta, StoryObj } from "@storybook/react";
+import ProductCarousel from ".";
+import Artboard from "$/atoms/Artboard/index";
+
+const meta = {
+  title: "E-commerce/ProductDetails/ProductCarousel",
+  component: ProductCarousel,
+  tags: ["autodocs"],
+  parameters: {
+    layout: "fullscreen",
+  },
+  decorators: [
+    (Story) => (
+      <Artboard classes="md:w-[100%] mx-auto p-4">
+        <Story />
+      </Artboard>
+    ),
+  ],
+} satisfies Meta<typeof ProductCarousel>;
+
+export default meta;
+type Story = StoryObj<typeof meta>;
+
+const images = [
+  {
+    color: "green",
+    image_url:
+      "https://vaqybtnqyonvlwtskzmv.supabase.co/storage/v1/object/public/e-commerce-track-images/voyager-hoodie/voyager-hoodie-1.jpg",
+  },
+  {
+    color: "brown",
+    image_url:
+      "https://vaqybtnqyonvlwtskzmv.supabase.co/storage/v1/object/public/e-commerce-track-images/voyager-hoodie/voyager-hoodie-1.jpg",
+  },
+  {
+    color: "green",
+    image_url:
+      "https://vaqybtnqyonvlwtskzmv.supabase.co/storage/v1/object/public/e-commerce-track-images/voyager-hoodie/voyager-hoodie-2.jpg",
+  },
+  {
+    color: "green",
+    image_url:
+      "https://vaqybtnqyonvlwtskzmv.supabase.co/storage/v1/object/public/e-commerce-track-images/voyager-hoodie/voyager-hoodie-3.jpg",
+  },
+  {
+    color: "green",
+    image_url:
+      "https://vaqybtnqyonvlwtskzmv.supabase.co/storage/v1/object/public/e-commerce-track-images/voyager-hoodie/voyager-hoodie-4.jpg",
+  },
+];
+
+export const Default: Story = {
+  args: {
+    images,
+    loading: false,
+    color: "green",
+    selected: (state) => {
+      console.warn(state);
+    },
+  },
+};
diff --git a/components/molecules/ProductCarousel/index.tsx b/components/molecules/ProductCarousel/index.tsx
new file mode 100644
index 00000000..1c515f96
--- /dev/null
+++ b/components/molecules/ProductCarousel/index.tsx
@@ -0,0 +1,70 @@
+import { useState } from "react";
+import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@headlessui/react";
+import { Images as ProductImages } from "$/organisms/ProductDetails/fetchProductDetailsAPI";
+import { v4 as uuidv4 } from "uuid";
+import { ProductDetailsStore } from "$/organisms/ProductDetails/useProductStore";
+
+interface ProductCarouselProps {
+  images: ProductImages[];
+  loading: boolean;
+  color: string | null;
+  selected: (state: Partial<ProductDetailsStore>) => void;
+}
+
+const ProductCarousel: React.FC<ProductCarouselProps> = ({
+  images,
+  loading,
+  color,
+  selected,
+}) => {
+  const selectImageBasedOnColor = images.findIndex(
+    (item) => item.color === color,
+  );
+  const validIndex =
+    selectImageBasedOnColor !== -1 ? selectImageBasedOnColor : 0;
+  const [selectedIndex, setSelectedIndex] = useState(validIndex);
+
+  if (loading) {
+    return <p>loading</p>;
+  }
+
+  return (
+    <TabGroup
+      className="flex flex-col gap-6"
+      selectedIndex={selectedIndex}
+      onChange={(index) => {
+        setSelectedIndex(index);
+        const selectedImage = images[index];
+        selected({ selectedColor: selectedImage.color });
+      }}
+    >
+      <TabPanels>
+        {images.map(({ image_url }) => (
+          <TabPanel key={uuidv4()} className="min-h-[500px] w-full">
+            <div
+              style={{ backgroundImage: `url(${image_url})` }}
+              className="block w-full min-h-[500px] bg-cover bg-center rounded-lg"
+            />
+          </TabPanel>
+        ))}
+      </TabPanels>
+      <div className="flex flex-1 py-5 overflow-y-auto p-1">
+        <TabList className="flex flex-nowrap gap-4 h-[120px]">
+          {images.map(({ image_url }) => (
+            <Tab
+              key={uuidv4()}
+              className="w-20 h-[130px] data-[selected]:outline data-[selected]:outline-indigo-600 rounded-lg"
+            >
+              <div
+                style={{ backgroundImage: `url(${image_url})` }}
+                className="block w-full min-h-[130px] bg-cover bg-center rounded-lg"
+              />
+            </Tab>
+          ))}
+        </TabList>
+      </div>
+    </TabGroup>
+  );
+};
+
+export default ProductCarousel;
diff --git a/components/molecules/ProductColors/ProductColors.stories.tsx b/components/molecules/ProductColors/ProductColors.stories.tsx
new file mode 100644
index 00000000..0a7bbc7a
--- /dev/null
+++ b/components/molecules/ProductColors/ProductColors.stories.tsx
@@ -0,0 +1,44 @@
+import type { Meta, StoryObj } from "@storybook/react";
+import ProductColors from ".";
+import Artboard from "$/atoms/Artboard";
+
+const meta = {
+  title: "E-commerce/ProductDetails/ProductColors",
+  component: ProductColors,
+  tags: ["autodocs"],
+  parameters: {
+    layout: "fullscreen",
+  },
+  decorators: [
+    (Story) => (
+      <Artboard classes="md:w-[100%] mx-auto p-4">
+        <Story />
+      </Artboard>
+    ),
+  ],
+} satisfies Meta<typeof ProductColors>;
+
+export default meta;
+type Story = StoryObj<typeof meta>;
+
+export const Default: Story = {
+  args: {
+    colors: ["green", "brown"],
+    selected: (state) => {
+      console.warn(state);
+    },
+    selectedColor: "green",
+    outOfStock: [],
+  },
+};
+
+export const OutOfStock: Story = {
+  args: {
+    colors: ["green", "brown"],
+    selected: (state) => {
+      console.warn(state);
+    },
+    selectedColor: "green",
+    outOfStock: ["brown"],
+  },
+};
diff --git a/components/molecules/ProductColors/index.tsx b/components/molecules/ProductColors/index.tsx
new file mode 100644
index 00000000..e71906c1
--- /dev/null
+++ b/components/molecules/ProductColors/index.tsx
@@ -0,0 +1,90 @@
+import React, { useState, useEffect } from "react";
+import "remixicon/fonts/remixicon.css";
+import { v4 as uuidv4 } from "uuid";
+import { Input, Button } from "@headlessui/react";
+import { ProductDetailsStore } from "$/organisms/ProductDetails/useProductStore";
+
+interface ProductColorsProps {
+  name: string;
+  colors: string[];
+  selected: (state: Partial<ProductDetailsStore>) => void;
+  classes?: string;
+  selectedColor: string | null;
+  outOfStock: (string | number)[];
+}
+
+const ProductColors: React.FC<ProductColorsProps> = ({
+  name,
+  colors,
+  selected,
+  classes = "",
+  selectedColor,
+  outOfStock,
+}) => {
+  const [activeIndex, setActiveIndex] = useState<number>(() =>
+    colors.findIndex((color) => color === selectedColor),
+  );
+  const handleButtonClick = (color: string, index: number) => {
+    setActiveIndex(index);
+    selected({ selectedColor: color });
+  };
+
+  useEffect(() => {
+    const selectedColorIndex = colors.findIndex(
+      (color) => color === selectedColor,
+    );
+    setActiveIndex(selectedColorIndex);
+  }, [colors, selectedColor]);
+
+  return (
+    <>
+      {colors.length > 0 && (
+        <fieldset
+          id="ColorOptions"
+          className={`${classes} flex flex-col gap-4`}
+        >
+          <div>
+            <legend className="text-sm text-neutral-500">
+              Available Colors
+            </legend>
+          </div>
+          <div className="flex gap-5">
+            {colors.map((color, index) => {
+              const isOutOfStock = outOfStock.includes(color);
+
+              return (
+                <Button
+                  key={uuidv4()}
+                  onClick={() => handleButtonClick(color, index)}
+                >
+                  <label
+                    className={`relative flex rounded-full w-[40px] h-[40px] items-center justify-center outline outline-offset-1 cursor-pointer ${activeIndex === index ? "outline-indigo-600" : "outline-neutral-200"} `}
+                    style={{ backgroundColor: color }}
+                    aria-label={`Select color ${color}`}
+                  >
+                    {isOutOfStock && (
+                      <div className="w-[125%] h-[1px] bg-neutral-600 -rotate-45 absolute" />
+                    )}
+                    {activeIndex === index && !isOutOfStock && (
+                      <i className="ri-check-fill text-white w-15 h-15" />
+                    )}
+                    <Input
+                      type="radio"
+                      id={color}
+                      name={name}
+                      value={color}
+                      defaultChecked={activeIndex === index}
+                      className="hidden"
+                    />
+                  </label>
+                </Button>
+              );
+            })}
+          </div>
+        </fieldset>
+      )}
+    </>
+  );
+};
+
+export default ProductColors;
diff --git a/components/molecules/ProductDetail/ProductDetail.stories.tsx b/components/molecules/ProductDetail/ProductDetail.stories.tsx
new file mode 100644
index 00000000..1f464514
--- /dev/null
+++ b/components/molecules/ProductDetail/ProductDetail.stories.tsx
@@ -0,0 +1,63 @@
+import type { Meta, StoryObj } from "@storybook/react";
+import ProductDetail from ".";
+import Artboard from "$/atoms/Artboard/index";
+
+const meta = {
+  title: "E-commerce/ProductDetails/ProductDetail",
+  component: ProductDetail,
+  tags: ["autodocs"],
+  parameters: {
+    layout: "fullscreen",
+  },
+  decorators: [
+    (Story) => (
+      <Artboard classes="md:w-[50%] mx-auto p-4">
+        <Story />
+      </Artboard>
+    ),
+  ],
+} satisfies Meta<typeof ProductDetail>;
+
+export default meta;
+type Story = StoryObj<typeof meta>;
+
+const inventory = {
+  sku: "vh-green-xl",
+  color: "green",
+  size: "xl",
+  list_price: 95,
+  discount: null,
+  discount_percentage: null,
+  sale_price: 76,
+  sold: 65,
+  stock: 435,
+};
+
+export const Default: Story = {
+  args: {
+    name: "Voyager Hoodie",
+    description:
+      "The Voyager Hoodie is for the explorer at heart. Its durable fabric and roomy pockets are perfect for those who are always searching for the next adventure.",
+    rating: 4.5,
+    reviews: 150,
+    inventory: inventory,
+  },
+};
+
+export const Discount: Story = {
+  args: {
+    name: "Voyager Hoodie",
+    description:
+      "The Voyager Hoodie is for the explorer at heart. Its durable fabric and roomy pockets are perfect for those who are always searching for the next adventure.",
+    rating: 0,
+    reviews: 0,
+    inventory: {
+      ...inventory,
+      list_price: 95,
+      discount: null,
+      discount_percentage: 20,
+      sale_price: 76,
+      sold: 65,
+    },
+  },
+};
diff --git a/components/molecules/ProductDetail/index.tsx b/components/molecules/ProductDetail/index.tsx
new file mode 100644
index 00000000..3182e16c
--- /dev/null
+++ b/components/molecules/ProductDetail/index.tsx
@@ -0,0 +1,50 @@
+import Rating from "$/molecules/Rating";
+import { Inventory } from "$/organisms/ProductDetails/fetchProductDetailsAPI";
+import Price from "$/atoms/Price";
+
+export interface ProductDetailsProps {
+  name: string;
+  description: string;
+  rating: number;
+  reviews: number;
+  inventory: Inventory;
+}
+
+const ProductDetail: React.FC<ProductDetailsProps> = ({
+  name,
+  description,
+  rating,
+  reviews,
+  inventory,
+}) => {
+  const { list_price, sale_price, discount_percentage } = inventory;
+  const price = new Intl.NumberFormat("en-US", {
+    style: "currency",
+    currency: "USD",
+  }).format(list_price);
+  const salePrice = new Intl.NumberFormat("en-US", {
+    style: "currency",
+    currency: "USD",
+  }).format(sale_price);
+  return (
+    <div className="flex flex-col gap-5">
+      <h2 className="font-semibold text-3xl text-neutral-600">{name}</h2>
+      <div className="flex flex-col gap-2">
+        <Price
+          salePrice={salePrice}
+          price={price}
+          discount_percentage={discount_percentage}
+        />
+        <Rating
+          score={rating}
+          total={reviews}
+          classes="flex flex-row gap-2"
+          href="#"
+        />
+        <div dangerouslySetInnerHTML={{ __html: description }} />
+      </div>
+    </div>
+  );
+};
+
+export default ProductDetail;
diff --git a/components/molecules/ProductInfo/ProductInfo.stories.tsx b/components/molecules/ProductInfo/ProductInfo.stories.tsx
new file mode 100644
index 00000000..dc6b9904
--- /dev/null
+++ b/components/molecules/ProductInfo/ProductInfo.stories.tsx
@@ -0,0 +1,58 @@
+import type { Meta, StoryObj } from "@storybook/react";
+import ProductInfo from ".";
+import Artboard from "$/atoms/Artboard/index";
+
+const meta = {
+  title: "E-commerce/ProductDetails/ProductInfo",
+  component: ProductInfo,
+  tags: ["autodocs"],
+  parameters: {
+    layout: "fullscreen",
+  },
+  decorators: [
+    (Story) => (
+      <Artboard classes="md:w-[50%] mx-auto p-4">
+        <Story />
+      </Artboard>
+    ),
+  ],
+} satisfies Meta<typeof ProductInfo>;
+
+export default meta;
+type Story = StoryObj<typeof meta>;
+
+const info = [
+  {
+    title: "Features",
+    description: [
+      "Designed for comfort and durability.",
+      "Soft, breathable fabric ideal for travel and adventure.",
+      "Large front pocket and adjustable hood.",
+      "Minimalist design pairs well with any style.",
+      "Made with eco-conscious materials.",
+    ],
+  },
+  {
+    title: "Fabric & Care",
+    description: [
+      "Machine wash cold on a gentle cycle.",
+      "Tumble dry low or hang to dry.",
+      "Do not use fabric softeners or bleach.",
+      "Iron on a low setting if necessary.",
+    ],
+  },
+  {
+    title: "Shipping",
+    description: [
+      "Free standard shipping on all orders - no minimum spend required.",
+      "Expedited shipping available at an additional cost.",
+      "Packaged in biodegradable materials to reduce environmental impact.",
+    ],
+  },
+];
+
+export const Default: Story = {
+  args: {
+    info: info,
+  },
+};
diff --git a/components/molecules/ProductInfo/index.tsx b/components/molecules/ProductInfo/index.tsx
new file mode 100644
index 00000000..75408ae9
--- /dev/null
+++ b/components/molecules/ProductInfo/index.tsx
@@ -0,0 +1,53 @@
+import { Info } from "$/organisms/ProductDetails/fetchProductDetailsAPI";
+import {
+  Disclosure,
+  DisclosureButton,
+  DisclosurePanel,
+} from "@headlessui/react";
+import { v4 as uuidv4 } from "uuid";
+import "remixicon/fonts/remixicon.css";
+
+interface ProductInfoProps {
+  info: Info[];
+}
+const ProductInfo: React.FC<ProductInfoProps> = ({ info }) => {
+  return (
+    <div>
+      {info?.map(({ title, description }) => {
+        return (
+          <Disclosure
+            key={uuidv4()}
+            as="div"
+            defaultOpen={false}
+            className="flex flex-col gap-4 border-solid border-b border-neutral-200 py-6 last:border-transparent"
+          >
+            {({ open }) => (
+              <>
+                <DisclosureButton className="group flex w-full justify-between">
+                  <span className="text-lg text-neutral-900">{title}</span>
+                  {open ? (
+                    <i className="ri-indeterminate-circle-line text-neutral-400" />
+                  ) : (
+                    <i className="ri-add-circle-line text-neutral-400" />
+                  )}
+                </DisclosureButton>
+                <DisclosurePanel className="flex flex-col gap-4 px-5">
+                  <ul className="list-disc text-neutral-600">
+                    {description.map((desc) => (
+                      <li
+                        key={uuidv4()}
+                        dangerouslySetInnerHTML={{ __html: desc }}
+                      />
+                    ))}
+                  </ul>
+                </DisclosurePanel>
+              </>
+            )}
+          </Disclosure>
+        );
+      })}
+    </div>
+  );
+};
+
+export default ProductInfo;
diff --git a/components/molecules/ProductQuantity/ProductQuantity.stories.tsx b/components/molecules/ProductQuantity/ProductQuantity.stories.tsx
new file mode 100644
index 00000000..aeab5677
--- /dev/null
+++ b/components/molecules/ProductQuantity/ProductQuantity.stories.tsx
@@ -0,0 +1,46 @@
+import type { Meta, StoryObj } from "@storybook/react";
+import ProductQuantity from ".";
+import Artboard from "$/atoms/Artboard";
+
+const meta = {
+  title: "E-commerce/ProductDetails/ProductQuantity",
+  component: ProductQuantity,
+  tags: ["autodocs"],
+  parameters: {
+    layout: "fullscreen",
+  },
+  decorators: [
+    (Story) => (
+      <Artboard classes="md:w-[50%] mx-auto p-4">
+        <Story />
+      </Artboard>
+    ),
+  ],
+} satisfies Meta<typeof ProductQuantity>;
+
+export default meta;
+type Story = StoryObj<typeof meta>;
+
+export const Default: Story = {
+  args: {
+    name: "Quantity",
+    initialQuantity: 1,
+    selected: (state) => {
+      console.log(state);
+    },
+    stock: 10,
+    outOfStock: false,
+  },
+};
+
+export const OutOfStock: Story = {
+  args: {
+    name: "Quantity",
+    initialQuantity: 1,
+    selected: (state) => {
+      console.log(state);
+    },
+    stock: 10,
+    outOfStock: true,
+  },
+};
diff --git a/components/molecules/ProductQuantity/index.tsx b/components/molecules/ProductQuantity/index.tsx
new file mode 100644
index 00000000..2e7e898c
--- /dev/null
+++ b/components/molecules/ProductQuantity/index.tsx
@@ -0,0 +1,90 @@
+import React from "react";
+import "remixicon/fonts/remixicon.css";
+import { Button, Input } from "@headlessui/react";
+import { ProductDetailsStore } from "$/organisms/ProductDetails/useProductStore";
+
+interface ProductQuantityProps {
+  name: string;
+  initialQuantity: number;
+  selected: (state: Partial<ProductDetailsStore>) => void;
+  stock: number;
+  classes?: string;
+  outOfStock: boolean;
+}
+
+interface TooltipProps {
+  content: string;
+}
+
+const Tooltip: React.FC<TooltipProps> = ({ content }) => {
+  return (
+    <div
+      className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:inline-block bg-gray-800 text-white text-xs text-nowrap rounded-md px-2 py-1 shadow-lg"
+      role="tooltip"
+    >
+      {content}
+      <div className="absolute inset-x-0 -bottom-1 h-2 w-2 bg-inherit mx-auto z-10 rotate-45 " />
+    </div>
+  );
+};
+
+const ProductQuantity: React.FC<ProductQuantityProps> = ({
+  name,
+  initialQuantity,
+  selected,
+  classes = "",
+  stock,
+  outOfStock = false,
+}) => {
+  const [quantity, setQuantity] = React.useState<number>(initialQuantity);
+  const handleQuantityChange = (value: number) => {
+    if (value >= 0) {
+      setQuantity(value);
+      selected({ selectedQuantity: value });
+    }
+  };
+
+  return (
+    <fieldset id="Quantity" className={`${classes} flex flex-col gap-4`}>
+      <div>
+        <legend className="flex flex-row text-sm text-neutral-500">
+          Quantity
+        </legend>
+      </div>
+      <div className="flex flex-row w-[125px] justify-center items-center gap-3 bg-neutral-50 p-0.5 rounded-md border border-solid border-neutral-200">
+        <Button
+          disabled={outOfStock}
+          onClick={() => handleQuantityChange(quantity - 1)}
+          className="relative group disabled:cursor-not-allowed"
+        >
+          <i
+            className={`ri-subtract-fill ${outOfStock ? "text-neutral-300" : ""}`}
+          />
+        </Button>
+        <div>
+          <Input
+            name={name}
+            type="number"
+            value={quantity}
+            className={`w-full bg-transparent text-center ${outOfStock ? "text-neutral-300 cursor-not-allowed" : ""}`}
+            min={`${initialQuantity}`}
+            max={`${stock}`}
+            onChange={(e) => handleQuantityChange(Number(e.target.value))}
+          />
+        </div>
+        <Button
+          disabled={quantity >= stock || outOfStock}
+          className="relative group disabled:cursor-not-allowed"
+          onClick={() => handleQuantityChange(quantity + 1)}
+        >
+          <i
+            className={`ri-add-line ${quantity >= stock || outOfStock ? "text-neutral-300 " : ""}`}
+          />
+          {quantity >= stock && <Tooltip content="Insufficient stock" />}
+        </Button>
+      </div>
+    </fieldset>
+  );
+};
+
+export default ProductQuantity;
diff --git a/components/molecules/ProductSize/ProductSize.stories.tsx b/components/molecules/ProductSize/ProductSize.stories.tsx
new file mode 100644
index 00000000..ac0b3980
--- /dev/null
+++ b/components/molecules/ProductSize/ProductSize.stories.tsx
@@ -0,0 +1,83 @@
+import type { Meta, StoryObj } from "@storybook/react";
+import ProductSize from ".";
+import Artboard from "$/atoms/Artboard/index";
+
+const meta = {
+  title: "E-commerce/ProductDetails/ProductSize",
+  component: ProductSize,
+  tags: ["autodocs"],
+  parameters: {
+    layout: "fullscreen",
+  },
+  decorators: [
+    (Story) => (
+      <Artboard classes="md:w-[50%] mx-auto p-4">
+        <Story />
+      </Artboard>
+    ),
+  ],
+} satisfies Meta<typeof ProductSize>;
+
+export default meta;
+type Story = StoryObj<typeof meta>;
+
+const sizes = ["XS", "S", "M", "L", "XL"];
+const sizesNumbers = [
+  4, 4.5, 5, 5.5, 6, 6.5, 7, 7.5, 8, 8.5, 9, 9.5, 10, 10.5, 11, 11.5, 12,
+];
+
+const inventory = {
+  sku: "vh-green-xl",
+  color: "green",
+  size: "xl",
+  list_price: 95,
+  discount: null,
+  discount_percentage: null,
+  sale_price: 76,
+  sold: 65,
+  stock: 435,
+};
+
+export const Default: Story = {
+  args: {
+    name: "Size",
+    sizes,
+    selected: (state) => {
+      console.log(state);
+    },
+    classes: "flex flex-col gap-4",
+    inventory: inventory,
+    outOfStock: [],
+    unavailableSizes: {},
+  },
+};
+
+export const Numbers: Story = {
+  args: {
+    name: "Size",
+    sizes: sizesNumbers,
+    selected: (state) => {
+      console.log(state);
+    },
+    classes: "flex flex-col gap-4",
+    inventory: inventory,
+    outOfStock: [],
+    unavailableSizes: {},
+  },
+};
+
+export const OutOfStock: Story = {
+  args: {
+    name: "Size",
+    sizes: sizesNumbers,
+    selected: (state) => {
+      console.log(state);
+    },
+    classes: "flex flex-col gap-4",
+    inventory: inventory,
+    outOfStock: [],
+    unavailableSizes: {
+      green: [7, 7.5, 8, 8.5, 9, 9.5],
+    },
+  },
+};
diff --git a/components/molecules/ProductSize/index.tsx b/components/molecules/ProductSize/index.tsx
new file mode 100644
index 00000000..383d6f72
--- /dev/null
+++ b/components/molecules/ProductSize/index.tsx
@@ -0,0 +1,80 @@
+import React, { useState } from "react";
+import { v4 as uuidv4 } from "uuid";
+import { Input, Button } from "@headlessui/react";
+import { ProductDetailsStore } from "$/organisms/ProductDetails/useProductStore";
+import { Inventory } from "$/organisms/ProductDetails/fetchProductDetailsAPI";
+
+interface ProductSizeProps {
+  name: string;
+  sizes: number[] | string[];
+  selected: (state: Partial<ProductDetailsStore>) => void;
+  classes?: string;
+  inventory: Inventory;
+  outOfStock: (string | number)[];
+  unavailableSizes: Record<string, (string | number)[]>;
+}
+
+const ProductSize: React.FC<ProductSizeProps> = ({
+  name,
+  sizes,
+  selected,
+  classes = "",
+  inventory,
+  outOfStock,
+  unavailableSizes,
+}) => {
+  const [activeIndex, setActiveIndex] = useState<number>(0);
+
+  const handleButtonClick = (size: number | string | null, index: number) => {
+    setActiveIndex(index);
+    selected({ selectedSize: size });
+  };
+
+  return (
+    <>
+      {sizes.length > 0 && (
+        <fieldset id="Sizes" className={`${classes} flex flex-col gap-4`}>
+          <div>
+            <legend className="text-sm text-neutral-500">
+              Available Sizes
+            </legend>
+          </div>
+          <div className="flex flex-row gap-4 flex-wrap">
+            {sizes?.map((size, index) => {
+              const isOutOfStock =
+                unavailableSizes[inventory.color]?.includes(size) ||
+                outOfStock.includes(size);
+
+              return (
+                <Button
+                  key={uuidv4()}
+                  onClick={() => handleButtonClick(size, index)}
+                >
+                  <label
+                    className={`w-16 flex justify-center items-center gap-1.5 px-5 py-3 rounded border border-solid uppercase hover:bg-neutral-50 focus:bg-neutral-200 cursor-pointer ${isOutOfStock ? `bg-neutral-100 text-neutral-400 cursor-not-allowed` : ""} ${activeIndex === index && !isOutOfStock ? "border-indigo-600" : "border-neutral-200"}`}
+                    aria-label={`Select size ${size}`}
+                    htmlFor={String(size)}
+                  >
+                    {size && (
+                      <Input
+                        type="radio"
+                        id={String(size)}
+                        name={name}
+                        value={String(size)}
+                        defaultChecked={activeIndex === index}
+                        className="hidden"
+                      />
+                    )}
+                    {size}
+                  </label>
+                </Button>
+              );
+            })}
+          </div>
+        </fieldset>
+      )}
+    </>
+  );
+};
+
+export default ProductSize;
diff --git a/components/organisms/ProductReview/Rating.tsx b/components/molecules/Rating/index.tsx
similarity index 74%
rename from components/organisms/ProductReview/Rating.tsx
rename to components/molecules/Rating/index.tsx
index ae1d8106..6e30e023 100644
--- a/components/organisms/ProductReview/Rating.tsx
+++ b/components/molecules/Rating/index.tsx
@@ -4,6 +4,7 @@ export interface RatingProps {
   score: number;
   classes?: string;
   total?: number;
+  href?: string;
   showScore?: boolean;
 }
 
@@ -23,6 +24,7 @@ const Rating: React.FC<RatingProps> = ({
   total,
   classes,
   showScore = true,
+  href,
 }) => {
   const fullStars = Math.floor(score);
   const hasHalfStar = score % 1 !== 0;
@@ -60,9 +62,20 @@ const Rating: React.FC<RatingProps> = ({
           }
         })}
       </div>
-      {total && total > 0 && (
-        <div className="text-nowrap font-normal text-sm text-neutral-600">
-          Based on {total} reviews
+      {total && total > 0 ? (
+        <div
+          className={`text-nowrap leading-[1.8] font-normal text-sm ${href ? "text-indigo-600 cursor-auto" : "text-neutral-600"}`}
+        >
+          {href ? `See all ${total} reviews` : `Based on ${total} reviews`}
+        </div>
+      ) : (
+        <div
+          className={`text-nowrap leading-[1.8] font-normal text-neutral-900 text-sm cursor-auto`}
+        >
+          No reviews yet.{" "}
+          <a href="" className="text-indigo-600 ">
+            Be the first
+          </a>
         </div>
       )}
     </div>
diff --git a/components/organisms/ProductDetails/ProductDetails.skeleton.tsx b/components/organisms/ProductDetails/ProductDetails.skeleton.tsx
new file mode 100644
index 00000000..775aae1f
--- /dev/null
+++ b/components/organisms/ProductDetails/ProductDetails.skeleton.tsx
@@ -0,0 +1,61 @@
+import { v4 as uuidv4 } from "uuid";
+const ProductDetailsSkeleton = () => (
+  <div className="flex flex-col gap-10 p-4 lg:flex-row">
+    <div className="flex flex-col gap-10 lg:w-[592px]">
+      <div className="flex  flex-col gap-6">
+        <div className="block w-full min-h-[500px] bg-slate-100 bg-center rounded-lg" />
+        <div className="flex flex-1 py-5 overflow-hidden p-1">
+          <div className="flex flex-nowrap gap-4 h-[120px]">
+            {Array.from({ length: 7 }).map((_, index) => (
+              <div
+                key={uuidv4()}
+                className="w-20 h-[130px] bg-slate-100 rounded-lg"
+              />
+            ))}
+          </div>
+        </div>
+      </div>
+    </div>
+    <div className="flex flex-col gap-10 grow">
+      <div className="flex flex-row gap-4">
+        <div className="block h-[2em] w-[5em] bg-slate-100 rounded-lg" />
+        <div className="block h-[2em] w-[10em] bg-slate-100 rounded-lg" />
+      </div>
+      <div className="flex gap-2">
+        <span className="block h-[2em] w-[2em] bg-slate-100 rounded-lg" />
+        <span className="block h-[1.5em] w-[1.5em] bg-slate-100 rounded-lg" />
+      </div>
+      <div className="flex flex-col gap-2">
+        <span className="inline h-[1em] w-[10em] bg-slate-100 rounded-lg" />
+        <span className="inline h-[1em] w-[5em] bg-slate-100 rounded-lg" />
+      </div>
+      <div className="flex flex-col gap-2">
+        <span className="inline h-[1em] w-[10em] bg-slate-100 rounded-lg" />
+        <div className="flex gap-2">
+          <span className="flex rounded-full w-[40px] h-[40px] bg-slate-100" />
+          <span className="flex rounded-full w-[40px] h-[40px] bg-slate-100" />
+        </div>
+      </div>
+      <div className="flex flex-col gap-2">
+        <span className="inline h-[1em] w-[10em] bg-slate-100 rounded-lg" />
+        <div className="flex gap-2">
+          {Array.from({ length: 3 }).map((_, index) => (
+            <span
+              key={uuidv4()}
+              className="w-16 h-14 flex justify-center items-center gap-1.5 px-5 py-3 rounded bg-slate-100"
+            />
+          ))}
+        </div>
+      </div>
+      <div className="flex flex-col gap-2">
+        <span className="inline h-[1em] w-[10em] bg-slate-100 rounded-lg" />
+        <div className="flex gap-2">
+          <span className="w-[9em] h-8 flex justify-center items-center gap-1.5 px-5 py-3 rounded bg-slate-100" />
+        </div>
+      </div>
+      <div className="flex h-[3em] w-full  bg-slate-100 rounded" />
+    </div>
+  </div>
+);
+
+export default ProductDetailsSkeleton;
diff --git a/components/organisms/ProductDetails/ProductDetails.stories.tsx b/components/organisms/ProductDetails/ProductDetails.stories.tsx
new file mode 100644
index 00000000..970f626e
--- /dev/null
+++ b/components/organisms/ProductDetails/ProductDetails.stories.tsx
@@ -0,0 +1,28 @@
+import type { Meta, StoryObj } from "@storybook/react";
+import ProductDetails from ".";
+import Artboard from "../../atoms/Artboard/index";
+
+const meta = {
+  title: "E-commerce/ProductDetails",
+  component: ProductDetails,
+  tags: ["autodocs"],
+  parameters: {
+    layout: "fullscreen",
+  },
+  decorators: [
+    (Story) => (
+      <Artboard>
+        <Story />
+      </Artboard>
+    ),
+  ],
+} satisfies Meta<typeof ProductDetails>;
+
+export default meta;
+type Story = StoryObj<typeof meta>;
+
+export const Default: Story = {
+  args: {
+    productId: "classic-canvas-tee", //"classic-canvas-tee", // "voyager-hoodie"
+  },
+};
diff --git a/components/organisms/ProductDetails/fetchProductDetailsAPI.ts b/components/organisms/ProductDetails/fetchProductDetailsAPI.ts
new file mode 100644
index 00000000..7a812b89
--- /dev/null
+++ b/components/organisms/ProductDetails/fetchProductDetailsAPI.ts
@@ -0,0 +1,70 @@
+import axios from "axios";
+
+interface Category {
+  category_id: string;
+  name: string;
+  created_at: string;
+}
+
+interface Collection {
+  collection_id: string;
+  name: string;
+  description: string;
+  image_url: string;
+  created_at: string;
+}
+
+export interface Images {
+  color: string;
+  image_url: string;
+}
+
+export interface Info {
+  title: string;
+  description: string[];
+}
+
+export interface Inventory {
+  sku: string;
+  color: string;
+  size: number | string;
+  list_price: number;
+  discount: number | null;
+  discount_percentage: number | null;
+  sale_price: number;
+  sold: number;
+  stock: number;
+}
+
+interface PriceRange {
+  highest: number;
+  lowest: number;
+}
+
+export interface ProductDetails {
+  product_id: string;
+  name: string;
+  description: string;
+  category: Category;
+  collection: Collection;
+  created_at: string;
+  colors: string[];
+  images: Images[];
+  info: Info[];
+  inventory: Inventory[];
+  priceRange: PriceRange;
+  rating: number;
+  reviews: number;
+  sizes: number[];
+  sold: number;
+}
+
+const fetchProductDetailsAPI = async (
+  productId: string,
+): Promise<ProductDetails> => {
+  const url = `https://www.greatfrontend.com/api/projects/challenges/e-commerce/products/${productId}`;
+  const { data } = await axios.get<ProductDetails>(url);
+  return data;
+};
+
+export default fetchProductDetailsAPI;
diff --git a/components/organisms/ProductDetails/index.tsx b/components/organisms/ProductDetails/index.tsx
new file mode 100644
index 00000000..e41631f3
--- /dev/null
+++ b/components/organisms/ProductDetails/index.tsx
@@ -0,0 +1,88 @@
+import React, { useEffect } from "react";
+import useProductStore from "./useProductStore";
+import ProductCarousel from "$/molecules/ProductCarousel";
+import ProductDetail from "$/molecules/ProductDetail";
+import ProductInfo from "$/molecules/ProductInfo";
+import ProductOptions from "$/organisms/ProductOptions";
+import ProductDetailsSkeleton from "./ProductDetails.skeleton";
+
+export interface ProductDetailsProps {
+  productId: string;
+}
+
+const ProductDetails: React.FC<ProductDetailsProps> = ({ productId }) => {
+  const {
+    product,
+    loading,
+    selectedQuantity: quantity,
+    updateState: updateProductState,
+    fetchProductDetails,
+    selectedInventory,
+    selectedColor,
+    unavailableSizes,
+    outOfStock,
+  } = useProductStore();
+
+  useEffect(() => {
+    const fetchProductData = async () => {
+      try {
+        await fetchProductDetails(productId);
+      } catch (err) {
+        console.error("Error fetching product details:", err);
+      }
+    };
+
+    fetchProductData();
+  }, [fetchProductDetails, productId]);
+
+  return (
+    <>
+      {loading ? (
+        <ProductDetailsSkeleton />
+      ) : (
+        <div className="flex flex-col gap-10 p-4 lg:flex-row">
+          <div className="flex flex-col gap-10 lg:w-[592px]">
+            {product?.images && (
+              <ProductCarousel
+                images={product?.images}
+                loading={loading}
+                color={selectedColor}
+                selected={updateProductState}
+              />
+            )}
+          </div>
+          <div className="flex flex-col gap-10">
+            {product?.name &&
+              product?.description &&
+              product?.rating &&
+              product.reviews &&
+              selectedInventory && (
+                <ProductDetail
+                  name={product?.name}
+                  description={product.description}
+                  rating={product.rating}
+                  reviews={product.reviews}
+                  inventory={selectedInventory}
+                />
+              )}
+            {product?.colors && product?.sizes && selectedInventory && (
+              <ProductOptions
+                colors={product.colors}
+                sizes={product.sizes}
+                selected={updateProductState}
+                quantity={quantity}
+                classes="flex flex-col gap-4"
+                inventory={selectedInventory}
+                outOfStock={outOfStock}
+                unavailableSizes={unavailableSizes}
+              />
+            )}
+            {product?.info && <ProductInfo info={product.info} />}
+          </div>
+        </div>
+      )}
+    </>
+  );
+};
+
+export default ProductDetails;
diff --git a/components/organisms/ProductDetails/useFetchProductDetailsStore.ts b/components/organisms/ProductDetails/useFetchProductDetailsStore.ts
new file mode 100644
index 00000000..9aa823c4
--- /dev/null
+++ b/components/organisms/ProductDetails/useFetchProductDetailsStore.ts
@@ -0,0 +1,84 @@
+import { create } from "zustand";
+import fetchProductDetailsFromAPI, {
+  ProductDetails,
+  Inventory,
+} from "./fetchProductDetailsAPI";
+import axios from "axios";
+
+interface ProductDetailsStore {
+  product: ProductDetails | null;
+  loading: boolean;
+  error: Error | null;
+  selectedInventory: Inventory | null;
+  selectedColor: string | undefined;
+  selectedSize: number | string | undefined;
+  selectedQuantity: number;
+  fetchProductDetails: (productId: string) => Promise<void>;
+  selectInventory: (inventory: Inventory, color: string, size?: string) => void;
+  selectOptions: (options: { color?: string; size?: number }) => void;
+  selectColor: (color: string) => void;
+  selectSize: (size: number | string) => void;
+  updateState: (state: Partial<ProductDetailsStore>) => void;
+}
+
+const useFetchProductDetailsStore = create<ProductDetailsStore>((set) => ({
+  product: null,
+  loading: true,
+  error: null,
+  fetchProductDetails: async (productId: string) => {
+    try {
+      set({ loading: true });
+      const data = await fetchProductDetailsFromAPI(productId);
+      set({ product: data, loading: false });
+
+      if (data.colors.length > 0) {
+        const defaultColor = data.colors[0];
+        const defaultSize = data.sizes[0];
+
+        set({
+          selectedColor: defaultColor,
+          selectedSize: defaultSize,
+        });
+      }
+    } catch (error) {
+      if (axios.isAxiosError(error)) {
+        set({
+          error: new Error(error.response?.data.message || error.message),
+        });
+      } else {
+        set({
+          error: error instanceof Error ? error : new Error(String(error)),
+        });
+      }
+    } finally {
+      set({ loading: false });
+    }
+  },
+  selectedInventory: null,
+  selectedColor: undefined,
+  selectedSize: undefined,
+  selectedQuantity: 1,
+  selectColor: (color) => {
+    set({ selectedColor: color });
+  },
+  selectSize: (size) => {
+    set({ selectedSize: size });
+  },
+  selectOptions: (options) => {
+    set((state) => ({
+      selectedColor: options.color ?? state.selectedColor,
+      selectedSize: options.size ?? state.selectedSize,
+    }));
+  },
+  selectInventory: (inventory: Inventory, color, size) => {
+    set({ selectedInventory: inventory });
+  },
+  updateState: (state) => {
+    set((prevState) => ({
+      ...prevState,
+      ...state,
+    }));
+  },
+}));
+
+export default useFetchProductDetailsStore;
diff --git a/components/organisms/ProductDetails/useProductStore.ts b/components/organisms/ProductDetails/useProductStore.ts
new file mode 100644
index 00000000..fbd2a7ff
--- /dev/null
+++ b/components/organisms/ProductDetails/useProductStore.ts
@@ -0,0 +1,136 @@
+import { create } from "zustand";
+import fetchProductDetailsFromAPI, {
+  ProductDetails,
+  Inventory,
+} from "./fetchProductDetailsAPI";
+import axios from "axios";
+
+export interface ProductDetailsStore {
+  product: ProductDetails | null;
+  loading: boolean;
+  error: Error | null;
+  selectedColor: string | null;
+  selectedSize: number | string | null;
+  selectedQuantity: number;
+  inventory: Inventory[] | null;
+  selectedInventory: Inventory | null;
+  unavailableSizes: Record<string, (string | number)[]>;
+  outOfStock: (string | number)[];
+  setColor: (color: string) => void;
+  setSize: (size: number | string) => void;
+  setQuantity: (quantity: number) => void;
+  fetchProductDetails: (productId: string) => Promise<void>;
+  getStock: (
+    color: string | null,
+    size: number | string | null,
+  ) => Inventory | null;
+  updateState: (state: Partial<ProductDetailsStore>) => void;
+}
+
+const isColorUnavailable = (
+  color: string,
+  colorSizeMap: Record<string, (string | number)[]>,
+  productSizes: (string | number)[],
+) => {
+  const outOfStockSizes = colorSizeMap[color] || [];
+  return productSizes.every((size) => outOfStockSizes.includes(size));
+};
+
+const getOutOfStockColorSizeMap = (inventory: Inventory[]) => {
+  return inventory.reduce((acc: Record<string, (string | number)[]>, item) => {
+    const { color, size, stock } = item;
+
+    if (stock === 0 && size) {
+      if (!acc[color]) {
+        acc[color] = [];
+      }
+
+      acc[color].push(size);
+    }
+    return acc;
+  }, {});
+};
+
+const useProductStore = create<ProductDetailsStore>((set, get) => ({
+  product: null,
+  error: null,
+  loading: true,
+  selectedColor: null,
+  selectedSize: null,
+  selectedQuantity: 1,
+  inventory: null,
+  selectedInventory: null,
+  unavailableSizes: {},
+  outOfStock: [],
+  setColor: (color: string) => {
+    set({ selectedColor: color });
+  },
+  setSize: (size: number | string) => {
+    set({ selectedSize: size });
+  },
+  setQuantity: (quantity: number) => set({ selectedQuantity: quantity }),
+  fetchProductDetails: async (productId: string) => {
+    try {
+      set({ loading: true });
+      const data = await fetchProductDetailsFromAPI(productId);
+      set({ product: data, loading: false });
+      set({ inventory: data.inventory });
+
+      const unavailableSizes = getOutOfStockColorSizeMap(data.inventory);
+      const unavailableColors = Object.keys(unavailableSizes).filter((color) =>
+        isColorUnavailable(color, unavailableSizes, data.sizes),
+      );
+      if (data.inventory.length > 0) {
+        set({
+          selectedColor: data.inventory[0].color,
+          selectedSize: data.inventory[0].size,
+          selectedInventory: data.inventory[0],
+          unavailableSizes: unavailableSizes,
+          outOfStock: unavailableColors,
+        });
+      }
+    } catch (error) {
+      if (axios.isAxiosError(error)) {
+        set({
+          error: new Error(error.response?.data.message || error.message),
+        });
+      } else {
+        set({
+          error: error instanceof Error ? error : new Error(String(error)),
+        });
+      }
+    } finally {
+      set({ loading: false });
+    }
+  },
+  getStock: (color, size) => {
+    const { inventory } = get();
+    return (
+      inventory?.find(
+        (item) =>
+          item.color === color && (item.size === size || item.size === null),
+      ) || null
+    );
+  },
+  updateState: (state) => {
+    set((prevState) => ({
+      ...prevState,
+      ...state,
+    }));
+  },
+}));
+
+useProductStore.subscribe((state) => {
+  const { selectedColor, selectedSize } = state;
+  const { getStock, selectedInventory } = useProductStore.getState();
+
+  const inventory = getStock(selectedColor, selectedSize);
+
+  if (inventory && selectedInventory !== inventory) {
+    useProductStore.setState({
+      selectedInventory: inventory,
+    });
+  }
+});
+
+export default useProductStore;
diff --git a/components/organisms/ProductOptions/index.tsx b/components/organisms/ProductOptions/index.tsx
new file mode 100644
index 00000000..9bc23188
--- /dev/null
+++ b/components/organisms/ProductOptions/index.tsx
@@ -0,0 +1,85 @@
+import React from "react";
+import "remixicon/fonts/remixicon.css";
+import ProductQuantity from "$/molecules/ProductQuantity";
+import ProductSize from "$/molecules/ProductSize";
+import ProductColors from "$/molecules/ProductColors";
+import { ProductDetailsStore } from "$/organisms/ProductDetails/useProductStore";
+import { Button } from "@headlessui/react";
+import { Inventory } from "$/organisms/ProductDetails/fetchProductDetailsAPI";
+
+interface ProductOptionsProps {
+  colors: string[];
+  sizes: number[];
+  quantity: number;
+  selected: (state: Partial<ProductDetailsStore>) => void;
+  classes?: string;
+  inventory: Inventory;
+  outOfStock: (string | number)[];
+  unavailableSizes: Record<string, (string | number)[]>;
+}
+
+const ProductOptions: React.FC<ProductOptionsProps> = ({
+  colors,
+  sizes,
+  quantity,
+  selected,
+  classes = "",
+  inventory,
+  outOfStock,
+  unavailableSizes,
+}) => {
+  const disabled = outOfStock.includes(inventory.color);
+  const handleAddCart = (event: React.FormEvent<HTMLFormElement>) => {
+    event.preventDefault();
+    const formData = new FormData(event.currentTarget);
+    const data = Object.fromEntries(formData.entries());
+
+    console.log("Adding to cart:", { data });
+    console.log("Selected inventory:", inventory);
+  };
+
+  return (
+    <form className={classes} onSubmit={handleAddCart}>
+      <ProductColors
+        name="color"
+        colors={colors}
+        selected={selected}
+        classes="flex flex-col gap-4"
+        selectedColor={inventory.color}
+        outOfStock={outOfStock}
+      />
+      <ProductSize
+        name="size"
+        sizes={sizes}
+        selected={selected}
+        classes="flex flex-col gap-4"
+        inventory={inventory}
+        outOfStock={outOfStock}
+        unavailableSizes={unavailableSizes}
+      />
+      <ProductQuantity
+        name="quantity"
+        classes="flex flex-col gap-4"
+        initialQuantity={quantity}
+        selected={selected}
+        stock={inventory.stock}
+        outOfStock={disabled}
+      />
+
+      {disabled && (
+        <div className="text-neutral-900 invisible md:visible">
+          Sorry, this item is out of stock
+        </div>
+      )}
+      <Button
+        type="submit"
+        disabled={disabled}
+        className={`flex w-full justify-center items-center gap-1.5 self-stretch bg-indigo-700 hover:bg-indigo-800 focus:bg-indigo-800 disabled:bg-neutral-100 disabled:text-neutral-400 px-5 py-3 rounded text-white ${disabled ? "cursor-not-allowed" : ""}`}
+      >
+        Add to cart
+      </Button>
+    </form>
+  );
+};
+
+export default ProductOptions;
diff --git a/components/organisms/ProductReview/OverallRating.tsx b/components/organisms/ProductReview/OverallRating.tsx
index 71bb4b66..575b3d10 100644
--- a/components/organisms/ProductReview/OverallRating.tsx
+++ b/components/organisms/ProductReview/OverallRating.tsx
@@ -1,6 +1,6 @@
 import { useMemo } from "react";
 import { Button } from "@headlessui/react";
-import Rating from "./Rating";
+import Rating from "$/molecules/Rating";
 import { Count } from "./useFetchReviewsStore";
 
 export interface OverallRatingProps {
@@ -68,7 +68,7 @@ const OverallRating: React.FC<OverallRatingProps> = ({
 }) => {
   const sortedCounts = useMemo(
     () => [...counts].sort((a, b) => b.rating - a.rating),
-    [counts]
+    [counts],
   );
   return (
     <div className={`flex flex-col gap-6 ${classes}`}>
@@ -81,7 +81,7 @@ const OverallRating: React.FC<OverallRatingProps> = ({
       <div className="flex flex-col gap-4 py-4">
         {sortedCounts.map((currentCount: Count) => {
           const percentageOfVotes = Math.round(
-            (currentCount.count / total) * 100
+            (currentCount.count / total) * 100,
           );
           return (
             <div
diff --git a/components/organisms/ProductReview/ProductReview.stories.tsx b/components/organisms/ProductReview/ProductReview.stories.tsx
index 845b7403..5e5fe17f 100644
--- a/components/organisms/ProductReview/ProductReview.stories.tsx
+++ b/components/organisms/ProductReview/ProductReview.stories.tsx
@@ -3,7 +3,7 @@ import { useState } from "react";
 import ProductReview from ".";
 
 const meta = {
-  title: "E-commerce/ProductReview",
+  title: "E-commerce/ProductDetails/ProductReview",
   component: ProductReview,
   tags: ["autodocs"],
   parameters: {
diff --git a/components/organisms/ProductReview/Reviewer.tsx b/components/organisms/ProductReview/Reviewer.tsx
index 2a530b51..34718966 100644
--- a/components/organisms/ProductReview/Reviewer.tsx
+++ b/components/organisms/ProductReview/Reviewer.tsx
@@ -1,6 +1,6 @@
 import React, { useState } from "react";
 import { Rating as RatingType } from "./fetchReviewFromAPI";
-import Rating from "./Rating";
+import Rating from "$/molecules/Rating";
 import ReviewDate from "./ReviewDate";
 import Image from "next/image";
 


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR adds a new ProductDetails feature, including several UI components and a data fetching logic to support product display in an e-commerce context. Key changes include the introduction of the ProductDetails component and its related subcomponents (carousel, detail, options, info), the implementation of a zustand store and debounce fetching hook, and minor updates to the OverallRating component.

### Reviewed Changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

<details>
<summary>Show a summary per file</summary>

| File                                                                                 | Description                                                               |
| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------- |
| components/organisms/ProductDetails/ProductDetails.stories.tsx                       | New Storybook file for visually testing the ProductDetails component.     |
| components/organisms/ProductDetails/useFetchProductDetailsStore.ts                    | Added a zustand store hook for fetching product details from the API.       |
| components/molecules/Rating/index.tsx                                                 | Introduced a new Rating component for displaying product ratings.         |
| components/organisms/ProductDetails/ProductInfo.tsx                                   | Added the ProductInfo component for rendering product information.        |
| components/organisms/ProductDetails/index.tsx                                         | Integrated ProductDetails subcomponents and overall layout structure.     |
| components/organisms/ProductDetails/ProductCarousel.tsx                               | New carousel component for product images with tab support.               |
| components/organisms/ProductDetails/useFetchProductDetails.ts                         | Added a debounce wrapper hook to fetch product details using the store hook.|
| components/organisms/ProductDetails/ProductDetail.tsx                                 | Implemented static product detail display with pricing and rating.          |
| components/organisms/ProductDetails/fetchProductDetailsAPI.ts                           | Implemented API call for product details retrieval.                       |
| components/organisms/ProductDetails/ProductOptions.tsx                                | Added a component for selecting product options (colors, sizes, quantity).  |
| components/organisms/ProductReview/OverallRating.tsx                                  | Updated OverallRating component with minor calculation adjustments.       |
</details>



<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**components/organisms/ProductDetails/ProductInfo.tsx:20**
* [nitpick] Using a randomly generated key (uuidv4()) for list items can hinder React's reconciliation process. Consider using a stable identifier from the data if available.
```
key={uuidv4()}
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-03-27T07:05:44Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR introduces a set of new components and hooks to display detailed product information, including fetching product data from an API, rendering a product carousel, and displaying product options such as sizes, colors, and quantities. The key changes include:
- Implementing a Zustand-based store and related hooks for fetching and managing product details.
- Introducing various presentation components (ProductDetails, ProductOptions, ProductInfo, etc.) with corresponding Storybook stories.
- Enhancing the UI with carousel functionality, rating updates, and skeleton loading states.

### Reviewed Changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 1 comment.

<details>
<summary>Show a summary per file</summary>

| File | Description |
| ---- | ----------- |
| components/organisms/ProductDetails/useFetchProductDetailsStore.ts | Introduces a Zustand store to manage product details and selection state. |
| components/organisms/ProductDetails/useFetchProductDetails.ts | Implements a hook to fetch product details and manage state updates. |
| components/organisms/ProductDetails/index.tsx | Integrates product detail components and fetch logic within the UI. |
| components/organisms/ProductDetails/fetchProductDetailsAPI.ts | Provides API integration to retrieve product details from a remote endpoint. |
| components/organisms/ProductDetails/ProductDetails.stories.tsx | Adds Storybook stories for the ProductDetails component. |
| components/organisms/ProductDetails/ProductDetails.skeleton.tsx | Provides a skeleton loader for the ProductDetails component. |
| components/molecules/Rating/index.tsx | Updates the Rating component to include optional linking and dynamic text. |
| components/molecules/ProductSize/index.tsx | Creates a component for displaying available product sizes with interactive selection. |
| components/molecules/ProductSize/ProductSize.stories.tsx | Adds Storybook stories for the ProductSize component. |
| components/molecules/ProductQuantity/index.tsx | Implements a product quantity selector with increment/decrement functionality. |
| components/molecules/ProductQuantity/ProductQuantity.stories.tsx | Provides Storybook stories for the ProductQuantity component. |
| components/molecules/ProductInfo/index.tsx | Implements a collapsible ProductInfo component for detailed product information. |
| components/molecules/ProductInfo/ProductInfo.stories.tsx | Adds Storybook stories for the ProductInfo component. |
| components/molecules/ProductDetail/index.tsx | Implements the ProductDetail component that displays detailed product info along with pricing and ratings. |
| components/molecules/ProductDetail/ProductDetail.stories.tsx | Provides Storybook stories for the ProductDetail component. |
| components/molecules/ProductColors/index.tsx | Implements a component for selecting product colors with visual indications for out-of-stock options. |
| components/molecules/ProductColors/ProductColors.stories.tsx | Adds Storybook stories for the ProductColors component. |
| components/molecules/ProductCarousel/index.tsx | Implements a carousel component for showcasing product images with color-based selection. |
| components/molecules/ProductCarousel/ProductCarousel.stories.tsx | Provides Storybook stories for the ProductCarousel component. |
| components/atoms/Price/index.tsx | Implements a Price component to display pricing and discount information. |
</details>



<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**components/organisms/ProductDetails/useFetchProductDetails.ts:10**
* The hook name 'useFetchProductDetail' is singular, which is inconsistent with the file name 'useFetchProductDetails.ts'. Consider renaming it to 'useFetchProductDetails' for clarity.
```
const useFetchProductDetail = (productId: string) => {
```
**components/molecules/ProductSize/index.tsx:43**
* There is a debug console.log statement in production code. It is recommended to remove or disable debug logging before final release.
```
console.log("keys:", Object.keys(unavailableSizes).includes(inventory.color),);
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-03-31T20:23:16Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR introduces new components and state management logic for the product details section of the E-commerce app, including product display, image carousel, option selectors, and quantity selection.  
- Added a zustand-based store for product details and fetching logic  
- Introduced UI components for product carousel, sizes, colors, quantity, details, and info  
- Updated Storybook stories for visual testing of these features

### Reviewed Changes

Copilot reviewed 24 out of 24 changed files in this pull request and generated 1 comment.

<details>
<summary>Show a summary per file</summary>

| File | Description |
| ---- | ----------- |
| components/organisms/ProductDetails/useProductStore.ts | New zustand store with fetching logic; includes helper functions for stock and unavailable options |
| components/organisms/ProductDetails/useFetchProductDetailsStore.ts | Alternative store implementation with similar product details fetching logic |
| components/organisms/ProductDetails/index.tsx | Main component assembling all product detail sub-components |
| components/organisms/ProductDetails/fetchProductDetailsAPI.ts | API call implementation to fetch product details |
| components/organisms/ProductDetails/ProductDetails.stories.tsx | Storybook entry for product details UI |
| components/organisms/ProductDetails/ProductDetails.skeleton.tsx | Skeleton loading state for product details |
| components/molecules/Rating/index.tsx | Updated rating component with conditional link rendering for reviews |
| components/molecules/ProductSize/index.tsx | UI for displaying product sizes with radio input for selection |
| components/molecules/ProductQuantity/index.tsx | Component handling quantity selection with tooltip for insufficient stock |
| components/molecules/ProductInfo/index.tsx | Component rendering product information in an accordion style |
| components/molecules/ProductDetail/index.tsx | Component that combines price, rating and detailed description |
| components/molecules/ProductColors/index.tsx | Color selector component for product options |
| components/molecules/ProductCarousel/index.tsx | Image carousel with color-based image selection |
| components/atoms/Price/index.tsx | Component for displaying product price and discount information |
</details>



<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**components/organisms/ProductDetails/useProductStore.ts:30**
* The parameter name 'colorSizeMao' appears to be a typo. Consider renaming it to 'colorSizeMap' for clarity.
```
const isColorUnavailable = ( color: string, colorSizeMao: Record<string, (string | number)[]>, productSizes: (string | number)[], ) => {
```
**components/molecules/ProductCarousel/index.tsx:21**
* findIndex returns -1 when no match is found; explicitly handle the -1 case (for example, default to 0) to ensure a valid selected index.
```
const selectImageBasedOnColor = images.findIndex((item) => item.color === color) ?? 0;
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-04-01T11:28:42Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR introduces new components and logic for displaying detailed product information in an e-commerce application. Key changes include:
- Addition of two Zustand stores (useProductStore and useFetchProductDetailsStore) to manage product state and fetching.
- Creation of several UI components such as ProductDetails, ProductCarousel, ProductDetail, ProductOptions, ProductInfo, ProductSize, ProductQuantity, and ProductColors, along with their Storybook integrations.
- Implementation of fetching logic, image carousel, and improved UI consistency (e.g., pricing, rating, and skeleton loaders).

### Reviewed Changes

Copilot reviewed 24 out of 24 changed files in this pull request and generated no comments.

<details>
<summary>Show a summary per file</summary>

| File | Description |
| ---- | ----------- |
| components/organisms/ProductDetails/useProductStore.ts | Implements a Zustand store to handle product details, inventory, and state updates. |
| components/organisms/ProductDetails/useFetchProductDetailsStore.ts | Provides an alternative store focused on fetching product details and selection options. |
| components/organisms/ProductDetails/index.tsx | Main ProductDetails component integrating carousel, details, options, and info. |
| components/organisms/ProductDetails/fetchProductDetailsAPI.ts | Implements the API call to fetch detailed product data. |
| components/organisms/ProductDetails/ProductDetails.stories.tsx | Storybook story for the ProductDetails component. |
| components/organisms/ProductDetails/ProductDetails.skeleton.tsx | Displays a skeleton loader for product details while loading data. |
| components/molecules/Rating/index.tsx | Updates the Rating display with conditional review links and messages. |
| components/molecules/ProductSize/index.tsx | Enables product size selection through interactive buttons, with conditional styling. |
| components/molecules/ProductSize/ProductSize.stories.tsx | Storybook story for the ProductSize component. |
| components/molecules/ProductQuantity/index.tsx | Implements quantity selection with support for insufficient stock warnings. |
| components/molecules/ProductQuantity/ProductQuantity.stories.tsx | Storybook story for the ProductQuantity component. |
| components/molecules/ProductInfo/index.tsx | Displays product information in collapsible sections using Disclosure. |
| components/molecules/ProductInfo/ProductInfo.stories.tsx | Storybook story for the ProductInfo component. |
| components/molecules/ProductDetail/index.tsx | Displays detailed product information including pricing and ratings. |
| components/molecules/ProductDetail/ProductDetail.stories.tsx | Storybook story for the ProductDetail component. |
| components/molecules/ProductColors/index.tsx | Provides a color selection interface with visual availability indications. |
| components/molecules/ProductColors/ProductColors.stories.tsx | Storybook story for the ProductColors component. |
| components/molecules/ProductCarousel/index.tsx | Implements an image carousel with tab navigation and dynamic selection based on color. |
| components/molecules/ProductCarousel/ProductCarousel.stories.tsx | Storybook story for the ProductCarousel component. |
| components/atoms/Price/index.tsx | Displays pricing information with discount styling when applicable. |
</details>



<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**components/molecules/ProductSize/index.tsx:44**
* The out-of-stock logic uses inventory.color to determine size availability, which may not correctly reflect whether a specific size is unavailable. Consider revising the condition to directly check the size's availability within the respective color mapping.
```
const isOutOfStock = outOfStock.includes(inventory.color) || (Object.keys(unavailableSizes).includes(inventory.color) && unavailableSizes[inventory.color]?.includes(size));
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-04-01T11:37:17Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR adds new components and stores to display detailed product information including fetching logic from an API, image carousel, product options (color, size, and quantity), and product info with expandable sections. Key changes include the implementation of zustand-based stores for managing product details, the creation of a ProductDetails component with associated skeleton loading UI, and the addition of storybook examples for each new component.

### Reviewed Changes

Copilot reviewed 24 out of 24 changed files in this pull request and generated no comments.

<details>
<summary>Show a summary per file</summary>

| File                                                              | Description                                               |
| ----------------------------------------------------------------- | --------------------------------------------------------- |
| components/organisms/ProductDetails/useProductStore.ts            | Implementation of the product store using zustand.        |
| components/organisms/ProductDetails/useFetchProductDetailsStore.ts  | Alternative store logic and API fetching implementation.  |
| components/organisms/ProductDetails/index.tsx                      | Main ProductDetails component wiring together UI pieces.  |
| components/organisms/ProductDetails/fetchProductDetailsAPI.ts       | API module for fetching product details data.             |
| Various story and skeleton files                                  | Storybook examples and skeleton components for loading UI.|
| Molecules (Rating, ProductSize, ProductQuantity, ProductInfo, etc.) | UI components for rating, size, quantity selection, etc.    |
| components/molecules/ProductCarousel                               | Image carousel component that supports color selection.   |
| components/atoms/Price/index.tsx                                   | Price display component with discount handling.           |
</details>



<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**components/organisms/ProductDetails/index.tsx:18**
* [nitpick] The alias 'selected' for the 'updateState' function is ambiguous. Consider renaming it to something more descriptive (e.g., 'updateProductState') to clearly indicate its purpose.
```
updateState: selected,
```
**components/molecules/Rating/index.tsx:66**
* When an href is provided, the review count text is rendered inside a div, which might reduce accessibility and the expectation of clickability. Consider using an anchor (<a>) element if the text is intended to be clickable.
```
<div className={`text-nowrap leading-[1.8] font-normal text-sm ${href ? "text-indigo-600 cursor-auto" : "text-neutral-600"}`}>
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-04-01T11:51:54Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR implements new components and stores for displaying detailed product information in an e-commerce application. Key changes include the addition of ProductDetails with fetching and carousel logic, enhancements to product options (size, color, quantity) using zustand stores, and associated Storybook stories for each component.

### Reviewed Changes

Copilot reviewed 24 out of 24 changed files in this pull request and generated 3 comments.

<details>
<summary>Show a summary per file</summary>

| File | Description |
| ---- | ----------- |
| components/organisms/ProductDetails/useProductStore.ts | Defines product store with fetching and state update logic. |
| components/organisms/ProductDetails/useFetchProductDetailsStore.ts | Provides an alternative zustand store for product detail fetching. |
| components/organisms/ProductDetails/index.tsx | Integrates product details, carousel, and options for product display. |
| components/organisms/ProductDetails/fetchProductDetailsAPI.ts | Implements API call to fetch product details. |
| components/molecules/Rating/index.tsx | Updates rating component with conditional linking for reviews info. |
| components/molecules/ProductSize/index.tsx | Renders available sizes with selection handling. |
| components/molecules/ProductQuantity/index.tsx | Provides UI for quantity selection with stock validation. |
| components/molecules/ProductInfo/index.tsx | Displays detailed product information with disclosure panels. |
| components/molecules/ProductDetail/index.tsx | Displays product name, pricing, rating and description. |
| components/molecules/ProductColors/index.tsx | Renders available product colors with selection and out-of-stock indication. |
| components/molecules/ProductCarousel/index.tsx | Provides image carousel integrated with zustand state. |
| components/atoms/Price/index.tsx | Displays pricing with discount formatting. |
</details>





state: COMMENTED
author_association: NONE
submitted_at: 2025-04-01T11:55:24Z

Review comments:
diff_hunk: @@ -0,0 +1,70 @@
+import axios from "axios";
+
+interface Category {
+  category_id: string;
+  name: string;
+  created_at: string;
+}
+
+interface Collection {
+  collection_id: string;
+  name: string;
+  description: string;
+  image_url: string;
+  created_at: string;
+}
+
+export interface Images {
+  color: string;
+  image_url: string;
+}
+
+export interface Info {
+  title: string;
+  description: string[];
+}
+
+interface Inventory {
+  sku: string;
+  color: string;
+  size: string;
+  list_price: number;
+  discount: number | null;
+  discount_percentage: number;
+  sale_price: number;
+  sold: number;
+  stock: number;
+}
+
+interface PriceRange {
+  highest: number;
+  lowest: number;
+}
+
+export interface ProductDetails {
+  product_id: string;
+  name: string;
+  description: string;
+  category: Category;
+  collection: Collection;
+  created_at: string;
+  colors: string[];
+  images: Images[];
+  info: Info[];
+  inventory: Inventory[];
+  priceRange: PriceRange;
+  rating: number;
+  reviews: number;
+  sizes: string[];
+  sold: number;
+}
+
+const fetchProductDetailsAPI = async (
+  productId: string,
+): Promise<ProductDetails> => {
+  const url = ` https://www.greatfrontend.com/api/projects/challenges/e-commerce/products/${productId}`;
path: components/organisms/ProductDetails/fetchProductDetailsAPI.ts
user: login: Copilot
body: The URL string contains a leading whitespace, which may result in an invalid endpoint. Removing the whitespace will ensure the correct API URL is used.
```suggestion
  const url = `https://www.greatfrontend.com/api/projects/challenges/e-commerce/products/${productId}`;
```
created_at: 2025-03-27T07:05:43Z
author_association: NONE
diff_hunk: @@ -0,0 +1,56 @@
+import "remixicon/fonts/remixicon.css";
+import { Button } from "@headlessui/react";
+
+const ProductOptions = () => {
+  return (
+    <div className="flex flex-col gap-4">
+      <div id="ColorOptions" className="flex flex-col gap-4">
+        <span className="text-sm text-neutral-500">Available Colors</span>
+        <div className="flex gap-2">
+          <div className="flex rounded-full bg-red-700 w-[40px] h-[40px] items-center justify-center outline outline-offset-1 outline-indigo-600">
+            <i className="ri-check-fill text-white w-15 h-15" />
+          </div>
+          <div className="flex rounded-full bg-pink-700 w-[40px] h-[40px] items-center justify-center"></div>
+        </div>
+      </div>
+      <div id="Sizes" className="flex flex-col gap-4">
+        <span className="text-sm text-neutral-500">Available Sizes</span>
+        <div className="flex flex-row gap-4 flex-wrap">
+          <Button className="w-16 flex justify-center items-center gap-1.5 px-5 py-3 rounded border border-solid border-indigo-600">
+            XS
+          </Button>
+          <Button className="w-16 flex justify-center items-center gap-1.5 px-5 py-3 rounded border border-solid border-neutral-200">
+            S
+          </Button>
+          <Button className="w-16 flex justify-center items-center gap-1.5 px-5 py-3 rounded border border-solid border-neutral-200">
+            M
+          </Button>
+          <Button className="w-16 flex justify-center items-center gap-1.5 px-5 py-3 rounded border border-solid border-neutral-200">
+            L
+          </Button>
+          <Button className="w-16 flex justify-center items-center gap-1.5 px-5 py-3 rounded border border-solid border-neutral-200">
+            XL
+          </Button>
+        </div>
+      </div>
+      <div id="Quanity" className="flex flex-col gap-4">
+        <span className="text-sm text-neutral-500">Quantity</span>
+        <div className="flex flex-row gap-4">
+          <div className="flex flex-row w-[125px] justify-center items-center gap-3 bg-neutral-50 p-.05 rounded-md border border-solid border-neutral-200">
path: components/organisms/ProductDetails/ProductOptions.tsx
user: login: Copilot
body: The Tailwind CSS class 'p-.05' appears to be an invalid padding value. Consider using a valid padding class like 'p-0.5' or another appropriate value.
```suggestion
          <div className="flex flex-row w-[125px] justify-center items-center gap-3 bg-neutral-50 p-0.5 rounded-md border border-solid border-neutral-200">
```
created_at: 2025-03-27T07:05:43Z
author_association: NONE
diff_hunk: @@ -0,0 +1,50 @@
+import React from "react";
+import "remixicon/fonts/remixicon.css";
+import { Button } from "@headlessui/react";
+import { ProductDetailsStore } from "$/organisms/ProductDetails/useProductStore";
+
+interface ProductQuantityProps {
+  initialQuantity: number;
+  selected: (state: Partial<ProductDetailsStore>) => void;
+  classes?: string;
+}
+
+const ProductQuantity: React.FC<ProductQuantityProps> = ({
+  initialQuantity,
+  selected,
+  classes = "",
+}) => {
+  const [quantity, setQuantity] = React.useState<number>(initialQuantity);
+
+  const handleQuantityChange = (value: number) => {
+    if (value >= 0) {
+      setQuantity(value);
+      selected({ selectedQuantity: value });
+    }
+  };
+
+  return (
+    <div id="Quanity" className={`${classes}`}>
path: components/molecules/ProductQuantity/index.tsx
user: login: Copilot
body: The id 'Quanity' appears to be misspelled; it should be 'Quantity'.
```suggestion
    <div id="Quantity" className={`${classes}`}>
```
created_at: 2025-03-31T20:23:16Z
author_association: NONE
diff_hunk: @@ -0,0 +1,137 @@
+import { create } from "zustand";
+import fetchProductDetailsFromAPI, {
+  ProductDetails,
+  Inventory,
+} from "./fetchProductDetailsAPI";
+import axios from "axios";
+
+export interface ProductDetailsStore {
+  product: ProductDetails | null;
+  loading: boolean;
+  error: Error | null;
+  selectedColor: string | null;
+  selectedSize: number | string | null;
+  selectedQuantity: number;
+  inventory: Inventory[] | null;
+  selectedInventory: Inventory | null;
+  unavailableSizes: Record<string, (string | number)[]>;
+  outOfStock: (string | number)[];
+  setColor: (color: string) => void;
+  setSize: (size: number | string) => void;
+  setQuantity: (quantity: number) => void;
+  fetchProductDetails: (productId: string) => Promise<void>;
+  getStock: (
+    color: string | null,
+    size: number | string | null,
+  ) => Inventory | null;
+  updateState: (state: Partial<ProductDetailsStore>) => void;
+}
+
+const isColorUnavailable = (
+  color: string,
+  colorSizeMao: Record<string, (string | number)[]>,
+  productSizes: (string | number)[],
+) => {
+  const outOfStockSizes = colorSizeMao[color] || [];
+  return productSizes.every((size) => outOfStockSizes.includes(size));
+};
+
+const getOutOfStockColorSizeMap = (inventory: Inventory[]) => {
+  return inventory.reduce((acc: Record<string, (string | number)[]>, item) => {
+    const { color, size, stock } = item;
+
+    if (stock === 0 && size) {
+      if (!acc[color]) {
+        acc[color] = [];
+      }
+
+      acc[color].push(size);
+    }
+    console.log({ acc });
path: components/organisms/ProductDetails/useProductStore.ts
user: login: Copilot
body: The debug console.log statement may be left over from development; remove it to avoid polluting production logs.
```suggestion
    // console.log({ acc });
```
created_at: 2025-04-01T11:28:41Z
author_association: NONE
diff_hunk: @@ -0,0 +1,90 @@
+import React, { useState, useEffect } from "react";
+import "remixicon/fonts/remixicon.css";
+import { v4 as uuidv4 } from "uuid";
+import { Input, Button } from "@headlessui/react";
+import { ProductDetailsStore } from "$/organisms/ProductDetails/useProductStore";
+
+interface ProductColorsProps {
+  name: string;
+  colors: string[];
+  selected: (state: Partial<ProductDetailsStore>) => void;
+  classes?: string;
+  selectedColor: string | null;
+  outOfStock: (string | number)[];
+}
+
+const ProductColors: React.FC<ProductColorsProps> = ({
+  name,
+  colors,
+  selected,
+  classes = "",
+  selectedColor,
+  outOfStock,
+}) => {
+  const [activeIndex, setActiveIndex] = useState<number>(() =>
+    colors.findIndex((color) => color === selectedColor),
+  );
+  const handleButtonClick = (color: string, index: number) => {
+    setActiveIndex(index);
+    selected({ selectedColor: color });
+  };
+
+  useEffect(() => {
+    const selectedColorIndex = colors.findIndex(
+      (color) => color === selectedColor,
+    );
+    setActiveIndex(selectedColorIndex);
+  }, [colors, selectedColor]);
+
+  return (
+    <>
+      {colors.length > 0 && (
+        <fieldset
+          id="ColorOptions"
+          className={`${classes} flex flex-col gap-4`}
+        >
+          <div>
+            <legend className="text-sm text-neutral-500">
+              Available Colors
+            </legend>
+          </div>
+          <div className="flex gap-5">
+            {colors.map((color, index) => {
+              const isOutOfStock = outOfStock.includes(color);
+
+              return (
+                <Button
+                  key={uuidv4()}
path: components/molecules/ProductColors/index.tsx
user: login: Copilot
body: Using uuidv4() to generate keys inside the map callback causes keys to change on every render. Consider using a stable key (e.g., the color value) to prevent unnecessary re-renders.
```suggestion
                  key={color}
```
created_at: 2025-04-01T11:55:23Z
author_association: NONE
diff_hunk: @@ -0,0 +1,80 @@
+import React, { useState } from "react";
+import { v4 as uuidv4 } from "uuid";
+import { Input, Button } from "@headlessui/react";
+import { ProductDetailsStore } from "$/organisms/ProductDetails/useProductStore";
+import { Inventory } from "$/organisms/ProductDetails/fetchProductDetailsAPI";
+
+interface ProductSizeProps {
+  name: string;
+  sizes: number[] | string[];
+  selected: (state: Partial<ProductDetailsStore>) => void;
+  classes?: string;
+  inventory: Inventory;
+  outOfStock: (string | number)[];
+  unavailableSizes: Record<string, (string | number)[]>;
+}
+
+const ProductSize: React.FC<ProductSizeProps> = ({
+  name,
+  sizes,
+  selected,
+  classes = "",
+  inventory,
+  outOfStock,
+  unavailableSizes,
+}) => {
+  const [activeIndex, setActiveIndex] = useState<number>(0);
+
+  const handleButtonClick = (size: number | string | null, index: number) => {
+    setActiveIndex(index);
+    selected({ selectedSize: size });
+  };
+
+  return (
+    <>
+      {sizes.length > 0 && (
+        <fieldset id="Sizes" className={`${classes} flex flex-col gap-4`}>
+          <div>
+            <legend className="text-sm text-neutral-500">
+              Available Sizes
+            </legend>
+          </div>
+          <div className="flex flex-row gap-4 flex-wrap">
+            {sizes?.map((size, index) => {
+              const isOutOfStock =
+                unavailableSizes[inventory.color]?.includes(size) ||
+                outOfStock.includes(size);
+
+              return (
+                <Button
+                  key={uuidv4()}
path: components/molecules/ProductSize/index.tsx
user: login: Copilot
body: Generating keys with uuidv4() within the render loop means keys will change on every render. Use a stable identifier (such as the size value or index if unique) as the key instead.
```suggestion
                  key={index}
```
created_at: 2025-04-01T11:55:23Z
author_association: NONE
diff_hunk: @@ -0,0 +1,70 @@
+import { useState } from "react";
+import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@headlessui/react";
+import { Images as ProductImages } from "$/organisms/ProductDetails/fetchProductDetailsAPI";
+import { v4 as uuidv4 } from "uuid";
+import { ProductDetailsStore } from "$/organisms/ProductDetails/useProductStore";
+
+interface ProductCarouselProps {
+  images: ProductImages[];
+  loading: boolean;
+  color: string | null;
+  selected: (state: Partial<ProductDetailsStore>) => void;
+}
+
+const ProductCarousel: React.FC<ProductCarouselProps> = ({
+  images,
+  loading,
+  color,
+  selected,
+}) => {
+  const selectImageBasedOnColor = images.findIndex(
+    (item) => item.color === color,
+  );
+  const validIndex =
+    selectImageBasedOnColor !== -1 ? selectImageBasedOnColor : 0;
+  const [selectedIndex, setSelectedIndex] = useState(validIndex);
+
+  if (loading) {
+    return <p>loading</p>;
+  }
+
+  return (
+    <TabGroup
+      className="flex flex-col gap-6"
+      selectedIndex={selectedIndex}
+      onChange={(index) => {
+        setSelectedIndex(index);
+        const selectedImage = images[index];
+        selected({ selectedColor: selectedImage.color });
+      }}
+    >
+      <TabPanels>
+        {images.map(({ image_url }) => (
+          <TabPanel key={uuidv4()} className="min-h-[500px] w-full">
+            <div
+              style={{ backgroundImage: `url(${image_url})` }}
+              className="block w-full min-h-[500px] bg-cover bg-center rounded-lg"
+            />
+          </TabPanel>
+        ))}
+      </TabPanels>
+      <div className="flex flex-1 py-5 overflow-y-auto p-1">
+        <TabList className="flex flex-nowrap gap-4 h-[120px]">
+          {images.map(({ image_url }) => (
+            <Tab
+              key={uuidv4()}
path: components/molecules/ProductCarousel/index.tsx
user: login: Copilot
body: Using uuidv4() as keys in the TabPanel mapping may lead to unnecessary re-mounting of components. Replace it with a stable key derived from the image data (for example, using the image URL or a combination of attributes).
```suggestion
              key={image_url}
```
created_at: 2025-04-01T11:55:24Z
author_association: NONE

PR comments:
user: login: vercel[bot]
created_at: 2025-03-27T07:04:44Z
body: [vc]: #/7aG6/Lhn+iqgFMZTRY2L92Yl0QoGw1rQjxiIY9R8BM=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJkZXNpZ24tc3lzdGVtLTIwMjUiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiZGVzaWduLXN5c3RlbS0yMDI1LWdpdC1mZWF0dXJlLWRzLTExMC1rYXJhYm9zb3VsZGllcnMtcHJvamVjdHMudmVyY2VsLmFwcCJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20va2FyYWJvc291bGRpZXJzLXByb2plY3RzL2Rlc2lnbi1zeXN0ZW0tMjAyNS85MnFqNEhqRkpHWkFNSng0U0dkYnI3b2JianpIIiwicHJldmlld1VybCI6ImRlc2lnbi1zeXN0ZW0tMjAyNS1naXQtZmVhdHVyZS1kcy0xMTAta2FyYWJvc291bGRpZXJzLXByb2plY3RzLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJyb290RGlyZWN0b3J5IjpudWxsfV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **design-system-2025** | ✅ Ready ([Inspect](https://vercel.com/karabosouldiers-projects/design-system-2025/92qj4HjFJGZAMJx4SGdbr7obbjzH)) | [Visit Preview](https://design-system-2025-git-feature-ds-110-karabosouldiers-projects.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/design-system-2025-git-feature-ds-110-karabosouldiers-projects.vercel.app?via=pr-comment-feedback-link) | Apr 1, 2025 11:54am |


author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-04-01T11:54:19Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=Karabo-Seipelo_design-system-2025&pullRequest=120) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1 New issue](https://sonarcloud.io/project/issues?id=Karabo-Seipelo_design-system-2025&pullRequest=120&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=Karabo-Seipelo_design-system-2025&pullRequest=120&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=Karabo-Seipelo_design-system-2025&pullRequest=120&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=Karabo-Seipelo_design-system-2025&pullRequest=120&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=Karabo-Seipelo_design-system-2025&pullRequest=120&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=Karabo-Seipelo_design-system-2025&pullRequest=120)


author_association: NONE