mirror of
https://github.com/vercel/commerce.git
synced 2025-05-15 14:06:59 +00:00
feat: implemented most cart operations
This commit is contained in:
parent
cfe181ac41
commit
a46f39bd4c
@ -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 });
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -42,7 +42,7 @@ export function AddToCart({
|
||||
const response = await fetch(`/api/cart`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
merchandiseId: selectedVariantId
|
||||
variantId: selectedVariantId
|
||||
})
|
||||
});
|
||||
|
||||
|
@ -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> {
|
||||
|
@ -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;
|
||||
};
|
||||
|
11
lib/utils.ts
11
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<string, string> = {};
|
||||
productOptions.forEach((option) => {
|
||||
map[option.id] = option.title;
|
||||
});
|
||||
return map;
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user