diff --git a/src/actions/cart.tsx b/src/actions/cart.tsx index 169ef1e0..2828d5af 100644 --- a/src/actions/cart.tsx +++ b/src/actions/cart.tsx @@ -1,100 +1,75 @@ -import { createCartId, getCartCookie } from "@/lib/utils/cart"; -import { cookies } from "next/headers"; +import { + createCookieCartProduct, + getCookieCart, + updateCookieCartProduct, + removeCookieCartProduct, + changeCookieCartProductQuanity, + getCartProductKey, +} from "@/lib/utils/cart"; export const GRBPWR_CART = "grbpwr-cart"; -export async function addItemToCookie(slug: string, size?: string) { +export async function addCartProduct(slug: string, size: string) { "use server"; - const cartId = createCartId(slug, size); - - const cookieStore = cookies(); - - if (!cookieStore.has(GRBPWR_CART)) { - cookieStore.set( - GRBPWR_CART, - JSON.stringify({ - products: { - [cartId]: { - slug: slug.replace("/product", ""), - size: size, - quantity: 1, - }, - }, - }), - ); - - return; - } + const cartData = getCookieCart(); try { - const cart = getCartCookie(); + if (!cartData) { + createCookieCartProduct(slug, size); - const currentProduct = cart.products[cartId]; + return; + } + + const productKey = getCartProductKey(slug, size); + const cartProduct = cartData.products[productKey]; + let newProductQuanity; - let newCurrentProduct = currentProduct - ? { ...currentProduct, quantity: currentProduct.quantity + 1, size: size } - : { quantity: 1, slug: slug, size: size }; + if (cartProduct) { + newProductQuanity = cartProduct.quanity + 1; + } else { + newProductQuanity = 1; + } - cookieStore.set( - GRBPWR_CART, - JSON.stringify({ - products: { ...cart.products, [cartId]: newCurrentProduct }, - }), - ); + updateCookieCartProduct(slug, size, newProductQuanity); } catch (error) { console.log("failed to parse cart", error); } } -// todo: check -export async function removeItemFromCookie(slug: string, size?: string) { +export async function removeCartProduct(slug: string, size: string) { "use server"; - const cookieStore = cookies(); - - if (!cookieStore.has(GRBPWR_CART)) return; - - const cartId = createCartId(slug, size); try { - const cart = getCartCookie(); - cookieStore.set( - GRBPWR_CART, - JSON.stringify({ - products: { ...cart.products, [cartId]: undefined }, - }), - ); + removeCookieCartProduct(slug, size); } catch (error) { console.log("failed to parse cart", error); } } -export async function decreaseItemCountFromCookie(slug: string, size?: string) { +export async function changeCartProductQuanity({ + slug, + size, + operation, +}: { + slug: string; + size: string; + operation: "increase" | "decrease"; +}) { "use server"; - const cookieStore = cookies(); - - if (!cookieStore.has(GRBPWR_CART)) return; - - const cartId = createCartId(slug, size); try { - const cart = getCartCookie(); - if (cart.products[cartId].quantity > 1) { - cookieStore.set( - GRBPWR_CART, - JSON.stringify({ - products: { - ...cart.products, - [cartId]: { - ...cart.products[cartId], - quantity: cart.products[cartId].quantity - 1, - }, - }, - }), - ); - } else { - removeItemFromCookie(cartId); + const cartData = getCookieCart(); + if ( + operation === "decrease" && + cartData?.products[getCartProductKey(slug, size)]?.quanity === 1 + ) { + removeCookieCartProduct(slug, size); + + return; } + + changeCookieCartProductQuanity(slug, size, operation); } catch (error) { console.log("failed to parse cart", error); } diff --git a/src/app/cart/checkout/page.tsx b/src/app/cart/checkout/page.tsx index e42ad2e0..8936d019 100644 --- a/src/app/cart/checkout/page.tsx +++ b/src/app/cart/checkout/page.tsx @@ -1,10 +1,10 @@ import CoreLayout from "@/components/layouts/CoreLayout"; -import ConfirmOrderForm from "@/components/forms/CheckoutForm"; +import CheckoutForm from "@/components/forms/CheckoutForm"; export default async function Page() { return ( - + ); } diff --git a/src/app/cart/page.tsx b/src/app/cart/page.tsx index 6c614b3c..b9c10dba 100644 --- a/src/app/cart/page.tsx +++ b/src/app/cart/page.tsx @@ -5,7 +5,6 @@ import { CartProductsSkeleton } from "@/components/ui/Skeleton"; import Button from "@/components/ui/Button"; import { ButtonStyle } from "@/components/ui/Button/styles"; import Link from "next/link"; -import ConfirmOrderForm from "@/components/forms/CheckoutForm"; export const dynamic = "force-dynamic"; diff --git a/src/app/product/[...productParams]/page.tsx b/src/app/product/[...productParams]/page.tsx index 93088d1d..e4118176 100644 --- a/src/app/product/[...productParams]/page.tsx +++ b/src/app/product/[...productParams]/page.tsx @@ -1,12 +1,11 @@ -import { addItemToCookie } from "@/actions/cart"; import { MediaProvider } from "@/components/global/MediaProvider"; import { ProductMediaItem } from "@/components/global/MediaProvider/ProductMediaItem"; import CoreLayout from "@/components/layouts/CoreLayout"; -import AddToCartButton from "@/components/productPage/AddToCartButton"; import { CURRENCY_MAP, MAX_LIMIT } from "@/constants"; import { serviceClient } from "@/lib/api"; import { notFound } from "next/navigation"; -import { Suspense } from "react"; +import { addCartProduct } from "@/actions/cart"; +import AddToCartForm from "@/components/forms/AddToCartForm"; interface ProductPageProps { params: { @@ -51,6 +50,9 @@ export default async function ProductPage({ params }: ProductPageProps) { id: parseInt(id), }); + console.log("product22"); + console.log(product); + return (
@@ -74,21 +76,12 @@ export default async function ProductPage({ params }: ProductPageProps) { {product?.product?.productDisplay?.productBody?.description}
measurements
-
-
- {product?.sizes?.map((size) => ( -
{size.sizeId}
- ))} -
- - {/* TO-DO pass size from form */} - - -
+
diff --git a/src/components/cart/CartItemAmount.tsx b/src/components/cart/CartItemAmount.tsx deleted file mode 100644 index 8c1ccbc6..00000000 --- a/src/components/cart/CartItemAmount.tsx +++ /dev/null @@ -1,50 +0,0 @@ -"use client"; - -import Button from "@/components/ui/Button"; -import { ButtonStyle } from "@/components/ui/Button/styles"; - -type Props = { - decreaseItemAmount: (slug: string, size?: string) => Promise; - increaseItemAmount: (slug: string, size?: string) => Promise; - quantity: number; - slug: string; - size?: string; -}; - -export default function RemoveFromCart({ - decreaseItemAmount, - increaseItemAmount, - quantity, - slug, - size, -}: Props) { - async function handleIncreaseButtonClick( - event: React.MouseEvent, - ) { - event.preventDefault(); - await increaseItemAmount(slug, size); - } - - async function handleDecreaseButtonClick( - event: React.MouseEvent, - ) { - event.preventDefault(); - await decreaseItemAmount(slug, size); - } - - // todo: check if product is in stock - - return ( -
-
amount: {quantity}
-
- - -
-
- ); -} diff --git a/src/components/cart/CartItemRow.tsx b/src/components/cart/CartItemRow.tsx index cd1e136a..2c574973 100644 --- a/src/components/cart/CartItemRow.tsx +++ b/src/components/cart/CartItemRow.tsx @@ -1,21 +1,16 @@ -import { - addItemToCookie, - decreaseItemCountFromCookie, - removeItemFromCookie, -} from "@/actions/cart"; +import { removeCartProduct, changeCartProductQuanity } from "@/actions/cart"; import type { common_ProductFull } from "@/api/proto-http/frontend"; import ImageComponent from "../global/Image"; -import CartItemAmount from "./CartItemAmount"; -import RemoveFromCartButton from "./RemoveFromCartButton"; +import ProductAmountButtons from "./ProductAmountButtons"; export default function CartItemRow({ product, - quantity, + quanity, size, }: { product?: common_ProductFull; - quantity: number; - size?: string; + quanity: number; + size: string; }) { if (!product) return null; @@ -38,21 +33,16 @@ export default function CartItemRow({

{size}

-
- + +
quanity: {quanity}
-

BTC {p?.productDisplay?.productBody?.price?.value}

diff --git a/src/components/cart/CartProductsList.tsx b/src/components/cart/CartProductsList.tsx index 30d4c8f8..0f8a21ab 100644 --- a/src/components/cart/CartProductsList.tsx +++ b/src/components/cart/CartProductsList.tsx @@ -2,53 +2,64 @@ import CartItemRow from "@/components/cart/CartItemRow"; import Button from "@/components/ui/Button"; import { serviceClient } from "@/lib/api"; import { getProductPrice } from "@/lib/utils"; -import { getCartCookie } from "@/lib/utils/cart"; +import { + getCartProductSlugAndSizeFromKey, + getCookieCart, +} from "@/lib/utils/cart"; import Link from "next/link"; export default async function CartProductsList() { - const cart = getCartCookie(); - - if (!cart || !cart.products) return null; - - const cartItems = Object.values(cart.products) as { - // todo: add price to calculate total amount in any place in the app - quantity: number; - slug: string; - size?: string; - }[]; - - const productsPromises = cartItems.map(async (item) => { - const [gender, brand, name, id] = item.slug.split("/"); - - const response = await serviceClient.GetProduct({ - gender, - brand, - name, - id: parseInt(id), - }); - const product = response.product; - - return { - quantity: item.quantity, - slug: item.slug, - size: item.size, - product: product, - }; - }); - - const products = await Promise.all(productsPromises); - - // TOTAL PRICE - let totalPrice = 0; - products.forEach((x) => (totalPrice += getProductPrice(x.product))); + const cartData = getCookieCart(); + + if (!cartData || !cartData.products) return null; + + const productsPromises = Object.entries(cartData.products).map( + async ([productCartKey, { quanity }]) => { + const productSlugAndSize = + getCartProductSlugAndSizeFromKey(productCartKey); + + if (productSlugAndSize) { + const { slug, size } = productSlugAndSize; + const item = { + slug, + size, + quanity, + }; + + const [gender, brand, name, id] = slug + ?.replaceAll("/product/", "") + .split("/"); + + try { + const response = await serviceClient.GetProduct({ + gender, + brand, + name, + id: parseInt(id), + }); + + const product = response.product; + + return { + ...item, + product: product, + }; + } catch (error) { + console.log("failed to fetch cart product", error); + } + } + }, + ); + + const products = (await Promise.all(productsPromises)).filter(Boolean); return products.map((p) => ( diff --git a/src/components/cart/ProductAmountButtons.tsx b/src/components/cart/ProductAmountButtons.tsx new file mode 100644 index 00000000..3ae2137a --- /dev/null +++ b/src/components/cart/ProductAmountButtons.tsx @@ -0,0 +1,57 @@ +"use client"; + +import Button from "@/components/ui/Button"; +import { ButtonStyle } from "@/components/ui/Button/styles"; + +type Props = { + changeProductAmount: ({ + slug, + size, + operation, + }: { + slug: string; + size: string; + operation: "increase" | "decrease"; + }) => void; + removeProduct: (slug: string, size: string) => void; + slug: string; + size: string; +}; + +export default function ProductAmountButtons({ + changeProductAmount, + removeProduct, + slug, + size, +}: Props) { + // todo: check if product is in stock + + return ( +
+
+ + + +
+
+ ); +} diff --git a/src/components/cart/RemoveFromCartButton.tsx b/src/components/cart/RemoveFromCartButton.tsx deleted file mode 100644 index 4695aa15..00000000 --- a/src/components/cart/RemoveFromCartButton.tsx +++ /dev/null @@ -1,29 +0,0 @@ -"use client"; - -import Button from "@/components/ui/Button"; -import { ButtonStyle } from "@/components/ui/Button/styles"; - -type Props = { - removeItemFromCookie: (slug: string, size?: string) => Promise; - slug: string; - size?: string; -}; - -export default function RemoveFromCart({ - removeItemFromCookie, - slug, - size, -}: Props) { - async function handleButtonClick(event: React.MouseEvent) { - event.preventDefault(); - await removeItemFromCookie(slug, size); - } - - // todo: check if product is in stock - - return ( - - ); -} diff --git a/src/components/forms/AddToCartForm/index.tsx b/src/components/forms/AddToCartForm/index.tsx new file mode 100644 index 00000000..09248985 --- /dev/null +++ b/src/components/forms/AddToCartForm/index.tsx @@ -0,0 +1,61 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { FormContainer } from "@/components/ui/Form/FormContainer"; +import { common_ProductSize } from "@/api/proto-http/frontend"; +import SelectField from "@/components/ui/Form/fields/SelectField"; +import { addToCartSchema, AddToCartData } from "./schema"; +import { useState } from "react"; + +export default function AddToCartForm({ + handleSubmit, + sizes, + slug, +}: { + handleSubmit: (slug: string, size: string) => Promise; + slug: string; + sizes: common_ProductSize[]; +}) { + const [loading, setLoadingStatus] = useState(false); + const form = useForm({ + resolver: zodResolver(addToCartSchema), + }); + + const onSubmit = async (data: AddToCartData) => { + if (loading) return; + + setLoadingStatus(true); + try { + await handleSubmit(slug, data.size); + } catch (error) { + console.error(error); + } finally { + setLoadingStatus(false); + } + }; + + return ( + + ({ + label: size.sizeId + "", + value: size.sizeId + "", + }))} + /> + + ); +} diff --git a/src/components/forms/AddToCartForm/schema.ts b/src/components/forms/AddToCartForm/schema.ts new file mode 100644 index 00000000..5a62d31a --- /dev/null +++ b/src/components/forms/AddToCartForm/schema.ts @@ -0,0 +1,8 @@ +import { z } from "zod"; + +export const addToCartSchema = z.object({ + size: z.string(), + color: z.string().optional(), +}); + +export type AddToCartData = z.infer; diff --git a/src/components/productPage/AddToCartButton.tsx b/src/components/productPage/AddToCartButton.tsx deleted file mode 100644 index 577e3341..00000000 --- a/src/components/productPage/AddToCartButton.tsx +++ /dev/null @@ -1,29 +0,0 @@ -"use client"; - -import Button from "@/components/ui/Button"; -import { ButtonStyle } from "@/components/ui/Button/styles"; - -type Props = { - addItemToCookie: (slug: string, size: string) => Promise; - slug: string; - size: string; -}; - -export default function AddToCartButton({ - addItemToCookie, - slug, - size, -}: Props) { - async function handleButtonClick(event: React.MouseEvent) { - event.preventDefault(); - await addItemToCookie(slug, size); - } - - // todo: check if product is in stock - - return ( - - ); -} diff --git a/src/components/productPage/ProductItem.tsx b/src/components/sections/ProductsGridSection/ProductItem.tsx similarity index 100% rename from src/components/productPage/ProductItem.tsx rename to src/components/sections/ProductsGridSection/ProductItem.tsx diff --git a/src/components/sections/ProductsGridSection.tsx b/src/components/sections/ProductsGridSection/index.tsx similarity index 93% rename from src/components/sections/ProductsGridSection.tsx rename to src/components/sections/ProductsGridSection/index.tsx index 494fe3b8..3bf78f09 100644 --- a/src/components/sections/ProductsGridSection.tsx +++ b/src/components/sections/ProductsGridSection/index.tsx @@ -1,6 +1,6 @@ import type { common_Product } from "@/api/proto-http/frontend"; // import { shouldInsertEmpty } from "@/lib/utils"; -import ProductItem from "@/components/productPage/ProductItem"; +import ProductItem from "./ProductItem"; export default function ProductsGridSection({ products, diff --git a/src/components/ui/Form/fields/SelectField/index.tsx b/src/components/ui/Form/fields/SelectField/index.tsx index e17d0bbb..140df098 100644 --- a/src/components/ui/Form/fields/SelectField/index.tsx +++ b/src/components/ui/Form/fields/SelectField/index.tsx @@ -21,6 +21,7 @@ export default function SelectField({ items, name, label, + ...props }: Props) { return ( ( {label} - )} diff --git a/src/components/ui/Select/index.tsx b/src/components/ui/Select/index.tsx index 05eccc0e..2c4b2077 100644 --- a/src/components/ui/Select/index.tsx +++ b/src/components/ui/Select/index.tsx @@ -13,7 +13,7 @@ export default function SelectComponent({ }) { return ( - arrow down + arrow down {items.map((item) => ( diff --git a/src/lib/utils/cart.ts b/src/lib/utils/cart.ts index 9450229d..5c400a97 100644 --- a/src/lib/utils/cart.ts +++ b/src/lib/utils/cart.ts @@ -2,7 +2,22 @@ import { GRBPWR_CART } from "@/actions/cart"; import type { RequestCookie } from "next/dist/compiled/@edge-runtime/cookies"; import { cookies } from "next/headers"; -export function getCartCookie() { +type CookieCartProductData = { quanity: number }; +type CookieCartProduct = Record; + +export function getCartProductKey(slug: string, size: string) { + return `${slug}-${size}`; +} + +export function getCartProductSlugAndSizeFromKey(key: string) { + const [slug, size] = key.split("-"); + + if (!slug || !size) return null; + + return { slug, size }; +} + +export function getCookieCart(): { products: CookieCartProduct } | null { const cookieStore = cookies(); if (!cookieStore.has(GRBPWR_CART)) return null; @@ -20,9 +35,92 @@ export function getCartCookie() { return null; } -export function createCartId(slug: string, size?: string): string { - if (!size) { - return slug; +export function createCookieCartProduct(productSlug: string, size: string) { + const cookieStore = cookies(); + + cookieStore.set( + GRBPWR_CART, + JSON.stringify({ + products: { + [getCartProductKey(productSlug, size)]: { + quanity: 1, + }, + }, + }), + ); +} + +export function updateCookieCartProduct( + productSlug: string, + size: string, + quanity: number, +) { + const cartData = getCookieCart(); + const cookieStore = cookies(); + + const productKey = getCartProductKey(productSlug, size); + + cookieStore.set( + GRBPWR_CART, + JSON.stringify({ + ...cartData, + products: { + ...cartData?.products, + [productKey]: { + ...cartData?.products[productKey], + quanity, + }, + }, + }), + ); +} + +export function removeCookieCartProduct(productSlug: string, size: string) { + const cartData = getCookieCart(); + const cookieStore = cookies(); + + cookieStore.set( + GRBPWR_CART, + JSON.stringify({ + ...cartData, + products: { + ...cartData?.products, + [getCartProductKey(productSlug, size)]: undefined, + }, + }), + ); +} + +export function changeCookieCartProductQuanity( + productSlug: string, + size: string, + operation: "increase" | "decrease", +) { + const cartData = getCookieCart(); + const cookieStore = cookies(); + + let operationValue = 0; + if (operation === "increase") { + operationValue = 1; } - return `${slug}_${size}`; + if (operation === "decrease") { + operationValue = -1; + } + + const productKey = getCartProductKey(productSlug, size); + const initialQuanity = cartData?.products[productKey].quanity || 0; + + cookieStore.set( + GRBPWR_CART, + JSON.stringify({ + ...cartData, + products: { + ...cartData?.products, + [productKey]: { + ...cartData?.products[productKey], + quanity: initialQuanity + operationValue, + }, + }, + }), + ); }