feat: implemented most cart operations

This commit is contained in:
Victor Gerbrands 2023-05-03 18:50:58 +02:00
parent cfe181ac41
commit a46f39bd4c
7 changed files with 216 additions and 43 deletions

View File

@ -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<Response> {
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<Response> {
]);
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<Response> {
export async function DELETE(req: NextRequest): Promise<Response> {
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<Response> {
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 });
}

View File

@ -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) {

View File

@ -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}
/>

View File

@ -42,7 +42,7 @@ export function AddToCart({
const response = await fetch(`/api/cart`, {
method: 'POST',
body: JSON.stringify({
merchandiseId: selectedVariantId
variantId: selectedVariantId
})
});

View File

@ -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<string, string> = {};
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<Cart> {
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<Cart> {
// 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<Cart | null> {
@ -215,7 +277,7 @@ export async function getCart(cartId: string): Promise<Cart | null> {
return null;
}
return res.body.cart;
return reshapeCart(res.body.cart);
}
export async function getCollection(handle: string): Promise<ProductCollection | undefined> {

View File

@ -52,24 +52,21 @@ export type MedusaProduct = {
tags?: ProductTag[];
};
export type Product = Omit<MedusaProduct, 'tags' | 'options' | 'variants'> & {
export type Product = Partial<Omit<MedusaProduct, 'tags' | 'options' | 'variants'>> & {
featuredImage: FeaturedImage;
seo?: {
title?: string;
description?: string;
};
priceRange: {
maxVariantPrice: {
amount: string;
currencyCode: string;
};
maxVariantPrice: Money;
};
updatedAt: Date;
descriptionHtml: string;
tags: Array<string>;
availableForSale: boolean;
options?: Array<ProductOption>;
variants: Array<ProductVariant>;
variants: Array<ProductVariant | undefined>;
};
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<string, any>;
sales_channel_id?: string;
// sales_channel?: SalesChannel;
created_at: string;
updated_at: string;
deleted_at?: string;
metadata?: Record<string, any>;
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<MedusaCart> & {
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;
};

View File

@ -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<string, string> = {};
productOptions.forEach((option) => {
map[option.id] = option.title;
});
return map;
};