fix: add on products on cart

Signed-off-by: Chloe <pinkcloudvnn@gmail.com>
This commit is contained in:
Chloe 2024-06-19 17:13:29 +07:00
parent 87685fda61
commit cc3982288a
No known key found for this signature in database
GPG Key ID: CFD53CE570D42DF5
10 changed files with 128 additions and 36 deletions

View File

@ -12,7 +12,10 @@ import {
import { revalidateTag } from 'next/cache'; import { revalidateTag } from 'next/cache';
import { cookies } from 'next/headers'; import { cookies } from 'next/headers';
export async function addItem(prevState: any, selectedVariantIds: Array<string>) { export async function addItem(
prevState: any,
selectedVariantIds: Array<{ merchandiseId: string; quantity: number }>
) {
let cartId = cookies().get('cartId')?.value; let cartId = cookies().get('cartId')?.value;
let cart; let cart;
@ -31,10 +34,8 @@ export async function addItem(prevState: any, selectedVariantIds: Array<string>)
} }
try { try {
await addToCart( const cart = await addToCart(cartId, selectedVariantIds);
cartId, console.log({ cartLines: cart.lines });
selectedVariantIds.map((variantId) => ({ merchandiseId: variantId, quantity: 1 }))
);
revalidateTag(TAGS.cart); revalidateTag(TAGS.cart);
} catch (e) { } catch (e) {
return 'Error adding item to cart'; return 'Error adding item to cart';
@ -65,7 +66,6 @@ export async function setMetafields(
revalidateTag(TAGS.cart); revalidateTag(TAGS.cart);
} catch (e) { } catch (e) {
console.log(e);
return 'Error set cart attributes'; return 'Error set cart attributes';
} }
} }

View File

