move selfservice tables to medreport schema
add base medusa store frontend
This commit is contained in:
14
packages/features/medusa-storefront/src/lib/config.ts
Normal file
14
packages/features/medusa-storefront/src/lib/config.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import Medusa from "@medusajs/js-sdk"
|
||||
|
||||
// Defaults to standard port for Medusa server
|
||||
let MEDUSA_BACKEND_URL = "http://localhost:9000"
|
||||
|
||||
if (process.env.MEDUSA_BACKEND_URL) {
|
||||
MEDUSA_BACKEND_URL = process.env.MEDUSA_BACKEND_URL
|
||||
}
|
||||
|
||||
export const sdk = new Medusa({
|
||||
baseUrl: MEDUSA_BACKEND_URL,
|
||||
debug: process.env.NODE_ENV === "development",
|
||||
publishableKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY,
|
||||
})
|
||||
68
packages/features/medusa-storefront/src/lib/constants.tsx
Normal file
68
packages/features/medusa-storefront/src/lib/constants.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import React from "react"
|
||||
import { CreditCard } from "@medusajs/icons"
|
||||
|
||||
import Ideal from "@modules/common/icons/ideal"
|
||||
import Bancontact from "@modules/common/icons/bancontact"
|
||||
import PayPal from "@modules/common/icons/paypal"
|
||||
|
||||
/* Map of payment provider_id to their title and icon. Add in any payment providers you want to use. */
|
||||
export const paymentInfoMap: Record<
|
||||
string,
|
||||
{ title: string; icon: React.JSX.Element }
|
||||
> = {
|
||||
pp_stripe_stripe: {
|
||||
title: "Credit card",
|
||||
icon: <CreditCard />,
|
||||
},
|
||||
"pp_stripe-ideal_stripe": {
|
||||
title: "iDeal",
|
||||
icon: <Ideal />,
|
||||
},
|
||||
"pp_stripe-bancontact_stripe": {
|
||||
title: "Bancontact",
|
||||
icon: <Bancontact />,
|
||||
},
|
||||
pp_paypal_paypal: {
|
||||
title: "PayPal",
|
||||
icon: <PayPal />,
|
||||
},
|
||||
pp_system_default: {
|
||||
title: "Manual Payment",
|
||||
icon: <CreditCard />,
|
||||
},
|
||||
// Add more payment providers here
|
||||
}
|
||||
|
||||
// This only checks if it is native stripe for card payments, it ignores the other stripe-based providers
|
||||
export const isStripe = (providerId?: string) => {
|
||||
return providerId?.startsWith("pp_stripe_")
|
||||
}
|
||||
export const isPaypal = (providerId?: string) => {
|
||||
return providerId?.startsWith("pp_paypal")
|
||||
}
|
||||
export const isManual = (providerId?: string) => {
|
||||
return providerId?.startsWith("pp_system_default")
|
||||
}
|
||||
|
||||
// Add currencies that don't need to be divided by 100
|
||||
export const noDivisionCurrencies = [
|
||||
"krw",
|
||||
"jpy",
|
||||
"vnd",
|
||||
"clp",
|
||||
"pyg",
|
||||
"xaf",
|
||||
"xof",
|
||||
"bif",
|
||||
"djf",
|
||||
"gnf",
|
||||
"kmf",
|
||||
"mga",
|
||||
"rwf",
|
||||
"xpf",
|
||||
"htg",
|
||||
"vuv",
|
||||
"xag",
|
||||
"xdr",
|
||||
"xau",
|
||||
]
|
||||
@@ -0,0 +1,34 @@
|
||||
"use client"
|
||||
|
||||
import React, { createContext, useContext } from "react"
|
||||
|
||||
interface ModalContext {
|
||||
close: () => void
|
||||
}
|
||||
|
||||
const ModalContext = createContext<ModalContext | null>(null)
|
||||
|
||||
interface ModalProviderProps {
|
||||
children?: React.ReactNode
|
||||
close: () => void
|
||||
}
|
||||
|
||||
export const ModalProvider = ({ children, close }: ModalProviderProps) => {
|
||||
return (
|
||||
<ModalContext.Provider
|
||||
value={{
|
||||
close,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ModalContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useModal = () => {
|
||||
const context = useContext(ModalContext)
|
||||
if (context === null) {
|
||||
throw new Error("useModal must be used within a ModalProvider")
|
||||
}
|
||||
return context
|
||||
}
|
||||
472
packages/features/medusa-storefront/src/lib/data/cart.ts
Normal file
472
packages/features/medusa-storefront/src/lib/data/cart.ts
Normal file
@@ -0,0 +1,472 @@
|
||||
"use server";
|
||||
|
||||
import medusaError from "@lib/util/medusa-error";
|
||||
import { HttpTypes } from "@medusajs/types";
|
||||
import { revalidateTag } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import {
|
||||
getAuthHeaders,
|
||||
getCacheOptions,
|
||||
getCacheTag,
|
||||
getCartId,
|
||||
removeCartId,
|
||||
setCartId,
|
||||
} from "./cookies";
|
||||
import { getRegion } from "./regions";
|
||||
import { sdk } from "@lib/config";
|
||||
|
||||
/**
|
||||
* Retrieves a cart by its ID. If no ID is provided, it will use the cart ID from the cookies.
|
||||
* @param cartId - optional - The ID of the cart to retrieve.
|
||||
* @returns The cart object if found, or null if not found.
|
||||
*/
|
||||
export async function retrieveCart(cartId?: string) {
|
||||
const id = cartId || (await getCartId());
|
||||
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const headers = {
|
||||
...(await getAuthHeaders()),
|
||||
};
|
||||
|
||||
const next = {
|
||||
...(await getCacheOptions("carts")),
|
||||
};
|
||||
|
||||
return await sdk.client
|
||||
.fetch<HttpTypes.StoreCartResponse>(`/store/carts/${id}`, {
|
||||
method: "GET",
|
||||
query: {
|
||||
fields:
|
||||
"*items, *region, *items.product, *items.variant, *items.thumbnail, *items.metadata, +items.total, *promotions, +shipping_methods.name",
|
||||
},
|
||||
headers,
|
||||
next,
|
||||
cache: "force-cache",
|
||||
})
|
||||
.then(({ cart }) => cart)
|
||||
.catch(() => null);
|
||||
}
|
||||
|
||||
export async function getOrSetCart(countryCode: string) {
|
||||
const region = await getRegion(countryCode);
|
||||
|
||||
if (!region) {
|
||||
throw new Error(`Region not found for country code: ${countryCode}`);
|
||||
}
|
||||
|
||||
let cart = await retrieveCart();
|
||||
|
||||
const headers = {
|
||||
...(await getAuthHeaders()),
|
||||
};
|
||||
|
||||
if (!cart) {
|
||||
const cartResp = await sdk.store.cart.create(
|
||||
{ region_id: region.id },
|
||||
{},
|
||||
headers
|
||||
);
|
||||
cart = cartResp.cart;
|
||||
|
||||
await setCartId(cart.id);
|
||||
|
||||
const cartCacheTag = await getCacheTag("carts");
|
||||
revalidateTag(cartCacheTag);
|
||||
}
|
||||
|
||||
if (cart && cart?.region_id !== region.id) {
|
||||
await sdk.store.cart.update(cart.id, { region_id: region.id }, {}, headers);
|
||||
const cartCacheTag = await getCacheTag("carts");
|
||||
revalidateTag(cartCacheTag);
|
||||
}
|
||||
|
||||
return cart;
|
||||
}
|
||||
|
||||
export async function updateCart(data: HttpTypes.StoreUpdateCart) {
|
||||
const cartId = await getCartId();
|
||||
|
||||
if (!cartId) {
|
||||
throw new Error(
|
||||
"No existing cart found, please create one before updating"
|
||||
);
|
||||
}
|
||||
|
||||
const headers = {
|
||||
...(await getAuthHeaders()),
|
||||
};
|
||||
|
||||
return sdk.store.cart
|
||||
.update(cartId, data, {}, headers)
|
||||
.then(async ({ cart }) => {
|
||||
const cartCacheTag = await getCacheTag("carts");
|
||||
revalidateTag(cartCacheTag);
|
||||
|
||||
const fulfillmentCacheTag = await getCacheTag("fulfillment");
|
||||
revalidateTag(fulfillmentCacheTag);
|
||||
|
||||
return cart;
|
||||
})
|
||||
.catch(medusaError);
|
||||
}
|
||||
|
||||
export async function addToCart({
|
||||
variantId,
|
||||
quantity,
|
||||
countryCode,
|
||||
}: {
|
||||
variantId: string;
|
||||
quantity: number;
|
||||
countryCode: string;
|
||||
}) {
|
||||
if (!variantId) {
|
||||
throw new Error("Missing variant ID when adding to cart");
|
||||
}
|
||||
|
||||
const cart = await getOrSetCart(countryCode);
|
||||
|
||||
if (!cart) {
|
||||
throw new Error("Error retrieving or creating cart");
|
||||
}
|
||||
|
||||
const headers = {
|
||||
...(await getAuthHeaders()),
|
||||
};
|
||||
|
||||
await sdk.store.cart
|
||||
.createLineItem(
|
||||
cart.id,
|
||||
{
|
||||
variant_id: variantId,
|
||||
quantity,
|
||||
},
|
||||
{},
|
||||
headers
|
||||
)
|
||||
.then(async () => {
|
||||
const cartCacheTag = await getCacheTag("carts");
|
||||
revalidateTag(cartCacheTag);
|
||||
|
||||
const fulfillmentCacheTag = await getCacheTag("fulfillment");
|
||||
revalidateTag(fulfillmentCacheTag);
|
||||
})
|
||||
.catch(medusaError);
|
||||
}
|
||||
|
||||
export async function updateLineItem({
|
||||
lineId,
|
||||
quantity,
|
||||
}: {
|
||||
lineId: string;
|
||||
quantity: number;
|
||||
}) {
|
||||
if (!lineId) {
|
||||
throw new Error("Missing lineItem ID when updating line item");
|
||||
}
|
||||
|
||||
const cartId = await getCartId();
|
||||
|
||||
if (!cartId) {
|
||||
throw new Error("Missing cart ID when updating line item");
|
||||
}
|
||||
|
||||
const headers = {
|
||||
...(await getAuthHeaders()),
|
||||
};
|
||||
|
||||
await sdk.store.cart
|
||||
.updateLineItem(cartId, lineId, { quantity }, {}, headers)
|
||||
.then(async () => {
|
||||
const cartCacheTag = await getCacheTag("carts");
|
||||
revalidateTag(cartCacheTag);
|
||||
|
||||
const fulfillmentCacheTag = await getCacheTag("fulfillment");
|
||||
revalidateTag(fulfillmentCacheTag);
|
||||
})
|
||||
.catch(medusaError);
|
||||
}
|
||||
|
||||
export async function deleteLineItem(lineId: string) {
|
||||
if (!lineId) {
|
||||
throw new Error("Missing lineItem ID when deleting line item");
|
||||
}
|
||||
|
||||
const cartId = await getCartId();
|
||||
|
||||
if (!cartId) {
|
||||
throw new Error("Missing cart ID when deleting line item");
|
||||
}
|
||||
|
||||
const headers = {
|
||||
...(await getAuthHeaders()),
|
||||
};
|
||||
|
||||
await sdk.store.cart
|
||||
.deleteLineItem(cartId, lineId, headers)
|
||||
.then(async () => {
|
||||
const cartCacheTag = await getCacheTag("carts");
|
||||
revalidateTag(cartCacheTag);
|
||||
|
||||
const fulfillmentCacheTag = await getCacheTag("fulfillment");
|
||||
revalidateTag(fulfillmentCacheTag);
|
||||
})
|
||||
.catch(medusaError);
|
||||
}
|
||||
|
||||
export async function setShippingMethod({
|
||||
cartId,
|
||||
shippingMethodId,
|
||||
}: {
|
||||
cartId: string;
|
||||
shippingMethodId: string;
|
||||
}) {
|
||||
const headers = {
|
||||
...(await getAuthHeaders()),
|
||||
};
|
||||
|
||||
return sdk.store.cart
|
||||
.addShippingMethod(cartId, { option_id: shippingMethodId }, {}, headers)
|
||||
.then(async () => {
|
||||
const cartCacheTag = await getCacheTag("carts");
|
||||
revalidateTag(cartCacheTag);
|
||||
})
|
||||
.catch(medusaError);
|
||||
}
|
||||
|
||||
export async function initiatePaymentSession(
|
||||
cart: HttpTypes.StoreCart,
|
||||
data: HttpTypes.StoreInitializePaymentSession
|
||||
) {
|
||||
const headers = {
|
||||
...(await getAuthHeaders()),
|
||||
};
|
||||
|
||||
return sdk.store.payment
|
||||
.initiatePaymentSession(cart, data, {}, headers)
|
||||
.then(async (resp) => {
|
||||
const cartCacheTag = await getCacheTag("carts");
|
||||
revalidateTag(cartCacheTag);
|
||||
return resp;
|
||||
})
|
||||
.catch(medusaError);
|
||||
}
|
||||
|
||||
export async function applyPromotions(codes: string[]) {
|
||||
const cartId = await getCartId();
|
||||
|
||||
if (!cartId) {
|
||||
throw new Error("No existing cart found");
|
||||
}
|
||||
|
||||
const headers = {
|
||||
...(await getAuthHeaders()),
|
||||
};
|
||||
|
||||
return sdk.store.cart
|
||||
.update(cartId, { promo_codes: codes }, {}, headers)
|
||||
.then(async () => {
|
||||
const cartCacheTag = await getCacheTag("carts");
|
||||
revalidateTag(cartCacheTag);
|
||||
|
||||
const fulfillmentCacheTag = await getCacheTag("fulfillment");
|
||||
revalidateTag(fulfillmentCacheTag);
|
||||
})
|
||||
.catch(medusaError);
|
||||
}
|
||||
|
||||
export async function applyGiftCard(code: string) {
|
||||
// const cartId = getCartId()
|
||||
// if (!cartId) return "No cartId cookie found"
|
||||
// try {
|
||||
// await updateCart(cartId, { gift_cards: [{ code }] }).then(() => {
|
||||
// revalidateTag("cart")
|
||||
// })
|
||||
// } catch (error: any) {
|
||||
// throw error
|
||||
// }
|
||||
}
|
||||
|
||||
export async function removeDiscount(code: string) {
|
||||
// const cartId = getCartId()
|
||||
// if (!cartId) return "No cartId cookie found"
|
||||
// try {
|
||||
// await deleteDiscount(cartId, code)
|
||||
// revalidateTag("cart")
|
||||
// } catch (error: any) {
|
||||
// throw error
|
||||
// }
|
||||
}
|
||||
|
||||
export async function removeGiftCard(
|
||||
codeToRemove: string,
|
||||
giftCards: any[]
|
||||
// giftCards: GiftCard[]
|
||||
) {
|
||||
// const cartId = getCartId()
|
||||
// if (!cartId) return "No cartId cookie found"
|
||||
// try {
|
||||
// await updateCart(cartId, {
|
||||
// gift_cards: [...giftCards]
|
||||
// .filter((gc) => gc.code !== codeToRemove)
|
||||
// .map((gc) => ({ code: gc.code })),
|
||||
// }).then(() => {
|
||||
// revalidateTag("cart")
|
||||
// })
|
||||
// } catch (error: any) {
|
||||
// throw error
|
||||
// }
|
||||
}
|
||||
|
||||
export async function submitPromotionForm(
|
||||
currentState: unknown,
|
||||
formData: FormData
|
||||
) {
|
||||
const code = formData.get("code") as string;
|
||||
try {
|
||||
await applyPromotions([code]);
|
||||
} catch (e: any) {
|
||||
return e.message;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Pass a POJO instead of a form entity here
|
||||
export async function setAddresses(currentState: unknown, formData: FormData) {
|
||||
try {
|
||||
if (!formData) {
|
||||
throw new Error("No form data found when setting addresses");
|
||||
}
|
||||
const cartId = getCartId();
|
||||
if (!cartId) {
|
||||
throw new Error("No existing cart found when setting addresses");
|
||||
}
|
||||
|
||||
const data = {
|
||||
shipping_address: {
|
||||
first_name: formData.get("shipping_address.first_name"),
|
||||
last_name: formData.get("shipping_address.last_name"),
|
||||
address_1: formData.get("shipping_address.address_1"),
|
||||
address_2: "",
|
||||
company: formData.get("shipping_address.company"),
|
||||
postal_code: formData.get("shipping_address.postal_code"),
|
||||
city: formData.get("shipping_address.city"),
|
||||
country_code: formData.get("shipping_address.country_code"),
|
||||
province: formData.get("shipping_address.province"),
|
||||
phone: formData.get("shipping_address.phone"),
|
||||
},
|
||||
email: formData.get("email"),
|
||||
} as any;
|
||||
|
||||
const sameAsBilling = formData.get("same_as_billing");
|
||||
if (sameAsBilling === "on") data.billing_address = data.shipping_address;
|
||||
|
||||
if (sameAsBilling !== "on")
|
||||
data.billing_address = {
|
||||
first_name: formData.get("billing_address.first_name"),
|
||||
last_name: formData.get("billing_address.last_name"),
|
||||
address_1: formData.get("billing_address.address_1"),
|
||||
address_2: "",
|
||||
company: formData.get("billing_address.company"),
|
||||
postal_code: formData.get("billing_address.postal_code"),
|
||||
city: formData.get("billing_address.city"),
|
||||
country_code: formData.get("billing_address.country_code"),
|
||||
province: formData.get("billing_address.province"),
|
||||
phone: formData.get("billing_address.phone"),
|
||||
};
|
||||
await updateCart(data);
|
||||
} catch (e: any) {
|
||||
return e.message;
|
||||
}
|
||||
|
||||
redirect(
|
||||
`/${formData.get("shipping_address.country_code")}/checkout?step=delivery`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Places an order for a cart. If no cart ID is provided, it will use the cart ID from the cookies.
|
||||
* @param cartId - optional - The ID of the cart to place an order for.
|
||||
* @returns The cart object if the order was successful, or null if not.
|
||||
*/
|
||||
export async function placeOrder(cartId?: string) {
|
||||
const id = cartId || (await getCartId());
|
||||
|
||||
if (!id) {
|
||||
throw new Error("No existing cart found when placing an order");
|
||||
}
|
||||
|
||||
const headers = {
|
||||
...(await getAuthHeaders()),
|
||||
};
|
||||
|
||||
const cartRes = await sdk.store.cart
|
||||
.complete(id, {}, headers)
|
||||
.then(async (cartRes) => {
|
||||
const cartCacheTag = await getCacheTag("carts");
|
||||
revalidateTag(cartCacheTag);
|
||||
return cartRes;
|
||||
})
|
||||
.catch(medusaError);
|
||||
|
||||
if (cartRes?.type === "order") {
|
||||
const countryCode =
|
||||
cartRes.order.shipping_address?.country_code?.toLowerCase();
|
||||
|
||||
const orderCacheTag = await getCacheTag("orders");
|
||||
revalidateTag(orderCacheTag);
|
||||
|
||||
removeCartId();
|
||||
redirect(`/${countryCode}/order/${cartRes?.order.id}/confirmed`);
|
||||
}
|
||||
|
||||
return cartRes.cart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the countrycode param and revalidates the regions cache
|
||||
* @param regionId
|
||||
* @param countryCode
|
||||
*/
|
||||
export async function updateRegion(countryCode: string, currentPath: string) {
|
||||
const cartId = await getCartId();
|
||||
const region = await getRegion(countryCode);
|
||||
|
||||
if (!region) {
|
||||
throw new Error(`Region not found for country code: ${countryCode}`);
|
||||
}
|
||||
|
||||
if (cartId) {
|
||||
await updateCart({ region_id: region.id });
|
||||
const cartCacheTag = await getCacheTag("carts");
|
||||
revalidateTag(cartCacheTag);
|
||||
}
|
||||
|
||||
const regionCacheTag = await getCacheTag("regions");
|
||||
revalidateTag(regionCacheTag);
|
||||
|
||||
const productsCacheTag = await getCacheTag("products");
|
||||
revalidateTag(productsCacheTag);
|
||||
|
||||
redirect(`/${countryCode}${currentPath}`);
|
||||
}
|
||||
|
||||
export async function listCartOptions() {
|
||||
const cartId = await getCartId();
|
||||
const headers = {
|
||||
...(await getAuthHeaders()),
|
||||
};
|
||||
const next = {
|
||||
...(await getCacheOptions("shippingOptions")),
|
||||
};
|
||||
|
||||
return await sdk.client.fetch<{
|
||||
shipping_options: HttpTypes.StoreCartShippingOption[];
|
||||
}>("/store/shipping-options", {
|
||||
query: { cart_id: cartId },
|
||||
next,
|
||||
headers,
|
||||
cache: "force-cache",
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { sdk } from "@lib/config"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { getCacheOptions } from "./cookies"
|
||||
|
||||
export const listCategories = async (query?: Record<string, any>) => {
|
||||
const next = {
|
||||
...(await getCacheOptions("categories")),
|
||||
}
|
||||
|
||||
const limit = query?.limit || 100
|
||||
|
||||
return sdk.client
|
||||
.fetch<{ product_categories: HttpTypes.StoreProductCategory[] }>(
|
||||
"/store/product-categories",
|
||||
{
|
||||
query: {
|
||||
fields:
|
||||
"*category_children, *products, *parent_category, *parent_category.parent_category",
|
||||
limit,
|
||||
...query,
|
||||
},
|
||||
next,
|
||||
cache: "force-cache",
|
||||
}
|
||||
)
|
||||
.then(({ product_categories }) => product_categories)
|
||||
}
|
||||
|
||||
export const getCategoryByHandle = async (categoryHandle: string[]) => {
|
||||
const handle = `${categoryHandle.join("/")}`
|
||||
|
||||
const next = {
|
||||
...(await getCacheOptions("categories")),
|
||||
}
|
||||
|
||||
return sdk.client
|
||||
.fetch<HttpTypes.StoreProductCategoryListResponse>(
|
||||
`/store/product-categories`,
|
||||
{
|
||||
query: {
|
||||
fields: "*category_children, *products",
|
||||
handle,
|
||||
},
|
||||
next,
|
||||
cache: "force-cache",
|
||||
}
|
||||
)
|
||||
.then(({ product_categories }) => product_categories[0])
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
"use server"
|
||||
|
||||
import { sdk } from "@lib/config"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { getCacheOptions } from "./cookies"
|
||||
|
||||
export const retrieveCollection = async (id: string) => {
|
||||
const next = {
|
||||
...(await getCacheOptions("collections")),
|
||||
}
|
||||
|
||||
return sdk.client
|
||||
.fetch<{ collection: HttpTypes.StoreCollection }>(
|
||||
`/store/collections/${id}`,
|
||||
{
|
||||
next,
|
||||
cache: "force-cache",
|
||||
}
|
||||
)
|
||||
.then(({ collection }) => collection)
|
||||
}
|
||||
|
||||
export const listCollections = async (
|
||||
queryParams: Record<string, string> = {}
|
||||
): Promise<{ collections: HttpTypes.StoreCollection[]; count: number }> => {
|
||||
const next = {
|
||||
...(await getCacheOptions("collections")),
|
||||
}
|
||||
|
||||
queryParams.limit = queryParams.limit || "100"
|
||||
queryParams.offset = queryParams.offset || "0"
|
||||
|
||||
return sdk.client
|
||||
.fetch<{ collections: HttpTypes.StoreCollection[]; count: number }>(
|
||||
"/store/collections",
|
||||
{
|
||||
query: queryParams,
|
||||
next,
|
||||
cache: "force-cache",
|
||||
}
|
||||
)
|
||||
.then(({ collections }) => ({ collections, count: collections.length }))
|
||||
}
|
||||
|
||||
export const getCollectionByHandle = async (
|
||||
handle: string
|
||||
): Promise<HttpTypes.StoreCollection> => {
|
||||
const next = {
|
||||
...(await getCacheOptions("collections")),
|
||||
}
|
||||
|
||||
return sdk.client
|
||||
.fetch<HttpTypes.StoreCollectionListResponse>(`/store/collections`, {
|
||||
query: { handle, fields: "*products" },
|
||||
next,
|
||||
cache: "force-cache",
|
||||
})
|
||||
.then(({ collections }) => collections[0])
|
||||
}
|
||||
89
packages/features/medusa-storefront/src/lib/data/cookies.ts
Normal file
89
packages/features/medusa-storefront/src/lib/data/cookies.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import "server-only"
|
||||
import { cookies as nextCookies } from "next/headers"
|
||||
|
||||
export const getAuthHeaders = async (): Promise<
|
||||
{ authorization: string } | {}
|
||||
> => {
|
||||
try {
|
||||
const cookies = await nextCookies()
|
||||
const token = cookies.get("_medusa_jwt")?.value
|
||||
|
||||
if (!token) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return { authorization: `Bearer ${token}` }
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export const getCacheTag = async (tag: string): Promise<string> => {
|
||||
try {
|
||||
const cookies = await nextCookies()
|
||||
const cacheId = cookies.get("_medusa_cache_id")?.value
|
||||
|
||||
if (!cacheId) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return `${tag}-${cacheId}`
|
||||
} catch (error) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
export const getCacheOptions = async (
|
||||
tag: string
|
||||
): Promise<{ tags: string[] } | {}> => {
|
||||
if (typeof window !== "undefined") {
|
||||
return {}
|
||||
}
|
||||
|
||||
const cacheTag = await getCacheTag(tag)
|
||||
|
||||
if (!cacheTag) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return { tags: [`${cacheTag}`] }
|
||||
}
|
||||
|
||||
export const setAuthToken = async (token: string) => {
|
||||
const cookies = await nextCookies()
|
||||
cookies.set("_medusa_jwt", token, {
|
||||
maxAge: 60 * 60 * 24 * 7,
|
||||
httpOnly: true,
|
||||
sameSite: "strict",
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
})
|
||||
}
|
||||
|
||||
export const removeAuthToken = async () => {
|
||||
const cookies = await nextCookies()
|
||||
cookies.set("_medusa_jwt", "", {
|
||||
maxAge: -1,
|
||||
})
|
||||
}
|
||||
|
||||
export const getCartId = async () => {
|
||||
const cookies = await nextCookies()
|
||||
return cookies.get("_medusa_cart_id")?.value
|
||||
}
|
||||
|
||||
export const setCartId = async (cartId: string) => {
|
||||
const cookies = await nextCookies()
|
||||
cookies.set("_medusa_cart_id", cartId, {
|
||||
maxAge: 60 * 60 * 24 * 7,
|
||||
httpOnly: true,
|
||||
sameSite: "strict",
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
})
|
||||
}
|
||||
|
||||
export const removeCartId = async () => {
|
||||
const cookies = await nextCookies()
|
||||
cookies.set("_medusa_cart_id", "", {
|
||||
maxAge: -1,
|
||||
})
|
||||
}
|
||||
261
packages/features/medusa-storefront/src/lib/data/customer.ts
Normal file
261
packages/features/medusa-storefront/src/lib/data/customer.ts
Normal file
@@ -0,0 +1,261 @@
|
||||
"use server"
|
||||
|
||||
import { sdk } from "@lib/config"
|
||||
import medusaError from "@lib/util/medusa-error"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { revalidateTag } from "next/cache"
|
||||
import { redirect } from "next/navigation"
|
||||
import {
|
||||
getAuthHeaders,
|
||||
getCacheOptions,
|
||||
getCacheTag,
|
||||
getCartId,
|
||||
removeAuthToken,
|
||||
removeCartId,
|
||||
setAuthToken,
|
||||
} from "./cookies"
|
||||
|
||||
export const retrieveCustomer =
|
||||
async (): Promise<HttpTypes.StoreCustomer | null> => {
|
||||
const authHeaders = await getAuthHeaders()
|
||||
|
||||
if (!authHeaders) return null
|
||||
|
||||
const headers = {
|
||||
...authHeaders,
|
||||
}
|
||||
|
||||
const next = {
|
||||
...(await getCacheOptions("customers")),
|
||||
}
|
||||
|
||||
return await sdk.client
|
||||
.fetch<{ customer: HttpTypes.StoreCustomer }>(`/store/customers/me`, {
|
||||
method: "GET",
|
||||
query: {
|
||||
fields: "*orders",
|
||||
},
|
||||
headers,
|
||||
next,
|
||||
cache: "force-cache",
|
||||
})
|
||||
.then(({ customer }) => customer)
|
||||
.catch(() => null)
|
||||
}
|
||||
|
||||
export const updateCustomer = async (body: HttpTypes.StoreUpdateCustomer) => {
|
||||
const headers = {
|
||||
...(await getAuthHeaders()),
|
||||
}
|
||||
|
||||
const updateRes = await sdk.store.customer
|
||||
.update(body, {}, headers)
|
||||
.then(({ customer }) => customer)
|
||||
.catch(medusaError)
|
||||
|
||||
const cacheTag = await getCacheTag("customers")
|
||||
revalidateTag(cacheTag)
|
||||
|
||||
return updateRes
|
||||
}
|
||||
|
||||
export async function signup(_currentState: unknown, formData: FormData) {
|
||||
const password = formData.get("password") as string
|
||||
const customerForm = {
|
||||
email: formData.get("email") as string,
|
||||
first_name: formData.get("first_name") as string,
|
||||
last_name: formData.get("last_name") as string,
|
||||
phone: formData.get("phone") as string,
|
||||
}
|
||||
|
||||
try {
|
||||
const token = await sdk.auth.register("customer", "emailpass", {
|
||||
email: customerForm.email,
|
||||
password: password,
|
||||
})
|
||||
|
||||
await setAuthToken(token as string)
|
||||
|
||||
const headers = {
|
||||
...(await getAuthHeaders()),
|
||||
}
|
||||
|
||||
const { customer: createdCustomer } = await sdk.store.customer.create(
|
||||
customerForm,
|
||||
{},
|
||||
headers
|
||||
)
|
||||
|
||||
const loginToken = await sdk.auth.login("customer", "emailpass", {
|
||||
email: customerForm.email,
|
||||
password,
|
||||
})
|
||||
|
||||
await setAuthToken(loginToken as string)
|
||||
|
||||
const customerCacheTag = await getCacheTag("customers")
|
||||
revalidateTag(customerCacheTag)
|
||||
|
||||
await transferCart()
|
||||
|
||||
return createdCustomer
|
||||
} catch (error: any) {
|
||||
return error.toString()
|
||||
}
|
||||
}
|
||||
|
||||
export async function login(_currentState: unknown, formData: FormData) {
|
||||
const email = formData.get("email") as string
|
||||
const password = formData.get("password") as string
|
||||
|
||||
try {
|
||||
await sdk.auth
|
||||
.login("customer", "emailpass", { email, password })
|
||||
.then(async (token) => {
|
||||
await setAuthToken(token as string)
|
||||
const customerCacheTag = await getCacheTag("customers")
|
||||
revalidateTag(customerCacheTag)
|
||||
})
|
||||
} catch (error: any) {
|
||||
return error.toString()
|
||||
}
|
||||
|
||||
try {
|
||||
await transferCart()
|
||||
} catch (error: any) {
|
||||
return error.toString()
|
||||
}
|
||||
}
|
||||
|
||||
export async function signout(countryCode: string) {
|
||||
await sdk.auth.logout()
|
||||
|
||||
await removeAuthToken()
|
||||
|
||||
const customerCacheTag = await getCacheTag("customers")
|
||||
revalidateTag(customerCacheTag)
|
||||
|
||||
await removeCartId()
|
||||
|
||||
const cartCacheTag = await getCacheTag("carts")
|
||||
revalidateTag(cartCacheTag)
|
||||
|
||||
redirect(`/${countryCode}/account`)
|
||||
}
|
||||
|
||||
export async function transferCart() {
|
||||
const cartId = await getCartId()
|
||||
|
||||
if (!cartId) {
|
||||
return
|
||||
}
|
||||
|
||||
const headers = await getAuthHeaders()
|
||||
|
||||
await sdk.store.cart.transferCart(cartId, {}, headers)
|
||||
|
||||
const cartCacheTag = await getCacheTag("carts")
|
||||
revalidateTag(cartCacheTag)
|
||||
}
|
||||
|
||||
export const addCustomerAddress = async (
|
||||
currentState: Record<string, unknown>,
|
||||
formData: FormData
|
||||
): Promise<any> => {
|
||||
const isDefaultBilling = (currentState.isDefaultBilling as boolean) || false
|
||||
const isDefaultShipping = (currentState.isDefaultShipping as boolean) || false
|
||||
|
||||
const address = {
|
||||
first_name: formData.get("first_name") as string,
|
||||
last_name: formData.get("last_name") as string,
|
||||
company: formData.get("company") as string,
|
||||
address_1: formData.get("address_1") as string,
|
||||
address_2: formData.get("address_2") as string,
|
||||
city: formData.get("city") as string,
|
||||
postal_code: formData.get("postal_code") as string,
|
||||
province: formData.get("province") as string,
|
||||
country_code: formData.get("country_code") as string,
|
||||
phone: formData.get("phone") as string,
|
||||
is_default_billing: isDefaultBilling,
|
||||
is_default_shipping: isDefaultShipping,
|
||||
}
|
||||
|
||||
const headers = {
|
||||
...(await getAuthHeaders()),
|
||||
}
|
||||
|
||||
return sdk.store.customer
|
||||
.createAddress(address, {}, headers)
|
||||
.then(async ({ customer }) => {
|
||||
const customerCacheTag = await getCacheTag("customers")
|
||||
revalidateTag(customerCacheTag)
|
||||
return { success: true, error: null }
|
||||
})
|
||||
.catch((err) => {
|
||||
return { success: false, error: err.toString() }
|
||||
})
|
||||
}
|
||||
|
||||
export const deleteCustomerAddress = async (
|
||||
addressId: string
|
||||
): Promise<void> => {
|
||||
const headers = {
|
||||
...(await getAuthHeaders()),
|
||||
}
|
||||
|
||||
await sdk.store.customer
|
||||
.deleteAddress(addressId, headers)
|
||||
.then(async () => {
|
||||
const customerCacheTag = await getCacheTag("customers")
|
||||
revalidateTag(customerCacheTag)
|
||||
return { success: true, error: null }
|
||||
})
|
||||
.catch((err) => {
|
||||
return { success: false, error: err.toString() }
|
||||
})
|
||||
}
|
||||
|
||||
export const updateCustomerAddress = async (
|
||||
currentState: Record<string, unknown>,
|
||||
formData: FormData
|
||||
): Promise<any> => {
|
||||
const addressId =
|
||||
(currentState.addressId as string) || (formData.get("addressId") as string)
|
||||
|
||||
if (!addressId) {
|
||||
return { success: false, error: "Address ID is required" }
|
||||
}
|
||||
|
||||
const address = {
|
||||
first_name: formData.get("first_name") as string,
|
||||
last_name: formData.get("last_name") as string,
|
||||
company: formData.get("company") as string,
|
||||
address_1: formData.get("address_1") as string,
|
||||
address_2: formData.get("address_2") as string,
|
||||
city: formData.get("city") as string,
|
||||
postal_code: formData.get("postal_code") as string,
|
||||
province: formData.get("province") as string,
|
||||
country_code: formData.get("country_code") as string,
|
||||
} as HttpTypes.StoreUpdateCustomerAddress
|
||||
|
||||
const phone = formData.get("phone") as string
|
||||
|
||||
if (phone) {
|
||||
address.phone = phone
|
||||
}
|
||||
|
||||
const headers = {
|
||||
...(await getAuthHeaders()),
|
||||
}
|
||||
|
||||
return sdk.store.customer
|
||||
.updateAddress(addressId, address, {}, headers)
|
||||
.then(async () => {
|
||||
const customerCacheTag = await getCacheTag("customers")
|
||||
revalidateTag(customerCacheTag)
|
||||
return { success: true, error: null }
|
||||
})
|
||||
.catch((err) => {
|
||||
return { success: false, error: err.toString() }
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
"use server"
|
||||
|
||||
import { sdk } from "@lib/config"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { getAuthHeaders, getCacheOptions } from "./cookies"
|
||||
|
||||
export const listCartShippingMethods = async (cartId: string) => {
|
||||
const headers = {
|
||||
...(await getAuthHeaders()),
|
||||
}
|
||||
|
||||
const next = {
|
||||
...(await getCacheOptions("fulfillment")),
|
||||
}
|
||||
|
||||
return sdk.client
|
||||
.fetch<HttpTypes.StoreShippingOptionListResponse>(
|
||||
`/store/shipping-options`,
|
||||
{
|
||||
method: "GET",
|
||||
query: {
|
||||
cart_id: cartId,
|
||||
fields:
|
||||
"+service_zone.fulfllment_set.type,*service_zone.fulfillment_set.location.address",
|
||||
},
|
||||
headers,
|
||||
next,
|
||||
cache: "force-cache",
|
||||
}
|
||||
)
|
||||
.then(({ shipping_options }) => shipping_options)
|
||||
.catch(() => {
|
||||
return null
|
||||
})
|
||||
}
|
||||
|
||||
export const calculatePriceForShippingOption = async (
|
||||
optionId: string,
|
||||
cartId: string,
|
||||
data?: Record<string, unknown>
|
||||
) => {
|
||||
const headers = {
|
||||
...(await getAuthHeaders()),
|
||||
}
|
||||
|
||||
const next = {
|
||||
...(await getCacheOptions("fulfillment")),
|
||||
}
|
||||
|
||||
const body = { cart_id: cartId, data }
|
||||
|
||||
if (data) {
|
||||
body.data = data
|
||||
}
|
||||
|
||||
return sdk.client
|
||||
.fetch<{ shipping_option: HttpTypes.StoreCartShippingOption }>(
|
||||
`/store/shipping-options/${optionId}/calculate`,
|
||||
{
|
||||
method: "POST",
|
||||
body,
|
||||
headers,
|
||||
next,
|
||||
}
|
||||
)
|
||||
.then(({ shipping_option }) => shipping_option)
|
||||
.catch((e) => {
|
||||
return null
|
||||
})
|
||||
}
|
||||
11
packages/features/medusa-storefront/src/lib/data/index.ts
Normal file
11
packages/features/medusa-storefront/src/lib/data/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export * from "./cart";
|
||||
export * from "./categories";
|
||||
export * from "./collections";
|
||||
export * from "./cookies";
|
||||
export * from "./customer";
|
||||
export * from "./fulfillment";
|
||||
export * from "./onboarding";
|
||||
export * from "./orders";
|
||||
export * from "./payment";
|
||||
export * from "./products";
|
||||
export * from "./regions";
|
||||
@@ -0,0 +1,9 @@
|
||||
"use server"
|
||||
import { cookies as nextCookies } from "next/headers"
|
||||
import { redirect } from "next/navigation"
|
||||
|
||||
export async function resetOnboardingState(orderId: string) {
|
||||
const cookies = await nextCookies()
|
||||
cookies.set("_medusa_onboarding", "false", { maxAge: -1 })
|
||||
redirect(`http://localhost:7001/a/orders/${orderId}`)
|
||||
}
|
||||
112
packages/features/medusa-storefront/src/lib/data/orders.ts
Normal file
112
packages/features/medusa-storefront/src/lib/data/orders.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
"use server"
|
||||
|
||||
import { sdk } from "@lib/config"
|
||||
import medusaError from "@lib/util/medusa-error"
|
||||
import { getAuthHeaders, getCacheOptions } from "./cookies"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
|
||||
export const retrieveOrder = async (id: string) => {
|
||||
const headers = {
|
||||
...(await getAuthHeaders()),
|
||||
}
|
||||
|
||||
const next = {
|
||||
...(await getCacheOptions("orders")),
|
||||
}
|
||||
|
||||
return sdk.client
|
||||
.fetch<HttpTypes.StoreOrderResponse>(`/store/orders/${id}`, {
|
||||
method: "GET",
|
||||
query: {
|
||||
fields:
|
||||
"*payment_collections.payments,*items,*items.metadata,*items.variant,*items.product",
|
||||
},
|
||||
headers,
|
||||
next,
|
||||
cache: "force-cache",
|
||||
})
|
||||
.then(({ order }) => order)
|
||||
.catch((err) => medusaError(err))
|
||||
}
|
||||
|
||||
export const listOrders = async (
|
||||
limit: number = 10,
|
||||
offset: number = 0,
|
||||
filters?: Record<string, any>
|
||||
) => {
|
||||
const headers = {
|
||||
...(await getAuthHeaders()),
|
||||
}
|
||||
|
||||
const next = {
|
||||
...(await getCacheOptions("orders")),
|
||||
}
|
||||
|
||||
return sdk.client
|
||||
.fetch<HttpTypes.StoreOrderListResponse>(`/store/orders`, {
|
||||
method: "GET",
|
||||
query: {
|
||||
limit,
|
||||
offset,
|
||||
order: "-created_at",
|
||||
fields: "*items,+items.metadata,*items.variant,*items.product",
|
||||
...filters,
|
||||
},
|
||||
headers,
|
||||
next,
|
||||
cache: "force-cache",
|
||||
})
|
||||
.then(({ orders }) => orders)
|
||||
.catch((err) => medusaError(err))
|
||||
}
|
||||
|
||||
export const createTransferRequest = async (
|
||||
state: {
|
||||
success: boolean
|
||||
error: string | null
|
||||
order: HttpTypes.StoreOrder | null
|
||||
},
|
||||
formData: FormData
|
||||
): Promise<{
|
||||
success: boolean
|
||||
error: string | null
|
||||
order: HttpTypes.StoreOrder | null
|
||||
}> => {
|
||||
const id = formData.get("order_id") as string
|
||||
|
||||
if (!id) {
|
||||
return { success: false, error: "Order ID is required", order: null }
|
||||
}
|
||||
|
||||
const headers = await getAuthHeaders()
|
||||
|
||||
return await sdk.store.order
|
||||
.requestTransfer(
|
||||
id,
|
||||
{},
|
||||
{
|
||||
fields: "id, email",
|
||||
},
|
||||
headers
|
||||
)
|
||||
.then(({ order }) => ({ success: true, error: null, order }))
|
||||
.catch((err) => ({ success: false, error: err.message, order: null }))
|
||||
}
|
||||
|
||||
export const acceptTransferRequest = async (id: string, token: string) => {
|
||||
const headers = await getAuthHeaders()
|
||||
|
||||
return await sdk.store.order
|
||||
.acceptTransfer(id, { token }, {}, headers)
|
||||
.then(({ order }) => ({ success: true, error: null, order }))
|
||||
.catch((err) => ({ success: false, error: err.message, order: null }))
|
||||
}
|
||||
|
||||
export const declineTransferRequest = async (id: string, token: string) => {
|
||||
const headers = await getAuthHeaders()
|
||||
|
||||
return await sdk.store.order
|
||||
.declineTransfer(id, { token }, {}, headers)
|
||||
.then(({ order }) => ({ success: true, error: null, order }))
|
||||
.catch((err) => ({ success: false, error: err.message, order: null }))
|
||||
}
|
||||
35
packages/features/medusa-storefront/src/lib/data/payment.ts
Normal file
35
packages/features/medusa-storefront/src/lib/data/payment.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
"use server"
|
||||
|
||||
import { sdk } from "@lib/config"
|
||||
import { getAuthHeaders, getCacheOptions } from "./cookies"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
|
||||
export const listCartPaymentMethods = async (regionId: string) => {
|
||||
const headers = {
|
||||
...(await getAuthHeaders()),
|
||||
}
|
||||
|
||||
const next = {
|
||||
...(await getCacheOptions("payment_providers")),
|
||||
}
|
||||
|
||||
return sdk.client
|
||||
.fetch<HttpTypes.StorePaymentProviderListResponse>(
|
||||
`/store/payment-providers`,
|
||||
{
|
||||
method: "GET",
|
||||
query: { region_id: regionId },
|
||||
headers,
|
||||
next,
|
||||
cache: "force-cache",
|
||||
}
|
||||
)
|
||||
.then(({ payment_providers }) =>
|
||||
payment_providers.sort((a, b) => {
|
||||
return a.id > b.id ? 1 : -1
|
||||
})
|
||||
)
|
||||
.catch(() => {
|
||||
return null
|
||||
})
|
||||
}
|
||||
136
packages/features/medusa-storefront/src/lib/data/products.ts
Normal file
136
packages/features/medusa-storefront/src/lib/data/products.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
"use server"
|
||||
|
||||
import { sdk } from "@lib/config"
|
||||
import { sortProducts } from "@lib/util/sort-products"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { SortOptions } from "@modules/store/components/refinement-list/sort-products"
|
||||
import { getAuthHeaders, getCacheOptions } from "./cookies"
|
||||
import { getRegion, retrieveRegion } from "./regions"
|
||||
|
||||
export const listProducts = async ({
|
||||
pageParam = 1,
|
||||
queryParams,
|
||||
countryCode,
|
||||
regionId,
|
||||
}: {
|
||||
pageParam?: number
|
||||
queryParams?: HttpTypes.FindParams & HttpTypes.StoreProductParams
|
||||
countryCode?: string
|
||||
regionId?: string
|
||||
}): Promise<{
|
||||
response: { products: HttpTypes.StoreProduct[]; count: number }
|
||||
nextPage: number | null
|
||||
queryParams?: HttpTypes.FindParams & HttpTypes.StoreProductParams
|
||||
}> => {
|
||||
if (!countryCode && !regionId) {
|
||||
throw new Error("Country code or region ID is required")
|
||||
}
|
||||
|
||||
const limit = queryParams?.limit || 12
|
||||
const _pageParam = Math.max(pageParam, 1)
|
||||
const offset = (_pageParam === 1) ? 0 : (_pageParam - 1) * limit;
|
||||
|
||||
let region: HttpTypes.StoreRegion | undefined | null
|
||||
|
||||
if (countryCode) {
|
||||
region = await getRegion(countryCode)
|
||||
} else {
|
||||
region = await retrieveRegion(regionId!)
|
||||
}
|
||||
|
||||
if (!region) {
|
||||
return {
|
||||
response: { products: [], count: 0 },
|
||||
nextPage: null,
|
||||
}
|
||||
}
|
||||
|
||||
const headers = {
|
||||
...(await getAuthHeaders()),
|
||||
}
|
||||
|
||||
const next = {
|
||||
...(await getCacheOptions("products")),
|
||||
}
|
||||
|
||||
return sdk.client
|
||||
.fetch<{ products: HttpTypes.StoreProduct[]; count: number }>(
|
||||
`/store/products`,
|
||||
{
|
||||
method: "GET",
|
||||
query: {
|
||||
limit,
|
||||
offset,
|
||||
region_id: region?.id,
|
||||
fields:
|
||||
"*variants.calculated_price,+variants.inventory_quantity,+metadata,+tags",
|
||||
...queryParams,
|
||||
},
|
||||
headers,
|
||||
next,
|
||||
cache: "force-cache",
|
||||
}
|
||||
)
|
||||
.then(({ products, count }) => {
|
||||
const nextPage = count > offset + limit ? pageParam + 1 : null
|
||||
|
||||
return {
|
||||
response: {
|
||||
products,
|
||||
count,
|
||||
},
|
||||
nextPage: nextPage,
|
||||
queryParams,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* This will fetch 100 products to the Next.js cache and sort them based on the sortBy parameter.
|
||||
* It will then return the paginated products based on the page and limit parameters.
|
||||
*/
|
||||
export const listProductsWithSort = async ({
|
||||
page = 0,
|
||||
queryParams,
|
||||
sortBy = "created_at",
|
||||
countryCode,
|
||||
}: {
|
||||
page?: number
|
||||
queryParams?: HttpTypes.FindParams & HttpTypes.StoreProductParams
|
||||
sortBy?: SortOptions
|
||||
countryCode: string
|
||||
}): Promise<{
|
||||
response: { products: HttpTypes.StoreProduct[]; count: number }
|
||||
nextPage: number | null
|
||||
queryParams?: HttpTypes.FindParams & HttpTypes.StoreProductParams
|
||||
}> => {
|
||||
const limit = queryParams?.limit || 12
|
||||
|
||||
const {
|
||||
response: { products, count },
|
||||
} = await listProducts({
|
||||
pageParam: 0,
|
||||
queryParams: {
|
||||
...queryParams,
|
||||
limit: 100,
|
||||
},
|
||||
countryCode,
|
||||
})
|
||||
|
||||
const sortedProducts = sortProducts(products, sortBy)
|
||||
|
||||
const pageParam = (page - 1) * limit
|
||||
|
||||
const nextPage = count > pageParam + limit ? pageParam + limit : null
|
||||
|
||||
const paginatedProducts = sortedProducts.slice(pageParam, pageParam + limit)
|
||||
|
||||
return {
|
||||
response: {
|
||||
products: paginatedProducts,
|
||||
count,
|
||||
},
|
||||
nextPage,
|
||||
queryParams,
|
||||
}
|
||||
}
|
||||
66
packages/features/medusa-storefront/src/lib/data/regions.ts
Normal file
66
packages/features/medusa-storefront/src/lib/data/regions.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
"use server"
|
||||
|
||||
import { sdk } from "@lib/config"
|
||||
import medusaError from "@lib/util/medusa-error"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { getCacheOptions } from "./cookies"
|
||||
|
||||
export const listRegions = async () => {
|
||||
const next = {
|
||||
...(await getCacheOptions("regions")),
|
||||
}
|
||||
|
||||
return sdk.client
|
||||
.fetch<{ regions: HttpTypes.StoreRegion[] }>(`/store/regions`, {
|
||||
method: "GET",
|
||||
next,
|
||||
cache: "force-cache",
|
||||
})
|
||||
.then(({ regions }) => regions)
|
||||
.catch(medusaError)
|
||||
}
|
||||
|
||||
export const retrieveRegion = async (id: string) => {
|
||||
const next = {
|
||||
...(await getCacheOptions(["regions", id].join("-"))),
|
||||
}
|
||||
|
||||
return sdk.client
|
||||
.fetch<{ region: HttpTypes.StoreRegion }>(`/store/regions/${id}`, {
|
||||
method: "GET",
|
||||
next,
|
||||
cache: "force-cache",
|
||||
})
|
||||
.then(({ region }) => region)
|
||||
.catch(medusaError)
|
||||
}
|
||||
|
||||
const regionMap = new Map<string, HttpTypes.StoreRegion>()
|
||||
|
||||
export const getRegion = async (countryCode: string) => {
|
||||
try {
|
||||
if (regionMap.has(countryCode)) {
|
||||
return regionMap.get(countryCode)
|
||||
}
|
||||
|
||||
const regions = await listRegions()
|
||||
|
||||
if (!regions) {
|
||||
return null
|
||||
}
|
||||
|
||||
regions.forEach((region) => {
|
||||
region.countries?.forEach((c) => {
|
||||
regionMap.set(c?.iso_2 ?? "", region)
|
||||
})
|
||||
})
|
||||
|
||||
const region = countryCode
|
||||
? regionMap.get(countryCode)
|
||||
: regionMap.get("us")
|
||||
|
||||
return region
|
||||
} catch (e: any) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { RefObject, useEffect, useState } from "react"
|
||||
|
||||
export const useIntersection = (
|
||||
element: RefObject<HTMLDivElement | null>,
|
||||
rootMargin: string
|
||||
) => {
|
||||
const [isVisible, setState] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!element.current) {
|
||||
return
|
||||
}
|
||||
|
||||
const el = element.current
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
setState(entry.isIntersecting)
|
||||
},
|
||||
{ rootMargin }
|
||||
)
|
||||
|
||||
observer.observe(el)
|
||||
|
||||
return () => observer.unobserve(el)
|
||||
}, [element, rootMargin])
|
||||
|
||||
return isVisible
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useState } from "react"
|
||||
|
||||
export type StateType = [boolean, () => void, () => void, () => void] & {
|
||||
state: boolean
|
||||
open: () => void
|
||||
close: () => void
|
||||
toggle: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param initialState - boolean
|
||||
* @returns An array like object with `state`, `open`, `close`, and `toggle` properties
|
||||
* to allow both object and array destructuring
|
||||
*
|
||||
* ```
|
||||
* const [showModal, openModal, closeModal, toggleModal] = useToggleState()
|
||||
* // or
|
||||
* const { state, open, close, toggle } = useToggleState()
|
||||
* ```
|
||||
*/
|
||||
|
||||
const useToggleState = (initialState = false) => {
|
||||
const [state, setState] = useState<boolean>(initialState)
|
||||
|
||||
const close = () => {
|
||||
setState(false)
|
||||
}
|
||||
|
||||
const open = () => {
|
||||
setState(true)
|
||||
}
|
||||
|
||||
const toggle = () => {
|
||||
setState((state) => !state)
|
||||
}
|
||||
|
||||
const hookData = [state, open, close, toggle] as StateType
|
||||
hookData.state = state
|
||||
hookData.open = open
|
||||
hookData.close = close
|
||||
hookData.toggle = toggle
|
||||
return hookData
|
||||
}
|
||||
|
||||
export default useToggleState
|
||||
@@ -0,0 +1,28 @@
|
||||
import { isEqual, pick } from "lodash"
|
||||
|
||||
export default function compareAddresses(address1: any, address2: any) {
|
||||
return isEqual(
|
||||
pick(address1, [
|
||||
"first_name",
|
||||
"last_name",
|
||||
"address_1",
|
||||
"company",
|
||||
"postal_code",
|
||||
"city",
|
||||
"country_code",
|
||||
"province",
|
||||
"phone",
|
||||
]),
|
||||
pick(address2, [
|
||||
"first_name",
|
||||
"last_name",
|
||||
"address_1",
|
||||
"company",
|
||||
"postal_code",
|
||||
"city",
|
||||
"country_code",
|
||||
"province",
|
||||
"phone",
|
||||
])
|
||||
)
|
||||
}
|
||||
3
packages/features/medusa-storefront/src/lib/util/env.ts
Normal file
3
packages/features/medusa-storefront/src/lib/util/env.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const getBaseURL = () => {
|
||||
return process.env.NEXT_PUBLIC_BASE_URL || "https://localhost:8000"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export const getPercentageDiff = (original: number, calculated: number) => {
|
||||
const diff = original - calculated
|
||||
const decrease = (diff / original) * 100
|
||||
|
||||
return decrease.toFixed()
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { getPercentageDiff } from "./get-precentage-diff"
|
||||
import { convertToLocale } from "./money"
|
||||
|
||||
export const getPricesForVariant = (variant: any) => {
|
||||
if (!variant?.calculated_price?.calculated_amount) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
calculated_price_number: variant.calculated_price.calculated_amount,
|
||||
calculated_price: convertToLocale({
|
||||
amount: variant.calculated_price.calculated_amount,
|
||||
currency_code: variant.calculated_price.currency_code,
|
||||
}),
|
||||
original_price_number: variant.calculated_price.original_amount,
|
||||
original_price: convertToLocale({
|
||||
amount: variant.calculated_price.original_amount,
|
||||
currency_code: variant.calculated_price.currency_code,
|
||||
}),
|
||||
currency_code: variant.calculated_price.currency_code,
|
||||
price_type: variant.calculated_price.calculated_price.price_list_type,
|
||||
percentage_diff: getPercentageDiff(
|
||||
variant.calculated_price.original_amount,
|
||||
variant.calculated_price.calculated_amount
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
export function getProductPrice({
|
||||
product,
|
||||
variantId,
|
||||
}: {
|
||||
product: HttpTypes.StoreProduct
|
||||
variantId?: string
|
||||
}) {
|
||||
if (!product || !product.id) {
|
||||
throw new Error("No product provided")
|
||||
}
|
||||
|
||||
const cheapestPrice = () => {
|
||||
if (!product || !product.variants?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const cheapestVariant: any = product.variants
|
||||
.filter((v: any) => !!v.calculated_price)
|
||||
.sort((a: any, b: any) => {
|
||||
return (
|
||||
a.calculated_price.calculated_amount -
|
||||
b.calculated_price.calculated_amount
|
||||
)
|
||||
})[0]
|
||||
|
||||
return getPricesForVariant(cheapestVariant)
|
||||
}
|
||||
|
||||
const variantPrice = () => {
|
||||
if (!product || !variantId) {
|
||||
return null
|
||||
}
|
||||
|
||||
const variant: any = product.variants?.find(
|
||||
(v) => v.id === variantId || v.sku === variantId
|
||||
)
|
||||
|
||||
if (!variant) {
|
||||
return null
|
||||
}
|
||||
|
||||
return getPricesForVariant(variant)
|
||||
}
|
||||
|
||||
return {
|
||||
product,
|
||||
cheapestPrice: cheapestPrice(),
|
||||
variantPrice: variantPrice(),
|
||||
}
|
||||
}
|
||||
10
packages/features/medusa-storefront/src/lib/util/index.ts
Normal file
10
packages/features/medusa-storefront/src/lib/util/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export * from "./compare-addresses";
|
||||
export * from "./env";
|
||||
export * from "./get-precentage-diff";
|
||||
export * from "./get-product-price";
|
||||
export * from "./isEmpty";
|
||||
export * from "./medusa-error";
|
||||
export * from "./money";
|
||||
export * from "./product";
|
||||
export * from "./repeat";
|
||||
export * from "./sort-products";
|
||||
11
packages/features/medusa-storefront/src/lib/util/isEmpty.ts
Normal file
11
packages/features/medusa-storefront/src/lib/util/isEmpty.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export const isObject = (input: any) => input instanceof Object
|
||||
export const isArray = (input: any) => Array.isArray(input)
|
||||
export const isEmpty = (input: any) => {
|
||||
return (
|
||||
input === null ||
|
||||
input === undefined ||
|
||||
(isObject(input) && Object.keys(input).length === 0) ||
|
||||
(isArray(input) && (input as any[]).length === 0) ||
|
||||
(typeof input === "string" && input.trim().length === 0)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
export default function medusaError(error: any): never {
|
||||
if (error.response) {
|
||||
// The request was made and the server responded with a status code
|
||||
// that falls out of the range of 2xx
|
||||
const u = new URL(error.config.url, error.config.baseURL)
|
||||
console.error("Resource:", u.toString())
|
||||
console.error("Response data:", error.response.data)
|
||||
console.error("Status code:", error.response.status)
|
||||
console.error("Headers:", error.response.headers)
|
||||
|
||||
// Extracting the error message from the response data
|
||||
const message = error.response.data.message || error.response.data
|
||||
|
||||
throw new Error(message.charAt(0).toUpperCase() + message.slice(1) + ".")
|
||||
} else if (error.request) {
|
||||
// The request was made but no response was received
|
||||
throw new Error("No response received: " + error.request)
|
||||
} else {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
throw new Error("Error setting up the request: " + error.message)
|
||||
}
|
||||
}
|
||||
26
packages/features/medusa-storefront/src/lib/util/money.ts
Normal file
26
packages/features/medusa-storefront/src/lib/util/money.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { isEmpty } from "./isEmpty"
|
||||
|
||||
type ConvertToLocaleParams = {
|
||||
amount: number
|
||||
currency_code: string
|
||||
minimumFractionDigits?: number
|
||||
maximumFractionDigits?: number
|
||||
locale?: string
|
||||
}
|
||||
|
||||
export const convertToLocale = ({
|
||||
amount,
|
||||
currency_code,
|
||||
minimumFractionDigits,
|
||||
maximumFractionDigits,
|
||||
locale = "en-US",
|
||||
}: ConvertToLocaleParams) => {
|
||||
return currency_code && !isEmpty(currency_code)
|
||||
? new Intl.NumberFormat(locale, {
|
||||
style: "currency",
|
||||
currency: currency_code,
|
||||
minimumFractionDigits,
|
||||
maximumFractionDigits,
|
||||
}).format(amount)
|
||||
: amount.toString()
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { HttpTypes } from "@medusajs/types";
|
||||
|
||||
export const isSimpleProduct = (product: HttpTypes.StoreProduct): boolean => {
|
||||
return product.options?.length === 1 && product.options[0].values?.length === 1;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
const repeat = (times: number) => {
|
||||
return Array.from(Array(times).keys())
|
||||
}
|
||||
|
||||
export default repeat
|
||||
@@ -0,0 +1,50 @@
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { SortOptions } from "@modules/store/components/refinement-list/sort-products"
|
||||
|
||||
interface MinPricedProduct extends HttpTypes.StoreProduct {
|
||||
_minPrice?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to sort products by price until the store API supports sorting by price
|
||||
* @param products
|
||||
* @param sortBy
|
||||
* @returns products sorted by price
|
||||
*/
|
||||
export function sortProducts(
|
||||
products: HttpTypes.StoreProduct[],
|
||||
sortBy: SortOptions
|
||||
): HttpTypes.StoreProduct[] {
|
||||
let sortedProducts = products as MinPricedProduct[]
|
||||
|
||||
if (["price_asc", "price_desc"].includes(sortBy)) {
|
||||
// Precompute the minimum price for each product
|
||||
sortedProducts.forEach((product) => {
|
||||
if (product.variants && product.variants.length > 0) {
|
||||
product._minPrice = Math.min(
|
||||
...product.variants.map(
|
||||
(variant) => variant?.calculated_price?.calculated_amount || 0
|
||||
)
|
||||
)
|
||||
} else {
|
||||
product._minPrice = Infinity
|
||||
}
|
||||
})
|
||||
|
||||
// Sort products based on the precomputed minimum prices
|
||||
sortedProducts.sort((a, b) => {
|
||||
const diff = a._minPrice! - b._minPrice!
|
||||
return sortBy === "price_asc" ? diff : -diff
|
||||
})
|
||||
}
|
||||
|
||||
if (sortBy === "created_at") {
|
||||
sortedProducts.sort((a, b) => {
|
||||
return (
|
||||
new Date(b.created_at!).getTime() - new Date(a.created_at!).getTime()
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return sortedProducts
|
||||
}
|
||||
Reference in New Issue
Block a user