From d984fa23d5a8c84609cb18349a536d6d8f6205b0 Mon Sep 17 00:00:00 2001 From: Mike Velko <44818580+mikevelko@users.noreply.github.com> Date: Fri, 23 Aug 2024 18:23:06 +0200 Subject: [PATCH] Feature/apply promo (#121) * wip apply promo * apply promo * added promo on submit order * wip validate order items * wip * update cookies on client side * fix flow --------- Co-authored-by: maksim hodasevich --- package.json | 1 + pnpm-lock.yaml | 14 + proto | 2 +- src/actions/cart.tsx | 70 ++-- src/api/proto-http/common/index.ts | 24 +- src/api/proto-http/frontend/index.ts | 175 +++----- src/app/archive/page.tsx | 1 + src/app/cart/checkout/page.tsx | 87 ++-- src/app/cart/page.tsx | 14 +- src/app/invoices/crypto/[uuid]/page.tsx | 2 - src/app/page.tsx | 5 +- src/app/product/[...productParams]/page.tsx | 19 +- src/components/forms/AddToCartForm/index.tsx | 26 +- .../forms/NewOrderForm/PromoCode.tsx | 55 +++ src/components/forms/NewOrderForm/index.tsx | 382 +++++++++++------- src/components/forms/NewOrderForm/schema.ts | 44 +- src/components/forms/NewOrderForm/utils.tsx | 21 +- src/components/layouts/CoreLayout.tsx | 35 +- src/components/layouts/RootLayout.tsx | 9 +- src/components/sections/Cart/CartItemRow.tsx | 36 +- src/components/sections/Cart/CartPopup.tsx | 52 +-- .../sections/Cart/CartProductsList.tsx | 69 ---- .../HACK__UpdateCookieCart.tsx | 19 + .../sections/Cart/CartProductsList/index.tsx | 60 +++ .../sections/Cart/ProductAmountButtons.tsx | 22 +- .../sections/Cart/TotalPrice/index.tsx | 1 + src/components/ui/Button/styles.tsx | 2 +- .../ui/Form/fields/RadioGroupField/index.tsx | 9 +- src/constants.ts | 2 + src/lib/api.ts | 1 + src/lib/utils/cart.ts | 68 ++-- 31 files changed, 705 insertions(+), 622 deletions(-) create mode 100644 src/components/forms/NewOrderForm/PromoCode.tsx delete mode 100644 src/components/sections/Cart/CartProductsList.tsx create mode 100644 src/components/sections/Cart/CartProductsList/HACK__UpdateCookieCart.tsx create mode 100644 src/components/sections/Cart/CartProductsList/index.tsx diff --git a/package.json b/package.json index 8abcb36d..b94374e5 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "react-hook-form": "^7.52.0", "react-intersection-observer": "^9.13.0", "react-photo-view": "^1.2.4", + "sonner": "^1.5.0", "zod": "^3.23.8" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 92b17707..01c3f923 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,6 +68,9 @@ importers: react-photo-view: specifier: ^1.2.4 version: 1.2.4(react-dom@19.0.0-rc-6d3110b4d9-20240531(react@19.0.0-rc-6d3110b4d9-20240531))(react@19.0.0-rc-6d3110b4d9-20240531) + sonner: + specifier: ^1.5.0 + version: 1.5.0(react-dom@19.0.0-rc-6d3110b4d9-20240531(react@19.0.0-rc-6d3110b4d9-20240531))(react@19.0.0-rc-6d3110b4d9-20240531) zod: specifier: ^3.23.8 version: 3.23.8 @@ -5700,6 +5703,12 @@ packages: resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} engines: {node: '>=14.16'} + sonner@1.5.0: + resolution: {integrity: sha512-FBjhG/gnnbN6FY0jaNnqZOMmB73R+5IiyYAw8yBj7L54ER7HB3fOSE5OFiQiE2iXWxeXKvg6fIP4LtVppHEdJA==} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + source-map-js@1.2.0: resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} engines: {node: '>=0.10.0'} @@ -13531,6 +13540,11 @@ snapshots: slash@5.1.0: {} + sonner@1.5.0(react-dom@19.0.0-rc-6d3110b4d9-20240531(react@19.0.0-rc-6d3110b4d9-20240531))(react@19.0.0-rc-6d3110b4d9-20240531): + dependencies: + react: 19.0.0-rc-6d3110b4d9-20240531 + react-dom: 19.0.0-rc-6d3110b4d9-20240531(react@19.0.0-rc-6d3110b4d9-20240531) + source-map-js@1.2.0: {} source-map-support@0.5.21: diff --git a/proto b/proto index d90a83ff..44dc1f2d 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit d90a83ff5a1e175b66c3e71a33b45dd12da704eb +Subproject commit 44dc1f2d12439a9542dbf61b6c2994686dc6c078 diff --git a/src/actions/cart.tsx b/src/actions/cart.tsx index b75a7379..2b216b81 100644 --- a/src/actions/cart.tsx +++ b/src/actions/cart.tsx @@ -7,16 +7,14 @@ import { updateCookieCartProduct, } from "@/lib/utils/cart"; -export const GRBPWR_CART = "grbpwr-cart"; - export async function addCartProduct({ - slug, + id, size, - price, + quantity = 1, }: { - slug: string; + id: number; size: string; - price: number; + quantity?: number; }) { "use server"; @@ -24,56 +22,53 @@ export async function addCartProduct({ try { if (!cartData) { - createCookieCartProduct({ productSlug: slug, size, price }); + createCookieCartProduct({ id, size, quantity }); return; } - const productKey = getCartProductKey(slug, size); - const cartProduct = cartData.products[productKey]; - const newProduct = { - quantity: 0, - price: 0, - }; + const productKey = getCartProductKey(id, size); + const cartProductQuantity = cartData.products[productKey]; + let newProductQuantity = 0; - if (cartProduct) { - newProduct.quantity = cartProduct.quantity + 1; - newProduct.price = cartProduct.price + price; + if (cartProductQuantity > 0) { + newProductQuantity = cartProductQuantity + quantity; } else { - newProduct.quantity = 1; - newProduct.price = price; + newProductQuantity = quantity; } - updateCookieCartProduct({ productSlug: slug, size, data: newProduct }); + updateCookieCartProduct({ + id, + size, + quantity: newProductQuantity, + }); } catch (error) { console.log("failed to parse cart", error); } } export async function removeCartProduct({ - productSlug, + id, size, }: { - productSlug: string; + id: number; size: string; }) { "use server"; try { - removeCookieCartProduct(productSlug, size); + removeCookieCartProduct(id, size); } catch (error) { console.log("failed to parse cart", error); } } export async function changeCartProductQuantity({ - slug, + id, size, operation, - price, }: { - slug: string; - price: number; + id: number; size: string; operation: "increase" | "decrease"; }) { @@ -83,34 +78,29 @@ export async function changeCartProductQuantity({ try { if (!cartData) return; - const productKey = getCartProductKey(slug, size); - const cartProduct = cartData.products[productKey]; + const productKey = getCartProductKey(id, size); + const cartProductQuantity = cartData.products[productKey]; - if (operation === "decrease" && cartProduct.quantity === 1) { - removeCookieCartProduct(slug, size); + if (operation === "decrease" && cartProductQuantity === 1) { + removeCookieCartProduct(id, size); return; } - const newProduct = { - quantity: cartProduct.quantity, - price: cartProduct.price, - }; + let newProductQuantity = cartProductQuantity; if (operation === "decrease") { - newProduct.quantity = cartProduct.quantity - 1; - newProduct.price = cartProduct.price - price; + newProductQuantity = cartProductQuantity - 1; } if (operation === "increase") { - newProduct.quantity = cartProduct.quantity + 1; - newProduct.price = cartProduct.price + price; + newProductQuantity = cartProductQuantity + 1; } updateCookieCartProduct({ - productSlug: slug, + id, size, - data: newProduct, + quantity: newProductQuantity, }); } catch (error) { console.log("failed to parse cart", error); diff --git a/src/api/proto-http/common/index.ts b/src/api/proto-http/common/index.ts index f25dd4b9..9668b065 100644 --- a/src/api/proto-http/common/index.ts +++ b/src/api/proto-http/common/index.ts @@ -51,7 +51,7 @@ export type Archive = { // ArchiveBody represents the insertable fields of an archive. export type ArchiveBody = { heading: string | undefined; - description: string | undefined; + text: string | undefined; }; // ArchiveItemFull represents an item within an archive. @@ -65,7 +65,7 @@ export type ArchiveItemFull = { export type ArchiveItem = { media: MediaFull | undefined; url: string | undefined; - title: string | undefined; + name: string | undefined; }; // ArchiveNew represents a new archive with items for insertion. @@ -77,7 +77,7 @@ export type ArchiveNew = { export type ArchiveItemInsert = { mediaId: number | undefined; url: string | undefined; - title: string | undefined; + name: string | undefined; }; export type Address = { @@ -86,12 +86,12 @@ export type Address = { }; export type AddressInsert = { - street: string | undefined; - houseNumber: string | undefined; - apartmentNumber: string | undefined; - city: string | undefined; - state: string | undefined; country: string | undefined; + state: string | undefined; + city: string | undefined; + addressLineOne: string | undefined; + addressLineTwo: string | undefined; + company: string | undefined; postalCode: string | undefined; }; @@ -433,7 +433,7 @@ export type OrderNew = { shippingAddress: AddressInsert | undefined; billingAddress: AddressInsert | undefined; buyer: BuyerInsert | undefined; - paymentMethodId: number | undefined; + paymentMethod: PaymentMethodNameEnum | undefined; shipmentCarrierId: number | undefined; promoCode: string | undefined; }; @@ -474,8 +474,11 @@ export type OrderItem = { thumbnail: string | undefined; productName: string | undefined; productPrice: string | undefined; + productPriceWithSale: string | undefined; productSalePercentage: string | undefined; productBrand: string | undefined; + slug: string | undefined; + color: string | undefined; categoryId: number | undefined; sku: string | undefined; orderItem: OrderItemInsert | undefined; @@ -531,18 +534,19 @@ export type HeroItemInsert = { mediaId: number | undefined; exploreLink: string | undefined; exploreText: string | undefined; + isMain: boolean | undefined; }; export type HeroItem = { media: MediaFull | undefined; exploreLink: string | undefined; exploreText: string | undefined; + isMain: boolean | undefined; }; export type HeroFull = { id: number | undefined; createdAt: wellKnownTimestamp | undefined; - main: HeroItem | undefined; ads: HeroItem[] | undefined; productsFeatured: Product[] | undefined; }; diff --git a/src/api/proto-http/frontend/index.ts b/src/api/proto-http/frontend/index.ts index 42dafaf5..a9cf5095 100644 --- a/src/api/proto-http/frontend/index.ts +++ b/src/api/proto-http/frontend/index.ts @@ -14,7 +14,6 @@ export type GetHeroResponse = { export type common_HeroFull = { id: number | undefined; createdAt: wellKnownTimestamp | undefined; - main: common_HeroItem | undefined; ads: common_HeroItem[] | undefined; productsFeatured: common_Product[] | undefined; }; @@ -28,6 +27,7 @@ export type common_HeroItem = { media: common_MediaFull | undefined; exploreLink: string | undefined; exploreText: string | undefined; + isMain: boolean | undefined; }; export type common_MediaFull = { @@ -371,7 +371,7 @@ export type common_OrderNew = { shippingAddress: common_AddressInsert | undefined; billingAddress: common_AddressInsert | undefined; buyer: common_BuyerInsert | undefined; - paymentMethodId: number | undefined; + paymentMethod: common_PaymentMethodNameEnum | undefined; shipmentCarrierId: number | undefined; promoCode: string | undefined; }; @@ -383,12 +383,12 @@ export type common_OrderItemInsert = { }; export type common_AddressInsert = { - street: string | undefined; - houseNumber: string | undefined; - apartmentNumber: string | undefined; - city: string | undefined; - state: string | undefined; country: string | undefined; + state: string | undefined; + city: string | undefined; + addressLineOne: string | undefined; + addressLineTwo: string | undefined; + company: string | undefined; postalCode: string | undefined; }; @@ -401,28 +401,27 @@ export type common_BuyerInsert = { }; export type SubmitOrderResponse = { - order: common_Order | undefined; + orderUuid: string | undefined; + orderStatus: common_OrderStatusEnum | undefined; + expiredAt: wellKnownTimestamp | undefined; + payment: common_PaymentInsert | undefined; }; -export type common_Order = { - id: number | undefined; - uuid: string | undefined; - buyerId: number | undefined; - placed: wellKnownTimestamp | undefined; - modified: wellKnownTimestamp | undefined; - paymentId: number | undefined; - totalPrice: googletype_Decimal | undefined; - orderStatusId: number | undefined; - shipmentId: number | undefined; - promoId: number | undefined; +export type common_PaymentInsert = { + paymentMethod: common_PaymentMethodNameEnum | undefined; + transactionId: string | undefined; + transactionAmount: googletype_Decimal | undefined; + transactionAmountPaymentCurrency: googletype_Decimal | undefined; + payer: string | undefined; + payee: string | undefined; + isTransactionDone: boolean | undefined; }; -export type UpdateOrderShippingCarrierRequest = { +export type GetOrderByUUIDRequest = { orderUuid: string | undefined; - shippingCarrierId: number | undefined; }; -export type UpdateOrderShippingCarrierResponse = { +export type GetOrderByUUIDResponse = { order: common_OrderFull | undefined; }; @@ -437,14 +436,30 @@ export type common_OrderFull = { shipping: common_Address | undefined; }; +export type common_Order = { + id: number | undefined; + uuid: string | undefined; + buyerId: number | undefined; + placed: wellKnownTimestamp | undefined; + modified: wellKnownTimestamp | undefined; + paymentId: number | undefined; + totalPrice: googletype_Decimal | undefined; + orderStatusId: number | undefined; + shipmentId: number | undefined; + promoId: number | undefined; +}; + export type common_OrderItem = { id: number | undefined; orderId: number | undefined; thumbnail: string | undefined; productName: string | undefined; productPrice: string | undefined; + productPriceWithSale: string | undefined; productSalePercentage: string | undefined; productBrand: string | undefined; + slug: string | undefined; + color: string | undefined; categoryId: number | undefined; sku: string | undefined; orderItem: common_OrderItemInsert | undefined; @@ -458,16 +473,6 @@ export type common_Payment = { paymentInsert: common_PaymentInsert | undefined; }; -export type common_PaymentInsert = { - paymentMethod: common_PaymentMethodNameEnum | undefined; - transactionId: string | undefined; - transactionAmount: googletype_Decimal | undefined; - transactionAmountPaymentCurrency: googletype_Decimal | undefined; - payer: string | undefined; - payee: string | undefined; - isTransactionDone: boolean | undefined; -}; - // Shipment represents the shipment table export type common_Shipment = { id: number | undefined; @@ -507,21 +512,18 @@ export type common_Address = { addressInsert: common_AddressInsert | undefined; }; -export type GetOrderByUUIDRequest = { - orderUuid: string | undefined; -}; - -export type GetOrderByUUIDResponse = { - order: common_OrderFull | undefined; -}; - export type ValidateOrderItemsInsertRequest = { items: common_OrderItemInsert[] | undefined; + promoCode: string | undefined; + shipmentCarrierId: number | undefined; }; export type ValidateOrderItemsInsertResponse = { - items: common_OrderItemInsert[] | undefined; + validItems: common_OrderItem[] | undefined; + hasChanged: boolean | undefined; subtotal: googletype_Decimal | undefined; + totalSale: googletype_Decimal | undefined; + promo: common_PromoCodeInsert | undefined; }; export type ValidateOrderByUUIDRequest = { @@ -557,24 +559,6 @@ export type CheckCryptoPaymentResponse = { payment: common_Payment | undefined; }; -export type ApplyPromoCodeRequest = { - orderUuid: string | undefined; - promoCode: string | undefined; -}; - -export type ApplyPromoCodeResponse = { - order: common_OrderFull | undefined; -}; - -export type UpdateOrderItemsRequest = { - orderUuid: string | undefined; - items: common_OrderItemInsert[] | undefined; -}; - -export type UpdateOrderItemsResponse = { - order: common_OrderFull | undefined; -}; - export type SubscribeNewsletterRequest = { email: string | undefined; }; @@ -616,7 +600,7 @@ export type common_Archive = { // ArchiveBody represents the insertable fields of an archive. export type common_ArchiveBody = { heading: string | undefined; - description: string | undefined; + text: string | undefined; }; // ArchiveItemFull represents an item within an archive. @@ -630,7 +614,7 @@ export type common_ArchiveItemFull = { export type common_ArchiveItem = { media: common_MediaFull | undefined; url: string | undefined; - title: string | undefined; + name: string | undefined; }; export interface FrontendService { @@ -642,7 +626,6 @@ export interface FrontendService { GetProductsPaged(request: GetProductsPagedRequest): Promise; // Submit an order SubmitOrder(request: SubmitOrderRequest): Promise; - UpdateOrderShippingCarrier(request: UpdateOrderShippingCarrierRequest): Promise; // Retrieves an order by its ID GetOrderByUUID(request: GetOrderByUUIDRequest): Promise; ValidateOrderItemsInsert(request: ValidateOrderItemsInsertRequest): Promise; @@ -653,10 +636,6 @@ export interface FrontendService { CancelOrderInvoice(request: CancelOrderInvoiceRequest): Promise; // CheckCryptoPayment checks the crypto payment if it has been received and updates the order status if it has been received CheckCryptoPayment(request: CheckCryptoPaymentRequest): Promise; - // ApplyPromoCode applies promo code on selected order id - ApplyPromoCode(request: ApplyPromoCodeRequest): Promise; - // Update order items - UpdateOrderItems(request: UpdateOrderItemsRequest): Promise; // Subscribe to the newsletter SubscribeNewsletter(request: SubscribeNewsletterRequest): Promise; // Unsubscribe from the newsletter @@ -802,31 +781,11 @@ export function createFrontendServiceClient( method: "SubmitOrder", }) as Promise; }, - UpdateOrderShippingCarrier(request) { // eslint-disable-line @typescript-eslint/no-unused-vars - if (!request.orderUuid) { - throw new Error("missing required field request.order_uuid"); - } - const path = `api/frontend/order/${request.orderUuid}/update-shipping-carrier`; // eslint-disable-line quotes - const body = JSON.stringify(request); - const queryParams: string[] = []; - let uri = path; - if (queryParams.length > 0) { - uri += `?${queryParams.join("&")}` - } - return handler({ - path: uri, - method: "POST", - body, - }, { - service: "FrontendService", - method: "UpdateOrderShippingCarrier", - }) as Promise; - }, GetOrderByUUID(request) { // eslint-disable-line @typescript-eslint/no-unused-vars if (!request.orderUuid) { throw new Error("missing required field request.order_uuid"); } - const path = `api/frontend/orders/${request.orderUuid}`; // eslint-disable-line quotes + const path = `api/frontend/order/${request.orderUuid}`; // eslint-disable-line quotes const body = null; const queryParams: string[] = []; let uri = path; @@ -843,7 +802,7 @@ export function createFrontendServiceClient( }) as Promise; }, ValidateOrderItemsInsert(request) { // eslint-disable-line @typescript-eslint/no-unused-vars - const path = `api/admin/orders/validate-items`; // eslint-disable-line quotes + const path = `api/frontend/orders/validate-items`; // eslint-disable-line quotes const body = JSON.stringify(request); const queryParams: string[] = []; let uri = path; @@ -863,7 +822,7 @@ export function createFrontendServiceClient( if (!request.orderUuid) { throw new Error("missing required field request.order_uuid"); } - const path = `api/admin/orders/validate/${request.orderUuid}`; // eslint-disable-line quotes + const path = `api/frontend/orders/validate/${request.orderUuid}`; // eslint-disable-line quotes const body = JSON.stringify(request); const queryParams: string[] = []; let uri = path; @@ -927,7 +886,7 @@ export function createFrontendServiceClient( throw new Error("missing required field request.order_uuid"); } const path = `api/frontend/order/check/${request.orderUuid}`; // eslint-disable-line quotes - const body = JSON.stringify(request); + const body = null; const queryParams: string[] = []; let uri = path; if (queryParams.length > 0) { @@ -935,47 +894,13 @@ export function createFrontendServiceClient( } return handler({ path: uri, - method: "POST", + method: "GET", body, }, { service: "FrontendService", method: "CheckCryptoPayment", }) as Promise; }, - ApplyPromoCode(request) { // eslint-disable-line @typescript-eslint/no-unused-vars - const path = `api/frontend/order/promo/apply`; // eslint-disable-line quotes - const body = JSON.stringify(request); - const queryParams: string[] = []; - let uri = path; - if (queryParams.length > 0) { - uri += `?${queryParams.join("&")}` - } - return handler({ - path: uri, - method: "POST", - body, - }, { - service: "FrontendService", - method: "ApplyPromoCode", - }) as Promise; - }, - UpdateOrderItems(request) { // eslint-disable-line @typescript-eslint/no-unused-vars - const path = `api/frontend/order/update/items`; // eslint-disable-line quotes - const body = JSON.stringify(request); - const queryParams: string[] = []; - let uri = path; - if (queryParams.length > 0) { - uri += `?${queryParams.join("&")}` - } - return handler({ - path: uri, - method: "POST", - body, - }, { - service: "FrontendService", - method: "UpdateOrderItems", - }) as Promise; - }, SubscribeNewsletter(request) { // eslint-disable-line @typescript-eslint/no-unused-vars const path = `api/frontend/newsletter/subscribe`; // eslint-disable-line quotes const body = JSON.stringify(request); diff --git a/src/app/archive/page.tsx b/src/app/archive/page.tsx index a6813c9b..f9087c96 100644 --- a/src/app/archive/page.tsx +++ b/src/app/archive/page.tsx @@ -34,6 +34,7 @@ export default async function Page() { {a.archive?.archiveBody?.heading}
+ {/* @ts-ignore */}

{a.archive?.archiveBody?.description}

{a.archive?.createdAt}

diff --git a/src/app/cart/checkout/page.tsx b/src/app/cart/checkout/page.tsx index 5a2f39a8..f4cd8adb 100644 --- a/src/app/cart/checkout/page.tsx +++ b/src/app/cart/checkout/page.tsx @@ -1,45 +1,25 @@ +import { addCartProduct, clearCartProducts } from "@/actions/cart"; import { + common_OrderItem, common_OrderItemInsert, common_OrderNew, } from "@/api/proto-http/frontend"; import NewOrderForm from "@/components/forms/NewOrderForm"; -import { redirect } from "next/navigation"; import CoreLayout from "@/components/layouts/CoreLayout"; -import { - getCartProductSlugAndSizeFromKey, - getCookieCart, -} from "@/lib/utils/cart"; import { serviceClient } from "@/lib/api"; -import { clearCartProducts } from "@/actions/cart"; - -export default async function Page() { - const cartData = getCookieCart(); - const cartProducts = cartData?.products; - - if (!cartProducts || !Object.keys(cartProducts)) return redirect("/cart"); - - const orderItems = Object.entries(cartProducts).reduce( - (acc, [key, value]) => { - const slugAndSize = getCartProductSlugAndSizeFromKey(key); - - if (!slugAndSize) return acc; - - const [_, __, ___, id] = slugAndSize.slug - .replaceAll("/product/", "") - .split("/"); +import { getValidateOrderItemsInsertItems } from "@/lib/utils/cart"; +import { redirect } from "next/navigation"; - const item = { - productId: Number(id), - quantity: value.quantity, - sizeId: Number(slugAndSize.size), - }; +export default async function CheckoutPage() { + const items = getValidateOrderItemsInsertItems(); - acc.push(item); + if (items.length === 0) return null; - return acc; - }, - [] as common_OrderItemInsert[], - ); + const response = await serviceClient.ValidateOrderItemsInsert({ + items, + shipmentCarrierId: undefined, + promoCode: undefined, + }); async function submitNewOrder(newOrderData: common_OrderNew) { "use server"; @@ -49,9 +29,7 @@ export default async function Page() { order: newOrderData, }); - const { order } = submitOrderResponse; - - if (!order?.uuid) { + if (!submitOrderResponse?.orderUuid) { console.log("no data to create order invoice"); return { @@ -59,23 +37,16 @@ export default async function Page() { }; } - const getOrderInvoiceResponse = await serviceClient.GetOrderInvoice({ - orderUuid: order.uuid, - paymentMethod: "PAYMENT_METHOD_NAME_ENUM_USDT_SHASTA", - }); - console.log({ ok: true, - order, - getOrderInvoiceResponse, + order: submitOrderResponse, }); clearCartProducts(); return { ok: true, - order, - getOrderInvoiceResponse, + order: submitOrderResponse, }; } catch (error) { console.error("Error submitting new order:", error); @@ -85,9 +56,33 @@ export default async function Page() { } } + const updateCookieCart = async (validItems: common_OrderItem[]) => { + "use server"; + + clearCartProducts(); + + for (const p of validItems) { + if ( + p.orderItem?.productId && + p.orderItem.quantity && + p.orderItem.sizeId + ) { + addCartProduct({ + id: p.orderItem.productId, + size: p.orderItem.sizeId.toString(), + quantity: p.orderItem.quantity, + }); + } + } + }; + return ( - - + + ); } diff --git a/src/app/cart/page.tsx b/src/app/cart/page.tsx index c30bc7f1..949d827f 100644 --- a/src/app/cart/page.tsx +++ b/src/app/cart/page.tsx @@ -1,12 +1,12 @@ -import { Suspense } from "react"; -import CartProductsList from "@/components/sections/Cart/CartProductsList"; import CoreLayout from "@/components/layouts/CoreLayout"; -import { CartProductsSkeleton } from "@/components/ui/Skeleton"; +import CartProductsList from "@/components/sections/Cart/CartProductsList"; +import TotalPrice from "@/components/sections/Cart/TotalPrice"; import Button from "@/components/ui/Button"; import { ButtonStyle } from "@/components/ui/Button/styles"; -import Link from "next/link"; -import TotalPrice from "@/components/sections/Cart/TotalPrice"; +import { CartProductsSkeleton } from "@/components/ui/Skeleton"; import { getCookieCart } from "@/lib/utils/cart"; +import Link from "next/link"; +import { Suspense } from "react"; export const dynamic = "force-dynamic"; @@ -14,7 +14,7 @@ export default async function CartPage() { const cartItems = getCookieCart(); return ( - +
@@ -30,7 +30,7 @@ export default async function CartPage() {
{/*

total:

170$

*/} - + {/* */} {Object.keys(cartItems?.products || {}).length && (
- {baseCurrencyPrice && - product?.product?.slug && - product?.sizes?.length && ( - - )} + {product?.product?.id && product?.sizes?.length && ( + + )}
diff --git a/src/components/forms/AddToCartForm/index.tsx b/src/components/forms/AddToCartForm/index.tsx index fda7c52b..0930fd6c 100644 --- a/src/components/forms/AddToCartForm/index.tsx +++ b/src/components/forms/AddToCartForm/index.tsx @@ -1,31 +1,21 @@ "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 { FormContainer } from "@/components/ui/Form/FormContainer"; +import { zodResolver } from "@hookform/resolvers/zod"; import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { AddToCartData, addToCartSchema } from "./schema"; export default function AddToCartForm({ handleSubmit, sizes, - slug, - price, + id, }: { - handleSubmit: ({ - slug, - size, - price, - }: { - slug: string; - size: string; - price: number; - }) => Promise; - slug: string; + handleSubmit: ({ id, size }: { id: number; size: string }) => Promise; + id: number; sizes: common_ProductSize[]; - price: number; }) { const [loading, setLoadingStatus] = useState(false); const form = useForm({ @@ -37,7 +27,7 @@ export default function AddToCartForm({ setLoadingStatus(true); try { - await handleSubmit({ slug, size: data.size, price }); + await handleSubmit({ id, size: data.size }); } catch (error) { console.error(error); } finally { diff --git a/src/components/forms/NewOrderForm/PromoCode.tsx b/src/components/forms/NewOrderForm/PromoCode.tsx new file mode 100644 index 00000000..01b4b126 --- /dev/null +++ b/src/components/forms/NewOrderForm/PromoCode.tsx @@ -0,0 +1,55 @@ +"use client"; + +import type { ValidateOrderItemsInsertResponse } from "@/api/proto-http/frontend"; +import Button from "@/components/ui/Button"; +import { ButtonStyle } from "@/components/ui/Button/styles"; +import InputField from "@/components/ui/Form/fields/InputField"; +import { useState } from "react"; +import { useFormContext, type Control } from "react-hook-form"; + +type Props = { + loading: boolean; + control: Control; + validateItemsAndUpdateCookie: () => Promise; + freeShipmentCarrierId?: number; +}; + +export default function PromoCode({ + loading, + control, + validateItemsAndUpdateCookie, + freeShipmentCarrierId, +}: Props) { + const [promoLoading, setPromoLoading] = useState(false); + const { setValue } = useFormContext(); + + async function handleApplyPromoClick() { + setPromoLoading(true); + + const response = await validateItemsAndUpdateCookie(); + + if (response?.promo?.freeShipping) { + setValue("shipmentCarrierId", freeShipmentCarrierId + ""); + } + + setPromoLoading(false); + } + + return ( + <> + + + + ); +} diff --git a/src/components/forms/NewOrderForm/index.tsx b/src/components/forms/NewOrderForm/index.tsx index 1060604b..23aa4d94 100644 --- a/src/components/forms/NewOrderForm/index.tsx +++ b/src/components/forms/NewOrderForm/index.tsx @@ -5,225 +5,293 @@ import CheckboxField from "@/components/ui/Form/fields/CheckboxField"; import InputField from "@/components/ui/Form/fields/InputField"; import RadioGroupField from "@/components/ui/Form/fields/RadioGroupField"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import AddressFields from "./AddressFields"; import type { - common_OrderItemInsert, - common_OrderNew, common_Order, + common_OrderNew, + SubmitOrderResponse, + ValidateOrderItemsInsertResponse, } from "@/api/proto-http/frontend"; import InputMaskedField from "@/components/ui/Form/fields/InputMaskedField"; +import { useRouter } from "next/navigation"; +import PromoCode from "./PromoCode"; import { CheckoutData, checkoutSchema, defaultData } from "./schema"; import { mapFormFieldToOrderDataFormat } from "./utils"; -import { useRouter } from "next/navigation"; +import { useHeroContext } from "@/components/contexts/HeroContext"; +import { serviceClient } from "@/lib/api"; +import { toast } from "sonner"; export default function NewOrderForm({ - initialData, - orderItems, + order, submitNewOrder, + updateCookieCart, }: { - initialData?: CheckoutData; - orderItems: common_OrderItemInsert[]; + order?: ValidateOrderItemsInsertResponse; submitNewOrder: ( newOrderData: common_OrderNew, - ) => Promise<{ ok: boolean; order?: common_Order }>; + ) => Promise<{ ok: boolean; order?: SubmitOrderResponse }>; + updateCookieCart: (validItems: any[]) => void; }) { const [loading, setLoading] = useState(false); + const [orderData, setOrderData] = useState< + ValidateOrderItemsInsertResponse | undefined + >(order); + const { dictionary } = useHeroContext(); const router = useRouter(); + const defaultValues = { + ...defaultData, + promoCustomConditions: { + totalSale: order?.totalSale, + subtotal: order?.subtotal, + promo: order?.promo, + }, + }; const form = useForm({ resolver: zodResolver(checkoutSchema), - defaultValues: initialData || defaultData, + defaultValues, }); const billingAddressIsSameAsAddress = form.watch( "billingAddressIsSameAsAddress", ); const paymentMethod = form.watch("paymentMethod"); + const promoCode = form.watch("promoCode"); + const shipmentCarrierId = form.watch("shipmentCarrierId"); const onSubmit = async (data: CheckoutData) => { - const newOrderData = mapFormFieldToOrderDataFormat(data, orderItems); + const response = await validateItemsAndUpdateCookie(); + + const newOrderData = mapFormFieldToOrderDataFormat( + data, + response?.validItems?.map((i) => i.orderItem!) || [], + ); try { - const data = await submitNewOrder(newOrderData); + const newOrderResponse = await submitNewOrder(newOrderData); console.log("submit new order response", data); console.log("New order submitted successfully"); - router.replace(`/invoices/crypto/${data.order?.uuid}`); + router.replace(`/invoices/crypto/${newOrderResponse?.order?.orderUuid}`); } catch (error) { console.error("Error submitting new order:", error); } }; + const validateItemsAndUpdateCookie = async ( + customShipmentCarrierId?: string, + ) => { + const items = orderData?.validItems?.map((i) => ({ + productId: i.orderItem?.productId, + quantity: i.orderItem?.quantity, + sizeId: i.orderItem?.sizeId, + })); + + if (!items || items?.length === 0) return null; + + const response = await serviceClient.ValidateOrderItemsInsert({ + items, + promoCode, + shipmentCarrierId: parseInt(customShipmentCarrierId || shipmentCarrierId), + }); + + setOrderData(response); + + if (response.hasChanged && response.validItems) { + toast("something has changed"); + updateCookieCart(response.validItems); + } + + return response; + }; + return ( -
-

contact

- - -
- +
+

contact

+ - +
+ + +
-
-
-

shipping address

- -
+
+

shipping address

+ +
-
-

shipping method

- -
-
-

billing address

-

- Select the address that matches your card or payment method -

- - - {!billingAddressIsSameAsAddress && ( - +

shipping method

+ + ({ + label: c.shipmentCarrier?.carrier || "", + value: c.id + "" || "", + }))} + /> +
+
+

billing address

+

+ Select the address that matches your card or payment method +

+ - )} -
-
-

payment

- - - - {paymentMethod === "card" && ( - <> - - -
-
- -
-
- + )} +
+ +
+

payment

+ + ({ + label: p.name as string, + value: p.name as string, + })) || [] + } + /> + + {paymentMethod === "PAYMENT_METHOD_NAME_ENUM_CARD" && ( + <> + + +
+
+ +
+
+ +
-
- - )} -
+ + )} +
-
-

remember me

- +
+

remember me

+ +
+
+
+
+

Order summary

+ {/* ПОМЕНЯТЬ ВСЕ ЦЕН НА ЗНАЧЕНИЯ ФОРМЫ, тк иначе сложно обновлять значения независимо из разных мест */} + +
+
subtotal:
+
{orderData?.subtotal?.value}
+
+
+
shipping price:
+ {/* to-do pass shipping price */} +
+ {orderData?.promo?.freeShipping + ? 0 + : dictionary?.shipmentCarriers?.find( + (c) => c.id + "" === shipmentCarrierId, + )?.shipmentCarrier?.price?.value || 0} +
+
+ {!!orderData?.promo?.discount?.value && ( +
+
discount:
+
{orderData?.promo?.discount?.value}%
+
+ )} +
+
+
grand total:
+
{orderData?.totalSale?.value}
+
+
); diff --git a/src/components/forms/NewOrderForm/schema.ts b/src/components/forms/NewOrderForm/schema.ts index 8474c714..78a2c722 100644 --- a/src/components/forms/NewOrderForm/schema.ts +++ b/src/components/forms/NewOrderForm/schema.ts @@ -12,7 +12,7 @@ const addressFields = { postalCode: z.string().min(2), }; -export const checkoutSchema = z.object({ +const baseCheckoutSchema = z.object({ email: z.string().email(), phone: z.string().min(5), subscribe: z.boolean().optional(), @@ -20,25 +20,37 @@ export const checkoutSchema = z.object({ message: "You must accept the terms & conditions", }), ...addressFields, - shippingMethod: z.string().min(1), + shipmentCarrierId: z.string().min(1), + promoCode: z.string().optional(), billingAddressIsSameAsAddress: z.boolean(), billingAddress: z.object(addressFields).optional(), - paymentMethod: z.string(), - creditCard: z - .object({ + rememberMe: z.boolean().optional(), +}); + +export const checkoutSchema = z.discriminatedUnion("paymentMethod", [ + baseCheckoutSchema.extend({ + paymentMethod: z.literal("PAYMENT_METHOD_NAME_ENUM_ETH"), + }), + baseCheckoutSchema.extend({ + paymentMethod: z.literal("PAYMENT_METHOD_NAME_ENUM_USDT_TRON"), + }), + baseCheckoutSchema.extend({ + paymentMethod: z.literal("PAYMENT_METHOD_NAME_ENUM_USDT_SHASTA"), + }), + baseCheckoutSchema.extend({ + paymentMethod: z.literal("PAYMENT_METHOD_NAME_ENUM_CARD"), + creditCard: z.object({ // todo: add validation of the mask // reuse same mask constant for inout and for schema number: z.string().length(19), fullName: z.string().min(3), expirationDate: z.string().length(5), cvc: z.string().length(3), - }) - .optional(), - - rememberMe: z.boolean().optional(), -}); + }), + }), +]); export const defaultData: z.infer = { email: "", @@ -53,13 +65,19 @@ export const defaultData: z.infer = { additionalAddress: "", company: "", postalCode: "", - shippingMethod: "", + shipmentCarrierId: "", subscribe: false, billingAddressIsSameAsAddress: true, billingAddress: undefined, - paymentMethod: "card", - creditCard: undefined, + paymentMethod: "PAYMENT_METHOD_NAME_ENUM_CARD", + creditCard: { + number: "", + fullName: "", + expirationDate: "", + cvc: "", + }, rememberMe: false, // todo: groom the feature + promoCode: "", }; export type CheckoutData = z.infer; diff --git a/src/components/forms/NewOrderForm/utils.tsx b/src/components/forms/NewOrderForm/utils.tsx index beabbfce..aab00e40 100644 --- a/src/components/forms/NewOrderForm/utils.tsx +++ b/src/components/forms/NewOrderForm/utils.tsx @@ -11,9 +11,9 @@ export function mapFormFieldToOrderDataFormat( orderItems: common_OrderItemInsert[], ) { const shippingAddress: common_AddressInsert = { - street: data.address, - houseNumber: "1", // common_AddressInsert will be changed to just have full address - apartmentNumber: data.additionalAddress, + addressLineOne: data.address, + addressLineTwo: data.additionalAddress, + company: data.company, city: data.city, state: data.state, country: data.country, @@ -25,9 +25,9 @@ export function mapFormFieldToOrderDataFormat( ? shippingAddress : data.billingAddress ? { - street: data.billingAddress.address, - houseNumber: "1", // common_AddressInsert will be changed to just have full address - apartmentNumber: data.billingAddress.additionalAddress, + addressLineOne: data.billingAddress.address, + addressLineTwo: data.billingAddress.additionalAddress, + company: data.billingAddress.company, city: data.billingAddress.city, state: data.billingAddress.state, country: data.billingAddress.country, @@ -48,12 +48,9 @@ export function mapFormFieldToOrderDataFormat( shippingAddress, billingAddress, buyer, - // TO-DO map payment method and carrier id from dictionary - // paymentMethodId: mapPaymentMethod(data.paymentMethod), - // shipmentCarrierId: mapShipmentCarrierId(data.shippingMethod), - paymentMethodId: 1, - shipmentCarrierId: 1, - promoCode: undefined, // Add promo code if applicable + paymentMethod: data.paymentMethod, + shipmentCarrierId: parseInt(data.shipmentCarrierId), + promoCode: data.promoCode, }; return newOrderData; diff --git a/src/components/layouts/CoreLayout.tsx b/src/components/layouts/CoreLayout.tsx index 0a57b21a..0f41386a 100644 --- a/src/components/layouts/CoreLayout.tsx +++ b/src/components/layouts/CoreLayout.tsx @@ -1,20 +1,22 @@ import CartPopup from "@/components/sections/Cart/CartPopup"; +import CartProductsList from "@/components/sections/Cart/CartProductsList"; +import TotalPrice from "@/components/sections/Cart/TotalPrice"; import Footer from "@/components/sections/Footer"; import Header from "@/components/sections/Header"; import Button from "@/components/ui/Button"; import { ButtonStyle } from "@/components/ui/Button/styles"; +import { getCookieCart } from "@/lib/utils/cart"; import Link from "next/link"; import { Suspense } from "react"; -import CartProductsList from "@/components/sections/Cart/CartProductsList"; -import TotalPrice from "@/components/sections/Cart/TotalPrice"; -import { getCookieCart } from "@/lib/utils/cart"; export default function CoreLayout({ children, hideForm, + hidePopupCart, }: Readonly<{ children: React.ReactNode; hideForm?: boolean; + hidePopupCart?: boolean; }>) { const cartData = getCookieCart(); const hasCartProducts = @@ -49,20 +51,23 @@ export default function CoreLayout({ - -
-
- -
+ {hidePopupCart ? null : ( + +
+
+ +
- {/* when cursor is in gradient area-scroll doesnt work */} -
-
-
- -
-
+ {/* when cursor is in gradient area-scroll doesnt work */} +
+
+
+ {/* */} +
+
+ )}
+
+