@ -80,11 +80,20 @@ export function AddToCart({
const coreVariantId = searchParams.get(CORE_VARIANT_ID_KEY); const coreVariantId = searchParams.get(CORE_VARIANT_ID_KEY);
// remove special core-waiver value as it is not a valid variant // remove special core-waiver value as it is not a valid variant
const selectedVariantIds = [coreVariantId, selectedVariantId] const addingVariants = (
.filter(Boolean) [coreVariantId, selectedVariantId]
.filter((value) => value !== CORE_WAIVER) as string[]; .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 ( return (
<form action={actionWithVariant}> <form action={actionWithVariant}>

View File

@ -36,10 +36,11 @@ function SubmitButton() {
export function DeleteItemButton({ item }: { item: CartItem }) { export function DeleteItemButton({ item }: { item: CartItem }) {
const [message, formAction] = useFormState(removeItem, null); const [message, formAction] = useFormState(removeItem, null);
const { id: itemId, coreCharge } = item; const { id: itemId, coreCharge, addOnProduct } = item;
const actionWithVariant = formAction.bind(null, [ const actionWithVariant = formAction.bind(null, [
itemId, itemId,
...(coreCharge?.id ? [coreCharge.id] : []) ...(coreCharge?.id ? [coreCharge.id] : []),
...(addOnProduct?.id ? [addOnProduct.id] : [])
]); ]);
return ( return (

View File

@ -51,11 +51,19 @@ export function EditItemQuantityButton({ item, type }: { item: CartItem; type: '
if (item.coreCharge) { if (item.coreCharge) {
payload.push({ payload.push({
lineId: item.coreCharge.id, lineId: item.coreCharge.id,
variantId: item.coreCharge.id, variantId: item.coreCharge.merchandise.id,
quantity 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); const actionWithVariant = formAction.bind(null, payload);
return ( return (

View File

@ -21,7 +21,11 @@ export default function CartModal({ cart }: { cart: Cart | undefined }) {
const openCart = () => setIsOpen(true); const openCart = () => setIsOpen(true);
const closeCart = () => setIsOpen(false); const closeCart = () => setIsOpen(false);
const { control, handleSubmit } = useForm<VehicleFormSchema>({ const { control, handleSubmit } = useForm<VehicleFormSchema>({
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); const [loading, setLoading] = useState(false);

View File

@ -53,3 +53,8 @@ export const CONDITIONS = {
}; };
export const DELIVERY_OPTION_KEY = 'delivery'; export const DELIVERY_OPTION_KEY = 'delivery';
export const ADD_ON_PRODUCT_TYPES = {
addOn: 'Add On',
coreCharge: 'Core Charge'
};

View File

@ -1,9 +1,13 @@
import productFragment from './product'; import imageFragment from './image';
const cartFragment = /* GraphQL */ ` const cartFragment = /* GraphQL */ `
fragment cart on Cart { fragment cart on Cart {
id id
checkoutUrl checkoutUrl
attributes {
key
value
}
cost { cost {
subtotalAmount { subtotalAmount {
amount amount
@ -38,11 +42,22 @@ const cartFragment = /* GraphQL */ `
value value
} }
product { product {
...product featuredImage {
...image
}
handle
title
productType
} }
coreVariantId: metafield(key: "coreVariant", namespace: "custom") { coreVariantId: metafield(key: "coreVariant", namespace: "custom") {
value 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 totalQuantity
} }
${productFragment} ${imageFragment}
`; `;
export default cartFragment; export default cartFragment;

View File

@ -64,6 +64,12 @@ const productFragment = /* GraphQL */ `
condition: metafield(namespace: "custom", key: "condition") { condition: metafield(namespace: "custom", key: "condition") {
value value
} }
addOnQuantity: metafield(namespace: "custom", key: "add_on_quantity") {
value
}
addOnProductId: metafield(namespace: "custom", key: "add_on") {
value
}
} }
} }
} }

View File

@ -1,4 +1,5 @@
import { import {
ADD_ON_PRODUCT_TYPES,
AVAILABILITY_FILTER_ID, AVAILABILITY_FILTER_ID,
HIDDEN_PRODUCT_TAG, HIDDEN_PRODUCT_TAG,
MAKE_FILTER_ID, MAKE_FILTER_ID,
@ -40,7 +41,6 @@ import {
import { import {
Cart, Cart,
CartAttributeInput, CartAttributeInput,
CartItem,
Collection, Collection,
Connection, Connection,
Filter, Filter,
@ -161,7 +161,7 @@ const reshapeCart = (cart: ShopifyCart): Cart => {
...lineItem, ...lineItem,
merchandise: { merchandise: {
...lineItem.merchandise, ...lineItem.merchandise,
product: reshapeProduct(lineItem.merchandise.product) product: lineItem.merchandise.product
} }
})) }))
}; };
@ -277,14 +277,22 @@ const reshapeImages = (images: Connection<Image>, productTitle: string) => {
}; };
const reshapeVariants = (variants: ShopifyProductVariant[]): ProductVariant[] => { const reshapeVariants = (variants: ShopifyProductVariant[]): ProductVariant[] => {
return variants.map((variant) => ({ return variants.map(({ addOnProductId, addOnQuantity, ...variant }) => ({
...variant, ...variant,
waiverAvailable: parseMetaFieldValue<boolean>(variant.waiverAvailable), waiverAvailable: parseMetaFieldValue<boolean>(variant.waiverAvailable),
coreVariantId: variant.coreVariantId?.value || null, coreVariantId: variant.coreVariantId?.value || null,
coreCharge: parseMetaFieldValue<Money>(variant.coreCharge), coreCharge: parseMetaFieldValue<Money>(variant.coreCharge),
mileage: variant.mileage?.value ?? null, mileage: variant.mileage?.value ?? null,
estimatedDelivery: variant.estimatedDelivery?.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<Cart | undefined> {
const cart = reshapeCart(res.body.data.cart); 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 // 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 extendedCartLines = cart?.lines
const coreVariantId = item.merchandise.coreVariantId?.value; .map((item) => {
if (coreVariantId) { const coreVariantId = item.merchandise.coreVariantId?.value;
const relatedCoreCharge = cart.lines.find((line) => line.merchandise.id === coreVariantId); const addOnProductId = item.merchandise.addOnProductId;
return lines.concat([ const _item = { ...item };
{
...item,
coreCharge: relatedCoreCharge
}
]);
}
return lines; if (coreVariantId) {
}, [] as CartItem[]); 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); const totalQuantity = extendedCartLines.reduce((sum, line) => sum + line.quantity, 0);

View File

@ -25,10 +25,19 @@ export type CartItem = {
name: string; name: string;
value: string; value: string;
}[]; }[];
product: Product; product: {
id: string;
handle: string;
title: string;
featuredImage: Image;
productType: string;
};
coreVariantId: { value: string } | null; coreVariantId: { value: string } | null;
addOnQuantity: { value: string } | null;
addOnProductId: { value: string } | null;
}; };
coreCharge?: CartItem; coreCharge?: CartItem;
addOnProduct?: CartItem & { quantity: number };
}; };
export type Collection = Omit<ShopifyCollection, 'helpfulLinks'> & { export type Collection = Omit<ShopifyCollection, 'helpfulLinks'> & {
@ -151,6 +160,10 @@ export type ProductVariant = {
condition: string | null; condition: string | null;
engineCylinders: string | null; engineCylinders: string | null;
fuelType: string | null; fuelType: string | null;
addOnProduct?: {
quantity: number;
id: string;
};
}; };
export type ShopifyCartProductVariant = { export type ShopifyCartProductVariant = {
@ -169,7 +182,13 @@ export type CartProductVariant = Omit<ShopifyCartProductVariant, 'coreVariantId'
export type ShopifyProductVariant = Omit< export type ShopifyProductVariant = Omit<
ProductVariant, ProductVariant,
'coreCharge' | 'waiverAvailable' | 'coreVariantId' | 'mileage' | 'estimatedDelivery' | 'condition' | 'coreCharge'
| 'waiverAvailable'
| 'coreVariantId'
| 'mileage'
| 'estimatedDelivery'
| 'condition'
| 'addOnProduct'
> & { > & {
waiverAvailable: { value: string }; waiverAvailable: { value: string };
coreVariantId: { value: string } | null; coreVariantId: { value: string } | null;
@ -177,6 +196,8 @@ export type ShopifyProductVariant = Omit<
mileage: { value: number } | null; mileage: { value: number } | null;
estimatedDelivery: { value: string } | null; estimatedDelivery: { value: string } | null;
condition: { value: string } | null; condition: { value: string } | null;
addOnProductId: { value: string } | null;
addOnQuantity: { value: string } | null;
}; };
export type SEO = { export type SEO = {
@ -187,6 +208,7 @@ export type SEO = {
export type ShopifyCart = { export type ShopifyCart = {
id: string; id: string;
checkoutUrl: string; checkoutUrl: string;
attributes: { key: string; value: string }[];
cost: { cost: {
subtotalAmount: Money; subtotalAmount: Money;
totalAmount: Money; totalAmount: Money;