mirror of
https://github.com/vercel/commerce.git
synced 2025-05-12 12:47:50 +00:00
fix: add on products on cart
Signed-off-by: Chloe <pinkcloudvnn@gmail.com>
This commit is contained in:
parent
87685fda61
commit
cc3982288a
@ -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';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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}>
|
||||||
|
@ -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 (
|
||||||
|
@ -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 (
|
||||||
|
@ -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);
|
||||||
|
@ -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'
|
||||||
|
};
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user