From a46f39bd4c3c471fa59c8c22af5ae7c01b67a518 Mon Sep 17 00:00:00 2001 From: Victor Gerbrands Date: Wed, 3 May 2023 18:50:58 +0200 Subject: [PATCH] feat: implemented most cart operations --- app/api/cart/route.ts | 11 +-- components/cart/delete-item-button.tsx | 9 +- components/cart/modal.tsx | 3 +- components/product/add-to-cart.tsx | 2 +- lib/medusa/index.ts | 106 +++++++++++++++++----- lib/medusa/types.ts | 117 +++++++++++++++++++++++-- lib/utils.ts | 11 +++ 7 files changed, 216 insertions(+), 43 deletions(-) diff --git a/app/api/cart/route.ts b/app/api/cart/route.ts index 788b6b781..031ff6aad 100644 --- a/app/api/cart/route.ts +++ b/app/api/cart/route.ts @@ -2,7 +2,7 @@ import { cookies } from 'next/headers'; import { NextRequest, NextResponse } from 'next/server'; import { addToCart, removeFromCart, updateCart } from 'lib/medusa'; -import { isShopifyError } from 'lib/type-guards'; +import { isMedusaError } from 'lib/type-guards'; function formatErrorMessage(err: Error): string { return JSON.stringify(err, Object.getOwnPropertyNames(err)); @@ -19,7 +19,7 @@ export async function POST(req: NextRequest): Promise { await addToCart(cartId, [{ variantId, quantity: 1 }]); return NextResponse.json({ status: 204 }); } catch (e) { - if (isShopifyError(e)) { + if (isMedusaError(e)) { return NextResponse.json({ message: formatErrorMessage(e.message) }, { status: e.status }); } @@ -47,7 +47,7 @@ export async function PUT(req: NextRequest): Promise { ]); return NextResponse.json({ status: 204 }); } catch (e) { - if (isShopifyError(e)) { + if (isMedusaError(e)) { return NextResponse.json({ message: formatErrorMessage(e.message) }, { status: e.status }); } @@ -57,7 +57,8 @@ export async function PUT(req: NextRequest): Promise { export async function DELETE(req: NextRequest): Promise { const cartId = cookies().get('cartId')?.value; - const { lineId } = await req.json(); + console.log(req.nextUrl); + const lineId = req.nextUrl.searchParams.get('lineId'); if (!cartId || !lineId) { return NextResponse.json({ error: 'Missing cartId or lineId' }, { status: 400 }); @@ -66,7 +67,7 @@ export async function DELETE(req: NextRequest): Promise { await removeFromCart(cartId, [lineId]); return NextResponse.json({ status: 204 }); } catch (e) { - if (isShopifyError(e)) { + if (isMedusaError(e)) { return NextResponse.json({ message: formatErrorMessage(e.message) }, { status: e.status }); } diff --git a/components/cart/delete-item-button.tsx b/components/cart/delete-item-button.tsx index 25f5a3b1a..b794607b1 100644 --- a/components/cart/delete-item-button.tsx +++ b/components/cart/delete-item-button.tsx @@ -13,12 +13,11 @@ export default function DeleteItemButton({ item }: { item: CartItem }) { async function handleRemove() { setRemoving(true); - const response = await fetch(`/api/cart`, { - method: 'DELETE', - body: JSON.stringify({ - lineId: item.id - }) + console.log(item.id); + const response = await fetch(`/api/cart?lineId=${item.id}`, { + method: 'DELETE' }); + const data = await response.json(); if (data.error) { diff --git a/components/cart/modal.tsx b/components/cart/modal.tsx index 0af119350..413e89337 100644 --- a/components/cart/modal.tsx +++ b/components/cart/modal.tsx @@ -107,7 +107,8 @@ export default function CartModal({ height={64} alt={ item.merchandise.product.featuredImage.altText || - item.merchandise.product.title + item.merchandise.product.title || + '' } src={item.merchandise.product.featuredImage.url} /> diff --git a/components/product/add-to-cart.tsx b/components/product/add-to-cart.tsx index 8142ba4cc..858f67b16 100644 --- a/components/product/add-to-cart.tsx +++ b/components/product/add-to-cart.tsx @@ -42,7 +42,7 @@ export function AddToCart({ const response = await fetch(`/api/cart`, { method: 'POST', body: JSON.stringify({ - merchandiseId: selectedVariantId + variantId: selectedVariantId }) }); diff --git a/lib/medusa/index.ts b/lib/medusa/index.ts index 2dcd272ab..94968a667 100644 --- a/lib/medusa/index.ts +++ b/lib/medusa/index.ts @@ -1,7 +1,11 @@ import { isMedusaError } from 'lib/type-guards'; + +import { mapOptionIds } from 'lib/utils'; import { Cart, + CartItem, MedusaCart, + MedusaLineItem, MedusaProduct, MedusaProductCollection, MedusaProductOption, @@ -60,20 +64,87 @@ export default async function medusaRequest( } const reshapeCart = (cart: MedusaCart): Cart => { - const lines = cart.items; - const totalQuantity = cart.items.length || 0; + const lines = cart.items?.map((item) => reshapeLineItem(item)) || []; + const totalQuantity = lines.length; + const checkoutUrl = '/'; + const currencyCode = 'EUR'; + const cost = { + subtotalAmount: { + amount: (cart.total && cart.tax_total && cart.total - cart.tax_total)?.toString() || '0', + currencyCode + }, + totalAmount: { + amount: (cart.tax_total && cart.tax_total.toString()) || '0', + currencyCode + }, + totalTaxAmount: { + amount: (cart.tax_total && cart.tax_total.toString()) || '0', + currencyCode + } + }; return { ...cart, totalQuantity, - lines + checkoutUrl, + lines, + cost + }; +}; + +const reshapeLineItem = (lineItem: MedusaLineItem): CartItem => { + const product = { + priceRange: { + maxVariantPrice: { + amount: lineItem.variant?.prices?.[0]?.amount.toString() ?? '0', + currencyCode: lineItem.variant?.prices?.[0]?.currency_code ?? 'EUR' + } + }, + updatedAt: lineItem.updated_at, + tags: [], + descriptionHtml: lineItem.description ?? '', + featuredImage: { + url: lineItem.thumbnail ?? '', + altText: lineItem.title ?? '' + }, + availableForSale: true, + variants: [lineItem.variant && reshapeProductVariant(lineItem.variant)], + handle: lineItem.variant?.product?.handle ?? '' + }; + + const selectedOptions = + lineItem.variant?.options?.map((option) => ({ + name: option.option?.title ?? '', + value: option.value + })) || []; + + const merchandise = { + id: lineItem.variant_id || lineItem.id, + selectedOptions, + product, + title: lineItem.title + }; + + const cost = { + totalAmount: { + amount: lineItem.total.toString() ?? '0', + currencyCode: 'EUR' + } + }; + const quantity = lineItem.quantity; + + return { + ...lineItem, + merchandise, + cost, + quantity }; }; const reshapeProduct = (product: MedusaProduct): Product => { const priceRange = { maxVariantPrice: { - amount: product.variants?.[0]?.prices?.[0]?.amount.toString() ?? '', + amount: product.variants?.[0]?.prices?.[0]?.amount.toString() ?? '0', currencyCode: product.variants?.[0]?.prices?.[0]?.currency_code ?? '' } }; @@ -119,14 +190,6 @@ const reshapeProductOption = (productOption: MedusaProductOption): ProductOption }; }; -const mapOptionIds = (productOptions: MedusaProductOption[]) => { - const map: Record = {}; - productOptions.forEach((option) => { - map[option.id] = option.title; - }); - return map; -}; - const reshapeProductVariant = ( productVariant: MedusaProductVariant, productOptions?: MedusaProductOption[] @@ -142,7 +205,7 @@ const reshapeProductVariant = ( const availableForSale = !!productVariant.inventory_quantity; const price = { - amount: productVariant.prices?.[0]?.amount.toString() ?? '', + amount: productVariant.prices?.[0]?.amount.toString() ?? 'ß', currencyCode: productVariant.prices?.[0]?.currency_code ?? '' }; return { @@ -173,8 +236,6 @@ const reshapeCollection = (collection: MedusaProductCollection): ProductCollecti export async function createCart(): Promise { const res = await medusaRequest('POST', '/carts', {}); - console.log('Cart created!'); - console.log(res); return reshapeCart(res.body.cart); } @@ -185,17 +246,18 @@ export async function addToCart( console.log(lineItems); // TODO: transform lines into Medusa line items const res = await medusaRequest('POST', `/carts/${cartId}/line-items`, { - lineItems + variant_id: lineItems[0]?.variantId, + quantity: lineItems[0]?.quantity }); - - return res.body.data.cart; + console.log(res.body); + return reshapeCart(res.body.cart); } export async function removeFromCart(cartId: string, lineIds: string[]): Promise { // TODO: We only allow you to pass a single line item to delete const res = await medusaRequest('DELETE', `/carts/${cartId}/line-items/${lineIds[0]}`); - - return res.body.data.cart; + console.log(res); + return reshapeCart(res.body.cart); } export async function updateCart( @@ -205,7 +267,7 @@ export async function updateCart( console.log(lines); // TODO: transform lines into Medusa line items const res = await medusaRequest('POST', `/carts/${cartId}`, {}); - return res.body.data.cart; + return reshapeCart(res.body.cart); } export async function getCart(cartId: string): Promise { @@ -215,7 +277,7 @@ export async function getCart(cartId: string): Promise { return null; } - return res.body.cart; + return reshapeCart(res.body.cart); } export async function getCollection(handle: string): Promise { diff --git a/lib/medusa/types.ts b/lib/medusa/types.ts index c282a9c31..001f27c82 100644 --- a/lib/medusa/types.ts +++ b/lib/medusa/types.ts @@ -52,24 +52,21 @@ export type MedusaProduct = { tags?: ProductTag[]; }; -export type Product = Omit & { +export type Product = Partial> & { featuredImage: FeaturedImage; seo?: { title?: string; description?: string; }; priceRange: { - maxVariantPrice: { - amount: string; - currencyCode: string; - }; + maxVariantPrice: Money; }; updatedAt: Date; descriptionHtml: string; tags: Array; availableForSale: boolean; options?: Array; - variants: Array; + variants: Array; }; export type FeaturedImage = { @@ -204,7 +201,7 @@ export type Money = { currencyCode: string; }; -type MoneyAmount = { +export type MoneyAmount = { id: string; currency_code: string; currency?: Currency | null; @@ -304,15 +301,117 @@ export type ShippingOptionRequirement = { export type MedusaCart = { id: string; - items: []; + email?: string; + billing_address_id: string; + // billing_address?: Address; + // shipping_address_id?: string; + // shipping_address?: Address; + items?: MedusaLineItem[]; + region_id: string; + region?: Region; + // discounts?: Discount[]; + // gift_cards?: GiftCard[]; + customer_id?: string; + // customer?: Customer; + // payment_session?: PaymentSession; + // payment_sessions?: PaymentSession[]; + payment_id?: string; + // payment?: Payment; + // shipping_methods?: ShippingMethod[]; + type: 'default' | 'swap' | 'draft_order' | 'payment_link' | 'claim'; + completed_at?: string; + payment_authorized_at?: string; + idempotency_key?: string; + context?: Record; + sales_channel_id?: string; + // sales_channel?: SalesChannel; + created_at: string; + updated_at: string; + deleted_at?: string; + metadata?: Record; + shipping_total?: number; + discount_total?: number; + raw_discount_total?: number; + item_tax_total?: number; + shipping_tax_total?: number; + tax_total?: number; + refunded_total?: number; + total?: number; }; export type Cart = Partial & { - lines: []; + lines: CartItem[]; + checkoutUrl: string; totalQuantity: number; + cost: { + subtotalAmount: Money; + totalAmount: Money; + totalTaxAmount: Money; + }; }; export type Menu = { title: string; path: string; }; + +export type MedusaLineItem = { + id: string; + cart_id?: string; + cart?: Cart; + order_id?: string; + // order?: Order; + swap_id?: string | null; + // swap?: Swap; + claim_order_id?: string | null; + // claim_order?: ClaimOrder; + // tax_lines?: LineItemTaxLine[]; + // adjustments?: LineItemAdjustment[]; + original_item_id?: string | null; + order_edit_id?: string | null; + // order_edit?: OrderEdit; + title: string; + description?: string | null; + thumbnail?: string | null; + is_return: boolean; + is_giftcard: boolean; + should_merge: boolean; + allow_discounts: boolean; + has_shipping?: boolean | null; + unit_price: number; + variant_id?: string | null; + variant?: MedusaProductVariant; + quantity: number; + fulfilled_quantity?: number | null; + returned_quantity?: number | null; + shipped_quantity?: number | null; + refundable: number; + subtotal: number; + tax_total: number; + total: number; + original_total: number; + original_tax_total: number; + discount_total: number; + raw_discount_total: number; + gift_card_total: number; + includes_tax: boolean; + created_at: Date; + updated_at: Date; + metadata?: { [key: string]: string } | null; +}; + +export type CartItem = MedusaLineItem & { + merchandise: { + id: string; + selectedOptions: SelectedOption[]; + product: Product; + title: string; + }; + cost: { + totalAmount: { + amount: string; + currencyCode: string; + }; + }; + quantity: number; +}; diff --git a/lib/utils.ts b/lib/utils.ts index e45a0df86..9c72e43a4 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,6 +1,17 @@ +import { MedusaProductOption } from './medusa/types'; + export const createUrl = (pathname: string, params: URLSearchParams) => { const paramsString = params.toString(); const queryString = `${paramsString.length ? '?' : ''}${paramsString}`; return `${pathname}${queryString}`; }; + +export const mapOptionIds = (productOptions: MedusaProductOption[]) => { + // Maps the option titles to their respective ids + const map: Record = {}; + productOptions.forEach((option) => { + map[option.id] = option.title; + }); + return map; +};