feat: add cart operations

This commit is contained in:
alex.saiannyi 2023-05-22 15:22:52 +02:00 committed by Volodymyr Krasnoshapka
parent b17652b26b
commit c2ec7d05b2
8 changed files with 97 additions and 44 deletions

View File

@ -10,15 +10,43 @@ function formatErrorMessage(err: Error): string {
export async function POST(req: NextRequest): Promise<Response> {
const cartId = cookies().get('cartId')?.value;
const { merchandiseId, isBigCommerceAPI } = await req.json();
const { merchandiseId, isBigCommerceAPI, lineId, productId, quantity, variantId } = await req.json();
if ((!isBigCommerceAPI && !cartId?.length) || !merchandiseId?.length) {
if (!isBigCommerceAPI && (!cartId?.length || !merchandiseId?.length)) {
return NextResponse.json({ error: 'Missing cartId or variantId' }, { status: 400 });
} else if (isBigCommerceAPI && !merchandiseId?.length) {
} else if (!isBigCommerceAPI && !merchandiseId?.length) {
return NextResponse.json({ error: 'Missing variantId' }, { status: 400 });
}
if (cartId && isBigCommerceAPI && lineId && !variantId || quantity === 0) {
try {
await addToCart(cartId || '', [{ merchandiseId, quantity: 1 }]);
await removeFromCart(cartId!, [lineId]);
return NextResponse.json({ status: 204 });
} catch (e) {
if (isVercelCommerceError(e)) {
return NextResponse.json({ message: formatErrorMessage(e.message) }, { status: e.status });
}
return NextResponse.json({ status: 500 });
}
}
try {
const { id: cartEntityId } = await addToCart(cartId || '', [{ merchandiseId, quantity: 1, productId }]);
if ( isBigCommerceAPI && cartEntityId) {
const response = NextResponse.json({ status: 204 });
response.cookies.set('cartId', cartEntityId, {
path: '/',
sameSite: 'strict',
secure: process.env.NODE_ENV === 'production'
});
return response;
}
return NextResponse.json({ status: 204 });
} catch (e) {
if (isVercelCommerceError(e)) {
@ -31,7 +59,7 @@ export async function POST(req: NextRequest): Promise<Response> {
export async function PUT(req: NextRequest): Promise<Response> {
const cartId = cookies().get('cartId')?.value;
const { variantId, quantity, lineId } = await req.json();
const { variantId, quantity, lineId, productId } = await req.json();
if (!cartId || !variantId || !quantity || !lineId) {
return NextResponse.json(
@ -44,9 +72,11 @@ export async function PUT(req: NextRequest): Promise<Response> {
{
id: lineId,
merchandiseId: variantId,
quantity
quantity,
productId
}
]);
return NextResponse.json({ status: 204 });
} catch (e) {
if (isVercelCommerceError(e)) {
@ -57,6 +87,8 @@ export async function PUT(req: NextRequest): Promise<Response> {
}
}
// NOTE: delete route handler fails
// https://github.com/vercel/next.js/issues/48096
export async function DELETE(req: NextRequest): Promise<Response> {
const cartId = cookies().get('cartId')?.value;
const { lineId } = await req.json();

View File

@ -14,9 +14,10 @@ export default function DeleteItemButton({ item }: { item: CartItem }) {
setRemoving(true);
const response = await fetch(`/api/cart`, {
method: 'DELETE',
method: 'POST',
body: JSON.stringify({
lineId: item.id
lineId: item.id,
isBigCommerceAPI: true,
})
});
const data = await response.json();

View File

@ -21,11 +21,13 @@ export default function EditItemQuantityButton({
setEditing(true);
const response = await fetch(`/api/cart`, {
method: type === 'minus' && item.quantity - 1 === 0 ? 'DELETE' : 'PUT',
method: type === 'minus' && item.quantity - 1 === 0 ? 'POST' : 'PUT',
body: JSON.stringify({
lineId: item.id,
productId: item.merchandise.product.handle,
variantId: item.merchandise.id,
quantity: type === 'plus' ? item.quantity + 1 : item.quantity - 1
quantity: type === 'plus' ? item.quantity + 1 : item.quantity - 1,
isBigCommerceAPI: true
})
});

View File

@ -6,8 +6,8 @@ import Link from 'next/link';
import CloseIcon from 'components/icons/close';
import ShoppingBagIcon from 'components/icons/shopping-bag';
import Price from 'components/price';
import { DEFAULT_OPTION } from 'lib/constants';
import type { VercelCart as Cart } from 'lib/bigcommerce/types';
import { DEFAULT_OPTION } from 'lib/constants';
import { createUrl } from 'lib/utils';
import DeleteItemButton from './delete-item-button';
import EditItemQuantityButton from './edit-item-quantity-button';
@ -81,8 +81,10 @@ export default function CartModal({
<ul className="flex-grow overflow-auto p-6">
{cart.lines.map((item, i) => {
const merchandiseSearchParams = {} as MerchandiseSearchParams;
let subTitleWithSelectedOptions = '';
item.merchandise.selectedOptions.forEach(({ name, value }) => {
subTitleWithSelectedOptions += `${name}: ${value} `;
if (value !== DEFAULT_OPTION) {
merchandiseSearchParams[name.toLowerCase()] = value;
}
@ -118,14 +120,14 @@ export default function CartModal({
</span>
{item.merchandise.title !== DEFAULT_OPTION ? (
<p className="text-sm" data-testid="cart-product-variant">
{item.merchandise.title}
{item.merchandise.title === item.merchandise.product.title && subTitleWithSelectedOptions ? subTitleWithSelectedOptions : item.merchandise.title}
</p>
) : null}
</div>
<Price
className="flex flex-col justify-between space-y-2 text-sm"
amount={item.cost.totalAmount.amount}
currencyCode={item.cost.totalAmount.currencyCode}
currencyCode={item.cost.totalAmount.currencyCode || 'USD'}
/>
</Link>
<div className="flex h-9 flex-row">
@ -146,7 +148,7 @@ export default function CartModal({
<Price
className="text-right"
amount={cart.cost.subtotalAmount.amount}
currencyCode={cart.cost.subtotalAmount.currencyCode}
currencyCode={cart.cost.subtotalAmount.currencyCode || 'USD'}
/>
</div>
<div className="mb-2 flex items-center justify-between">
@ -154,7 +156,7 @@ export default function CartModal({
<Price
className="text-right"
amount={cart.cost.totalTaxAmount.amount}
currencyCode={cart.cost.totalTaxAmount.currencyCode}
currencyCode={cart.cost.totalTaxAmount.currencyCode || 'USD'}
/>
</div>
<div className="mb-2 flex items-center justify-between border-b border-gray-200 pb-2">
@ -166,7 +168,7 @@ export default function CartModal({
<Price
className="text-right"
amount={cart.cost.totalAmount.amount}
currencyCode={cart.cost.totalAmount.currencyCode}
currencyCode={cart.cost.totalAmount.currencyCode || 'USD'}
/>
</div>
</div>

View File

@ -14,8 +14,10 @@ export function AddToCart({
variants: ProductVariant[];
availableForSale: boolean;
}) {
const productEntityId = variants[0]?.parentId || variants[0]?.id;
const [selectedVariantId, setSelectedVariantId] = useState(productEntityId);
const productEntityId = variants[0]?.parentId;
const varianEntitytId = variants[0]?.id;
const [selectedVariantId, setSelectedVariantId] = useState(varianEntitytId);
const [selectedProductId, setSelectedProductId] = useState(productEntityId);
const router = useRouter();
const searchParams = useSearchParams();
const [isPending, startTransition] = useTransition();
@ -29,7 +31,8 @@ export function AddToCart({
);
if (variant) {
setSelectedVariantId(variant.parentId || variant.id);
setSelectedVariantId(variant.id);
setSelectedProductId(variant.parentId);
}
}, [searchParams, variants, setSelectedVariantId]);
@ -44,6 +47,7 @@ export function AddToCart({
method: 'POST',
body: JSON.stringify({
merchandiseId: selectedVariantId,
productId: selectedProductId,
isBigCommerceAPI: true
})
});

View File

@ -167,7 +167,7 @@ const getBigCommerceProductsWithCheckout = async (
) => {
const bigCommerceProducts = await Promise.all(
lines.map(async ({ merchandiseId }) => {
const productId = Number(merchandiseId);
const productId = parseInt(merchandiseId, 10);
const resp = await bigcommerceFetch<BigCommerceProductOperation>({
query: getProductQuery,
@ -191,10 +191,24 @@ const getBigCommerceProductsWithCheckout = async (
},
cache: 'no-store'
});
const checkout = resCheckout.body.data.site.checkout ?? {
subtotal: {
value: 0,
currencyCode: '',
},
grandTotal: {
value: 0,
currencyCode: '',
},
taxTotal: {
value: 0,
currencyCode: '',
},
};
return {
productsByIdList: bigCommerceProducts,
checkout: resCheckout.body.data.site.checkout
checkout,
};
};
@ -225,7 +239,7 @@ export async function createCart(): Promise<VercelCart> {
export async function addToCart(
cartId: string,
lines: { merchandiseId: string; quantity: number }[]
lines: { merchandiseId: string; quantity: number, productId?: string }[]
): Promise<VercelCart> {
let bigCommerceCart: BigCommerceCart;
@ -236,8 +250,9 @@ export async function addToCart(
addCartLineItemsInput: {
cartEntityId: cartId,
data: {
lineItems: lines.map(({ merchandiseId, quantity }) => ({
productEntityId: parseInt(merchandiseId, 10),
lineItems: lines.map(({ merchandiseId, quantity, productId }) => ({
productEntityId: parseInt(productId!, 10),
variantEntityId: parseInt(merchandiseId, 10),
quantity
}))
}
@ -252,8 +267,9 @@ export async function addToCart(
query: createCartMutation,
variables: {
createCartInput: {
lineItems: lines.map(({ merchandiseId, quantity }) => ({
productEntityId: parseInt(merchandiseId, 10),
lineItems: lines.map(({ merchandiseId, quantity, productId }) => ({
productEntityId: parseInt(productId!, 10),
variantEntityId: parseInt(merchandiseId, 10),
quantity
}))
}
@ -303,12 +319,12 @@ export async function removeFromCart(cartId: string, lineIds: string[]): Promise
// Update on selected options requires variantEntityId, optionEntityId
export async function updateCart(
cartId: string,
lines: { id: string; merchandiseId: string; quantity: number }[]
lines: { id: string; merchandiseId: string; quantity: number, productId?: string}[]
): Promise<VercelCart> {
let cartState: { status: number; body: BigCommerceUpdateCartItemOperation } | undefined;
for (let updates = lines.length; updates > 0; updates--) {
const { id, merchandiseId, quantity } = lines[updates - 1]!;
const { id, merchandiseId, quantity, productId } = lines[updates - 1]!;
const res = await bigcommerceFetch<BigCommerceUpdateCartItemOperation>({
query: updateCartLineItemMutation,
variables: {
@ -317,8 +333,9 @@ export async function updateCart(
lineItemEntityId: id,
data: {
lineItem: {
quantity,
productEntityId: Number(merchandiseId)
productEntityId: parseInt(productId!, 10),
variantEntityId: parseInt(merchandiseId, 10),
quantity
}
}
}
@ -335,7 +352,6 @@ export async function updateCart(
return bigcommerceToVercelCart(updatedCart, productsByIdList, checkout);
}
// NOTE: DONE & review if it works
export async function getCart(cartId: string): Promise<VercelCart | null> {
const res = await bigcommerceFetch<BigCommerceCartOperation>({
query: getCartQuery,
@ -351,11 +367,11 @@ export async function getCart(cartId: string): Promise<VercelCart | null> {
const lines = vercelFromBigCommerceLineItems(cart.lineItems);
const { productsByIdList, checkout } = await getBigCommerceProductsWithCheckout(cartId, lines);
return bigcommerceToVercelCart(cart, productsByIdList, checkout);
return bigcommerceToVercelCart(cart, productsByIdList, checkout);;
}
export async function getCollection(handle: string): Promise<VercelCollection> {
const entityId = await getCategoryEntityIdbyHandle(handle); // NOTE: check if this approach suits us
const entityId = await getCategoryEntityIdbyHandle(handle);
const res = await bigcommerceFetch<BigCommerceCollectionOperation>({
query: getCategoryQuery,
variables: {
@ -481,7 +497,7 @@ export async function getMenu(handle: string): Promise<VercelMenu[]> {
title: name,
path: createVercelCollectionPath(verceLTitle!)
};
// NOTE: for NavBar we probably should keep it only high level categories
// NOTE: keep only high level categories for NavBar
// if (hasChildren && children) {
// return configureVercelMenu(children, hasChildren);
// }
@ -504,7 +520,6 @@ export async function getMenu(handle: string): Promise<VercelMenu[]> {
return [];
}
// TODO: replace with BC API next Page(s) Methods
export async function getPage(handle: string): Promise<VercelPage> {
const entityId = await getEntityIdByHandle(handle);
const res = await bigcommerceFetch<BigCommercePageOperation>({
@ -528,7 +543,6 @@ export async function getPages(): Promise<VercelPage[]> {
}
export async function getProduct(handle: string): Promise<VercelProduct | undefined> {
// const productId = await getEntityIdByHandle(handle); // NOTE: check of this approach work
const res = await bigcommerceFetch<BigCommerceProductOperation>({
query: getProductQuery,
variables: {

View File

@ -22,8 +22,8 @@ type ProductsList = { productId: number; productData: BigCommerceProduct }[];
const vercelFromBigCommerceLineItems = (lineItems: BigCommerceCart['lineItems']) => {
const { physicalItems, digitalItems, customItems } = lineItems;
const cartItemMapper = ({ entityId, quantity }: DigitalOrPhysicalItem | CartCustomItem) => ({
merchandiseId: entityId.toString(),
const cartItemMapper = ({ entityId, quantity, productEntityId }: DigitalOrPhysicalItem | CartCustomItem) => ({
merchandiseId: productEntityId ? productEntityId.toString() : entityId.toString(),
quantity
});
@ -201,7 +201,7 @@ const bigcommerceToVercelCartItems = (
}
return {
id: item.entityId.toString(),
id: item.entityId.toString(), // NOTE: used as lineId || lineItemId
quantity: item.quantity,
cost: {
totalAmount: {
@ -211,7 +211,7 @@ const bigcommerceToVercelCartItems = (
}
},
merchandise: {
id: item.entityId.toString(),
id: isCustomItem ? item.entityId.toString() : (item as DigitalOrPhysicalItem).variantEntityId!.toString(),
title: `${item.name}`,
selectedOptions,
product
@ -238,9 +238,8 @@ const bigcommerceToVercelCart = (
): VercelCart => {
return {
id: cart.entityId,
checkoutUrl: '', // NOTE: where to get checkoutUrl??
checkoutUrl: '', // TODO: add later
cost: {
// NOTE: these props lay down in checkout not cart
subtotalAmount: {
amount: checkout.subtotal.value.toString(),
currencyCode: checkout.subtotal.currencyCode

View File

@ -111,7 +111,6 @@ const updateCartLineItemMutation = /* GraphQL */ `
updatedAt {
utc
}
totalQuantity
lineItems {
totalQuantity
physicalItems {