From cc3982288ad3bb80340d794c7e98714311a23748 Mon Sep 17 00:00:00 2001 From: Chloe Date: Wed, 19 Jun 2024 17:13:29 +0700 Subject: [PATCH] fix: add on products on cart Signed-off-by: Chloe --- components/cart/actions.ts | 12 ++-- components/cart/add-to-cart.tsx | 17 ++++-- components/cart/delete-item-button.tsx | 5 +- components/cart/edit-item-quantity-button.tsx | 10 +++- components/cart/modal.tsx | 6 +- lib/constants.ts | 5 ++ lib/shopify/fragments/cart.ts | 21 ++++++- lib/shopify/fragments/product.ts | 6 ++ lib/shopify/index.ts | 56 +++++++++++++------ lib/shopify/types.ts | 26 ++++++++- 10 files changed, 128 insertions(+), 36 deletions(-) diff --git a/components/cart/actions.ts b/components/cart/actions.ts index d52bc6b08..6bc81e3b6 100644 --- a/components/cart/actions.ts +++ b/components/cart/actions.ts @@ -12,7 +12,10 @@ import { import { revalidateTag } from 'next/cache'; import { cookies } from 'next/headers'; -export async function addItem(prevState: any, selectedVariantIds: Array) { +export async function addItem( + prevState: any, + selectedVariantIds: Array<{ merchandiseId: string; quantity: number }> +) { let cartId = cookies().get('cartId')?.value; let cart; @@ -31,10 +34,8 @@ export async function addItem(prevState: any, selectedVariantIds: Array) } try { - await addToCart( - cartId, - selectedVariantIds.map((variantId) => ({ merchandiseId: variantId, quantity: 1 })) - ); + const cart = await addToCart(cartId, selectedVariantIds); + console.log({ cartLines: cart.lines }); revalidateTag(TAGS.cart); } catch (e) { return 'Error adding item to cart'; @@ -65,7 +66,6 @@ export async function setMetafields( revalidateTag(TAGS.cart); } catch (e) { - console.log(e); return 'Error set cart attributes'; } } diff --git a/components/cart/add-to-cart.tsx b/components/cart/add-to-cart.tsx index 22e1dde50..603f785da 100644 --- a/components/cart/add-to-cart.tsx +++ b/components/cart/add-to-cart.tsx @@ -80,11 +80,20 @@ export function AddToCart({ const coreVariantId = searchParams.get(CORE_VARIANT_ID_KEY); // remove special core-waiver value as it is not a valid variant - const selectedVariantIds = [coreVariantId, selectedVariantId] - .filter(Boolean) - .filter((value) => value !== CORE_WAIVER) as string[]; + const addingVariants = ( + [coreVariantId, selectedVariantId] + .filter(Boolean) + .filter((value) => value !== CORE_WAIVER) as string[] + ).map((id) => ({ merchandiseId: id, quantity: 1 })); - const actionWithVariant = formAction.bind(null, selectedVariantIds); + if (variant?.addOnProduct) { + addingVariants.push({ + merchandiseId: variant.addOnProduct.id, + quantity: variant.addOnProduct.quantity + }); + } + + const actionWithVariant = formAction.bind(null, addingVariants); return (
diff --git a/components/cart/delete-item-button.tsx b/components/cart/delete-item-button.tsx index 242ecae9b..1adf874db 100644 --- a/components/cart/delete-item-button.tsx +++ b/components/cart/delete-item-button.tsx @@ -36,10 +36,11 @@ function SubmitButton() { export function DeleteItemButton({ item }: { item: CartItem }) { const [message, formAction] = useFormState(removeItem, null); - const { id: itemId, coreCharge } = item; + const { id: itemId, coreCharge, addOnProduct } = item; const actionWithVariant = formAction.bind(null, [ itemId, - ...(coreCharge?.id ? [coreCharge.id] : []) + ...(coreCharge?.id ? [coreCharge.id] : []), + ...(addOnProduct?.id ? [addOnProduct.id] : []) ]); return ( diff --git a/components/cart/edit-item-quantity-button.tsx b/components/cart/edit-item-quantity-button.tsx index de0745c4b..8dae12ad2 100644 --- a/components/cart/edit-item-quantity-button.tsx +++ b/components/cart/edit-item-quantity-button.tsx @@ -51,11 +51,19 @@ export function EditItemQuantityButton({ item, type }: { item: CartItem; type: ' if (item.coreCharge) { payload.push({ lineId: item.coreCharge.id, - variantId: item.coreCharge.id, + variantId: item.coreCharge.merchandise.id, quantity }); } + if (item.addOnProduct) { + payload.push({ + lineId: item.addOnProduct.id, + variantId: item.addOnProduct.merchandise.id, + quantity: quantity * item.addOnProduct.quantity + }); + } + const actionWithVariant = formAction.bind(null, payload); return ( diff --git a/components/cart/modal.tsx b/components/cart/modal.tsx index 2a709cb2e..f67d70129 100644 --- a/components/cart/modal.tsx +++ b/components/cart/modal.tsx @@ -21,7 +21,11 @@ export default function CartModal({ cart }: { cart: Cart | undefined }) { const openCart = () => setIsOpen(true); const closeCart = () => setIsOpen(false); const { control, handleSubmit } = useForm({ - resolver: zodResolver(vehicleFormSchema) + resolver: zodResolver(vehicleFormSchema), + defaultValues: { + customer_vin: cart?.attributes.find((a) => a.key === 'customer_vin')?.value || '', + customer_mileage: cart?.attributes.find((a) => a.key === 'customer_mileage')?.value || '' + } }); const [loading, setLoading] = useState(false); diff --git a/lib/constants.ts b/lib/constants.ts index 4f1f8ee3a..eeaf84cfb 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -53,3 +53,8 @@ export const CONDITIONS = { }; export const DELIVERY_OPTION_KEY = 'delivery'; + +export const ADD_ON_PRODUCT_TYPES = { + addOn: 'Add On', + coreCharge: 'Core Charge' +}; diff --git a/lib/shopify/fragments/cart.ts b/lib/shopify/fragments/cart.ts index 40d1beb77..66df6eaa8 100644 --- a/lib/shopify/fragments/cart.ts +++ b/lib/shopify/fragments/cart.ts @@ -1,9 +1,13 @@ -import productFragment from './product'; +import imageFragment from './image'; const cartFragment = /* GraphQL */ ` fragment cart on Cart { id checkoutUrl + attributes { + key + value + } cost { subtotalAmount { amount @@ -38,11 +42,22 @@ const cartFragment = /* GraphQL */ ` value } product { - ...product + featuredImage { + ...image + } + handle + title + productType } coreVariantId: metafield(key: "coreVariant", namespace: "custom") { value } + addOnQuantity: metafield(namespace: "custom", key: "add_on_quantity") { + value + } + addOnProductId: metafield(namespace: "custom", key: "add_on") { + value + } } } } @@ -50,7 +65,7 @@ const cartFragment = /* GraphQL */ ` } totalQuantity } - ${productFragment} + ${imageFragment} `; export default cartFragment; diff --git a/lib/shopify/fragments/product.ts b/lib/shopify/fragments/product.ts index 840843ddf..f5fd32e0a 100644 --- a/lib/shopify/fragments/product.ts +++ b/lib/shopify/fragments/product.ts @@ -64,6 +64,12 @@ const productFragment = /* GraphQL */ ` condition: metafield(namespace: "custom", key: "condition") { value } + addOnQuantity: metafield(namespace: "custom", key: "add_on_quantity") { + value + } + addOnProductId: metafield(namespace: "custom", key: "add_on") { + value + } } } } diff --git a/lib/shopify/index.ts b/lib/shopify/index.ts index 7c02126c1..ad869c077 100644 --- a/lib/shopify/index.ts +++ b/lib/shopify/index.ts @@ -1,4 +1,5 @@ import { + ADD_ON_PRODUCT_TYPES, AVAILABILITY_FILTER_ID, HIDDEN_PRODUCT_TAG, MAKE_FILTER_ID, @@ -40,7 +41,6 @@ import { import { Cart, CartAttributeInput, - CartItem, Collection, Connection, Filter, @@ -161,7 +161,7 @@ const reshapeCart = (cart: ShopifyCart): Cart => { ...lineItem, merchandise: { ...lineItem.merchandise, - product: reshapeProduct(lineItem.merchandise.product) + product: lineItem.merchandise.product } })) }; @@ -277,14 +277,22 @@ const reshapeImages = (images: Connection, productTitle: string) => { }; const reshapeVariants = (variants: ShopifyProductVariant[]): ProductVariant[] => { - return variants.map((variant) => ({ + return variants.map(({ addOnProductId, addOnQuantity, ...variant }) => ({ ...variant, waiverAvailable: parseMetaFieldValue(variant.waiverAvailable), coreVariantId: variant.coreVariantId?.value || null, coreCharge: parseMetaFieldValue(variant.coreCharge), mileage: variant.mileage?.value ?? null, estimatedDelivery: variant.estimatedDelivery?.value || null, - condition: variant.condition?.value || null + condition: variant.condition?.value || null, + ...(addOnProductId + ? { + addOnProduct: { + id: addOnProductId.value, + quantity: addOnQuantity?.value ? Number(addOnQuantity.value) : 1 + } + } + : {}) })); }; @@ -410,20 +418,34 @@ export async function getCart(cartId: string): Promise { const cart = reshapeCart(res.body.data.cart); // attach core charge as an additional attribute of a cart line, and remove the core charge line from cart - const extendedCartLines = cart?.lines.reduce((lines, item) => { - const coreVariantId = item.merchandise.coreVariantId?.value; - if (coreVariantId) { - const relatedCoreCharge = cart.lines.find((line) => line.merchandise.id === coreVariantId); - return lines.concat([ - { - ...item, - coreCharge: relatedCoreCharge - } - ]); - } + const extendedCartLines = cart?.lines + .map((item) => { + const coreVariantId = item.merchandise.coreVariantId?.value; + const addOnProductId = item.merchandise.addOnProductId; + const _item = { ...item }; - return lines; - }, [] as CartItem[]); + if (coreVariantId) { + const relatedCoreCharge = cart.lines.find((line) => line.merchandise.id === coreVariantId); + _item.coreCharge = relatedCoreCharge; + } + + if (addOnProductId) { + const relatedAddOnProduct = cart.lines.find( + (line) => line.merchandise.id === addOnProductId.value + ); + _item.addOnProduct = relatedAddOnProduct + ? { + ...relatedAddOnProduct, + quantity: item.merchandise.addOnQuantity + ? Number(item.merchandise.addOnQuantity.value) + : 1 + } + : undefined; + } + return _item; + }) + // core charge shouldn't present as a dedicated product as it's tightly coupled with the product + .filter((item) => item.merchandise.product.productType !== ADD_ON_PRODUCT_TYPES.coreCharge); const totalQuantity = extendedCartLines.reduce((sum, line) => sum + line.quantity, 0); diff --git a/lib/shopify/types.ts b/lib/shopify/types.ts index a65eb7692..cd20d4dd4 100644 --- a/lib/shopify/types.ts +++ b/lib/shopify/types.ts @@ -25,10 +25,19 @@ export type CartItem = { name: string; value: string; }[]; - product: Product; + product: { + id: string; + handle: string; + title: string; + featuredImage: Image; + productType: string; + }; coreVariantId: { value: string } | null; + addOnQuantity: { value: string } | null; + addOnProductId: { value: string } | null; }; coreCharge?: CartItem; + addOnProduct?: CartItem & { quantity: number }; }; export type Collection = Omit & { @@ -151,6 +160,10 @@ export type ProductVariant = { condition: string | null; engineCylinders: string | null; fuelType: string | null; + addOnProduct?: { + quantity: number; + id: string; + }; }; export type ShopifyCartProductVariant = { @@ -169,7 +182,13 @@ export type CartProductVariant = Omit & { waiverAvailable: { value: string }; coreVariantId: { value: string } | null; @@ -177,6 +196,8 @@ export type ShopifyProductVariant = Omit< mileage: { value: number } | null; estimatedDelivery: { value: string } | null; condition: { value: string } | null; + addOnProductId: { value: string } | null; + addOnQuantity: { value: string } | null; }; export type SEO = { @@ -187,6 +208,7 @@ export type SEO = { export type ShopifyCart = { id: string; checkoutUrl: string; + attributes: { key: string; value: string }[]; cost: { subtotalAmount: Money; totalAmount: Money;