prettier fix

This commit is contained in:
Danel Kungla
2025-09-19 17:22:36 +03:00
parent efa94b3322
commit 0c2cfe6d18
509 changed files with 17988 additions and 9920 deletions

View File

@@ -1,7 +1,7 @@
import Medusa from "@medusajs/js-sdk";
import Medusa from '@medusajs/js-sdk';
// Defaults to standard port for Medusa server
let MEDUSA_BACKEND_URL = "http://localhost:9000";
let MEDUSA_BACKEND_URL = 'http://localhost:9000';
if (process.env.MEDUSA_BACKEND_URL) {
MEDUSA_BACKEND_URL = process.env.MEDUSA_BACKEND_URL;
@@ -9,7 +9,7 @@ if (process.env.MEDUSA_BACKEND_URL) {
export const SDK_CONFIG = {
baseUrl: MEDUSA_BACKEND_URL,
debug: process.env.NODE_ENV === "development",
debug: process.env.NODE_ENV === 'development',
publishableKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY,
};

View File

@@ -1,9 +1,9 @@
import React from "react"
import { CreditCard } from "@medusajs/icons"
import React from 'react';
import Ideal from "@modules/common/icons/ideal"
import Bancontact from "@modules/common/icons/bancontact"
import PayPal from "@modules/common/icons/paypal"
import { CreditCard } from '@medusajs/icons';
import Bancontact from '@modules/common/icons/bancontact';
import Ideal from '@modules/common/icons/ideal';
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<
@@ -11,58 +11,58 @@ export const paymentInfoMap: Record<
{ title: string; icon: React.JSX.Element }
> = {
pp_stripe_stripe: {
title: "Credit card",
title: 'Credit card',
icon: <CreditCard />,
},
"pp_stripe-ideal_stripe": {
title: "iDeal",
'pp_stripe-ideal_stripe': {
title: 'iDeal',
icon: <Ideal />,
},
"pp_stripe-bancontact_stripe": {
title: "Bancontact",
'pp_stripe-bancontact_stripe': {
title: 'Bancontact',
icon: <Bancontact />,
},
pp_paypal_paypal: {
title: "PayPal",
title: 'PayPal',
icon: <PayPal />,
},
pp_system_default: {
title: "Manual Payment",
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_")
}
return providerId?.startsWith('pp_stripe_');
};
export const isPaypal = (providerId?: string) => {
return providerId?.startsWith("pp_paypal")
}
return providerId?.startsWith('pp_paypal');
};
export const isManual = (providerId?: string) => {
return providerId?.startsWith("pp_system_default")
}
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",
]
'krw',
'jpy',
'vnd',
'clp',
'pyg',
'xaf',
'xof',
'bif',
'djf',
'gnf',
'kmf',
'mga',
'rwf',
'xpf',
'htg',
'vuv',
'xag',
'xdr',
'xau',
];

View File

@@ -1,16 +1,16 @@
"use client"
'use client';
import React, { createContext, useContext } from "react"
import React, { createContext, useContext } from 'react';
interface ModalContext {
close: () => void
close: () => void;
}
const ModalContext = createContext<ModalContext | null>(null)
const ModalContext = createContext<ModalContext | null>(null);
interface ModalProviderProps {
children?: React.ReactNode
close: () => void
children?: React.ReactNode;
close: () => void;
}
export const ModalProvider = ({ children, close }: ModalProviderProps) => {
@@ -22,13 +22,13 @@ export const ModalProvider = ({ children, close }: ModalProviderProps) => {
>
{children}
</ModalContext.Provider>
)
}
);
};
export const useModal = () => {
const context = useContext(ModalContext)
const context = useContext(ModalContext);
if (context === null) {
throw new Error("useModal must be used within a ModalProvider")
throw new Error('useModal must be used within a ModalProvider');
}
return context
}
return context;
};

View File

@@ -1,9 +1,12 @@
"use server";
'use server';
import { revalidateTag } from 'next/cache';
import { redirect } from 'next/navigation';
import { sdk } from '@lib/config';
import medusaError from '@lib/util/medusa-error';
import { HttpTypes } from '@medusajs/types';
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,
@@ -11,10 +14,9 @@ import {
getCartId,
removeCartId,
setCartId,
} from "./cookies";
import { getRegion } from "./regions";
import { sdk } from "@lib/config";
import { retrieveOrder } from "./orders";
} from './cookies';
import { retrieveOrder } from './orders';
import { getRegion } from './regions';
/**
* Retrieves a cart by its ID. If no ID is provided, it will use the cart ID from the cookies.
@@ -33,15 +35,15 @@ export async function retrieveCart(cartId?: string) {
};
const next = {
...(await getCacheOptions("carts")),
...(await getCacheOptions('carts')),
};
return await sdk.client
.fetch<HttpTypes.StoreCartResponse>(`/store/carts/${id}`, {
method: "GET",
method: 'GET',
query: {
fields:
"*items, *region, *items.product, *items.variant, *items.thumbnail, *items.metadata, +items.total, *promotions, +shipping_methods.name",
'*items, *region, *items.product, *items.variant, *items.thumbnail, *items.metadata, +items.total, *promotions, +shipping_methods.name',
},
headers,
next,
@@ -68,19 +70,19 @@ export async function getOrSetCart(countryCode: string) {
const cartResp = await sdk.store.cart.create(
{ region_id: region.id },
{},
headers
headers,
);
cart = cartResp.cart;
await setCartId(cart.id);
const cartCacheTag = await getCacheTag("carts");
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");
const cartCacheTag = await getCacheTag('carts');
revalidateTag(cartCacheTag);
}
@@ -89,13 +91,16 @@ export async function getOrSetCart(countryCode: string) {
export async function updateCart(
{ id, ...data }: HttpTypes.StoreUpdateCart & { id?: string },
{ onSuccess, onError }: { onSuccess: () => void, onError: () => void } = { onSuccess: () => {}, onError: () => {} },
{ onSuccess, onError }: { onSuccess: () => void; onError: () => void } = {
onSuccess: () => {},
onError: () => {},
},
) {
const cartId = id || (await getCartId());
if (!cartId) {
throw new Error(
"No existing cart found, please create one before updating"
'No existing cart found, please create one before updating',
);
}
@@ -106,10 +111,10 @@ export async function updateCart(
return sdk.store.cart
.update(cartId, data, {}, headers)
.then(async ({ cart }) => {
const cartCacheTag = await getCacheTag("carts");
const cartCacheTag = await getCacheTag('carts');
revalidateTag(cartCacheTag);
const fulfillmentCacheTag = await getCacheTag("fulfillment");
const fulfillmentCacheTag = await getCacheTag('fulfillment');
revalidateTag(fulfillmentCacheTag);
onSuccess();
@@ -131,13 +136,13 @@ export async function addToCart({
countryCode: string;
}) {
if (!variantId) {
throw new Error("Missing variant ID when adding to cart");
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");
throw new Error('Error retrieving or creating cart');
}
const headers = {
@@ -152,13 +157,13 @@ export async function addToCart({
quantity,
},
{},
headers
headers,
)
.then(async () => {
const cartCacheTag = await getCacheTag("carts");
const cartCacheTag = await getCacheTag('carts');
revalidateTag(cartCacheTag);
const fulfillmentCacheTag = await getCacheTag("fulfillment");
const fulfillmentCacheTag = await getCacheTag('fulfillment');
revalidateTag(fulfillmentCacheTag);
})
.catch(medusaError);
@@ -176,13 +181,13 @@ export async function updateLineItem({
metadata?: Record<string, any>;
}) {
if (!lineId) {
throw new Error("Missing lineItem ID when updating line item");
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");
throw new Error('Missing cart ID when updating line item');
}
const headers = {
@@ -192,10 +197,10 @@ export async function updateLineItem({
await sdk.store.cart
.updateLineItem(cartId, lineId, { quantity, metadata }, {}, headers)
.then(async () => {
const cartCacheTag = await getCacheTag("carts");
const cartCacheTag = await getCacheTag('carts');
revalidateTag(cartCacheTag);
const fulfillmentCacheTag = await getCacheTag("fulfillment");
const fulfillmentCacheTag = await getCacheTag('fulfillment');
revalidateTag(fulfillmentCacheTag);
})
.catch(medusaError);
@@ -203,13 +208,13 @@ export async function updateLineItem({
export async function deleteLineItem(lineId: string) {
if (!lineId) {
throw new Error("Missing lineItem ID when deleting line item");
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");
throw new Error('Missing cart ID when deleting line item');
}
const headers = {
@@ -219,10 +224,10 @@ export async function deleteLineItem(lineId: string) {
await sdk.store.cart
.deleteLineItem(cartId, lineId, headers)
.then(async () => {
const cartCacheTag = await getCacheTag("carts");
const cartCacheTag = await getCacheTag('carts');
revalidateTag(cartCacheTag);
const fulfillmentCacheTag = await getCacheTag("fulfillment");
const fulfillmentCacheTag = await getCacheTag('fulfillment');
revalidateTag(fulfillmentCacheTag);
})
.catch(medusaError);
@@ -242,7 +247,7 @@ export async function setShippingMethod({
return sdk.store.cart
.addShippingMethod(cartId, { option_id: shippingMethodId }, {}, headers)
.then(async () => {
const cartCacheTag = await getCacheTag("carts");
const cartCacheTag = await getCacheTag('carts');
revalidateTag(cartCacheTag);
})
.catch(medusaError);
@@ -250,7 +255,7 @@ export async function setShippingMethod({
export async function initiatePaymentSession(
cart: HttpTypes.StoreCart,
data: HttpTypes.StoreInitializePaymentSession
data: HttpTypes.StoreInitializePaymentSession,
) {
const headers = {
...(await getAuthHeaders()),
@@ -259,7 +264,7 @@ export async function initiatePaymentSession(
return sdk.store.payment
.initiatePaymentSession(cart, data, {}, headers)
.then(async (resp) => {
const cartCacheTag = await getCacheTag("carts");
const cartCacheTag = await getCacheTag('carts');
revalidateTag(cartCacheTag);
return resp;
})
@@ -268,12 +273,15 @@ export async function initiatePaymentSession(
export async function applyPromotions(
codes: string[],
{ onSuccess, onError }: { onSuccess: () => void, onError: () => void } = { onSuccess: () => {}, onError: () => {} },
{ onSuccess, onError }: { onSuccess: () => void; onError: () => void } = {
onSuccess: () => {},
onError: () => {},
},
) {
const cartId = await getCartId();
if (!cartId) {
throw new Error("No existing cart found");
throw new Error('No existing cart found');
}
const headers = {
@@ -283,10 +291,10 @@ export async function applyPromotions(
return sdk.store.cart
.update(cartId, { promo_codes: codes }, {}, headers)
.then(async () => {
const cartCacheTag = await getCacheTag("carts");
const cartCacheTag = await getCacheTag('carts');
revalidateTag(cartCacheTag);
const fulfillmentCacheTag = await getCacheTag("fulfillment");
const fulfillmentCacheTag = await getCacheTag('fulfillment');
revalidateTag(fulfillmentCacheTag);
onSuccess();
@@ -322,7 +330,7 @@ export async function removeDiscount(code: string) {
export async function removeGiftCard(
codeToRemove: string,
giftCards: any[]
giftCards: any[],
// giftCards: GiftCard[]
) {
// const cartId = getCartId()
@@ -342,9 +350,9 @@ export async function removeGiftCard(
export async function submitPromotionForm(
currentState: unknown,
formData: FormData
formData: FormData,
) {
const code = formData.get("code") as string;
const code = formData.get('code') as string;
try {
await applyPromotions([code]);
} catch (e: any) {
@@ -356,44 +364,44 @@ export async function submitPromotionForm(
export async function setAddresses(currentState: unknown, formData: FormData) {
try {
if (!formData) {
throw new Error("No form data found when setting addresses");
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");
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"),
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"),
email: formData.get('email'),
} as any;
const sameAsBilling = formData.get("same_as_billing");
if (sameAsBilling === "on") data.billing_address = data.shipping_address;
const sameAsBilling = formData.get('same_as_billing');
if (sameAsBilling === 'on') data.billing_address = data.shipping_address;
if (sameAsBilling !== "on")
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"),
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) {
@@ -401,7 +409,7 @@ export async function setAddresses(currentState: unknown, formData: FormData) {
}
redirect(
`/${formData.get("shipping_address.country_code")}/checkout?step=delivery`
`/${formData.get('shipping_address.country_code')}/checkout?step=delivery`,
);
}
@@ -410,11 +418,14 @@ export async function setAddresses(currentState: unknown, formData: FormData) {
* @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, options: { revalidateCacheTags: boolean } = { revalidateCacheTags: true }) {
export async function placeOrder(
cartId?: string,
options: { revalidateCacheTags: boolean } = { revalidateCacheTags: true },
) {
const id = cartId || (await getCartId());
if (!id) {
throw new Error("No existing cart found when placing an order");
throw new Error('No existing cart found when placing an order');
}
const headers = {
@@ -425,22 +436,22 @@ export async function placeOrder(cartId?: string, options: { revalidateCacheTags
.complete(id, {}, headers)
.then(async (cartRes) => {
if (options?.revalidateCacheTags) {
const cartCacheTag = await getCacheTag("carts");
const cartCacheTag = await getCacheTag('carts');
revalidateTag(cartCacheTag);
}
return cartRes;
})
.catch(medusaError);
if (cartRes?.type === "order") {
if (cartRes?.type === 'order') {
if (options?.revalidateCacheTags) {
const orderCacheTag = await getCacheTag("orders");
const orderCacheTag = await getCacheTag('orders');
revalidateTag(orderCacheTag);
}
removeCartId();
} else {
throw new Error("Cart is not an order");
throw new Error('Cart is not an order');
}
return retrieveOrder(cartRes.order.id);
@@ -461,14 +472,14 @@ export async function updateRegion(countryCode: string, currentPath: string) {
if (cartId) {
await updateCart({ region_id: region.id });
const cartCacheTag = await getCacheTag("carts");
const cartCacheTag = await getCacheTag('carts');
revalidateTag(cartCacheTag);
}
const regionCacheTag = await getCacheTag("regions");
const regionCacheTag = await getCacheTag('regions');
revalidateTag(regionCacheTag);
const productsCacheTag = await getCacheTag("products");
const productsCacheTag = await getCacheTag('products');
revalidateTag(productsCacheTag);
redirect(`/${countryCode}${currentPath}`);
@@ -480,15 +491,15 @@ export async function listCartOptions() {
...(await getAuthHeaders()),
};
const next = {
...(await getCacheOptions("shippingOptions")),
...(await getCacheOptions('shippingOptions')),
};
return await sdk.client.fetch<{
shipping_options: HttpTypes.StoreCartShippingOption[];
}>("/store/shipping-options", {
}>('/store/shipping-options', {
query: { cart_id: cartId },
next,
headers,
cache: "force-cache",
cache: 'force-cache',
});
}

View File

@@ -1,34 +1,35 @@
import { sdk } from "@lib/config";
import { HttpTypes } from "@medusajs/types";
import { getCacheOptions } from "./cookies";
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")),
...(await getCacheOptions('categories')),
};
const limit = query?.limit || 100;
return sdk.client
.fetch<{ product_categories: HttpTypes.StoreProductCategory[] }>(
"/store/product-categories",
'/store/product-categories',
{
query: {
fields:
"*category_children, *products, *parent_category, *parent_category.parent_category",
'*category_children, *products, *parent_category, *parent_category.parent_category',
limit,
...query,
},
next,
cache: "force-cache",
}
cache: 'force-cache',
},
)
.then(({ product_categories }) => product_categories);
};
export const getCategoryByHandle = async (categoryHandle: string[]) => {
const { product_categories } = await getProductCategories({
handle: `${categoryHandle.join("/")}`,
handle: `${categoryHandle.join('/')}`,
limit: 1,
});
return product_categories[0];
@@ -37,14 +38,14 @@ export const getCategoryByHandle = async (categoryHandle: string[]) => {
export const getProductCategories = async ({
handle,
limit,
fields = "*category_children, *products",
fields = '*category_children, *products',
}: {
handle?: string;
limit?: number;
fields?: string;
} = {}) => {
const next = {
...(await getCacheOptions("categories")),
...(await getCacheOptions('categories')),
};
return sdk.client.fetch<HttpTypes.StoreProductCategoryListResponse>(
@@ -57,6 +58,6 @@ export const getProductCategories = async ({
},
next,
//cache: "force-cache",
}
},
);
};

View File

@@ -1,12 +1,13 @@
"use server";
'use server';
import { sdk, SDK_CONFIG } from "@lib/config";
import { HttpTypes } from "@medusajs/types";
import { getCacheOptions } from "./cookies";
import { SDK_CONFIG, sdk } from '@lib/config';
import { HttpTypes } from '@medusajs/types';
import { getCacheOptions } from './cookies';
export const retrieveCollection = async (id: string) => {
const next = {
...(await getCacheOptions("collections")),
...(await getCacheOptions('collections')),
};
return sdk.client
@@ -14,46 +15,46 @@ export const retrieveCollection = async (id: string) => {
`/store/collections/${id}`,
{
next,
cache: "force-cache",
}
cache: 'force-cache',
},
)
.then(({ collection }) => collection);
};
export const listCollections = async (
queryParams: Record<string, string> = {}
queryParams: Record<string, string> = {},
): Promise<{ collections: HttpTypes.StoreCollection[]; count: number }> => {
const next = {
...(await getCacheOptions("collections")),
...(await getCacheOptions('collections')),
};
queryParams.limit = queryParams.limit || "100";
queryParams.offset = queryParams.offset || "0";
console.log("SDK_CONFIG: ", SDK_CONFIG.baseUrl);
queryParams.limit = queryParams.limit || '100';
queryParams.offset = queryParams.offset || '0';
console.log('SDK_CONFIG: ', SDK_CONFIG.baseUrl);
return sdk.client
.fetch<{ collections: HttpTypes.StoreCollection[]; count: number }>(
"/store/collections",
'/store/collections',
{
query: queryParams,
next,
cache: "force-cache",
}
cache: 'force-cache',
},
)
.then(({ collections }) => ({ collections, count: collections.length }));
};
export const getCollectionByHandle = async (
handle: string
handle: string,
): Promise<HttpTypes.StoreCollection> => {
const next = {
...(await getCacheOptions("collections")),
...(await getCacheOptions('collections')),
};
return sdk.client
.fetch<HttpTypes.StoreCollectionListResponse>(`/store/collections`, {
query: { handle, fields: "*products" },
query: { handle, fields: '*products' },
next,
cache: "force-cache",
cache: 'force-cache',
})
.then(({ collections }) => collections[0]);
};

View File

@@ -1,124 +1,124 @@
import "server-only"
import 'server-only';
import { cookies as nextCookies } from "next/headers"
import { cookies as nextCookies } from 'next/headers';
const CookieName = {
MEDUSA_CUSTOMER_ID: "_medusa_customer_id",
MEDUSA_JWT: "_medusa_jwt",
MEDUSA_CART_ID: "_medusa_cart_id",
MEDUSA_CACHE_ID: "_medusa_cache_id",
}
MEDUSA_CUSTOMER_ID: '_medusa_customer_id',
MEDUSA_JWT: '_medusa_jwt',
MEDUSA_CART_ID: '_medusa_cart_id',
MEDUSA_CACHE_ID: '_medusa_cache_id',
};
export const getAuthHeaders = async (): Promise<
{ authorization: string } | {}
> => {
try {
const cookies = await nextCookies()
const token = cookies.get(CookieName.MEDUSA_JWT)?.value
const cookies = await nextCookies();
const token = cookies.get(CookieName.MEDUSA_JWT)?.value;
if (!token) {
return {}
return {};
}
return { authorization: `Bearer ${token}` }
return { authorization: `Bearer ${token}` };
} catch {
return {}
return {};
}
}
};
export const getMedusaCustomerId = async (): Promise<
{ customerId: string | null }
> => {
export const getMedusaCustomerId = async (): Promise<{
customerId: string | null;
}> => {
try {
const cookies = await nextCookies()
const customerId = cookies.get(CookieName.MEDUSA_CUSTOMER_ID)?.value
const cookies = await nextCookies();
const customerId = cookies.get(CookieName.MEDUSA_CUSTOMER_ID)?.value;
if (!customerId) {
return { customerId: null }
return { customerId: null };
}
return { customerId }
return { customerId };
} catch {
return { customerId: null }
return { customerId: null };
}
}
};
export const getCacheTag = async (tag: string): Promise<string> => {
try {
const cookies = await nextCookies()
const cacheId = cookies.get(CookieName.MEDUSA_CACHE_ID)?.value
const cookies = await nextCookies();
const cacheId = cookies.get(CookieName.MEDUSA_CACHE_ID)?.value;
if (!cacheId) {
return ""
return '';
}
return `${tag}-${cacheId}`
return `${tag}-${cacheId}`;
} catch (error) {
return ""
return '';
}
}
};
export const getCacheOptions = async (
tag: string
tag: string,
): Promise<{ tags: string[] } | {}> => {
if (typeof window !== "undefined") {
return {}
if (typeof window !== 'undefined') {
return {};
}
const cacheTag = await getCacheTag(tag)
const cacheTag = await getCacheTag(tag);
if (!cacheTag) {
return {}
return {};
}
return { tags: [`${cacheTag}`] }
}
return { tags: [`${cacheTag}`] };
};
const getCookieSharedOptions = () => ({
maxAge: 60 * 60 * 24 * 7,
httpOnly: false,
secure: process.env.NODE_ENV === "production",
secure: process.env.NODE_ENV === 'production',
});
const getCookieResetOptions = () => ({
maxAge: -1,
});
export const setAuthToken = async (token: string) => {
const cookies = await nextCookies()
const cookies = await nextCookies();
cookies.set(CookieName.MEDUSA_JWT, token, {
...getCookieSharedOptions(),
})
}
});
};
export const setMedusaCustomerId = async (customerId: string) => {
const cookies = await nextCookies()
const cookies = await nextCookies();
cookies.set(CookieName.MEDUSA_CUSTOMER_ID, customerId, {
...getCookieSharedOptions(),
})
}
});
};
export const removeAuthToken = async () => {
const cookies = await nextCookies()
cookies.set(CookieName.MEDUSA_JWT, "", {
const cookies = await nextCookies();
cookies.set(CookieName.MEDUSA_JWT, '', {
...getCookieResetOptions(),
})
}
});
};
export const getCartId = async () => {
const cookies = await nextCookies()
return cookies.get(CookieName.MEDUSA_CART_ID)?.value
}
const cookies = await nextCookies();
return cookies.get(CookieName.MEDUSA_CART_ID)?.value;
};
export const setCartId = async (cartId: string) => {
const cookies = await nextCookies()
const cookies = await nextCookies();
cookies.set(CookieName.MEDUSA_CART_ID, cartId, {
...getCookieSharedOptions(),
})
}
});
};
export const removeCartId = async () => {
const cookies = await nextCookies()
cookies.set(CookieName.MEDUSA_CART_ID, "", {
const cookies = await nextCookies();
cookies.set(CookieName.MEDUSA_CART_ID, '', {
...getCookieResetOptions(),
})
}
});
};

View File

@@ -1,9 +1,11 @@
"use server"
'use server';
import { revalidateTag } from 'next/cache';
import { sdk } from '@lib/config';
import medusaError from '@lib/util/medusa-error';
import { HttpTypes } from '@medusajs/types';
import { sdk } from "@lib/config"
import medusaError from "@lib/util/medusa-error"
import { HttpTypes } from "@medusajs/types"
import { revalidateTag } from "next/cache"
import {
getAuthHeaders,
getCacheOptions,
@@ -12,268 +14,275 @@ import {
removeAuthToken,
removeCartId,
setAuthToken,
} from "./cookies"
} from './cookies';
export const retrieveCustomer =
async (): Promise<HttpTypes.StoreCustomer | null> => {
const authHeaders = await getAuthHeaders()
const authHeaders = await getAuthHeaders();
if (!authHeaders) return null
if (!authHeaders) return null;
const headers = {
...authHeaders,
}
};
const next = {
...(await getCacheOptions("customers")),
}
...(await getCacheOptions('customers')),
};
return await sdk.client
.fetch<{ customer: HttpTypes.StoreCustomer }>(`/store/customers/me`, {
method: "GET",
method: 'GET',
query: {
fields: "*orders",
fields: '*orders',
},
headers,
next,
cache: "force-cache",
cache: 'force-cache',
})
.then(({ customer }) => customer)
.catch(() => null)
}
.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)
.catch(medusaError);
const cacheTag = await getCacheTag("customers")
revalidateTag(cacheTag)
const cacheTag = await getCacheTag('customers');
revalidateTag(cacheTag);
return updateRes
}
return updateRes;
};
export async function signup(_currentState: unknown, formData: FormData) {
const password = formData.get("password") as string
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,
}
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", {
const token = await sdk.auth.register('customer', 'emailpass', {
email: customerForm.email,
password: password,
})
});
await setAuthToken(token as string)
await setAuthToken(token as string);
const headers = {
...(await getAuthHeaders()),
}
};
const { customer: createdCustomer } = await sdk.store.customer.create(
customerForm,
{},
headers
)
headers,
);
const loginToken = await sdk.auth.login("customer", "emailpass", {
const loginToken = await sdk.auth.login('customer', 'emailpass', {
email: customerForm.email,
password,
})
});
await setAuthToken(loginToken as string)
await setAuthToken(loginToken as string);
const customerCacheTag = await getCacheTag("customers")
revalidateTag(customerCacheTag)
const customerCacheTag = await getCacheTag('customers');
revalidateTag(customerCacheTag);
await transferCart()
await transferCart();
return createdCustomer
return createdCustomer;
} catch (error: any) {
return error.toString()
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
const email = formData.get('email') as string;
const password = formData.get('password') as string;
try {
await sdk.auth
.login("customer", "emailpass", { email, password })
.login('customer', 'emailpass', { email, password })
.then(async (token) => {
await setAuthToken(token as string)
const customerCacheTag = await getCacheTag("customers")
revalidateTag(customerCacheTag)
})
await setAuthToken(token as string);
const customerCacheTag = await getCacheTag('customers');
revalidateTag(customerCacheTag);
});
} catch (error: any) {
return error.toString()
return error.toString();
}
try {
await transferCart()
await transferCart();
} catch (error: any) {
return error.toString()
return error.toString();
}
}
export async function medusaLogout(countryCode = 'ee', canRevalidateTags = true) {
await sdk.auth.logout()
export async function medusaLogout(
countryCode = 'ee',
canRevalidateTags = true,
) {
await sdk.auth.logout();
await removeAuthToken()
await removeAuthToken();
if (canRevalidateTags) {
const customerCacheTag = await getCacheTag("customers")
revalidateTag(customerCacheTag)
const customerCacheTag = await getCacheTag('customers');
revalidateTag(customerCacheTag);
}
await removeCartId()
await removeCartId();
if (canRevalidateTags) {
const cartCacheTag = await getCacheTag("carts")
revalidateTag(cartCacheTag)
const cartCacheTag = await getCacheTag('carts');
revalidateTag(cartCacheTag);
}
}
export async function transferCart() {
const cartId = await getCartId()
const cartId = await getCartId();
if (!cartId) {
return
return;
}
const headers = await getAuthHeaders()
const headers = await getAuthHeaders();
await sdk.store.cart.transferCart(cartId, {}, headers)
await sdk.store.cart.transferCart(cartId, {}, headers);
const cartCacheTag = await getCacheTag("carts")
revalidateTag(cartCacheTag)
const cartCacheTag = await getCacheTag('carts');
revalidateTag(cartCacheTag);
}
export const addCustomerAddress = async (
currentState: Record<string, unknown>,
formData: FormData
formData: FormData,
): Promise<any> => {
const isDefaultBilling = (currentState.isDefaultBilling as boolean) || false
const isDefaultShipping = (currentState.isDefaultShipping as boolean) || false
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,
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 }
const customerCacheTag = await getCacheTag('customers');
revalidateTag(customerCacheTag);
return { success: true, error: null };
})
.catch((err) => {
return { success: false, error: err.toString() }
})
}
return { success: false, error: err.toString() };
});
};
export const deleteCustomerAddress = async (
addressId: string
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 }
const customerCacheTag = await getCacheTag('customers');
revalidateTag(customerCacheTag);
return { success: true, error: null };
})
.catch((err) => {
return { success: false, error: err.toString() }
})
}
return { success: false, error: err.toString() };
});
};
export const updateCustomerAddress = async (
currentState: Record<string, unknown>,
formData: FormData
formData: FormData,
): Promise<any> => {
const addressId =
(currentState.addressId as string) || (formData.get("addressId") as string)
(currentState.addressId as string) || (formData.get('addressId') as string);
if (!addressId) {
return { success: false, error: "Address ID is required" }
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
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
const phone = formData.get('phone') as string;
if (phone) {
address.phone = 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 }
const customerCacheTag = await getCacheTag('customers');
revalidateTag(customerCacheTag);
return { success: true, error: null };
})
.catch((err) => {
return { success: false, error: err.toString() }
})
}
return { success: false, error: err.toString() };
});
};
async function medusaLogin(email: string, password: string) {
const token = await sdk.auth.login("customer", "emailpass", { email, password });
const token = await sdk.auth.login('customer', 'emailpass', {
email,
password,
});
await setAuthToken(token as string);
try {
await transferCart();
} catch (e) {
console.error("Failed to transfer cart", e);
console.error('Failed to transfer cart', e);
}
const customer = await retrieveCustomer();
if (!customer) {
throw new Error("Customer not found for active session");
throw new Error('Customer not found for active session');
}
return customer.id;
@@ -290,29 +299,41 @@ async function medusaRegister({
name: string | undefined;
lastName: string | undefined;
}) {
console.info(`Creating new Medusa account for Keycloak user with email=${email}`);
const registerToken = await sdk.auth.register("customer", "emailpass", { email, password });
console.info(
`Creating new Medusa account for Keycloak user with email=${email}`,
);
const registerToken = await sdk.auth.register('customer', 'emailpass', {
email,
password,
});
await setAuthToken(registerToken);
console.info(`Creating new Medusa customer profile for Keycloak user with email=${email} and name=${name} and lastName=${lastName}`);
console.info(
`Creating new Medusa customer profile for Keycloak user with email=${email} and name=${name} and lastName=${lastName}`,
);
await sdk.store.customer.create(
{ email, first_name: name, last_name: lastName },
{},
{
...(await getAuthHeaders()),
});
},
);
}
export async function medusaLoginOrRegister(credentials: {
email: string
supabaseUserId?: string
name?: string,
lastName?: string,
} & ({ isDevPasswordLogin: true; password: string } | { isDevPasswordLogin?: false; password?: undefined })) {
export async function medusaLoginOrRegister(
credentials: {
email: string;
supabaseUserId?: string;
name?: string;
lastName?: string;
} & (
| { isDevPasswordLogin: true; password: string }
| { isDevPasswordLogin?: false; password?: undefined }
),
) {
const { email, supabaseUserId, name, lastName } = credentials;
const password = await (async () => {
if (credentials.isDevPasswordLogin) {
return credentials.password;
@@ -324,13 +345,19 @@ export async function medusaLoginOrRegister(credentials: {
try {
return await medusaLogin(email, password);
} catch (loginError) {
console.error("Failed to login customer, attempting to register", loginError);
console.error(
'Failed to login customer, attempting to register',
loginError,
);
try {
await medusaRegister({ email, password, name, lastName });
return await medusaLogin(email, password);
} catch (registerError) {
console.error("Failed to create Medusa account for user with email=${email}", registerError);
console.error(
'Failed to create Medusa account for user with email=${email}',
registerError,
);
throw medusaError(registerError);
}
}
@@ -340,7 +367,10 @@ export async function medusaLoginOrRegister(credentials: {
* Generate a deterministic password based on user identifier
* This ensures the same user always gets the same password for Medusa
*/
async function generateDeterministicPassword(email: string, userId?: string): Promise<string> {
async function generateDeterministicPassword(
email: string,
userId?: string,
): Promise<string> {
// Use the user ID or email as the base for deterministic generation
const baseString = userId || email;
const secret = process.env.MEDUSA_PASSWORD_SECRET!;
@@ -356,13 +386,15 @@ async function generateDeterministicPassword(email: string, userId?: string): Pr
keyData,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
['sign'],
);
// Generate HMAC
const signature = await crypto.subtle.sign('HMAC', key, messageData);
// Convert to base64 and make it a valid password
const hashArray = Array.from(new Uint8Array(signature));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
const hashHex = hashArray
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
// Take first 24 characters and add some complexity
const basePassword = hashHex.substring(0, 24);
// Add some required complexity for Medusa (uppercase, lowercase, numbers, symbols)

View File

@@ -1,70 +1,71 @@
"use server"
'use server';
import { sdk } from "@lib/config"
import { HttpTypes } from "@medusajs/types"
import { getAuthHeaders, getCacheOptions } from "./cookies"
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")),
}
...(await getCacheOptions('fulfillment')),
};
return sdk.client
.fetch<HttpTypes.StoreShippingOptionListResponse>(
`/store/shipping-options`,
{
method: "GET",
method: 'GET',
query: {
cart_id: cartId,
fields:
"+service_zone.fulfllment_set.type,*service_zone.fulfillment_set.location.address",
'+service_zone.fulfllment_set.type,*service_zone.fulfillment_set.location.address',
},
headers,
next,
cache: "force-cache",
}
cache: 'force-cache',
},
)
.then(({ shipping_options }) => shipping_options)
.catch(() => {
return null
})
}
return null;
});
};
export const calculatePriceForShippingOption = async (
optionId: string,
cartId: string,
data?: Record<string, unknown>
data?: Record<string, unknown>,
) => {
const headers = {
...(await getAuthHeaders()),
}
};
const next = {
...(await getCacheOptions("fulfillment")),
}
...(await getCacheOptions('fulfillment')),
};
const body = { cart_id: cartId, data }
const body = { cart_id: cartId, data };
if (data) {
body.data = data
body.data = data;
}
return sdk.client
.fetch<{ shipping_option: HttpTypes.StoreCartShippingOption }>(
`/store/shipping-options/${optionId}/calculate`,
{
method: "POST",
method: 'POST',
body,
headers,
next,
}
},
)
.then(({ shipping_option }) => shipping_option)
.catch((e) => {
return null
})
}
return null;
});
};

View File

@@ -1,11 +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";
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';

View File

@@ -1,9 +1,10 @@
"use server"
import { cookies as nextCookies } from "next/headers"
import { redirect } from "next/navigation"
'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}`)
const cookies = await nextCookies();
cookies.set('_medusa_onboarding', 'false', { maxAge: -1 });
redirect(`http://localhost:7001/a/orders/${orderId}`);
}

View File

@@ -1,111 +1,112 @@
"use server"
'use server';
import { sdk } from "@lib/config"
import medusaError from "@lib/util/medusa-error"
import { getAuthHeaders, getCacheOptions } from "./cookies"
import { HttpTypes } from "@medusajs/types"
import { sdk } from '@lib/config';
import medusaError from '@lib/util/medusa-error';
import { HttpTypes } from '@medusajs/types';
import { getAuthHeaders, getCacheOptions } from './cookies';
export const retrieveOrder = async (id: string) => {
const headers = {
...(await getAuthHeaders()),
}
};
const next = {
...(await getCacheOptions("orders")),
}
...(await getCacheOptions('orders')),
};
return sdk.client
.fetch<HttpTypes.StoreOrderResponse>(`/store/orders/${id}`, {
method: "GET",
method: 'GET',
query: {
fields:
"*payment_collections.payments,*items,*items.metadata,*items.variant,*items.product",
'*payment_collections.payments,*items,*items.metadata,*items.variant,*items.product',
},
headers,
next,
cache: "force-cache",
cache: 'force-cache',
})
.then(({ order }) => order)
.catch((err) => medusaError(err))
}
.catch((err) => medusaError(err));
};
export const listOrders = async (
limit: number = 10,
offset: number = 0,
filters?: Record<string, any>
filters?: Record<string, any>,
) => {
const headers = {
...(await getAuthHeaders()),
}
};
const next = {
...(await getCacheOptions("orders")),
}
...(await getCacheOptions('orders')),
};
return sdk.client
.fetch<HttpTypes.StoreOrderListResponse>(`/store/orders`, {
method: "GET",
method: 'GET',
query: {
limit,
offset,
order: "-created_at",
fields: "*items,+items.metadata,*items.variant,*items.product",
order: '-created_at',
fields: '*items,+items.metadata,*items.variant,*items.product',
...filters,
},
headers,
next,
})
.then(({ orders }) => orders)
.catch((err) => medusaError(err))
}
.catch((err) => medusaError(err));
};
export const createTransferRequest = async (
state: {
success: boolean
error: string | null
order: HttpTypes.StoreOrder | null
success: boolean;
error: string | null;
order: HttpTypes.StoreOrder | null;
},
formData: FormData
formData: FormData,
): Promise<{
success: boolean
error: string | null
order: HttpTypes.StoreOrder | null
success: boolean;
error: string | null;
order: HttpTypes.StoreOrder | null;
}> => {
const id = formData.get("order_id") as string
const id = formData.get('order_id') as string;
if (!id) {
return { success: false, error: "Order ID is required", order: null }
return { success: false, error: 'Order ID is required', order: null };
}
const headers = await getAuthHeaders()
const headers = await getAuthHeaders();
return await sdk.store.order
.requestTransfer(
id,
{},
{
fields: "id, email",
fields: 'id, email',
},
headers
headers,
)
.then(({ order }) => ({ success: true, error: null, order }))
.catch((err) => ({ success: false, error: err.message, order: null }))
}
.catch((err) => ({ success: false, error: err.message, order: null }));
};
export const acceptTransferRequest = async (id: string, token: string) => {
const headers = await getAuthHeaders()
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 }))
}
.catch((err) => ({ success: false, error: err.message, order: null }));
};
export const declineTransferRequest = async (id: string, token: string) => {
const headers = await getAuthHeaders()
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 }))
}
.catch((err) => ({ success: false, error: err.message, order: null }));
};

View File

@@ -1,35 +1,36 @@
"use server"
'use server';
import { sdk } from "@lib/config"
import { getAuthHeaders, getCacheOptions } from "./cookies"
import { HttpTypes } from "@medusajs/types"
import { sdk } from '@lib/config';
import { HttpTypes } from '@medusajs/types';
import { getAuthHeaders, getCacheOptions } from './cookies';
export const listCartPaymentMethods = async (regionId: string) => {
const headers = {
...(await getAuthHeaders()),
}
};
const next = {
...(await getCacheOptions("payment_providers")),
}
...(await getCacheOptions('payment_providers')),
};
return sdk.client
.fetch<HttpTypes.StorePaymentProviderListResponse>(
`/store/payment-providers`,
{
method: "GET",
method: 'GET',
query: { region_id: regionId },
headers,
next,
cache: "force-cache",
}
cache: 'force-cache',
},
)
.then(({ payment_providers }) =>
payment_providers.sort((a, b) => {
return a.id > b.id ? 1 : -1
})
return a.id > b.id ? 1 : -1;
}),
)
.catch(() => {
return null
})
}
return null;
});
};

View File

@@ -1,11 +1,12 @@
"use server"
'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"
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,
@@ -13,70 +14,71 @@ export const listProducts = async ({
countryCode,
regionId,
}: {
pageParam?: number
queryParams?: HttpTypes.FindParams & HttpTypes.StoreProductParams & {
"type_id[0]"?: string;
id?: string[],
category_id?: string;
order?: 'title';
}
countryCode?: string
regionId?: string
pageParam?: number;
queryParams?: HttpTypes.FindParams &
HttpTypes.StoreProductParams & {
'type_id[0]'?: string;
id?: string[];
category_id?: string;
order?: 'title';
};
countryCode?: string;
regionId?: string;
}): Promise<{
response: { products: HttpTypes.StoreProduct[]; count: number }
nextPage: number | null
queryParams?: HttpTypes.FindParams & HttpTypes.StoreProductParams
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")
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;
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
let region: HttpTypes.StoreRegion | undefined | null;
if (countryCode) {
region = await getRegion(countryCode)
region = await getRegion(countryCode);
} else {
region = await retrieveRegion(regionId!)
region = await retrieveRegion(regionId!);
}
if (!region) {
return {
response: { products: [], count: 0 },
nextPage: null,
}
};
}
const headers = {
...(await getAuthHeaders()),
}
};
const next = {
...(await getCacheOptions("products")),
}
...(await getCacheOptions('products')),
};
return sdk.client
.fetch<{ products: HttpTypes.StoreProduct[]; count: number }>(
`/store/products`,
{
method: "GET",
method: 'GET',
query: {
limit,
offset,
region_id: region?.id,
fields:
"*variants.calculated_price,+variants.inventory_quantity,+metadata,+tags,+status",
'*variants.calculated_price,+variants.inventory_quantity,+metadata,+tags,+status',
...queryParams,
},
headers,
next,
}
},
)
.then(({ products, count }) => {
const nextPage = count > offset + limit ? pageParam + 1 : null
const nextPage = count > offset + limit ? pageParam + 1 : null;
return {
response: {
@@ -85,9 +87,9 @@ export const listProducts = async ({
},
nextPage: nextPage,
queryParams,
}
})
}
};
});
};
/**
* This will fetch 100 products to the Next.js cache and sort them based on the sortBy parameter.
@@ -96,19 +98,19 @@ export const listProducts = async ({
export const listProductsWithSort = async ({
page = 0,
queryParams,
sortBy = "created_at",
sortBy = 'created_at',
countryCode,
}: {
page?: number
queryParams?: HttpTypes.FindParams & HttpTypes.StoreProductParams
sortBy?: SortOptions
countryCode: string
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
response: { products: HttpTypes.StoreProduct[]; count: number };
nextPage: number | null;
queryParams?: HttpTypes.FindParams & HttpTypes.StoreProductParams;
}> => {
const limit = queryParams?.limit || 12
const limit = queryParams?.limit || 12;
const {
response: { products, count },
@@ -119,15 +121,15 @@ export const listProductsWithSort = async ({
limit: 100,
},
countryCode,
})
});
const sortedProducts = sortProducts(products, sortBy)
const sortedProducts = sortProducts(products, sortBy);
const pageParam = (page - 1) * limit
const pageParam = (page - 1) * limit;
const nextPage = count > pageParam + limit ? pageParam + limit : null
const nextPage = count > pageParam + limit ? pageParam + limit : null;
const paginatedProducts = sortedProducts.slice(pageParam, pageParam + limit)
const paginatedProducts = sortedProducts.slice(pageParam, pageParam + limit);
return {
response: {
@@ -136,24 +138,27 @@ export const listProductsWithSort = async ({
},
nextPage,
queryParams,
}
}
};
};
export const listProductTypes = async (): Promise<{ productTypes: HttpTypes.StoreProductType[]; count: number }> => {
export const listProductTypes = async (): Promise<{
productTypes: HttpTypes.StoreProductType[];
count: number;
}> => {
const next = {
...(await getCacheOptions("productTypes")),
...(await getCacheOptions('productTypes')),
};
return sdk.client
.fetch<{ product_types: HttpTypes.StoreProductType[]; count: number }>(
"/store/product-types",
'/store/product-types',
{
next,
//cache: "force-cache",
query: {
fields: "id,value,metadata",
fields: 'id,value,metadata',
},
}
},
)
.then(({ product_types, count }) => {
return { productTypes: product_types, count };

View File

@@ -1,66 +1,67 @@
"use server"
'use server';
import { sdk } from "@lib/config"
import medusaError from "@lib/util/medusa-error"
import { HttpTypes } from "@medusajs/types"
import { getCacheOptions } from "./cookies"
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")),
}
...(await getCacheOptions('regions')),
};
return sdk.client
.fetch<{ regions: HttpTypes.StoreRegion[] }>(`/store/regions`, {
method: "GET",
method: 'GET',
next,
cache: "force-cache",
cache: 'force-cache',
})
.then(({ regions }) => regions)
.catch(medusaError)
}
.catch(medusaError);
};
export const retrieveRegion = async (id: string) => {
const next = {
...(await getCacheOptions(["regions", id].join("-"))),
}
...(await getCacheOptions(['regions', id].join('-'))),
};
return sdk.client
.fetch<{ region: HttpTypes.StoreRegion }>(`/store/regions/${id}`, {
method: "GET",
method: 'GET',
next,
cache: "force-cache",
cache: 'force-cache',
})
.then(({ region }) => region)
.catch(medusaError)
}
.catch(medusaError);
};
const regionMap = new Map<string, HttpTypes.StoreRegion>()
const regionMap = new Map<string, HttpTypes.StoreRegion>();
export const getRegion = async (countryCode: string) => {
try {
if (regionMap.has(countryCode)) {
return regionMap.get(countryCode)
return regionMap.get(countryCode);
}
const regions = await listRegions()
const regions = await listRegions();
if (!regions) {
return null
return null;
}
regions.forEach((region) => {
region.countries?.forEach((c) => {
regionMap.set(c?.iso_2 ?? "", region)
})
})
regionMap.set(c?.iso_2 ?? '', region);
});
});
const region = countryCode
? regionMap.get(countryCode)
: regionMap.get("et")
: regionMap.get('et');
return region
return region;
} catch (e: any) {
return null
return null;
}
}
};

View File

@@ -1,29 +1,29 @@
import { RefObject, useEffect, useState } from "react"
import { RefObject, useEffect, useState } from 'react';
export const useIntersection = (
element: RefObject<HTMLDivElement | null>,
rootMargin: string
rootMargin: string,
) => {
const [isVisible, setState] = useState(false)
const [isVisible, setState] = useState(false);
useEffect(() => {
if (!element.current) {
return
return;
}
const el = element.current
const el = element.current;
const observer = new IntersectionObserver(
([entry]) => {
setState(entry.isIntersecting)
setState(entry.isIntersecting);
},
{ rootMargin }
)
{ rootMargin },
);
observer.observe(el)
observer.observe(el);
return () => observer.unobserve(el)
}, [element, rootMargin])
return () => observer.unobserve(el);
}, [element, rootMargin]);
return isVisible
}
return isVisible;
};

View File

@@ -1,11 +1,11 @@
import { useState } from "react"
import { useState } from 'react';
export type StateType = [boolean, () => void, () => void, () => void] & {
state: boolean
open: () => void
close: () => void
toggle: () => void
}
state: boolean;
open: () => void;
close: () => void;
toggle: () => void;
};
/**
*
@@ -21,26 +21,26 @@ export type StateType = [boolean, () => void, () => void, () => void] & {
*/
const useToggleState = (initialState = false) => {
const [state, setState] = useState<boolean>(initialState)
const [state, setState] = useState<boolean>(initialState);
const close = () => {
setState(false)
}
setState(false);
};
const open = () => {
setState(true)
}
setState(true);
};
const toggle = () => {
setState((state) => !state)
}
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
}
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
export default useToggleState;

View File

@@ -1,28 +1,28 @@
import { isEqual, pick } from "lodash"
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",
'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",
])
)
'first_name',
'last_name',
'address_1',
'company',
'postal_code',
'city',
'country_code',
'province',
'phone',
]),
);
}

View File

@@ -1,3 +1,3 @@
export const getBaseURL = () => {
return process.env.NEXT_PUBLIC_BASE_URL || "https://localhost:8000"
}
return process.env.NEXT_PUBLIC_BASE_URL || 'https://localhost:8000';
};

View File

@@ -1,6 +1,6 @@
export const getPercentageDiff = (original: number, calculated: number) => {
const diff = original - calculated
const decrease = (diff / original) * 100
const diff = original - calculated;
const decrease = (diff / original) * 100;
return decrease.toFixed()
}
return decrease.toFixed();
};

View File

@@ -1,10 +1,11 @@
import { HttpTypes } from "@medusajs/types"
import { getPercentageDiff } from "./get-precentage-diff"
import { convertToLocale } from "./money"
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 null;
}
return {
@@ -22,25 +23,25 @@ export const getPricesForVariant = (variant: any) => {
price_type: variant.calculated_price.calculated_price.price_list_type,
percentage_diff: getPercentageDiff(
variant.calculated_price.original_amount,
variant.calculated_price.calculated_amount
variant.calculated_price.calculated_amount,
),
}
}
};
};
export function getProductPrice({
product,
variantId,
}: {
product: HttpTypes.StoreProduct
variantId?: string
product: HttpTypes.StoreProduct;
variantId?: string;
}) {
if (!product || !product.id) {
throw new Error("No product provided")
throw new Error('No product provided');
}
const cheapestPrice = () => {
if (!product || !product.variants?.length) {
return null
return null;
}
const cheapestVariant: any = product.variants
@@ -49,31 +50,31 @@ export function getProductPrice({
return (
a.calculated_price.calculated_amount -
b.calculated_price.calculated_amount
)
})[0]
);
})[0];
return getPricesForVariant(cheapestVariant)
}
return getPricesForVariant(cheapestVariant);
};
const variantPrice = () => {
if (!product || !variantId) {
return null
return null;
}
const variant: any = product.variants?.find(
(v) => v.id === variantId || v.sku === variantId
)
(v) => v.id === variantId || v.sku === variantId,
);
if (!variant) {
return null
return null;
}
return getPricesForVariant(variant)
}
return getPricesForVariant(variant);
};
return {
product,
cheapestPrice: cheapestPrice(),
variantPrice: variantPrice(),
}
};
}

View File

@@ -1,10 +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";
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';

View File

@@ -1,11 +1,11 @@
export const isObject = (input: any) => input instanceof Object
export const isArray = (input: any) => Array.isArray(input)
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)
)
}
(typeof input === 'string' && input.trim().length === 0)
);
};

View File

@@ -2,21 +2,21 @@ 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)
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
const message = error.response.data.message || error.response.data;
throw new Error(message.charAt(0).toUpperCase() + message.slice(1) + ".")
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)
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)
throw new Error('Error setting up the request: ' + error.message);
}
}

View File

@@ -1,26 +1,26 @@
import { isEmpty } from "./isEmpty"
import { isEmpty } from './isEmpty';
type ConvertToLocaleParams = {
amount: number
currency_code: string
minimumFractionDigits?: number
maximumFractionDigits?: number
locale?: string
}
amount: number;
currency_code: string;
minimumFractionDigits?: number;
maximumFractionDigits?: number;
locale?: string;
};
export const convertToLocale = ({
amount,
currency_code,
minimumFractionDigits,
maximumFractionDigits,
locale = "en-US",
locale = 'en-US',
}: ConvertToLocaleParams) => {
return currency_code && !isEmpty(currency_code)
? new Intl.NumberFormat(locale, {
style: "currency",
style: 'currency',
currency: currency_code,
minimumFractionDigits,
maximumFractionDigits,
}).format(amount)
: amount.toString()
}
: amount.toString();
};

View File

@@ -1,5 +1,7 @@
import { HttpTypes } from "@medusajs/types";
import { HttpTypes } from '@medusajs/types';
export const isSimpleProduct = (product: HttpTypes.StoreProduct): boolean => {
return product.options?.length === 1 && product.options[0].values?.length === 1;
}
return (
product.options?.length === 1 && product.options[0].values?.length === 1
);
};

View File

@@ -1,5 +1,5 @@
const repeat = (times: number) => {
return Array.from(Array(times).keys())
}
return Array.from(Array(times).keys());
};
export default repeat
export default repeat;

View File

@@ -1,8 +1,8 @@
import { HttpTypes } from "@medusajs/types"
import { SortOptions } from "@modules/store/components/refinement-list/sort-products"
import { HttpTypes } from '@medusajs/types';
import { SortOptions } from '@modules/store/components/refinement-list/sort-products';
interface MinPricedProduct extends HttpTypes.StoreProduct {
_minPrice?: number
_minPrice?: number;
}
/**
@@ -13,38 +13,38 @@ interface MinPricedProduct extends HttpTypes.StoreProduct {
*/
export function sortProducts(
products: HttpTypes.StoreProduct[],
sortBy: SortOptions
sortBy: SortOptions,
): HttpTypes.StoreProduct[] {
let sortedProducts = products as MinPricedProduct[]
let sortedProducts = products as MinPricedProduct[];
if (["price_asc", "price_desc"].includes(sortBy)) {
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
)
)
(variant) => variant?.calculated_price?.calculated_amount || 0,
),
);
} else {
product._minPrice = Infinity
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
})
const diff = a._minPrice! - b._minPrice!;
return sortBy === 'price_asc' ? diff : -diff;
});
}
if (sortBy === "created_at") {
if (sortBy === 'created_at') {
sortedProducts.sort((a, b) => {
return (
new Date(b.created_at!).getTime() - new Date(a.created_at!).getTime()
)
})
);
});
}
return sortedProducts
return sortedProducts;
}

View File

@@ -1,9 +1,10 @@
import { HttpTypes } from "@medusajs/types";
import { NextRequest, NextResponse } from "next/server";
import { NextRequest, NextResponse } from 'next/server';
import { HttpTypes } from '@medusajs/types';
const BACKEND_URL = process.env.MEDUSA_BACKEND_URL;
const PUBLISHABLE_API_KEY = process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY;
const DEFAULT_REGION = process.env.NEXT_PUBLIC_DEFAULT_REGION || "ee";
const DEFAULT_REGION = process.env.NEXT_PUBLIC_DEFAULT_REGION || 'ee';
const regionMapCache = {
regionMap: new Map<string, HttpTypes.StoreRegion>(),
@@ -15,7 +16,7 @@ async function getRegionMap(cacheId: string) {
if (!BACKEND_URL) {
throw new Error(
"Middleware.ts: Error fetching regions. Did you set up regions in your Medusa Admin and define a MEDUSA_BACKEND_URL environment variable? Note that the variable is no longer named NEXT_PUBLIC_MEDUSA_BACKEND_URL."
'Middleware.ts: Error fetching regions. Did you set up regions in your Medusa Admin and define a MEDUSA_BACKEND_URL environment variable? Note that the variable is no longer named NEXT_PUBLIC_MEDUSA_BACKEND_URL.',
);
}
@@ -26,13 +27,13 @@ async function getRegionMap(cacheId: string) {
// Fetch regions from Medusa. We can't use the JS client here because middleware is running on Edge and the client needs a Node environment.
const { regions } = await fetch(`${BACKEND_URL}/store/regions`, {
headers: {
"x-publishable-api-key": PUBLISHABLE_API_KEY!,
'x-publishable-api-key': PUBLISHABLE_API_KEY!,
},
next: {
revalidate: 3600,
tags: [`regions-${cacheId}`],
},
cache: "force-cache",
cache: 'force-cache',
}).then(async (response) => {
const json = await response.json();
@@ -45,14 +46,14 @@ async function getRegionMap(cacheId: string) {
if (!regions?.length) {
throw new Error(
"No regions found. Please set up regions in your Medusa Admin."
'No regions found. Please set up regions in your Medusa Admin.',
);
}
// Create a map of country codes to regions.
regions.forEach((region: HttpTypes.StoreRegion) => {
region.countries?.forEach((c) => {
regionMapCache.regionMap.set(c.iso_2 ?? "", region);
regionMapCache.regionMap.set(c.iso_2 ?? '', region);
});
});
@@ -69,17 +70,17 @@ async function getRegionMap(cacheId: string) {
*/
async function getCountryCode(
request: NextRequest,
regionMap: Map<string, HttpTypes.StoreRegion | number>
regionMap: Map<string, HttpTypes.StoreRegion | number>,
) {
try {
let countryCode;
const vercelCountryCode = request.headers
.get("x-vercel-ip-country")
.get('x-vercel-ip-country')
?.toLowerCase();
const urlCountryCode = request.nextUrl.pathname
.split("/")[1]
.split('/')[1]
?.toLowerCase();
if (urlCountryCode && regionMap.has(urlCountryCode)) {
@@ -94,9 +95,9 @@ async function getCountryCode(
return countryCode;
} catch (error) {
if (process.env.NODE_ENV === "development") {
if (process.env.NODE_ENV === 'development') {
console.error(
"Middleware.ts: Error getting the country code. Did you set up regions in your Medusa Admin and define a MEDUSA_BACKEND_URL environment variable? Note that the variable is no longer named NEXT_PUBLIC_MEDUSA_BACKEND_URL."
'Middleware.ts: Error getting the country code. Did you set up regions in your Medusa Admin and define a MEDUSA_BACKEND_URL environment variable? Note that the variable is no longer named NEXT_PUBLIC_MEDUSA_BACKEND_URL.',
);
}
}
@@ -110,7 +111,7 @@ export async function middleware(request: NextRequest) {
let response = NextResponse.redirect(redirectUrl, 307);
let cacheIdCookie = request.cookies.get("_medusa_cache_id");
let cacheIdCookie = request.cookies.get('_medusa_cache_id');
let cacheId = cacheIdCookie?.value || crypto.randomUUID();
@@ -118,7 +119,7 @@ export async function middleware(request: NextRequest) {
try {
regionMap = await getRegionMap(cacheId);
} catch (error) {
console.error("Error fetching regions", error);
console.error('Error fetching regions', error);
return {
redirect: {
destination: '/auth/sign-in',
@@ -130,7 +131,8 @@ export async function middleware(request: NextRequest) {
const countryCode = regionMap && (await getCountryCode(request, regionMap));
const urlHasCountryCode =
countryCode && request.nextUrl.pathname.split("/")[1]?.includes(countryCode);
countryCode &&
request.nextUrl.pathname.split('/')[1]?.includes(countryCode);
// if one of the country codes is in the url and the cache id is set, return next
if (urlHasCountryCode && cacheIdCookie) {
@@ -139,7 +141,7 @@ export async function middleware(request: NextRequest) {
// if one of the country codes is in the url and the cache id is not set, set the cache id and redirect
if (urlHasCountryCode && !cacheIdCookie) {
response.cookies.set("_medusa_cache_id", cacheId, {
response.cookies.set('_medusa_cache_id', cacheId, {
maxAge: 60 * 60 * 24,
});
@@ -147,14 +149,14 @@ export async function middleware(request: NextRequest) {
}
// check if the url is a static asset
if (request.nextUrl.pathname.includes(".")) {
if (request.nextUrl.pathname.includes('.')) {
return NextResponse.next();
}
const redirectPath =
request.nextUrl.pathname === "/" ? "" : request.nextUrl.pathname;
request.nextUrl.pathname === '/' ? '' : request.nextUrl.pathname;
const queryString = request.nextUrl.search ? request.nextUrl.search : "";
const queryString = request.nextUrl.search ? request.nextUrl.search : '';
// If no country code is set, we redirect to the relevant region.
if (!urlHasCountryCode && countryCode) {
@@ -167,6 +169,6 @@ export async function middleware(request: NextRequest) {
export const config = {
matcher: [
"/((?!api|_next/static|_next/image|favicon.ico|images|assets|png|svg|jpg|jpeg|gif|webp).*)",
'/((?!api|_next/static|_next/image|favicon.ico|images|assets|png|svg|jpg|jpeg|gif|webp).*)',
],
};

View File

@@ -1,20 +1,21 @@
import { Disclosure } from "@headlessui/react"
import { Badge, Button, clx } from "@medusajs/ui"
import { useEffect } from "react"
import { useEffect } from 'react';
import useToggleState from "@lib/hooks/use-toggle-state"
import { useFormStatus } from "react-dom"
import { useFormStatus } from 'react-dom';
import { Disclosure } from '@headlessui/react';
import useToggleState from '@lib/hooks/use-toggle-state';
import { Badge, Button, clx } from '@medusajs/ui';
type AccountInfoProps = {
label: string
currentInfo: string | React.ReactNode
isSuccess?: boolean
isError?: boolean
errorMessage?: string
clearState: () => void
children?: React.ReactNode
'data-testid'?: string
}
label: string;
currentInfo: string | React.ReactNode;
isSuccess?: boolean;
isError?: boolean;
errorMessage?: string;
clearState: () => void;
children?: React.ReactNode;
'data-testid'?: string;
};
const AccountInfo = ({
label,
@@ -22,33 +23,35 @@ const AccountInfo = ({
isSuccess,
isError,
clearState,
errorMessage = "An error occurred, please try again",
errorMessage = 'An error occurred, please try again',
children,
'data-testid': dataTestid
'data-testid': dataTestid,
}: AccountInfoProps) => {
const { state, close, toggle } = useToggleState()
const { state, close, toggle } = useToggleState();
const { pending } = useFormStatus()
const { pending } = useFormStatus();
const handleToggle = () => {
clearState()
setTimeout(() => toggle(), 100)
}
clearState();
setTimeout(() => toggle(), 100);
};
useEffect(() => {
if (isSuccess) {
close()
close();
}
}, [isSuccess, close])
}, [isSuccess, close]);
return (
<div className="text-small-regular" data-testid={dataTestid}>
<div className="flex items-end justify-between">
<div className="flex flex-col">
<span className="uppercase text-ui-fg-base">{label}</span>
<div className="flex items-center flex-1 basis-0 justify-end gap-x-4">
{typeof currentInfo === "string" ? (
<span className="font-semibold" data-testid="current-info">{currentInfo}</span>
<span className="text-ui-fg-base uppercase">{label}</span>
<div className="flex flex-1 basis-0 items-center justify-end gap-x-4">
{typeof currentInfo === 'string' ? (
<span className="font-semibold" data-testid="current-info">
{currentInfo}
</span>
) : (
currentInfo
)}
@@ -57,13 +60,13 @@ const AccountInfo = ({
<div>
<Button
variant="secondary"
className="w-[100px] min-h-[25px] py-1"
className="min-h-[25px] w-[100px] py-1"
onClick={handleToggle}
type={state ? "reset" : "button"}
type={state ? 'reset' : 'button'}
data-testid="edit-button"
data-active={state}
>
{state ? "Cancel" : "Edit"}
{state ? 'Cancel' : 'Edit'}
</Button>
</div>
</div>
@@ -73,15 +76,15 @@ const AccountInfo = ({
<Disclosure.Panel
static
className={clx(
"transition-[max-height,opacity] duration-300 ease-in-out overflow-hidden",
'overflow-hidden transition-[max-height,opacity] duration-300 ease-in-out',
{
"max-h-[1000px] opacity-100": isSuccess,
"max-h-0 opacity-0": !isSuccess,
}
'max-h-[1000px] opacity-100': isSuccess,
'max-h-0 opacity-0': !isSuccess,
},
)}
data-testid="success-message"
>
<Badge className="p-2 my-4" color="green">
<Badge className="my-4 p-2" color="green">
<span>{label} updated succesfully</span>
</Badge>
</Disclosure.Panel>
@@ -92,15 +95,15 @@ const AccountInfo = ({
<Disclosure.Panel
static
className={clx(
"transition-[max-height,opacity] duration-300 ease-in-out overflow-hidden",
'overflow-hidden transition-[max-height,opacity] duration-300 ease-in-out',
{
"max-h-[1000px] opacity-100": isError,
"max-h-0 opacity-0": !isError,
}
'max-h-[1000px] opacity-100': isError,
'max-h-0 opacity-0': !isError,
},
)}
data-testid="error-message"
>
<Badge className="p-2 my-4" color="red">
<Badge className="my-4 p-2" color="red">
<span>{errorMessage}</span>
</Badge>
</Disclosure.Panel>
@@ -110,19 +113,19 @@ const AccountInfo = ({
<Disclosure.Panel
static
className={clx(
"transition-[max-height,opacity] duration-300 ease-in-out overflow-visible",
'overflow-visible transition-[max-height,opacity] duration-300 ease-in-out',
{
"max-h-[1000px] opacity-100": state,
"max-h-0 opacity-0": !state,
}
'max-h-[1000px] opacity-100': state,
'max-h-0 opacity-0': !state,
},
)}
>
<div className="flex flex-col gap-y-2 py-4">
<div>{children}</div>
<div className="flex items-center justify-end mt-2">
<div className="mt-2 flex items-center justify-end">
<Button
isLoading={pending}
className="w-full small:max-w-[140px]"
className="small:max-w-[140px] w-full"
type="submit"
data-testid="save-button"
>
@@ -133,7 +136,7 @@ const AccountInfo = ({
</Disclosure.Panel>
</Disclosure>
</div>
)
}
);
};
export default AccountInfo
export default AccountInfo;

View File

@@ -1,28 +1,28 @@
"use client"
'use client';
import { clx } from "@medusajs/ui"
import { ArrowRightOnRectangle } from "@medusajs/icons"
import { useParams, usePathname } from "next/navigation"
import { useParams, usePathname } from 'next/navigation';
import ChevronDown from "@modules/common/icons/chevron-down"
import User from "@modules/common/icons/user"
import MapPin from "@modules/common/icons/map-pin"
import Package from "@modules/common/icons/package"
import LocalizedClientLink from "@modules/common/components/localized-client-link"
import { HttpTypes } from "@medusajs/types"
import { medusaLogout } from "@lib/data/customer"
import { medusaLogout } from '@lib/data/customer';
import { ArrowRightOnRectangle } from '@medusajs/icons';
import { HttpTypes } from '@medusajs/types';
import { clx } from '@medusajs/ui';
import LocalizedClientLink from '@modules/common/components/localized-client-link';
import ChevronDown from '@modules/common/icons/chevron-down';
import MapPin from '@modules/common/icons/map-pin';
import Package from '@modules/common/icons/package';
import User from '@modules/common/icons/user';
const AccountNav = ({
customer,
}: {
customer: HttpTypes.StoreCustomer | null
customer: HttpTypes.StoreCustomer | null;
}) => {
const route = usePathname()
const { countryCode } = useParams() as { countryCode: string }
const route = usePathname();
const { countryCode } = useParams() as { countryCode: string };
const handleLogout = async () => {
await medusaLogout(countryCode)
}
await medusaLogout(countryCode);
};
return (
<div>
@@ -30,11 +30,11 @@ const AccountNav = ({
{route !== `/${countryCode}/account` ? (
<LocalizedClientLink
href="/account"
className="flex items-center gap-x-2 text-small-regular py-2"
className="text-small-regular flex items-center gap-x-2 py-2"
data-testid="account-main-link"
>
<>
<ChevronDown className="transform rotate-90" />
<ChevronDown className="rotate-90 transform" />
<span>Account</span>
</>
</LocalizedClientLink>
@@ -48,7 +48,7 @@ const AccountNav = ({
<li>
<LocalizedClientLink
href="/account/profile"
className="flex items-center justify-between py-4 border-b border-gray-200 px-8"
className="flex items-center justify-between border-b border-gray-200 px-8 py-4"
data-testid="profile-link"
>
<>
@@ -56,14 +56,14 @@ const AccountNav = ({
<User size={20} />
<span>Profile</span>
</div>
<ChevronDown className="transform -rotate-90" />
<ChevronDown className="-rotate-90 transform" />
</>
</LocalizedClientLink>
</li>
<li>
<LocalizedClientLink
href="/account/addresses"
className="flex items-center justify-between py-4 border-b border-gray-200 px-8"
className="flex items-center justify-between border-b border-gray-200 px-8 py-4"
data-testid="addresses-link"
>
<>
@@ -71,27 +71,27 @@ const AccountNav = ({
<MapPin size={20} />
<span>Addresses</span>
</div>
<ChevronDown className="transform -rotate-90" />
<ChevronDown className="-rotate-90 transform" />
</>
</LocalizedClientLink>
</li>
<li>
<LocalizedClientLink
href="/account/orders"
className="flex items-center justify-between py-4 border-b border-gray-200 px-8"
className="flex items-center justify-between border-b border-gray-200 px-8 py-4"
data-testid="orders-link"
>
<div className="flex items-center gap-x-2">
<Package size={20} />
<span>Orders</span>
</div>
<ChevronDown className="transform -rotate-90" />
<ChevronDown className="-rotate-90 transform" />
</LocalizedClientLink>
</li>
<li>
<button
type="button"
className="flex items-center justify-between py-4 border-b border-gray-200 px-8 w-full"
className="flex w-full items-center justify-between border-b border-gray-200 px-8 py-4"
onClick={handleLogout}
data-testid="logout-button"
>
@@ -99,7 +99,7 @@ const AccountNav = ({
<ArrowRightOnRectangle />
<span>Log out</span>
</div>
<ChevronDown className="transform -rotate-90" />
<ChevronDown className="-rotate-90 transform" />
</button>
</li>
</ul>
@@ -107,13 +107,13 @@ const AccountNav = ({
</>
)}
</div>
<div className="hidden small:block" data-testid="account-nav">
<div className="small:block hidden" data-testid="account-nav">
<div>
<div className="pb-4">
<h3 className="text-base-semi">Account</h3>
</div>
<div className="text-base-regular">
<ul className="flex mb-0 justify-start items-start flex-col gap-y-4">
<ul className="mb-0 flex flex-col items-start justify-start gap-y-4">
<li>
<AccountNavLink
href="/account"
@@ -164,36 +164,36 @@ const AccountNav = ({
</div>
</div>
</div>
)
}
);
};
type AccountNavLinkProps = {
href: string
route: string
children: React.ReactNode
"data-testid"?: string
}
href: string;
route: string;
children: React.ReactNode;
'data-testid'?: string;
};
const AccountNavLink = ({
href,
route,
children,
"data-testid": dataTestId,
'data-testid': dataTestId,
}: AccountNavLinkProps) => {
const { countryCode }: { countryCode: string } = useParams()
const { countryCode }: { countryCode: string } = useParams();
const active = route.split(countryCode)[1] === href
const active = route.split(countryCode)[1] === href;
return (
<LocalizedClientLink
href={href}
className={clx("text-ui-fg-subtle hover:text-ui-fg-base", {
"text-ui-fg-base font-semibold": active,
className={clx('text-ui-fg-subtle hover:text-ui-fg-base', {
'text-ui-fg-base font-semibold': active,
})}
data-testid={dataTestId}
>
{children}
</LocalizedClientLink>
)
}
);
};
export default AccountNav
export default AccountNav;

View File

@@ -1,28 +1,29 @@
import React from "react"
import React from 'react';
import AddAddress from "../address-card/add-address"
import EditAddress from "../address-card/edit-address-modal"
import { HttpTypes } from "@medusajs/types"
import { HttpTypes } from '@medusajs/types';
import AddAddress from '../address-card/add-address';
import EditAddress from '../address-card/edit-address-modal';
type AddressBookProps = {
customer: HttpTypes.StoreCustomer
region: HttpTypes.StoreRegion
}
customer: HttpTypes.StoreCustomer;
region: HttpTypes.StoreRegion;
};
const AddressBook: React.FC<AddressBookProps> = ({ customer, region }) => {
const { addresses } = customer
const { addresses } = customer;
return (
<div className="w-full">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 flex-1 mt-4">
<div className="mt-4 grid flex-1 grid-cols-1 gap-4 lg:grid-cols-2">
<AddAddress region={region} addresses={addresses} />
{addresses.map((address) => {
return (
<EditAddress region={region} address={address} key={address.id} />
)
);
})}
</div>
</div>
)
}
);
};
export default AddressBook
export default AddressBook;

View File

@@ -1,55 +1,55 @@
"use client"
'use client';
import { Plus } from "@medusajs/icons"
import { Button, Heading } from "@medusajs/ui"
import { useEffect, useState, useActionState } from "react"
import { useActionState, useEffect, useState } from 'react';
import useToggleState from "@lib/hooks/use-toggle-state"
import CountrySelect from "@modules/checkout/components/country-select"
import Input from "@modules/common/components/input"
import Modal from "@modules/common/components/modal"
import { SubmitButton } from "@modules/checkout/components/submit-button"
import { HttpTypes } from "@medusajs/types"
import { addCustomerAddress } from "@lib/data/customer"
import { addCustomerAddress } from '@lib/data/customer';
import useToggleState from '@lib/hooks/use-toggle-state';
import { Plus } from '@medusajs/icons';
import { HttpTypes } from '@medusajs/types';
import { Button, Heading } from '@medusajs/ui';
import CountrySelect from '@modules/checkout/components/country-select';
import { SubmitButton } from '@modules/checkout/components/submit-button';
import Input from '@modules/common/components/input';
import Modal from '@modules/common/components/modal';
const AddAddress = ({
region,
addresses,
}: {
region: HttpTypes.StoreRegion
addresses: HttpTypes.StoreCustomerAddress[]
region: HttpTypes.StoreRegion;
addresses: HttpTypes.StoreCustomerAddress[];
}) => {
const [successState, setSuccessState] = useState(false)
const { state, open, close: closeModal } = useToggleState(false)
const [successState, setSuccessState] = useState(false);
const { state, open, close: closeModal } = useToggleState(false);
const [formState, formAction] = useActionState(addCustomerAddress, {
isDefaultShipping: addresses.length === 0,
success: false,
error: null,
})
});
const close = () => {
setSuccessState(false)
closeModal()
}
setSuccessState(false);
closeModal();
};
useEffect(() => {
if (successState) {
close()
close();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [successState])
}, [successState]);
useEffect(() => {
if (formState.success) {
setSuccessState(true)
setSuccessState(true);
}
}, [formState])
}, [formState]);
return (
<>
<button
className="border border-ui-border-base rounded-rounded p-5 min-h-[220px] h-full w-full flex flex-col justify-between"
className="border-ui-border-base rounded-rounded flex h-full min-h-[220px] w-full flex-col justify-between border p-5"
onClick={open}
data-testid="add-address-button"
>
@@ -137,7 +137,7 @@ const AddAddress = ({
</div>
{formState.error && (
<div
className="text-rose-500 text-small-regular py-2"
className="text-small-regular py-2 text-rose-500"
data-testid="address-error"
>
{formState.error}
@@ -145,7 +145,7 @@ const AddAddress = ({
)}
</Modal.Body>
<Modal.Footer>
<div className="flex gap-3 mt-6">
<div className="mt-6 flex gap-3">
<Button
type="reset"
variant="secondary"
@@ -161,7 +161,7 @@ const AddAddress = ({
</form>
</Modal>
</>
)
}
);
};
export default AddAddress
export default AddAddress;

View File

@@ -1,80 +1,80 @@
"use client"
'use client';
import React, { useEffect, useState, useActionState } from "react"
import { PencilSquare as Edit, Trash } from "@medusajs/icons"
import { Button, Heading, Text, clx } from "@medusajs/ui"
import React, { useActionState, useEffect, useState } from 'react';
import useToggleState from "@lib/hooks/use-toggle-state"
import CountrySelect from "@modules/checkout/components/country-select"
import Input from "@modules/common/components/input"
import Modal from "@modules/common/components/modal"
import Spinner from "@modules/common/icons/spinner"
import { SubmitButton } from "@modules/checkout/components/submit-button"
import { HttpTypes } from "@medusajs/types"
import {
deleteCustomerAddress,
updateCustomerAddress,
} from "@lib/data/customer"
} from '@lib/data/customer';
import useToggleState from '@lib/hooks/use-toggle-state';
import { PencilSquare as Edit, Trash } from '@medusajs/icons';
import { HttpTypes } from '@medusajs/types';
import { Button, Heading, Text, clx } from '@medusajs/ui';
import CountrySelect from '@modules/checkout/components/country-select';
import { SubmitButton } from '@modules/checkout/components/submit-button';
import Input from '@modules/common/components/input';
import Modal from '@modules/common/components/modal';
import Spinner from '@modules/common/icons/spinner';
type EditAddressProps = {
region: HttpTypes.StoreRegion
address: HttpTypes.StoreCustomerAddress
isActive?: boolean
}
region: HttpTypes.StoreRegion;
address: HttpTypes.StoreCustomerAddress;
isActive?: boolean;
};
const EditAddress: React.FC<EditAddressProps> = ({
region,
address,
isActive = false,
}) => {
const [removing, setRemoving] = useState(false)
const [successState, setSuccessState] = useState(false)
const { state, open, close: closeModal } = useToggleState(false)
const [removing, setRemoving] = useState(false);
const [successState, setSuccessState] = useState(false);
const { state, open, close: closeModal } = useToggleState(false);
const [formState, formAction] = useActionState(updateCustomerAddress, {
success: false,
error: null,
addressId: address.id,
})
});
const close = () => {
setSuccessState(false)
closeModal()
}
setSuccessState(false);
closeModal();
};
useEffect(() => {
if (successState) {
close()
close();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [successState])
}, [successState]);
useEffect(() => {
if (formState.success) {
setSuccessState(true)
setSuccessState(true);
}
}, [formState])
}, [formState]);
const removeAddress = async () => {
setRemoving(true)
await deleteCustomerAddress(address.id)
setRemoving(false)
}
setRemoving(true);
await deleteCustomerAddress(address.id);
setRemoving(false);
};
return (
<>
<div
className={clx(
"border rounded-rounded p-5 min-h-[220px] h-full w-full flex flex-col justify-between transition-colors",
'rounded-rounded flex h-full min-h-[220px] w-full flex-col justify-between border p-5 transition-colors',
{
"border-gray-900": isActive,
}
'border-gray-900': isActive,
},
)}
data-testid="address-container"
>
<div className="flex flex-col">
<Heading
className="text-left text-base-semi"
className="text-base-semi text-left"
data-testid="address-name"
>
{address.first_name} {address.last_name}
@@ -87,7 +87,7 @@ const EditAddress: React.FC<EditAddressProps> = ({
{address.company}
</Text>
)}
<Text className="flex flex-col text-left text-base-regular mt-2">
<Text className="text-base-regular mt-2 flex flex-col text-left">
<span data-testid="address-address">
{address.address_1}
{address.address_2 && <span>, {address.address_2}</span>}
@@ -211,13 +211,13 @@ const EditAddress: React.FC<EditAddressProps> = ({
/>
</div>
{formState.error && (
<div className="text-rose-500 text-small-regular py-2">
<div className="text-small-regular py-2 text-rose-500">
{formState.error}
</div>
)}
</Modal.Body>
<Modal.Footer>
<div className="flex gap-3 mt-6">
<div className="mt-6 flex gap-3">
<Button
type="reset"
variant="secondary"
@@ -233,7 +233,7 @@ const EditAddress: React.FC<EditAddressProps> = ({
</form>
</Modal>
</>
)
}
);
};
export default EditAddress
export default EditAddress;

View File

@@ -1,55 +1,55 @@
// account-info
export { default as AccountInfo } from "./account-info";
export * from "./account-info";
export { default as AccountInfo } from './account-info';
export * from './account-info';
// account-nav
export { default as AccountNav } from "./account-nav";
export * from "./account-nav";
export { default as AccountNav } from './account-nav';
export * from './account-nav';
// address-book
export { default as AddressBook } from "./address-book";
export * from "./address-book";
export { default as AddressBook } from './address-book';
export * from './address-book';
// login
export { default as Login } from "./login";
export * from "./login";
export { default as Login } from './login';
export * from './login';
// order-card
export { default as OrderCard } from "./order-card";
export * from "./order-card";
export { default as OrderCard } from './order-card';
export * from './order-card';
// order-overview
export { default as OrderOverview } from "./order-overview";
export * from "./order-overview";
export { default as OrderOverview } from './order-overview';
export * from './order-overview';
// overview
export { default as Overview } from "./overview";
export * from "./overview";
export { default as Overview } from './overview';
export * from './overview';
// profile-billing-address
export { default as ProfileBillingAddress } from "./profile-billing-address";
export * from "./profile-billing-address";
export { default as ProfileBillingAddress } from './profile-billing-address';
export * from './profile-billing-address';
// profile-email
export { default as ProfileEmail } from "./profile-email";
export * from "./profile-email";
export { default as ProfileEmail } from './profile-email';
export * from './profile-email';
// profile-name
export { default as ProfileName } from "./profile-name";
export * from "./profile-name";
export { default as ProfileName } from './profile-name';
export * from './profile-name';
// profile-password
export { default as ProfilePassword } from "./profile-password";
export * from "./profile-password";
export { default as ProfilePassword } from './profile-password';
export * from './profile-password';
// profile-phone
export { default as ProfilePhone } from "./profile-phone";
export * from "./profile-phone";
export { default as ProfilePhone } from './profile-phone';
export * from './profile-phone';
// register
export { default as Register } from "./register";
export * from "./register";
export { default as Register } from './register';
export * from './register';
// transfer-request-form
export { default as TransferRequestForm } from "./transfer-request-form";
export * from "./transfer-request-form";
export { default as TransferRequestForm } from './transfer-request-form';
export * from './transfer-request-form';

View File

@@ -1,28 +1,29 @@
import { login } from "@lib/data/customer"
import { LOGIN_VIEW } from "@modules/account/templates/login-template"
import ErrorMessage from "@modules/checkout/components/error-message"
import { SubmitButton } from "@modules/checkout/components/submit-button"
import Input from "@modules/common/components/input"
import { useActionState } from "react"
import { useActionState } from 'react';
import { login } from '@lib/data/customer';
import { LOGIN_VIEW } from '@modules/account/templates/login-template';
import ErrorMessage from '@modules/checkout/components/error-message';
import { SubmitButton } from '@modules/checkout/components/submit-button';
import Input from '@modules/common/components/input';
type Props = {
setCurrentView: (view: LOGIN_VIEW) => void
}
setCurrentView: (view: LOGIN_VIEW) => void;
};
const Login = ({ setCurrentView }: Props) => {
const [message, formAction] = useActionState(login, null)
const [message, formAction] = useActionState(login, null);
return (
<div
className="max-w-sm w-full flex flex-col items-center"
className="flex w-full max-w-sm flex-col items-center"
data-testid="login-page"
>
<h1 className="text-large-semi uppercase mb-6">Welcome back</h1>
<p className="text-center text-base-regular text-ui-fg-base mb-8">
<h1 className="text-large-semi mb-6 uppercase">Welcome back</h1>
<p className="text-base-regular text-ui-fg-base mb-8 text-center">
Sign in to access an enhanced shopping experience.
</p>
<form className="w-full" action={formAction}>
<div className="flex flex-col w-full gap-y-2">
<div className="flex w-full flex-col gap-y-2">
<Input
label="Email"
name="email"
@@ -42,12 +43,12 @@ const Login = ({ setCurrentView }: Props) => {
/>
</div>
<ErrorMessage error={message} data-testid="login-error-message" />
<SubmitButton data-testid="sign-in-button" className="w-full mt-6">
<SubmitButton data-testid="sign-in-button" className="mt-6 w-full">
Sign in
</SubmitButton>
</form>
<span className="text-center text-ui-fg-base text-small-regular mt-6">
Not a member?{" "}
<span className="text-ui-fg-base text-small-regular mt-6 text-center">
Not a member?{' '}
<button
onClick={() => setCurrentView(LOGIN_VIEW.REGISTER)}
className="underline"
@@ -58,7 +59,7 @@ const Login = ({ setCurrentView }: Props) => {
.
</span>
</div>
)
}
);
};
export default Login
export default Login;

View File

@@ -1,34 +1,34 @@
import { Button } from "@medusajs/ui"
import { useMemo } from "react"
import { useMemo } from 'react';
import Thumbnail from "@modules/products/components/thumbnail"
import LocalizedClientLink from "@modules/common/components/localized-client-link"
import { convertToLocale } from "@lib/util/money"
import { HttpTypes } from "@medusajs/types"
import { convertToLocale } from '@lib/util/money';
import { HttpTypes } from '@medusajs/types';
import { Button } from '@medusajs/ui';
import LocalizedClientLink from '@modules/common/components/localized-client-link';
import Thumbnail from '@modules/products/components/thumbnail';
type OrderCardProps = {
order: HttpTypes.StoreOrder
}
order: HttpTypes.StoreOrder;
};
const OrderCard = ({ order }: OrderCardProps) => {
const numberOfLines = useMemo(() => {
return (
order.items?.reduce((acc, item) => {
return acc + item.quantity
return acc + item.quantity;
}, 0) ?? 0
)
}, [order])
);
}, [order]);
const numberOfProducts = useMemo(() => {
return order.items?.length ?? 0
}, [order])
return order.items?.length ?? 0;
}, [order]);
return (
<div className="bg-white flex flex-col" data-testid="order-card">
<div className="uppercase text-large-semi mb-1">
<div className="flex flex-col bg-white" data-testid="order-card">
<div className="text-large-semi mb-1 uppercase">
#<span data-testid="order-display-id">{order.display_id}</span>
</div>
<div className="flex items-center divide-x divide-gray-200 text-small-regular text-ui-fg-base">
<div className="text-small-regular text-ui-fg-base flex items-center divide-x divide-gray-200">
<span className="pr-2" data-testid="order-created-at">
{new Date(order.created_at).toDateString()}
</span>
@@ -39,10 +39,10 @@ const OrderCard = ({ order }: OrderCardProps) => {
})}
</span>
<span className="pl-2">{`${numberOfLines} ${
numberOfLines > 1 ? "items" : "item"
numberOfLines > 1 ? 'items' : 'item'
}`}</span>
</div>
<div className="grid grid-cols-2 small:grid-cols-4 gap-4 my-4">
<div className="small:grid-cols-4 my-4 grid grid-cols-2 gap-4">
{order.items?.slice(0, 3).map((i) => {
return (
<div
@@ -51,7 +51,7 @@ const OrderCard = ({ order }: OrderCardProps) => {
data-testid="order-item"
>
<Thumbnail thumbnail={i.thumbnail} images={[]} size="full" />
<div className="flex items-center text-small-regular text-ui-fg-base">
<div className="text-small-regular text-ui-fg-base flex items-center">
<span
className="text-ui-fg-base font-semibold"
data-testid="item-title"
@@ -62,10 +62,10 @@ const OrderCard = ({ order }: OrderCardProps) => {
<span data-testid="item-quantity">{i.quantity}</span>
</div>
</div>
)
);
})}
{numberOfProducts > 4 && (
<div className="w-full h-full flex flex-col items-center justify-center">
<div className="flex h-full w-full flex-col items-center justify-center">
<span className="text-small-regular text-ui-fg-base">
+ {numberOfLines - 4}
</span>
@@ -81,7 +81,7 @@ const OrderCard = ({ order }: OrderCardProps) => {
</LocalizedClientLink>
</div>
</div>
)
}
);
};
export default OrderCard
export default OrderCard;

View File

@@ -1,35 +1,35 @@
"use client"
'use client';
import { Button } from "@medusajs/ui"
import { HttpTypes } from '@medusajs/types';
import { Button } from '@medusajs/ui';
import LocalizedClientLink from '@modules/common/components/localized-client-link';
import OrderCard from "../order-card"
import LocalizedClientLink from "@modules/common/components/localized-client-link"
import { HttpTypes } from "@medusajs/types"
import OrderCard from '../order-card';
const OrderOverview = ({ orders }: { orders: HttpTypes.StoreOrder[] }) => {
if (orders?.length) {
return (
<div className="flex flex-col gap-y-8 w-full">
<div className="flex w-full flex-col gap-y-8">
{orders.map((o) => (
<div
key={o.id}
className="border-b border-gray-200 pb-6 last:pb-0 last:border-none"
className="border-b border-gray-200 pb-6 last:border-none last:pb-0"
>
<OrderCard order={o} />
</div>
))}
</div>
)
);
}
return (
<div
className="w-full flex flex-col items-center gap-y-4"
className="flex w-full flex-col items-center gap-y-4"
data-testid="no-orders-container"
>
<h2 className="text-large-semi">Nothing to see here</h2>
<p className="text-base-regular">
You don&apos;t have any orders yet, let us change that {":)"}
You don&apos;t have any orders yet, let us change that {':)'}
</p>
<div className="mt-4">
<LocalizedClientLink href="/" passHref>
@@ -39,7 +39,7 @@ const OrderOverview = ({ orders }: { orders: HttpTypes.StoreOrder[] }) => {
</LocalizedClientLink>
</div>
</div>
)
}
);
};
export default OrderOverview
export default OrderOverview;

View File

@@ -1,25 +1,24 @@
import { Container } from "@medusajs/ui"
import ChevronDown from "@modules/common/icons/chevron-down"
import LocalizedClientLink from "@modules/common/components/localized-client-link"
import { convertToLocale } from "@lib/util/money"
import { HttpTypes } from "@medusajs/types"
import { convertToLocale } from '@lib/util/money';
import { HttpTypes } from '@medusajs/types';
import { Container } from '@medusajs/ui';
import LocalizedClientLink from '@modules/common/components/localized-client-link';
import ChevronDown from '@modules/common/icons/chevron-down';
type OverviewProps = {
customer: HttpTypes.StoreCustomer | null
orders: HttpTypes.StoreOrder[] | null
}
customer: HttpTypes.StoreCustomer | null;
orders: HttpTypes.StoreOrder[] | null;
};
const Overview = ({ customer, orders }: OverviewProps) => {
return (
<div data-testid="overview-page-wrapper">
<div className="hidden small:block">
<div className="text-xl-semi flex justify-between items-center mb-4">
<div className="small:block hidden">
<div className="text-xl-semi mb-4 flex items-center justify-between">
<span data-testid="welcome-message" data-value={customer?.first_name}>
Hello {customer?.first_name}
</span>
<span className="text-small-regular text-ui-fg-base">
Signed in as:{" "}
Signed in as:{' '}
<span
className="font-semibold"
data-testid="customer-email"
@@ -29,9 +28,9 @@ const Overview = ({ customer, orders }: OverviewProps) => {
</span>
</span>
</div>
<div className="flex flex-col py-8 border-t border-gray-200">
<div className="flex flex-col gap-y-4 h-full col-span-1 row-span-2 flex-1">
<div className="flex items-start gap-x-16 mb-6">
<div className="flex flex-col border-t border-gray-200 py-8">
<div className="col-span-1 row-span-2 flex h-full flex-1 flex-col gap-y-4">
<div className="mb-6 flex items-start gap-x-16">
<div className="flex flex-col gap-y-4">
<h3 className="text-large-semi">Profile</h3>
<div className="flex items-end gap-x-2">
@@ -42,7 +41,7 @@ const Overview = ({ customer, orders }: OverviewProps) => {
>
{getProfileCompletion(customer)}%
</span>
<span className="uppercase text-base-regular text-ui-fg-subtle">
<span className="text-base-regular text-ui-fg-subtle uppercase">
Completed
</span>
</div>
@@ -58,7 +57,7 @@ const Overview = ({ customer, orders }: OverviewProps) => {
>
{customer?.addresses?.length || 0}
</span>
<span className="uppercase text-base-regular text-ui-fg-subtle">
<span className="text-base-regular text-ui-fg-subtle uppercase">
Saved
</span>
</div>
@@ -84,8 +83,8 @@ const Overview = ({ customer, orders }: OverviewProps) => {
<LocalizedClientLink
href={`/account/orders/details/${order.id}`}
>
<Container className="bg-gray-50 flex justify-between items-center p-4">
<div className="grid grid-cols-3 grid-rows-2 text-small-regular gap-x-4 flex-1">
<Container className="flex items-center justify-between bg-gray-50 p-4">
<div className="text-small-regular grid flex-1 grid-cols-3 grid-rows-2 gap-x-4">
<span className="font-semibold">Date placed</span>
<span className="font-semibold">
Order number
@@ -121,7 +120,7 @@ const Overview = ({ customer, orders }: OverviewProps) => {
</Container>
</LocalizedClientLink>
</li>
)
);
})
) : (
<span data-testid="no-orders-message">No recent orders</span>
@@ -132,37 +131,37 @@ const Overview = ({ customer, orders }: OverviewProps) => {
</div>
</div>
</div>
)
}
);
};
const getProfileCompletion = (customer: HttpTypes.StoreCustomer | null) => {
let count = 0
let count = 0;
if (!customer) {
return 0
return 0;
}
if (customer.email) {
count++
count++;
}
if (customer.first_name && customer.last_name) {
count++
count++;
}
if (customer.phone) {
count++
count++;
}
const billingAddress = customer.addresses?.find(
(addr) => addr.is_default_billing
)
(addr) => addr.is_default_billing,
);
if (billingAddress) {
count++
count++;
}
return (count / 4) * 100
}
return (count / 4) * 100;
};
export default Overview
export default Overview;

View File

@@ -1,18 +1,18 @@
"use client"
'use client';
import React, { useEffect, useMemo, useActionState } from "react"
import React, { useActionState, useEffect, useMemo } from 'react';
import Input from "@modules/common/components/input"
import NativeSelect from "@modules/common/components/native-select"
import { addCustomerAddress, updateCustomerAddress } from '@lib/data/customer';
import { HttpTypes } from '@medusajs/types';
import Input from '@modules/common/components/input';
import NativeSelect from '@modules/common/components/native-select';
import AccountInfo from "../account-info"
import { HttpTypes } from "@medusajs/types"
import { addCustomerAddress, updateCustomerAddress } from "@lib/data/customer"
import AccountInfo from '../account-info';
type MyInformationProps = {
customer: HttpTypes.StoreCustomer
regions: HttpTypes.StoreRegion[]
}
customer: HttpTypes.StoreCustomer;
regions: HttpTypes.StoreRegion[];
};
const ProfileBillingAddress: React.FC<MyInformationProps> = ({
customer,
@@ -25,51 +25,51 @@ const ProfileBillingAddress: React.FC<MyInformationProps> = ({
return region.countries?.map((country) => ({
value: country.iso_2,
label: country.display_name,
}))
}));
})
.flat() || []
)
}, [regions])
);
}, [regions]);
const [successState, setSuccessState] = React.useState(false)
const [successState, setSuccessState] = React.useState(false);
const billingAddress = customer.addresses?.find(
(addr) => addr.is_default_billing
)
(addr) => addr.is_default_billing,
);
const initialState: Record<string, any> = {
isDefaultBilling: true,
isDefaultShipping: false,
error: false,
success: false,
}
};
if (billingAddress) {
initialState.addressId = billingAddress.id
initialState.addressId = billingAddress.id;
}
const [state, formAction] = useActionState(
billingAddress ? updateCustomerAddress : addCustomerAddress,
initialState
)
initialState,
);
const clearState = () => {
setSuccessState(false)
}
setSuccessState(false);
};
useEffect(() => {
setSuccessState(state.success)
}, [state])
setSuccessState(state.success);
}, [state]);
const currentInfo = useMemo(() => {
if (!billingAddress) {
return "No billing address"
return 'No billing address';
}
const country =
regionOptions?.find(
(country) => country?.value === billingAddress.country_code
)?.label || billingAddress.country_code?.toUpperCase()
(country) => country?.value === billingAddress.country_code,
)?.label || billingAddress.country_code?.toUpperCase();
return (
<div className="flex flex-col font-semibold" data-testid="current-info">
@@ -79,15 +79,15 @@ const ProfileBillingAddress: React.FC<MyInformationProps> = ({
<span>{billingAddress.company}</span>
<span>
{billingAddress.address_1}
{billingAddress.address_2 ? `, ${billingAddress.address_2}` : ""}
{billingAddress.address_2 ? `, ${billingAddress.address_2}` : ''}
</span>
<span>
{billingAddress.postal_code}, {billingAddress.city}
</span>
<span>{country}</span>
</div>
)
}, [billingAddress, regionOptions])
);
}, [billingAddress, regionOptions]);
return (
<form action={formAction} onReset={() => clearState()} className="w-full">
@@ -170,13 +170,13 @@ const ProfileBillingAddress: React.FC<MyInformationProps> = ({
<option key={i} value={option?.value}>
{option?.label}
</option>
)
);
})}
</NativeSelect>
</div>
</AccountInfo>
</form>
)
}
);
};
export default ProfileBillingAddress
export default ProfileBillingAddress;

View File

@@ -1,49 +1,50 @@
"use client"
'use client';
import React, { useEffect, useActionState } from "react";
import React, { useActionState, useEffect } from 'react';
import Input from "@modules/common/components/input"
import { HttpTypes } from '@medusajs/types';
import Input from '@modules/common/components/input';
import AccountInfo from '../account-info';
import AccountInfo from "../account-info"
import { HttpTypes } from "@medusajs/types"
// import { updateCustomer } from "@lib/data/customer"
type MyInformationProps = {
customer: HttpTypes.StoreCustomer
}
customer: HttpTypes.StoreCustomer;
};
const ProfileEmail: React.FC<MyInformationProps> = ({ customer }) => {
const [successState, setSuccessState] = React.useState(false)
const [successState, setSuccessState] = React.useState(false);
// TODO: It seems we don't support updating emails now?
const updateCustomerEmail = (
_currentState: Record<string, unknown>,
formData: FormData
formData: FormData,
) => {
const customer = {
email: formData.get("email") as string,
}
email: formData.get('email') as string,
};
try {
// await updateCustomer(customer)
return { success: true, error: null }
return { success: true, error: null };
} catch (error: any) {
return { success: false, error: error.toString() }
return { success: false, error: error.toString() };
}
}
};
const [state, formAction] = useActionState(updateCustomerEmail, {
error: false,
success: false,
})
});
const clearState = () => {
setSuccessState(false)
}
setSuccessState(false);
};
useEffect(() => {
setSuccessState(state.success)
}, [state])
setSuccessState(state.success);
}, [state]);
return (
<form action={formAction} className="w-full">
@@ -69,7 +70,7 @@ const ProfileEmail: React.FC<MyInformationProps> = ({ customer }) => {
</div>
</AccountInfo>
</form>
)
}
);
};
export default ProfileEmail
export default ProfileEmail;

View File

@@ -1,49 +1,49 @@
"use client"
'use client';
import React, { useEffect, useActionState } from "react";
import React, { useActionState, useEffect } from 'react';
import Input from "@modules/common/components/input"
import { updateCustomer } from '@lib/data/customer';
import { HttpTypes } from '@medusajs/types';
import Input from '@modules/common/components/input';
import AccountInfo from "../account-info"
import { HttpTypes } from "@medusajs/types"
import { updateCustomer } from "@lib/data/customer"
import AccountInfo from '../account-info';
type MyInformationProps = {
customer: HttpTypes.StoreCustomer
}
customer: HttpTypes.StoreCustomer;
};
const ProfileName: React.FC<MyInformationProps> = ({ customer }) => {
const [successState, setSuccessState] = React.useState(false)
const [successState, setSuccessState] = React.useState(false);
const updateCustomerName = async (
_currentState: Record<string, unknown>,
formData: FormData
formData: FormData,
) => {
const customer = {
first_name: formData.get("first_name") as string,
last_name: formData.get("last_name") as string,
}
first_name: formData.get('first_name') as string,
last_name: formData.get('last_name') as string,
};
try {
await updateCustomer(customer)
return { success: true, error: null }
await updateCustomer(customer);
return { success: true, error: null };
} catch (error: any) {
return { success: false, error: error.toString() }
return { success: false, error: error.toString() };
}
}
};
const [state, formAction] = useActionState(updateCustomerName, {
error: false,
success: false,
})
});
const clearState = () => {
setSuccessState(false)
}
setSuccessState(false);
};
useEffect(() => {
setSuccessState(state.success)
}, [state])
setSuccessState(state.success);
}, [state]);
return (
<form action={formAction} className="w-full overflow-visible">
@@ -60,20 +60,20 @@ const ProfileName: React.FC<MyInformationProps> = ({ customer }) => {
label="First name"
name="first_name"
required
defaultValue={customer.first_name ?? ""}
defaultValue={customer.first_name ?? ''}
data-testid="first-name-input"
/>
<Input
label="Last name"
name="last_name"
required
defaultValue={customer.last_name ?? ""}
defaultValue={customer.last_name ?? ''}
data-testid="last-name-input"
/>
</div>
</AccountInfo>
</form>
)
}
);
};
export default ProfileName
export default ProfileName;

View File

@@ -1,26 +1,28 @@
"use client"
'use client';
import React, { useEffect, useActionState } from "react"
import Input from "@modules/common/components/input"
import AccountInfo from "../account-info"
import { HttpTypes } from "@medusajs/types"
import { toast } from "@medusajs/ui"
import React, { useActionState, useEffect } from 'react';
import { HttpTypes } from '@medusajs/types';
import { toast } from '@medusajs/ui';
import Input from '@modules/common/components/input';
import AccountInfo from '../account-info';
type MyInformationProps = {
customer: HttpTypes.StoreCustomer
}
customer: HttpTypes.StoreCustomer;
};
const ProfilePassword: React.FC<MyInformationProps> = ({ customer }) => {
const [successState, setSuccessState] = React.useState(false)
const [successState, setSuccessState] = React.useState(false);
// TODO: Add support for password updates
const updatePassword = async () => {
toast.info("Password update is not implemented")
}
toast.info('Password update is not implemented');
};
const clearState = () => {
setSuccessState(false)
}
setSuccessState(false);
};
return (
<form
@@ -64,7 +66,7 @@ const ProfilePassword: React.FC<MyInformationProps> = ({ customer }) => {
</div>
</AccountInfo>
</form>
)
}
);
};
export default ProfilePassword
export default ProfilePassword;

View File

@@ -1,48 +1,48 @@
"use client"
'use client';
import React, { useEffect, useActionState } from "react";
import React, { useActionState, useEffect } from 'react';
import Input from "@modules/common/components/input"
import { updateCustomer } from '@lib/data/customer';
import { HttpTypes } from '@medusajs/types';
import Input from '@modules/common/components/input';
import AccountInfo from "../account-info"
import { HttpTypes } from "@medusajs/types"
import { updateCustomer } from "@lib/data/customer"
import AccountInfo from '../account-info';
type MyInformationProps = {
customer: HttpTypes.StoreCustomer
}
customer: HttpTypes.StoreCustomer;
};
const ProfileEmail: React.FC<MyInformationProps> = ({ customer }) => {
const [successState, setSuccessState] = React.useState(false)
const [successState, setSuccessState] = React.useState(false);
const updateCustomerPhone = async (
_currentState: Record<string, unknown>,
formData: FormData
formData: FormData,
) => {
const customer = {
phone: formData.get("phone") as string,
}
phone: formData.get('phone') as string,
};
try {
await updateCustomer(customer)
return { success: true, error: null }
await updateCustomer(customer);
return { success: true, error: null };
} catch (error: any) {
return { success: false, error: error.toString() }
return { success: false, error: error.toString() };
}
}
};
const [state, formAction] = useActionState(updateCustomerPhone, {
error: false,
success: false,
})
});
const clearState = () => {
setSuccessState(false)
}
setSuccessState(false);
};
useEffect(() => {
setSuccessState(state.success)
}, [state])
setSuccessState(state.success);
}, [state]);
return (
<form action={formAction} className="w-full">
@@ -62,13 +62,13 @@ const ProfileEmail: React.FC<MyInformationProps> = ({ customer }) => {
type="phone"
autoComplete="phone"
required
defaultValue={customer.phone ?? ""}
defaultValue={customer.phone ?? ''}
data-testid="phone-input"
/>
</div>
</AccountInfo>
</form>
)
}
);
};
export default ProfileEmail
export default ProfileEmail;

View File

@@ -1,34 +1,35 @@
"use client"
'use client';
import { useActionState } from "react"
import Input from "@modules/common/components/input"
import { LOGIN_VIEW } from "@modules/account/templates/login-template"
import ErrorMessage from "@modules/checkout/components/error-message"
import { SubmitButton } from "@modules/checkout/components/submit-button"
import LocalizedClientLink from "@modules/common/components/localized-client-link"
import { signup } from "@lib/data/customer"
import { useActionState } from 'react';
import { signup } from '@lib/data/customer';
import { LOGIN_VIEW } from '@modules/account/templates/login-template';
import ErrorMessage from '@modules/checkout/components/error-message';
import { SubmitButton } from '@modules/checkout/components/submit-button';
import Input from '@modules/common/components/input';
import LocalizedClientLink from '@modules/common/components/localized-client-link';
type Props = {
setCurrentView: (view: LOGIN_VIEW) => void
}
setCurrentView: (view: LOGIN_VIEW) => void;
};
const Register = ({ setCurrentView }: Props) => {
const [message, formAction] = useActionState(signup, null)
const [message, formAction] = useActionState(signup, null);
return (
<div
className="max-w-sm flex flex-col items-center"
className="flex max-w-sm flex-col items-center"
data-testid="register-page"
>
<h1 className="text-large-semi uppercase mb-6">
<h1 className="text-large-semi mb-6 uppercase">
Become a Medusa Store Member
</h1>
<p className="text-center text-base-regular text-ui-fg-base mb-4">
<p className="text-base-regular text-ui-fg-base mb-4 text-center">
Create your Medusa Store Member profile, and get access to an enhanced
shopping experience.
</p>
<form className="w-full flex flex-col" action={formAction}>
<div className="flex flex-col w-full gap-y-2">
<form className="flex w-full flex-col" action={formAction}>
<div className="flex w-full flex-col gap-y-2">
<Input
label="First name"
name="first_name"
@@ -68,15 +69,15 @@ const Register = ({ setCurrentView }: Props) => {
/>
</div>
<ErrorMessage error={message} data-testid="register-error" />
<span className="text-center text-ui-fg-base text-small-regular mt-6">
By creating an account, you agree to Medusa Store&apos;s{" "}
<span className="text-ui-fg-base text-small-regular mt-6 text-center">
By creating an account, you agree to Medusa Store&apos;s{' '}
<LocalizedClientLink
href="/content/privacy-policy"
className="underline"
>
Privacy Policy
</LocalizedClientLink>{" "}
and{" "}
</LocalizedClientLink>{' '}
and{' '}
<LocalizedClientLink
href="/content/terms-of-use"
className="underline"
@@ -85,12 +86,12 @@ const Register = ({ setCurrentView }: Props) => {
</LocalizedClientLink>
.
</span>
<SubmitButton className="w-full mt-6" data-testid="register-button">
<SubmitButton className="mt-6 w-full" data-testid="register-button">
Join
</SubmitButton>
</form>
<span className="text-center text-ui-fg-base text-small-regular mt-6">
Already a member?{" "}
<span className="text-ui-fg-base text-small-regular mt-6 text-center">
Already a member?{' '}
<button
onClick={() => setCurrentView(LOGIN_VIEW.SIGN_IN)}
className="underline"
@@ -100,7 +101,7 @@ const Register = ({ setCurrentView }: Props) => {
.
</span>
</div>
)
}
);
};
export default Register
export default Register;

View File

@@ -1,30 +1,38 @@
"use client"
'use client';
import { useActionState } from "react"
import { createTransferRequest } from "@lib/data/orders"
import { Text, Heading, Input, Button, IconButton, Toaster } from "@medusajs/ui"
import { SubmitButton } from "@modules/checkout/components/submit-button"
import { CheckCircleMiniSolid, XCircleSolid } from "@medusajs/icons"
import { useEffect, useState } from "react"
import { useActionState } from 'react';
import { useEffect, useState } from 'react';
import { createTransferRequest } from '@lib/data/orders';
import { CheckCircleMiniSolid, XCircleSolid } from '@medusajs/icons';
import {
Button,
Heading,
IconButton,
Input,
Text,
Toaster,
} from '@medusajs/ui';
import { SubmitButton } from '@modules/checkout/components/submit-button';
export default function TransferRequestForm() {
const [showSuccess, setShowSuccess] = useState(false)
const [showSuccess, setShowSuccess] = useState(false);
const [state, formAction] = useActionState(createTransferRequest, {
success: false,
error: null,
order: null,
})
});
useEffect(() => {
if (state.success && state.order) {
setShowSuccess(true)
setShowSuccess(true);
}
}, [state.success, state.order])
}, [state.success, state.order]);
return (
<div className="flex flex-col gap-y-4 w-full">
<div className="grid sm:grid-cols-2 items-center gap-x-8 gap-y-4 w-full">
<div className="flex w-full flex-col gap-y-4">
<div className="grid w-full items-center gap-x-8 gap-y-4 sm:grid-cols-2">
<div className="flex flex-col gap-y-1">
<Heading level="h3" className="text-lg text-neutral-950">
Order transfers
@@ -38,11 +46,11 @@ export default function TransferRequestForm() {
action={formAction}
className="flex flex-col gap-y-1 sm:items-end"
>
<div className="flex flex-col gap-y-2 w-full">
<div className="flex w-full flex-col gap-y-2">
<Input className="w-full" name="order_id" placeholder="Order ID" />
<SubmitButton
variant="secondary"
className="w-fit whitespace-nowrap self-end"
className="w-fit self-end whitespace-nowrap"
>
Request transfer
</SubmitButton>
@@ -50,14 +58,14 @@ export default function TransferRequestForm() {
</form>
</div>
{!state.success && state.error && (
<Text className="text-base-regular text-rose-500 text-right">
<Text className="text-base-regular text-right text-rose-500">
{state.error}
</Text>
)}
{showSuccess && (
<div className="flex justify-between p-4 bg-neutral-50 shadow-borders-base w-full self-stretch items-center">
<div className="flex gap-x-2 items-center">
<CheckCircleMiniSolid className="w-4 h-4 text-emerald-500" />
<div className="shadow-borders-base flex w-full items-center justify-between self-stretch bg-neutral-50 p-4">
<div className="flex items-center gap-x-2">
<CheckCircleMiniSolid className="h-4 w-4 text-emerald-500" />
<div className="flex flex-col gap-y-1">
<Text className="text-medim-pl text-neutral-950">
Transfer for order {state.order?.id} requested
@@ -72,10 +80,10 @@ export default function TransferRequestForm() {
className="h-fit"
onClick={() => setShowSuccess(false)}
>
<XCircleSolid className="w-4 h-4 text-neutral-500" />
<XCircleSolid className="h-4 w-4 text-neutral-500" />
</IconButton>
</div>
)}
</div>
)
);
}

View File

@@ -1,13 +1,13 @@
import React from "react"
import React from 'react';
import UnderlineLink from "@modules/common/components/interactive-link"
import { HttpTypes } from '@medusajs/types';
import UnderlineLink from '@modules/common/components/interactive-link';
import AccountNav from "../components/account-nav"
import { HttpTypes } from "@medusajs/types"
import AccountNav from '../components/account-nav';
interface AccountLayoutProps {
customer: HttpTypes.StoreCustomer | null
children: React.ReactNode
customer: HttpTypes.StoreCustomer | null;
children: React.ReactNode;
}
const AccountLayout: React.FC<AccountLayoutProps> = ({
@@ -15,13 +15,13 @@ const AccountLayout: React.FC<AccountLayoutProps> = ({
children,
}) => {
return (
<div className="flex-1 small:py-12" data-testid="account-page">
<div className="flex-1 content-container h-full max-w-5xl mx-auto bg-white flex flex-col">
<div className="grid grid-cols-1 small:grid-cols-[240px_1fr] py-12">
<div className="small:py-12 flex-1" data-testid="account-page">
<div className="content-container mx-auto flex h-full max-w-5xl flex-1 flex-col bg-white">
<div className="small:grid-cols-[240px_1fr] grid grid-cols-1 py-12">
<div>{customer && <AccountNav customer={customer} />}</div>
<div className="flex-1">{children}</div>
</div>
<div className="flex flex-col small:flex-row items-end justify-between small:border-t border-gray-200 py-12 gap-8">
<div className="small:flex-row small:border-t flex flex-col items-end justify-between gap-8 border-gray-200 py-12">
<div>
<h3 className="text-xl-semi mb-4">Got questions?</h3>
<span className="txt-medium">
@@ -37,7 +37,7 @@ const AccountLayout: React.FC<AccountLayoutProps> = ({
</div>
</div>
</div>
)
}
);
};
export default AccountLayout
export default AccountLayout;

View File

@@ -1,7 +1,7 @@
// account-info
export { default as AccountLayout } from "./account-layout";
export * from "./account-layout";
export { default as AccountLayout } from './account-layout';
export * from './account-layout';
// account-nav
export { default as LoginTemplate } from "./login-template";
export * from "./login-template";
export { default as LoginTemplate } from './login-template';
export * from './login-template';

View File

@@ -1,19 +1,20 @@
"use client";
'use client';
import { useState } from "react";
import { Login, Register } from "../components";
import { useState } from 'react';
import { Login, Register } from '../components';
export enum LOGIN_VIEW {
SIGN_IN = "sign-in",
REGISTER = "register",
SIGN_IN = 'sign-in',
REGISTER = 'register',
}
const LoginTemplate = () => {
const [currentView, setCurrentView] = useState("sign-in");
const [currentView, setCurrentView] = useState('sign-in');
return (
<div className="w-full flex justify-start px-8 py-8">
{currentView === "sign-in" ? (
<div className="flex w-full justify-start px-8 py-8">
{currentView === 'sign-in' ? (
<Login setCurrentView={setCurrentView} />
) : (
<Register setCurrentView={setCurrentView} />

View File

@@ -1,6 +1,5 @@
"use client"
'use client';
import { IconBadge, clx } from "@medusajs/ui"
import {
SelectHTMLAttributes,
forwardRef,
@@ -8,33 +7,34 @@ import {
useImperativeHandle,
useRef,
useState,
} from "react"
} from 'react';
import ChevronDown from "@modules/common/icons/chevron-down"
import { IconBadge, clx } from '@medusajs/ui';
import ChevronDown from '@modules/common/icons/chevron-down';
type NativeSelectProps = {
placeholder?: string
errors?: Record<string, unknown>
touched?: Record<string, unknown>
} & Omit<SelectHTMLAttributes<HTMLSelectElement>, "size">
placeholder?: string;
errors?: Record<string, unknown>;
touched?: Record<string, unknown>;
} & Omit<SelectHTMLAttributes<HTMLSelectElement>, 'size'>;
const CartItemSelect = forwardRef<HTMLSelectElement, NativeSelectProps>(
({ placeholder = "Select...", className, children, ...props }, ref) => {
const innerRef = useRef<HTMLSelectElement>(null)
const [isPlaceholder, setIsPlaceholder] = useState(false)
({ placeholder = 'Select...', className, children, ...props }, ref) => {
const innerRef = useRef<HTMLSelectElement>(null);
const [isPlaceholder, setIsPlaceholder] = useState(false);
useImperativeHandle<HTMLSelectElement | null, HTMLSelectElement | null>(
ref,
() => innerRef.current
)
() => innerRef.current,
);
useEffect(() => {
if (innerRef.current && innerRef.current.value === "") {
setIsPlaceholder(true)
if (innerRef.current && innerRef.current.value === '') {
setIsPlaceholder(true);
} else {
setIsPlaceholder(false)
setIsPlaceholder(false);
}
}, [innerRef.current?.value])
}, [innerRef.current?.value]);
return (
<div>
@@ -42,32 +42,32 @@ const CartItemSelect = forwardRef<HTMLSelectElement, NativeSelectProps>(
onFocus={() => innerRef.current?.focus()}
onBlur={() => innerRef.current?.blur()}
className={clx(
"relative flex items-center txt-compact-small border text-ui-fg-base group",
'txt-compact-small text-ui-fg-base group relative flex items-center border',
className,
{
"text-ui-fg-subtle": isPlaceholder,
}
'text-ui-fg-subtle': isPlaceholder,
},
)}
>
<select
ref={innerRef}
{...props}
className="appearance-none bg-transparent border-none px-4 transition-colors duration-150 focus:border-gray-700 outline-none w-16 h-16 items-center justify-center"
className="h-16 w-16 appearance-none items-center justify-center border-none bg-transparent px-4 transition-colors duration-150 outline-none focus:border-gray-700"
>
<option disabled value="">
{placeholder}
</option>
{children}
</select>
<span className="absolute flex pointer-events-none justify-end w-8 group-hover:animate-pulse">
<span className="pointer-events-none absolute flex w-8 justify-end group-hover:animate-pulse">
<ChevronDown />
</span>
</IconBadge>
</div>
)
}
)
);
},
);
CartItemSelect.displayName = "CartItemSelect"
CartItemSelect.displayName = 'CartItemSelect';
export default CartItemSelect
export default CartItemSelect;

View File

@@ -1,13 +1,15 @@
import { Heading, Text } from "@medusajs/ui"
import InteractiveLink from "@modules/common/components/interactive-link"
import { Heading, Text } from '@medusajs/ui';
import InteractiveLink from '@modules/common/components/interactive-link';
const EmptyCartMessage = () => {
return (
<div className="py-48 px-2 flex flex-col justify-center items-start" data-testid="empty-cart-message">
<div
className="flex flex-col items-start justify-center px-2 py-48"
data-testid="empty-cart-message"
>
<Heading
level="h1"
className="flex flex-row text-3xl-regular gap-x-2 items-baseline"
className="text-3xl-regular flex flex-row items-baseline gap-x-2"
>
Cart
</Heading>
@@ -19,7 +21,7 @@ const EmptyCartMessage = () => {
<InteractiveLink href="/store">Explore products</InteractiveLink>
</div>
</div>
)
}
);
};
export default EmptyCartMessage
export default EmptyCartMessage;

View File

@@ -1,57 +1,58 @@
"use client"
'use client';
import { Table, Text, clx } from "@medusajs/ui"
import { updateLineItem } from "@lib/data/cart"
import { HttpTypes } from "@medusajs/types"
import CartItemSelect from "@modules/cart/components/cart-item-select"
import ErrorMessage from "@modules/checkout/components/error-message"
import DeleteButton from "@modules/common/components/delete-button"
import LineItemOptions from "@modules/common/components/line-item-options"
import LineItemPrice from "@modules/common/components/line-item-price"
import LineItemUnitPrice from "@modules/common/components/line-item-unit-price"
import LocalizedClientLink from "@modules/common/components/localized-client-link"
import Spinner from "@modules/common/icons/spinner"
import Thumbnail from "@modules/products/components/thumbnail"
import { useState } from "react"
import { useState } from 'react';
import { updateLineItem } from '@lib/data/cart';
import { HttpTypes } from '@medusajs/types';
import { Table, Text, clx } from '@medusajs/ui';
import CartItemSelect from '@modules/cart/components/cart-item-select';
import ErrorMessage from '@modules/checkout/components/error-message';
import DeleteButton from '@modules/common/components/delete-button';
import LineItemOptions from '@modules/common/components/line-item-options';
import LineItemPrice from '@modules/common/components/line-item-price';
import LineItemUnitPrice from '@modules/common/components/line-item-unit-price';
import LocalizedClientLink from '@modules/common/components/localized-client-link';
import Spinner from '@modules/common/icons/spinner';
import Thumbnail from '@modules/products/components/thumbnail';
type ItemProps = {
item: HttpTypes.StoreCartLineItem
type?: "full" | "preview"
currencyCode: string
}
item: HttpTypes.StoreCartLineItem;
type?: 'full' | 'preview';
currencyCode: string;
};
const Item = ({ item, type = "full", currencyCode }: ItemProps) => {
const [updating, setUpdating] = useState(false)
const [error, setError] = useState<string | null>(null)
const Item = ({ item, type = 'full', currencyCode }: ItemProps) => {
const [updating, setUpdating] = useState(false);
const [error, setError] = useState<string | null>(null);
const changeQuantity = async (quantity: number) => {
setError(null)
setUpdating(true)
setError(null);
setUpdating(true);
await updateLineItem({
lineId: item.id,
quantity,
})
.catch((err) => {
setError(err.message)
setError(err.message);
})
.finally(() => {
setUpdating(false)
})
}
setUpdating(false);
});
};
// TODO: Update this to grab the actual max inventory
const maxQtyFromInventory = 10
const maxQuantity = item.variant?.manage_inventory ? 10 : maxQtyFromInventory
const maxQtyFromInventory = 10;
const maxQuantity = item.variant?.manage_inventory ? 10 : maxQtyFromInventory;
return (
<Table.Row className="w-full" data-testid="product-row">
<Table.Cell className="!pl-0 p-4 w-24">
<Table.Cell className="w-24 p-4 !pl-0">
<LocalizedClientLink
href={`/products/${item.product_handle}`}
className={clx("flex", {
"w-16": type === "preview",
"small:w-24 w-12": type === "full",
className={clx('flex', {
'w-16': type === 'preview',
'small:w-24 w-12': type === 'full',
})}
>
<Thumbnail
@@ -72,14 +73,14 @@ const Item = ({ item, type = "full", currencyCode }: ItemProps) => {
<LineItemOptions variant={item.variant} data-testid="product-variant" />
</Table.Cell>
{type === "full" && (
{type === 'full' && (
<Table.Cell>
<div className="flex gap-2 items-center w-28">
<div className="flex w-28 items-center gap-2">
<DeleteButton id={item.id} data-testid="product-delete-button" />
<CartItemSelect
value={item.quantity}
onChange={(value) => changeQuantity(parseInt(value.target.value))}
className="w-14 h-10 p-4"
className="h-10 w-14 p-4"
data-testid="product-select-button"
>
{/* TODO: Update this with the v2 way of managing inventory */}
@@ -91,7 +92,7 @@ const Item = ({ item, type = "full", currencyCode }: ItemProps) => {
<option value={i + 1} key={i}>
{i + 1}
</option>
)
),
)}
<option value={1} key={1}>
@@ -104,8 +105,8 @@ const Item = ({ item, type = "full", currencyCode }: ItemProps) => {
</Table.Cell>
)}
{type === "full" && (
<Table.Cell className="hidden small:table-cell">
{type === 'full' && (
<Table.Cell className="small:table-cell hidden">
<LineItemUnitPrice
item={item}
style="tight"
@@ -116,12 +117,12 @@ const Item = ({ item, type = "full", currencyCode }: ItemProps) => {
<Table.Cell className="!pr-0">
<span
className={clx("!pr-0", {
"flex flex-col items-end h-full justify-center": type === "preview",
className={clx('!pr-0', {
'flex h-full flex-col items-end justify-center': type === 'preview',
})}
>
{type === "preview" && (
<span className="flex gap-x-1 ">
{type === 'preview' && (
<span className="flex gap-x-1">
<Text className="text-ui-fg-muted">{item.quantity}x </Text>
<LineItemUnitPrice
item={item}
@@ -138,7 +139,7 @@ const Item = ({ item, type = "full", currencyCode }: ItemProps) => {
</span>
</Table.Cell>
</Table.Row>
)
}
);
};
export default Item
export default Item;

View File

@@ -1,9 +1,9 @@
import { Button, Heading, Text } from "@medusajs/ui"
import LocalizedClientLink from "@modules/common/components/localized-client-link"
import { Button, Heading, Text } from '@medusajs/ui';
import LocalizedClientLink from '@modules/common/components/localized-client-link';
const SignInPrompt = () => {
return (
<div className="bg-white flex items-center justify-between">
<div className="flex items-center justify-between bg-white">
<div>
<Heading level="h2" className="txt-xlarge">
Already have an account?
@@ -14,13 +14,17 @@ const SignInPrompt = () => {
</div>
<div>
<LocalizedClientLink href="/account">
<Button variant="secondary" className="h-10" data-testid="sign-in-button">
<Button
variant="secondary"
className="h-10"
data-testid="sign-in-button"
>
Sign in
</Button>
</LocalizedClientLink>
</div>
</div>
)
}
);
};
export default SignInPrompt
export default SignInPrompt;

View File

@@ -1,23 +1,24 @@
import ItemsTemplate from "./items"
import Summary from "./summary"
import EmptyCartMessage from "../components/empty-cart-message"
import SignInPrompt from "../components/sign-in-prompt"
import Divider from "@modules/common/components/divider"
import { HttpTypes } from "@medusajs/types"
import { HttpTypes } from '@medusajs/types';
import Divider from '@modules/common/components/divider';
import EmptyCartMessage from '../components/empty-cart-message';
import SignInPrompt from '../components/sign-in-prompt';
import ItemsTemplate from './items';
import Summary from './summary';
const CartTemplate = ({
cart,
customer,
}: {
cart: HttpTypes.StoreCart | null
customer: HttpTypes.StoreCustomer | null
cart: HttpTypes.StoreCart | null;
customer: HttpTypes.StoreCustomer | null;
}) => {
return (
<div className="py-12">
<div className="content-container" data-testid="cart-container">
{cart?.items?.length ? (
<div className="grid grid-cols-1 small:grid-cols-[1fr_360px] gap-x-40">
<div className="flex flex-col bg-white py-6 gap-y-6">
<div className="small:grid-cols-[1fr_360px] grid grid-cols-1 gap-x-40">
<div className="flex flex-col gap-y-6 bg-white py-6">
{!customer && (
<>
<SignInPrompt />
@@ -27,7 +28,7 @@ const CartTemplate = ({
<ItemsTemplate cart={cart} />
</div>
<div className="relative">
<div className="flex flex-col gap-y-8 sticky top-12">
<div className="sticky top-12 flex flex-col gap-y-8">
{cart && cart.region && (
<>
<div className="bg-white py-6">
@@ -45,7 +46,7 @@ const CartTemplate = ({
)}
</div>
</div>
)
}
);
};
export default CartTemplate
export default CartTemplate;

View File

@@ -1,19 +1,18 @@
import repeat from "@lib/util/repeat"
import { HttpTypes } from "@medusajs/types"
import { Heading, Table } from "@medusajs/ui"
import Item from "@modules/cart/components/item"
import SkeletonLineItem from "@modules/skeletons/components/skeleton-line-item"
import repeat from '@lib/util/repeat';
import { HttpTypes } from '@medusajs/types';
import { Heading, Table } from '@medusajs/ui';
import Item from '@modules/cart/components/item';
import SkeletonLineItem from '@modules/skeletons/components/skeleton-line-item';
type ItemsTemplateProps = {
cart?: HttpTypes.StoreCart
}
cart?: HttpTypes.StoreCart;
};
const ItemsTemplate = ({ cart }: ItemsTemplateProps) => {
const items = cart?.items
const items = cart?.items;
return (
<div>
<div className="pb-3 flex items-center">
<div className="flex items-center pb-3">
<Heading className="text-[2rem] leading-[2.75rem]">Cart</Heading>
</div>
<Table>
@@ -22,7 +21,7 @@ const ItemsTemplate = ({ cart }: ItemsTemplateProps) => {
<Table.HeaderCell className="!pl-0">Item</Table.HeaderCell>
<Table.HeaderCell></Table.HeaderCell>
<Table.HeaderCell>Quantity</Table.HeaderCell>
<Table.HeaderCell className="hidden small:table-cell">
<Table.HeaderCell className="small:table-cell hidden">
Price
</Table.HeaderCell>
<Table.HeaderCell className="!pr-0 text-right">
@@ -34,7 +33,7 @@ const ItemsTemplate = ({ cart }: ItemsTemplateProps) => {
{items
? items
.sort((a, b) => {
return (a.created_at ?? "") > (b.created_at ?? "") ? -1 : 1
return (a.created_at ?? '') > (b.created_at ?? '') ? -1 : 1;
})
.map((item) => {
return (
@@ -43,15 +42,15 @@ const ItemsTemplate = ({ cart }: ItemsTemplateProps) => {
item={item}
currencyCode={cart?.currency_code}
/>
)
);
})
: repeat(5).map((i) => {
return <SkeletonLineItem key={i} />
return <SkeletonLineItem key={i} />;
})}
</Table.Body>
</Table>
</div>
)
}
);
};
export default ItemsTemplate
export default ItemsTemplate;

View File

@@ -1,24 +1,23 @@
"use client"
'use client';
import repeat from "@lib/util/repeat"
import { HttpTypes } from "@medusajs/types"
import { Table, clx } from "@medusajs/ui"
import Item from "@modules/cart/components/item"
import SkeletonLineItem from "@modules/skeletons/components/skeleton-line-item"
import repeat from '@lib/util/repeat';
import { HttpTypes } from '@medusajs/types';
import { Table, clx } from '@medusajs/ui';
import Item from '@modules/cart/components/item';
import SkeletonLineItem from '@modules/skeletons/components/skeleton-line-item';
type ItemsTemplateProps = {
cart: HttpTypes.StoreCart
}
cart: HttpTypes.StoreCart;
};
const ItemsPreviewTemplate = ({ cart }: ItemsTemplateProps) => {
const items = cart.items
const hasOverflow = items && items.length > 4
const items = cart.items;
const hasOverflow = items && items.length > 4;
return (
<div
className={clx({
"pl-[1px] overflow-y-scroll overflow-x-hidden no-scrollbar max-h-[420px]":
'no-scrollbar max-h-[420px] overflow-x-hidden overflow-y-scroll pl-[1px]':
hasOverflow,
})}
>
@@ -27,7 +26,7 @@ const ItemsPreviewTemplate = ({ cart }: ItemsTemplateProps) => {
{items
? items
.sort((a, b) => {
return (a.created_at ?? "") > (b.created_at ?? "") ? -1 : 1
return (a.created_at ?? '') > (b.created_at ?? '') ? -1 : 1;
})
.map((item) => {
return (
@@ -37,15 +36,15 @@ const ItemsPreviewTemplate = ({ cart }: ItemsTemplateProps) => {
type="preview"
currencyCode={cart.currency_code}
/>
)
);
})
: repeat(5).map((i) => {
return <SkeletonLineItem key={i} />
return <SkeletonLineItem key={i} />;
})}
</Table.Body>
</Table>
</div>
)
}
);
};
export default ItemsPreviewTemplate
export default ItemsPreviewTemplate;

View File

@@ -1,31 +1,30 @@
"use client"
'use client';
import { Button, Heading } from "@medusajs/ui"
import CartTotals from "@modules/common/components/cart-totals"
import Divider from "@modules/common/components/divider"
import DiscountCode from "@modules/checkout/components/discount-code"
import LocalizedClientLink from "@modules/common/components/localized-client-link"
import { HttpTypes } from "@medusajs/types"
import { HttpTypes } from '@medusajs/types';
import { Button, Heading } from '@medusajs/ui';
import DiscountCode from '@modules/checkout/components/discount-code';
import CartTotals from '@modules/common/components/cart-totals';
import Divider from '@modules/common/components/divider';
import LocalizedClientLink from '@modules/common/components/localized-client-link';
type SummaryProps = {
cart: HttpTypes.StoreCart & {
promotions: HttpTypes.StorePromotion[]
}
}
promotions: HttpTypes.StorePromotion[];
};
};
function getCheckoutStep(cart: HttpTypes.StoreCart) {
if (!cart?.shipping_address?.address_1 || !cart.email) {
return "address"
return 'address';
} else if (cart?.shipping_methods?.length === 0) {
return "delivery"
return 'delivery';
} else {
return "payment"
return 'payment';
}
}
const Summary = ({ cart }: SummaryProps) => {
const step = getCheckoutStep(cart)
const step = getCheckoutStep(cart);
return (
<div className="flex flex-col gap-y-4">
@@ -36,13 +35,13 @@ const Summary = ({ cart }: SummaryProps) => {
<Divider />
<CartTotals totals={cart} />
<LocalizedClientLink
href={"/checkout?step=" + step}
href={'/checkout?step=' + step}
data-testid="checkout-button"
>
<Button className="w-full h-10">Go to checkout</Button>
<Button className="h-10 w-full">Go to checkout</Button>
</LocalizedClientLink>
</div>
)
}
);
};
export default Summary
export default Summary;

View File

@@ -1,13 +1,14 @@
import { notFound } from "next/navigation"
import { Suspense } from "react"
import { Suspense } from 'react';
import InteractiveLink from "@modules/common/components/interactive-link"
import SkeletonProductGrid from "@modules/skeletons/templates/skeleton-product-grid"
import RefinementList from "@modules/store/components/refinement-list"
import { SortOptions } from "@modules/store/components/refinement-list/sort-products"
import PaginatedProducts from "@modules/store/templates/paginated-products"
import LocalizedClientLink from "@modules/common/components/localized-client-link"
import { HttpTypes } from "@medusajs/types"
import { notFound } from 'next/navigation';
import { HttpTypes } from '@medusajs/types';
import InteractiveLink from '@modules/common/components/interactive-link';
import LocalizedClientLink from '@modules/common/components/localized-client-link';
import SkeletonProductGrid from '@modules/skeletons/templates/skeleton-product-grid';
import RefinementList from '@modules/store/components/refinement-list';
import { SortOptions } from '@modules/store/components/refinement-list/sort-products';
import PaginatedProducts from '@modules/store/templates/paginated-products';
export default function CategoryTemplate({
category,
@@ -15,35 +16,35 @@ export default function CategoryTemplate({
page,
countryCode,
}: {
category: HttpTypes.StoreProductCategory
sortBy?: SortOptions
page?: string
countryCode: string
category: HttpTypes.StoreProductCategory;
sortBy?: SortOptions;
page?: string;
countryCode: string;
}) {
const pageNumber = page ? parseInt(page) : 1
const sort = sortBy || "created_at"
const pageNumber = page ? parseInt(page) : 1;
const sort = sortBy || 'created_at';
if (!category || !countryCode) notFound()
if (!category || !countryCode) notFound();
const parents = [] as HttpTypes.StoreProductCategory[]
const parents = [] as HttpTypes.StoreProductCategory[];
const getParents = (category: HttpTypes.StoreProductCategory) => {
if (category.parent_category) {
parents.push(category.parent_category)
getParents(category.parent_category)
parents.push(category.parent_category);
getParents(category.parent_category);
}
}
};
getParents(category)
getParents(category);
return (
<div
className="flex flex-col small:flex-row small:items-start py-6 content-container"
className="small:flex-row small:items-start content-container flex flex-col py-6"
data-testid="category-container"
>
<RefinementList sortBy={sort} data-testid="sort-by-container" />
<div className="w-full">
<div className="flex flex-row mb-8 text-2xl-semi gap-4">
<div className="text-2xl-semi mb-8 flex flex-row gap-4">
{parents &&
parents.map((parent) => (
<span key={parent.id} className="text-ui-fg-subtle">
@@ -60,12 +61,12 @@ export default function CategoryTemplate({
<h1 data-testid="category-page-title">{category.name}</h1>
</div>
{category.description && (
<div className="mb-8 text-base-regular">
<div className="text-base-regular mb-8">
<p>{category.description}</p>
</div>
)}
{category.category_children && (
<div className="mb-8 text-base-large">
<div className="text-base-large mb-8">
<ul className="grid grid-cols-1 gap-2">
{category.category_children?.map((c) => (
<li key={c.id}>
@@ -93,5 +94,5 @@ export default function CategoryTemplate({
</Suspense>
</div>
</div>
)
);
}

View File

@@ -1,20 +1,20 @@
import { Listbox, Transition } from "@headlessui/react"
import { ChevronUpDown } from "@medusajs/icons"
import { clx } from "@medusajs/ui"
import { Fragment, useMemo } from "react"
import { Fragment, useMemo } from 'react';
import Radio from "@modules/common/components/radio"
import compareAddresses from "@lib/util/compare-addresses"
import { HttpTypes } from "@medusajs/types"
import { Listbox, Transition } from '@headlessui/react';
import compareAddresses from '@lib/util/compare-addresses';
import { ChevronUpDown } from '@medusajs/icons';
import { HttpTypes } from '@medusajs/types';
import { clx } from '@medusajs/ui';
import Radio from '@modules/common/components/radio';
type AddressSelectProps = {
addresses: HttpTypes.StoreCustomerAddress[]
addressInput: HttpTypes.StoreCartAddress | null
addresses: HttpTypes.StoreCustomerAddress[];
addressInput: HttpTypes.StoreCartAddress | null;
onSelect: (
address: HttpTypes.StoreCartAddress | undefined,
email?: string
) => void
}
email?: string,
) => void;
};
const AddressSelect = ({
addresses,
@@ -22,21 +22,21 @@ const AddressSelect = ({
onSelect,
}: AddressSelectProps) => {
const handleSelect = (id: string) => {
const savedAddress = addresses.find((a) => a.id === id)
const savedAddress = addresses.find((a) => a.id === id);
if (savedAddress) {
onSelect(savedAddress as HttpTypes.StoreCartAddress)
onSelect(savedAddress as HttpTypes.StoreCartAddress);
}
}
};
const selectedAddress = useMemo(() => {
return addresses.find((a) => compareAddresses(a, addressInput))
}, [addresses, addressInput])
return addresses.find((a) => compareAddresses(a, addressInput));
}, [addresses, addressInput]);
return (
<Listbox onChange={handleSelect} value={selectedAddress?.id}>
<div className="relative">
<Listbox.Button
className="relative w-full flex justify-between items-center px-4 py-[10px] text-left bg-white cursor-default focus:outline-none border rounded-rounded focus-visible:ring-2 focus-visible:ring-opacity-75 focus-visible:ring-white focus-visible:ring-offset-gray-300 focus-visible:ring-offset-2 focus-visible:border-gray-300 text-base-regular"
className="rounded-rounded focus-visible:ring-opacity-75 text-base-regular relative flex w-full cursor-default items-center justify-between border bg-white px-4 py-[10px] text-left focus:outline-none focus-visible:border-gray-300 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-gray-300"
data-testid="shipping-address-select"
>
{({ open }) => (
@@ -44,11 +44,11 @@ const AddressSelect = ({
<span className="block truncate">
{selectedAddress
? selectedAddress.address_1
: "Choose an address"}
: 'Choose an address'}
</span>
<ChevronUpDown
className={clx("transition-rotate duration-200", {
"transform rotate-180": open,
className={clx('transition-rotate duration-200', {
'rotate-180 transform': open,
})}
/>
</>
@@ -61,7 +61,7 @@ const AddressSelect = ({
leaveTo="opacity-0"
>
<Listbox.Options
className="absolute z-20 w-full overflow-auto text-small-regular bg-white border border-top-0 max-h-60 focus:outline-none sm:text-sm"
className="text-small-regular border-top-0 absolute z-20 max-h-60 w-full overflow-auto border bg-white focus:outline-none sm:text-sm"
data-testid="shipping-address-options"
>
{addresses.map((address) => {
@@ -69,16 +69,16 @@ const AddressSelect = ({
<Listbox.Option
key={address.id}
value={address.id}
className="cursor-default select-none relative pl-6 pr-10 hover:bg-gray-50 py-4"
className="relative cursor-default py-4 pr-10 pl-6 select-none hover:bg-gray-50"
data-testid="shipping-address-option"
>
<div className="flex gap-x-4 items-start">
<div className="flex items-start gap-x-4">
<Radio
checked={selectedAddress?.id === address.id}
data-testid="shipping-address-radio"
/>
<div className="flex flex-col">
<span className="text-left text-base-semi">
<span className="text-base-semi text-left">
{address.first_name} {address.last_name}
</span>
{address.company && (
@@ -86,7 +86,7 @@ const AddressSelect = ({
{address.company}
</span>
)}
<div className="flex flex-col text-left text-base-regular mt-2">
<div className="text-base-regular mt-2 flex flex-col text-left">
<span>
{address.address_1}
{address.address_2 && (
@@ -104,13 +104,13 @@ const AddressSelect = ({
</div>
</div>
</Listbox.Option>
)
);
})}
</Listbox.Options>
</Transition>
</div>
</Listbox>
)
}
);
};
export default AddressSelect
export default AddressSelect;

View File

@@ -1,50 +1,53 @@
"use client"
'use client';
import { setAddresses } from "@lib/data/cart"
import compareAddresses from "@lib/util/compare-addresses"
import { CheckCircleSolid } from "@medusajs/icons"
import { HttpTypes } from "@medusajs/types"
import { Heading, Text, useToggleState } from "@medusajs/ui"
import Divider from "@modules/common/components/divider"
import Spinner from "@modules/common/icons/spinner"
import { usePathname, useRouter, useSearchParams } from "next/navigation"
import { useActionState } from "react"
import BillingAddress from "../billing_address"
import ErrorMessage from "../error-message"
import ShippingAddress from "../shipping-address"
import { SubmitButton } from "../submit-button"
import { useActionState } from 'react';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { setAddresses } from '@lib/data/cart';
import compareAddresses from '@lib/util/compare-addresses';
import { CheckCircleSolid } from '@medusajs/icons';
import { HttpTypes } from '@medusajs/types';
import { Heading, Text, useToggleState } from '@medusajs/ui';
import Divider from '@modules/common/components/divider';
import Spinner from '@modules/common/icons/spinner';
import BillingAddress from '../billing_address';
import ErrorMessage from '../error-message';
import ShippingAddress from '../shipping-address';
import { SubmitButton } from '../submit-button';
const Addresses = ({
cart,
customer,
}: {
cart: HttpTypes.StoreCart | null
customer: HttpTypes.StoreCustomer | null
cart: HttpTypes.StoreCart | null;
customer: HttpTypes.StoreCustomer | null;
}) => {
const searchParams = useSearchParams()
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams();
const router = useRouter();
const pathname = usePathname();
const isOpen = searchParams.get("step") === "address"
const isOpen = searchParams.get('step') === 'address';
const { state: sameAsBilling, toggle: toggleSameAsBilling } = useToggleState(
cart?.shipping_address && cart?.billing_address
? compareAddresses(cart?.shipping_address, cart?.billing_address)
: true
)
: true,
);
const handleEdit = () => {
router.push(pathname + "?step=address")
}
router.push(pathname + '?step=address');
};
const [message, formAction] = useActionState(setAddresses, null)
const [message, formAction] = useActionState(setAddresses, null);
return (
<div className="bg-white">
<div className="flex flex-row items-center justify-between mb-6">
<div className="mb-6 flex flex-row items-center justify-between">
<Heading
level="h2"
className="flex flex-row text-3xl-regular gap-x-2 items-baseline"
className="text-3xl-regular flex flex-row items-baseline gap-x-2"
>
Shipping Address
{!isOpen && <CheckCircleSolid />}
@@ -75,7 +78,7 @@ const Addresses = ({
<div>
<Heading
level="h2"
className="text-3xl-regular gap-x-4 pb-6 pt-8"
className="text-3xl-regular gap-x-4 pt-8 pb-6"
>
Billing address
</Heading>
@@ -94,24 +97,24 @@ const Addresses = ({
<div className="text-small-regular">
{cart && cart.shipping_address ? (
<div className="flex items-start gap-x-8">
<div className="flex items-start gap-x-1 w-full">
<div className="flex w-full items-start gap-x-1">
<div
className="flex flex-col w-1/3"
className="flex w-1/3 flex-col"
data-testid="shipping-address-summary"
>
<Text className="txt-medium-plus text-ui-fg-base mb-1">
Shipping Address
</Text>
<Text className="txt-medium text-ui-fg-subtle">
{cart.shipping_address.first_name}{" "}
{cart.shipping_address.first_name}{' '}
{cart.shipping_address.last_name}
</Text>
<Text className="txt-medium text-ui-fg-subtle">
{cart.shipping_address.address_1}{" "}
{cart.shipping_address.address_1}{' '}
{cart.shipping_address.address_2}
</Text>
<Text className="txt-medium text-ui-fg-subtle">
{cart.shipping_address.postal_code},{" "}
{cart.shipping_address.postal_code},{' '}
{cart.shipping_address.city}
</Text>
<Text className="txt-medium text-ui-fg-subtle">
@@ -120,7 +123,7 @@ const Addresses = ({
</div>
<div
className="flex flex-col w-1/3 "
className="flex w-1/3 flex-col"
data-testid="shipping-contact-summary"
>
<Text className="txt-medium-plus text-ui-fg-base mb-1">
@@ -135,7 +138,7 @@ const Addresses = ({
</div>
<div
className="flex flex-col w-1/3"
className="flex w-1/3 flex-col"
data-testid="billing-address-summary"
>
<Text className="txt-medium-plus text-ui-fg-base mb-1">
@@ -149,15 +152,15 @@ const Addresses = ({
) : (
<>
<Text className="txt-medium text-ui-fg-subtle">
{cart.billing_address?.first_name}{" "}
{cart.billing_address?.first_name}{' '}
{cart.billing_address?.last_name}
</Text>
<Text className="txt-medium text-ui-fg-subtle">
{cart.billing_address?.address_1}{" "}
{cart.billing_address?.address_1}{' '}
{cart.billing_address?.address_2}
</Text>
<Text className="txt-medium text-ui-fg-subtle">
{cart.billing_address?.postal_code},{" "}
{cart.billing_address?.postal_code},{' '}
{cart.billing_address?.city}
</Text>
<Text className="txt-medium text-ui-fg-subtle">
@@ -178,7 +181,7 @@ const Addresses = ({
)}
<Divider className="mt-8" />
</div>
)
}
);
};
export default Addresses
export default Addresses;

View File

@@ -1,31 +1,33 @@
import { HttpTypes } from "@medusajs/types"
import Input from "@modules/common/components/input"
import React, { useState } from "react"
import CountrySelect from "../country-select"
import React, { useState } from 'react';
import { HttpTypes } from '@medusajs/types';
import Input from '@modules/common/components/input';
import CountrySelect from '../country-select';
const BillingAddress = ({ cart }: { cart: HttpTypes.StoreCart | null }) => {
const [formData, setFormData] = useState<any>({
"billing_address.first_name": cart?.billing_address?.first_name || "",
"billing_address.last_name": cart?.billing_address?.last_name || "",
"billing_address.address_1": cart?.billing_address?.address_1 || "",
"billing_address.company": cart?.billing_address?.company || "",
"billing_address.postal_code": cart?.billing_address?.postal_code || "",
"billing_address.city": cart?.billing_address?.city || "",
"billing_address.country_code": cart?.billing_address?.country_code || "",
"billing_address.province": cart?.billing_address?.province || "",
"billing_address.phone": cart?.billing_address?.phone || "",
})
'billing_address.first_name': cart?.billing_address?.first_name || '',
'billing_address.last_name': cart?.billing_address?.last_name || '',
'billing_address.address_1': cart?.billing_address?.address_1 || '',
'billing_address.company': cart?.billing_address?.company || '',
'billing_address.postal_code': cart?.billing_address?.postal_code || '',
'billing_address.city': cart?.billing_address?.city || '',
'billing_address.country_code': cart?.billing_address?.country_code || '',
'billing_address.province': cart?.billing_address?.province || '',
'billing_address.phone': cart?.billing_address?.phone || '',
});
const handleChange = (
e: React.ChangeEvent<
HTMLInputElement | HTMLInputElement | HTMLSelectElement
>
>,
) => {
setFormData({
...formData,
[e.target.name]: e.target.value,
})
}
});
};
return (
<>
@@ -34,7 +36,7 @@ const BillingAddress = ({ cart }: { cart: HttpTypes.StoreCart | null }) => {
label="First name"
name="billing_address.first_name"
autoComplete="given-name"
value={formData["billing_address.first_name"]}
value={formData['billing_address.first_name']}
onChange={handleChange}
required
data-testid="billing-first-name-input"
@@ -43,7 +45,7 @@ const BillingAddress = ({ cart }: { cart: HttpTypes.StoreCart | null }) => {
label="Last name"
name="billing_address.last_name"
autoComplete="family-name"
value={formData["billing_address.last_name"]}
value={formData['billing_address.last_name']}
onChange={handleChange}
required
data-testid="billing-last-name-input"
@@ -52,7 +54,7 @@ const BillingAddress = ({ cart }: { cart: HttpTypes.StoreCart | null }) => {
label="Address"
name="billing_address.address_1"
autoComplete="address-line1"
value={formData["billing_address.address_1"]}
value={formData['billing_address.address_1']}
onChange={handleChange}
required
data-testid="billing-address-input"
@@ -60,7 +62,7 @@ const BillingAddress = ({ cart }: { cart: HttpTypes.StoreCart | null }) => {
<Input
label="Company"
name="billing_address.company"
value={formData["billing_address.company"]}
value={formData['billing_address.company']}
onChange={handleChange}
autoComplete="organization"
data-testid="billing-company-input"
@@ -69,7 +71,7 @@ const BillingAddress = ({ cart }: { cart: HttpTypes.StoreCart | null }) => {
label="Postal code"
name="billing_address.postal_code"
autoComplete="postal-code"
value={formData["billing_address.postal_code"]}
value={formData['billing_address.postal_code']}
onChange={handleChange}
required
data-testid="billing-postal-input"
@@ -78,14 +80,14 @@ const BillingAddress = ({ cart }: { cart: HttpTypes.StoreCart | null }) => {
label="City"
name="billing_address.city"
autoComplete="address-level2"
value={formData["billing_address.city"]}
value={formData['billing_address.city']}
onChange={handleChange}
/>
<CountrySelect
name="billing_address.country_code"
autoComplete="country"
region={cart?.region}
value={formData["billing_address.country_code"]}
value={formData['billing_address.country_code']}
onChange={handleChange}
required
data-testid="billing-country-select"
@@ -94,7 +96,7 @@ const BillingAddress = ({ cart }: { cart: HttpTypes.StoreCart | null }) => {
label="State / Province"
name="billing_address.province"
autoComplete="address-level1"
value={formData["billing_address.province"]}
value={formData['billing_address.province']}
onChange={handleChange}
data-testid="billing-province-input"
/>
@@ -102,13 +104,13 @@ const BillingAddress = ({ cart }: { cart: HttpTypes.StoreCart | null }) => {
label="Phone"
name="billing_address.phone"
autoComplete="tel"
value={formData["billing_address.phone"]}
value={formData['billing_address.phone']}
onChange={handleChange}
data-testid="billing-phone-input"
/>
</div>
</>
)
}
);
};
export default BillingAddress
export default BillingAddress;

View File

@@ -1,33 +1,33 @@
import { forwardRef, useImperativeHandle, useMemo, useRef } from "react"
import { forwardRef, useImperativeHandle, useMemo, useRef } from 'react';
import { HttpTypes } from '@medusajs/types';
import NativeSelect, {
NativeSelectProps,
} from "@modules/common/components/native-select"
import { HttpTypes } from "@medusajs/types"
} from '@modules/common/components/native-select';
const CountrySelect = forwardRef<
HTMLSelectElement,
NativeSelectProps & {
region?: HttpTypes.StoreRegion
region?: HttpTypes.StoreRegion;
}
>(({ placeholder = "Country", region, defaultValue, ...props }, ref) => {
const innerRef = useRef<HTMLSelectElement>(null)
>(({ placeholder = 'Country', region, defaultValue, ...props }, ref) => {
const innerRef = useRef<HTMLSelectElement>(null);
useImperativeHandle<HTMLSelectElement | null, HTMLSelectElement | null>(
ref,
() => innerRef.current
)
() => innerRef.current,
);
const countryOptions = useMemo(() => {
if (!region) {
return []
return [];
}
return region.countries?.map((country) => ({
value: country.iso_2,
label: country.display_name,
}))
}, [region])
}));
}, [region]);
return (
<NativeSelect
@@ -42,9 +42,9 @@ const CountrySelect = forwardRef<
</option>
))}
</NativeSelect>
)
})
);
});
CountrySelect.displayName = "CountrySelect"
CountrySelect.displayName = 'CountrySelect';
export default CountrySelect
export default CountrySelect;

View File

@@ -1,61 +1,64 @@
"use client"
'use client';
import { Badge, Heading, Input, Label, Text, Tooltip } from "@medusajs/ui"
import React, { useActionState } from "react";
import React, { useActionState } from 'react';
import { applyPromotions, submitPromotionForm } from "@lib/data/cart"
import { convertToLocale } from "@lib/util/money"
import { InformationCircleSolid } from "@medusajs/icons"
import { HttpTypes } from "@medusajs/types"
import Trash from "@modules/common/icons/trash"
import ErrorMessage from "../error-message"
import { SubmitButton } from "../submit-button"
import { applyPromotions, submitPromotionForm } from '@lib/data/cart';
import { convertToLocale } from '@lib/util/money';
import { InformationCircleSolid } from '@medusajs/icons';
import { HttpTypes } from '@medusajs/types';
import { Badge, Heading, Input, Label, Text, Tooltip } from '@medusajs/ui';
import Trash from '@modules/common/icons/trash';
import ErrorMessage from '../error-message';
import { SubmitButton } from '../submit-button';
type DiscountCodeProps = {
cart: HttpTypes.StoreCart & {
promotions: HttpTypes.StorePromotion[]
}
}
promotions: HttpTypes.StorePromotion[];
};
};
const DiscountCode: React.FC<DiscountCodeProps> = ({ cart }) => {
const [isOpen, setIsOpen] = React.useState(false)
const [isOpen, setIsOpen] = React.useState(false);
const { items = [], promotions = [] } = cart
const { items = [], promotions = [] } = cart;
const removePromotionCode = async (code: string) => {
const validPromotions = promotions.filter(
(promotion) => promotion.code !== code
)
(promotion) => promotion.code !== code,
);
await applyPromotions(
validPromotions.filter((p) => p.code === undefined).map((p) => p.code!)
)
}
validPromotions.filter((p) => p.code === undefined).map((p) => p.code!),
);
};
const addPromotionCode = async (formData: FormData) => {
const code = formData.get("code")
const code = formData.get('code');
if (!code) {
return
return;
}
const input = document.getElementById("promotion-input") as HTMLInputElement
const input = document.getElementById(
'promotion-input',
) as HTMLInputElement;
const codes = promotions
.filter((p) => p.code === undefined)
.map((p) => p.code!)
codes.push(code.toString())
.map((p) => p.code!);
codes.push(code.toString());
await applyPromotions(codes)
await applyPromotions(codes);
if (input) {
input.value = ""
input.value = '';
}
}
};
const [message, formAction] = useActionState(submitPromotionForm, null)
const [message, formAction] = useActionState(submitPromotionForm, null);
return (
<div className="w-full bg-white flex flex-col">
<div className="flex w-full flex-col bg-white">
<div className="txt-medium">
<form action={(a) => addPromotionCode(a)} className="w-full mb-5">
<Label className="flex gap-x-1 my-2 items-center">
<form action={(a) => addPromotionCode(a)} className="mb-5 w-full">
<Label className="my-2 flex items-center gap-x-1">
<button
onClick={() => setIsOpen(!isOpen)}
type="button"
@@ -98,8 +101,8 @@ const DiscountCode: React.FC<DiscountCodeProps> = ({ cart }) => {
</form>
{promotions.length > 0 && (
<div className="w-full flex items-center">
<div className="flex flex-col w-full">
<div className="flex w-full items-center">
<div className="flex w-full flex-col">
<Heading className="txt-medium mb-2">
Promotion(s) applied:
</Heading>
@@ -108,24 +111,24 @@ const DiscountCode: React.FC<DiscountCodeProps> = ({ cart }) => {
return (
<div
key={promotion.id}
className="flex items-center justify-between w-full max-w-full mb-2"
className="mb-2 flex w-full max-w-full items-center justify-between"
data-testid="discount-row"
>
<Text className="flex gap-x-1 items-baseline txt-small-plus w-4/5 pr-1">
<Text className="txt-small-plus flex w-4/5 items-baseline gap-x-1 pr-1">
<span className="truncate" data-testid="discount-code">
<Badge
color={promotion.is_automatic ? "green" : "grey"}
color={promotion.is_automatic ? 'green' : 'grey'}
size="small"
>
{promotion.code}
</Badge>{" "}
</Badge>{' '}
(
{promotion.application_method?.value !== undefined &&
promotion.application_method.currency_code !==
undefined && (
<>
{promotion.application_method.type ===
"percentage"
'percentage'
? `${promotion.application_method.value}%`
: convertToLocale({
amount: promotion.application_method.value,
@@ -148,10 +151,10 @@ const DiscountCode: React.FC<DiscountCodeProps> = ({ cart }) => {
className="flex items-center"
onClick={() => {
if (!promotion.code) {
return
return;
}
removePromotionCode(promotion.code)
removePromotionCode(promotion.code);
}}
data-testid="remove-discount-button"
>
@@ -162,14 +165,14 @@ const DiscountCode: React.FC<DiscountCodeProps> = ({ cart }) => {
</button>
)}
</div>
)
);
})}
</div>
</div>
)}
</div>
</div>
)
}
);
};
export default DiscountCode
export default DiscountCode;

View File

@@ -1,13 +1,22 @@
const ErrorMessage = ({ error, 'data-testid': dataTestid }: { error?: string | null, 'data-testid'?: string }) => {
const ErrorMessage = ({
error,
'data-testid': dataTestid,
}: {
error?: string | null;
'data-testid'?: string;
}) => {
if (!error) {
return null
return null;
}
return (
<div className="pt-2 text-rose-500 text-small-regular" data-testid={dataTestid}>
<div
className="text-small-regular pt-2 text-rose-500"
data-testid={dataTestid}
>
<span>{error}</span>
</div>
)
}
);
};
export default ErrorMessage
export default ErrorMessage;

View File

@@ -1,30 +1,32 @@
"use client"
'use client';
import { isManual, isStripe } from "@lib/constants"
import { placeOrder } from "@lib/data/cart"
import { HttpTypes } from "@medusajs/types"
import { Button } from "@medusajs/ui"
import { useElements, useStripe } from "@stripe/react-stripe-js"
import React, { useState } from "react"
import ErrorMessage from "../error-message"
import React, { useState } from 'react';
import { isManual, isStripe } from '@lib/constants';
import { placeOrder } from '@lib/data/cart';
import { HttpTypes } from '@medusajs/types';
import { Button } from '@medusajs/ui';
import { useElements, useStripe } from '@stripe/react-stripe-js';
import ErrorMessage from '../error-message';
type PaymentButtonProps = {
cart: HttpTypes.StoreCart
"data-testid": string
}
cart: HttpTypes.StoreCart;
'data-testid': string;
};
const PaymentButton: React.FC<PaymentButtonProps> = ({
cart,
"data-testid": dataTestId,
'data-testid': dataTestId,
}) => {
const notReady =
!cart ||
!cart.shipping_address ||
!cart.billing_address ||
!cart.email ||
(cart.shipping_methods?.length ?? 0) < 1
(cart.shipping_methods?.length ?? 0) < 1;
const paymentSession = cart.payment_collection?.payment_sessions?.[0]
const paymentSession = cart.payment_collection?.payment_sessions?.[0];
switch (true) {
case isStripe(paymentSession?.provider_id):
@@ -34,54 +36,54 @@ const PaymentButton: React.FC<PaymentButtonProps> = ({
cart={cart}
data-testid={dataTestId}
/>
)
);
case isManual(paymentSession?.provider_id):
return (
<ManualTestPaymentButton notReady={notReady} data-testid={dataTestId} />
)
);
default:
return <Button disabled>Select a payment method</Button>
return <Button disabled>Select a payment method</Button>;
}
}
};
const StripePaymentButton = ({
cart,
notReady,
"data-testid": dataTestId,
'data-testid': dataTestId,
}: {
cart: HttpTypes.StoreCart
notReady: boolean
"data-testid"?: string
cart: HttpTypes.StoreCart;
notReady: boolean;
'data-testid'?: string;
}) => {
const [submitting, setSubmitting] = useState(false)
const [errorMessage, setErrorMessage] = useState<string | null>(null)
const [submitting, setSubmitting] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const onPaymentCompleted = async () => {
await placeOrder()
.catch((err) => {
setErrorMessage(err.message)
setErrorMessage(err.message);
})
.finally(() => {
setSubmitting(false)
})
}
setSubmitting(false);
});
};
const stripe = useStripe()
const elements = useElements()
const card = elements?.getElement("card")
const stripe = useStripe();
const elements = useElements();
const card = elements?.getElement('card');
const session = cart.payment_collection?.payment_sessions?.find(
(s) => s.status === "pending"
)
(s) => s.status === 'pending',
);
const disabled = !stripe || !elements ? true : false
const disabled = !stripe || !elements ? true : false;
const handlePayment = async () => {
setSubmitting(true)
setSubmitting(true);
if (!stripe || !elements || !card || !cart) {
setSubmitting(false)
return
setSubmitting(false);
return;
}
await stripe
@@ -91,7 +93,7 @@ const StripePaymentButton = ({
billing_details: {
name:
cart.billing_address?.first_name +
" " +
' ' +
cart.billing_address?.last_name,
address: {
city: cart.billing_address?.city ?? undefined,
@@ -108,29 +110,29 @@ const StripePaymentButton = ({
})
.then(({ error, paymentIntent }) => {
if (error) {
const pi = error.payment_intent
const pi = error.payment_intent;
if (
(pi && pi.status === "requires_capture") ||
(pi && pi.status === "succeeded")
(pi && pi.status === 'requires_capture') ||
(pi && pi.status === 'succeeded')
) {
onPaymentCompleted()
onPaymentCompleted();
}
setErrorMessage(error.message || null)
return
setErrorMessage(error.message || null);
return;
}
if (
(paymentIntent && paymentIntent.status === "requires_capture") ||
paymentIntent.status === "succeeded"
(paymentIntent && paymentIntent.status === 'requires_capture') ||
paymentIntent.status === 'succeeded'
) {
return onPaymentCompleted()
return onPaymentCompleted();
}
return
})
}
return;
});
};
return (
<>
@@ -148,28 +150,28 @@ const StripePaymentButton = ({
data-testid="stripe-payment-error-message"
/>
</>
)
}
);
};
const ManualTestPaymentButton = ({ notReady }: { notReady: boolean }) => {
const [submitting, setSubmitting] = useState(false)
const [errorMessage, setErrorMessage] = useState<string | null>(null)
const [submitting, setSubmitting] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const onPaymentCompleted = async () => {
await placeOrder()
.catch((err) => {
setErrorMessage(err.message)
setErrorMessage(err.message);
})
.finally(() => {
setSubmitting(false)
})
}
setSubmitting(false);
});
};
const handlePayment = () => {
setSubmitting(true)
setSubmitting(true);
onPaymentCompleted()
}
onPaymentCompleted();
};
return (
<>
@@ -187,7 +189,7 @@ const ManualTestPaymentButton = ({ notReady }: { notReady: boolean }) => {
data-testid="manual-payment-error-message"
/>
</>
)
}
);
};
export default PaymentButton
export default PaymentButton;

View File

@@ -1,23 +1,23 @@
import { Radio as RadioGroupOption } from "@headlessui/react"
import { Text, clx } from "@medusajs/ui"
import React, { useContext, useMemo, type JSX } from "react"
import React, { type JSX, useContext, useMemo } from 'react';
import Radio from "@modules/common/components/radio"
import { Radio as RadioGroupOption } from '@headlessui/react';
import { isManual } from '@lib/constants';
import { Text, clx } from '@medusajs/ui';
import Radio from '@modules/common/components/radio';
import SkeletonCardDetails from '@modules/skeletons/components/skeleton-card-details';
import { CardElement } from '@stripe/react-stripe-js';
import { StripeCardElementOptions } from '@stripe/stripe-js';
import { isManual } from "@lib/constants"
import SkeletonCardDetails from "@modules/skeletons/components/skeleton-card-details"
import { CardElement } from "@stripe/react-stripe-js"
import { StripeCardElementOptions } from "@stripe/stripe-js"
import PaymentTest from "../payment-test"
import { StripeContext } from "../payment-wrapper/stripe-wrapper"
import PaymentTest from '../payment-test';
import { StripeContext } from '../payment-wrapper/stripe-wrapper';
type PaymentContainerProps = {
paymentProviderId: string
selectedPaymentOptionId: string | null
disabled?: boolean
paymentInfoMap: Record<string, { title: string; icon: JSX.Element }>
children?: React.ReactNode
}
paymentProviderId: string;
selectedPaymentOptionId: string | null;
disabled?: boolean;
paymentInfoMap: Record<string, { title: string; icon: JSX.Element }>;
children?: React.ReactNode;
};
const PaymentContainer: React.FC<PaymentContainerProps> = ({
paymentProviderId,
@@ -26,7 +26,7 @@ const PaymentContainer: React.FC<PaymentContainerProps> = ({
disabled = false,
children,
}) => {
const isDevelopment = process.env.NODE_ENV === "development"
const isDevelopment = process.env.NODE_ENV === 'development';
return (
<RadioGroupOption
@@ -34,24 +34,24 @@ const PaymentContainer: React.FC<PaymentContainerProps> = ({
value={paymentProviderId}
disabled={disabled}
className={clx(
"flex flex-col gap-y-2 text-small-regular cursor-pointer py-4 border rounded-rounded px-8 mb-2 hover:shadow-borders-interactive-with-active",
'text-small-regular rounded-rounded hover:shadow-borders-interactive-with-active mb-2 flex cursor-pointer flex-col gap-y-2 border px-8 py-4',
{
"border-ui-border-interactive":
'border-ui-border-interactive':
selectedPaymentOptionId === paymentProviderId,
}
},
)}
>
<div className="flex items-center justify-between ">
<div className="flex items-center justify-between">
<div className="flex items-center gap-x-4">
<Radio checked={selectedPaymentOptionId === paymentProviderId} />
<Text className="text-base-regular">
{paymentInfoMap[paymentProviderId]?.title || paymentProviderId}
</Text>
{isManual(paymentProviderId) && isDevelopment && (
<PaymentTest className="hidden small:block" />
<PaymentTest className="small:block hidden" />
)}
</div>
<span className="justify-self-end text-ui-fg-base">
<span className="text-ui-fg-base justify-self-end">
{paymentInfoMap[paymentProviderId]?.icon}
</span>
</div>
@@ -60,10 +60,10 @@ const PaymentContainer: React.FC<PaymentContainerProps> = ({
)}
{children}
</RadioGroupOption>
)
}
);
};
export default PaymentContainer
export default PaymentContainer;
export const StripeCardContainer = ({
paymentProviderId,
@@ -73,29 +73,29 @@ export const StripeCardContainer = ({
setCardBrand,
setError,
setCardComplete,
}: Omit<PaymentContainerProps, "children"> & {
setCardBrand: (brand: string) => void
setError: (error: string | null) => void
setCardComplete: (complete: boolean) => void
}: Omit<PaymentContainerProps, 'children'> & {
setCardBrand: (brand: string) => void;
setError: (error: string | null) => void;
setCardComplete: (complete: boolean) => void;
}) => {
const stripeReady = useContext(StripeContext)
const stripeReady = useContext(StripeContext);
const useOptions: StripeCardElementOptions = useMemo(() => {
return {
style: {
base: {
fontFamily: "Inter, sans-serif",
color: "#424270",
"::placeholder": {
color: "rgb(107 114 128)",
fontFamily: 'Inter, sans-serif',
color: '#424270',
'::placeholder': {
color: 'rgb(107 114 128)',
},
},
},
classes: {
base: "pt-3 pb-1 block w-full h-11 px-4 mt-0 bg-ui-bg-field border rounded-md appearance-none focus:outline-none focus:ring-0 focus:shadow-borders-interactive-with-active border-ui-border-base hover:bg-ui-bg-field-hover transition-all duration-300 ease-in-out",
base: 'pt-3 pb-1 block w-full h-11 px-4 mt-0 bg-ui-bg-field border rounded-md appearance-none focus:outline-none focus:ring-0 focus:shadow-borders-interactive-with-active border-ui-border-base hover:bg-ui-bg-field-hover transition-all duration-300 ease-in-out',
},
}
}, [])
};
}, []);
return (
<PaymentContainer
@@ -114,10 +114,10 @@ export const StripeCardContainer = ({
options={useOptions as StripeCardElementOptions}
onChange={(e) => {
setCardBrand(
e.brand && e.brand.charAt(0).toUpperCase() + e.brand.slice(1)
)
setError(e.error?.message || null)
setCardComplete(e.complete)
e.brand && e.brand.charAt(0).toUpperCase() + e.brand.slice(1),
);
setError(e.error?.message || null);
setCardComplete(e.complete);
}}
/>
</div>
@@ -125,5 +125,5 @@ export const StripeCardContainer = ({
<SkeletonCardDetails />
))}
</PaymentContainer>
)
}
);
};

View File

@@ -1,4 +1,4 @@
import { Badge } from "@medusajs/ui"
import { Badge } from '@medusajs/ui';
const PaymentTest = ({ className }: { className?: string }) => {
return (
@@ -6,7 +6,7 @@ const PaymentTest = ({ className }: { className?: string }) => {
<span className="font-semibold">Attention:</span> For testing purposes
only.
</Badge>
)
}
);
};
export default PaymentTest
export default PaymentTest;

View File

@@ -1,23 +1,25 @@
"use client"
'use client';
import { loadStripe } from "@stripe/stripe-js"
import React from "react"
import StripeWrapper from "./stripe-wrapper"
import { HttpTypes } from "@medusajs/types"
import { isStripe } from "@lib/constants"
import React from 'react';
import { isStripe } from '@lib/constants';
import { HttpTypes } from '@medusajs/types';
import { loadStripe } from '@stripe/stripe-js';
import StripeWrapper from './stripe-wrapper';
type PaymentWrapperProps = {
cart: HttpTypes.StoreCart
children: React.ReactNode
}
cart: HttpTypes.StoreCart;
children: React.ReactNode;
};
const stripeKey = process.env.NEXT_PUBLIC_STRIPE_KEY
const stripePromise = stripeKey ? loadStripe(stripeKey) : null
const stripeKey = process.env.NEXT_PUBLIC_STRIPE_KEY;
const stripePromise = stripeKey ? loadStripe(stripeKey) : null;
const PaymentWrapper: React.FC<PaymentWrapperProps> = ({ cart, children }) => {
const paymentSession = cart.payment_collection?.payment_sessions?.find(
(s) => s.status === "pending"
)
(s) => s.status === 'pending',
);
if (
isStripe(paymentSession?.provider_id) &&
@@ -32,10 +34,10 @@ const PaymentWrapper: React.FC<PaymentWrapperProps> = ({ cart, children }) => {
>
{children}
</StripeWrapper>
)
);
}
return <div>{children}</div>
}
return <div>{children}</div>;
};
export default PaymentWrapper
export default PaymentWrapper;

View File

@@ -1,18 +1,19 @@
"use client"
'use client';
import { Stripe, StripeElementsOptions } from "@stripe/stripe-js"
import { Elements } from "@stripe/react-stripe-js"
import { HttpTypes } from "@medusajs/types"
import { createContext } from "react"
import { createContext } from 'react';
import { HttpTypes } from '@medusajs/types';
import { Elements } from '@stripe/react-stripe-js';
import { Stripe, StripeElementsOptions } from '@stripe/stripe-js';
type StripeWrapperProps = {
paymentSession: HttpTypes.StorePaymentSession
stripeKey?: string
stripePromise: Promise<Stripe | null> | null
children: React.ReactNode
}
paymentSession: HttpTypes.StorePaymentSession;
stripeKey?: string;
stripePromise: Promise<Stripe | null> | null;
children: React.ReactNode;
};
export const StripeContext = createContext(false)
export const StripeContext = createContext(false);
const StripeWrapper: React.FC<StripeWrapperProps> = ({
paymentSession,
@@ -22,24 +23,24 @@ const StripeWrapper: React.FC<StripeWrapperProps> = ({
}) => {
const options: StripeElementsOptions = {
clientSecret: paymentSession!.data?.client_secret as string | undefined,
}
};
if (!stripeKey) {
throw new Error(
"Stripe key is missing. Set NEXT_PUBLIC_STRIPE_KEY environment variable."
)
'Stripe key is missing. Set NEXT_PUBLIC_STRIPE_KEY environment variable.',
);
}
if (!stripePromise) {
throw new Error(
"Stripe promise is missing. Make sure you have provided a valid Stripe key."
)
'Stripe promise is missing. Make sure you have provided a valid Stripe key.',
);
}
if (!paymentSession?.data?.client_secret) {
throw new Error(
"Stripe client secret is missing. Cannot initialize Stripe."
)
'Stripe client secret is missing. Cannot initialize Stripe.',
);
}
return (
@@ -48,7 +49,7 @@ const StripeWrapper: React.FC<StripeWrapperProps> = ({
{children}
</Elements>
</StripeContext.Provider>
)
}
);
};
export default StripeWrapper
export default StripeWrapper;

View File

@@ -1,122 +1,124 @@
"use client"
'use client';
import { RadioGroup } from "@headlessui/react"
import { isStripe as isStripeFunc, paymentInfoMap } from "@lib/constants"
import { initiatePaymentSession } from "@lib/data/cart"
import { CheckCircleSolid, CreditCard } from "@medusajs/icons"
import { Button, Container, Heading, Text, clx } from "@medusajs/ui"
import ErrorMessage from "@modules/checkout/components/error-message"
import { useCallback, useEffect, useState } from 'react';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { RadioGroup } from '@headlessui/react';
import { isStripe as isStripeFunc, paymentInfoMap } from '@lib/constants';
import { initiatePaymentSession } from '@lib/data/cart';
import { CheckCircleSolid, CreditCard } from '@medusajs/icons';
import { Button, Container, Heading, Text, clx } from '@medusajs/ui';
import ErrorMessage from '@modules/checkout/components/error-message';
import PaymentContainer, {
StripeCardContainer,
} from "@modules/checkout/components/payment-container"
import Divider from "@modules/common/components/divider"
import { usePathname, useRouter, useSearchParams } from "next/navigation"
import { useCallback, useEffect, useState } from "react"
} from '@modules/checkout/components/payment-container';
import Divider from '@modules/common/components/divider';
const Payment = ({
cart,
availablePaymentMethods,
}: {
cart: any
availablePaymentMethods: any[]
cart: any;
availablePaymentMethods: any[];
}) => {
const activeSession = cart.payment_collection?.payment_sessions?.find(
(paymentSession: any) => paymentSession.status === "pending"
)
(paymentSession: any) => paymentSession.status === 'pending',
);
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [cardBrand, setCardBrand] = useState<string | null>(null)
const [cardComplete, setCardComplete] = useState(false)
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [cardBrand, setCardBrand] = useState<string | null>(null);
const [cardComplete, setCardComplete] = useState(false);
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(
activeSession?.provider_id ?? ""
)
activeSession?.provider_id ?? '',
);
const searchParams = useSearchParams()
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams();
const router = useRouter();
const pathname = usePathname();
const isOpen = searchParams.get("step") === "payment"
const isOpen = searchParams.get('step') === 'payment';
const isStripe = isStripeFunc(selectedPaymentMethod)
const isStripe = isStripeFunc(selectedPaymentMethod);
const setPaymentMethod = async (method: string) => {
setError(null)
setSelectedPaymentMethod(method)
setError(null);
setSelectedPaymentMethod(method);
if (isStripeFunc(method)) {
await initiatePaymentSession(cart, {
provider_id: method,
})
});
}
}
};
const paidByGiftcard =
cart?.gift_cards && cart?.gift_cards?.length > 0 && cart?.total === 0
cart?.gift_cards && cart?.gift_cards?.length > 0 && cart?.total === 0;
const paymentReady =
(activeSession && cart?.shipping_methods.length !== 0) || paidByGiftcard
(activeSession && cart?.shipping_methods.length !== 0) || paidByGiftcard;
const createQueryString = useCallback(
(name: string, value: string) => {
const params = new URLSearchParams(searchParams)
params.set(name, value)
const params = new URLSearchParams(searchParams);
params.set(name, value);
return params.toString()
return params.toString();
},
[searchParams]
)
[searchParams],
);
const handleEdit = () => {
router.push(pathname + "?" + createQueryString("step", "payment"), {
router.push(pathname + '?' + createQueryString('step', 'payment'), {
scroll: false,
})
}
});
};
const handleSubmit = async () => {
setIsLoading(true)
setIsLoading(true);
try {
const shouldInputCard =
isStripeFunc(selectedPaymentMethod) && !activeSession
isStripeFunc(selectedPaymentMethod) && !activeSession;
const checkActiveSession =
activeSession?.provider_id === selectedPaymentMethod
activeSession?.provider_id === selectedPaymentMethod;
if (!checkActiveSession) {
await initiatePaymentSession(cart, {
provider_id: selectedPaymentMethod,
})
});
}
if (!shouldInputCard) {
return router.push(
pathname + "?" + createQueryString("step", "review"),
pathname + '?' + createQueryString('step', 'review'),
{
scroll: false,
}
)
},
);
}
} catch (err: any) {
setError(err.message)
setError(err.message);
} finally {
setIsLoading(false)
setIsLoading(false);
}
}
};
useEffect(() => {
setError(null)
}, [isOpen])
setError(null);
}, [isOpen]);
return (
<div className="bg-white">
<div className="flex flex-row items-center justify-between mb-6">
<div className="mb-6 flex flex-row items-center justify-between">
<Heading
level="h2"
className={clx(
"flex flex-row text-3xl-regular gap-x-2 items-baseline",
'text-3xl-regular flex flex-row items-baseline gap-x-2',
{
"opacity-50 pointer-events-none select-none":
'pointer-events-none opacity-50 select-none':
!isOpen && !paymentReady,
}
},
)}
>
Payment
@@ -135,7 +137,7 @@ const Payment = ({
)}
</div>
<div>
<div className={isOpen ? "block" : "hidden"}>
<div className={isOpen ? 'block' : 'hidden'}>
{!paidByGiftcard && availablePaymentMethods?.length && (
<>
<RadioGroup
@@ -167,7 +169,7 @@ const Payment = ({
)}
{paidByGiftcard && (
<div className="flex flex-col w-1/3">
<div className="flex w-1/3 flex-col">
<Text className="txt-medium-plus text-ui-fg-base mb-1">
Payment method
</Text>
@@ -197,15 +199,15 @@ const Payment = ({
data-testid="submit-payment-button"
>
{!activeSession && isStripeFunc(selectedPaymentMethod)
? " Enter card details"
: "Continue to review"}
? ' Enter card details'
: 'Continue to review'}
</Button>
</div>
<div className={isOpen ? "hidden" : "block"}>
<div className={isOpen ? 'hidden' : 'block'}>
{cart && paymentReady && activeSession ? (
<div className="flex items-start gap-x-1 w-full">
<div className="flex flex-col w-1/3">
<div className="flex w-full items-start gap-x-1">
<div className="flex w-1/3 flex-col">
<Text className="txt-medium-plus text-ui-fg-base mb-1">
Payment method
</Text>
@@ -217,15 +219,15 @@ const Payment = ({
activeSession?.provider_id}
</Text>
</div>
<div className="flex flex-col w-1/3">
<div className="flex w-1/3 flex-col">
<Text className="txt-medium-plus text-ui-fg-base mb-1">
Payment details
</Text>
<div
className="flex gap-2 txt-medium text-ui-fg-subtle items-center"
className="txt-medium text-ui-fg-subtle flex items-center gap-2"
data-testid="payment-details-summary"
>
<Container className="flex items-center h-7 w-fit p-2 bg-ui-button-neutral-hover">
<Container className="bg-ui-button-neutral-hover flex h-7 w-fit items-center p-2">
{paymentInfoMap[selectedPaymentMethod]?.icon || (
<CreditCard />
)}
@@ -233,13 +235,13 @@ const Payment = ({
<Text>
{isStripeFunc(selectedPaymentMethod) && cardBrand
? cardBrand
: "Another step will appear"}
: 'Another step will appear'}
</Text>
</div>
</div>
</div>
) : paidByGiftcard ? (
<div className="flex flex-col w-1/3">
<div className="flex w-1/3 flex-col">
<Text className="txt-medium-plus text-ui-fg-base mb-1">
Payment method
</Text>
@@ -255,7 +257,7 @@ const Payment = ({
</div>
<Divider className="mt-8" />
</div>
)
}
);
};
export default Payment
export default Payment;

View File

@@ -1,33 +1,34 @@
"use client"
'use client';
import { Heading, Text, clx } from "@medusajs/ui"
import { useSearchParams } from 'next/navigation';
import PaymentButton from "../payment-button"
import { useSearchParams } from "next/navigation"
import { Heading, Text, clx } from '@medusajs/ui';
import PaymentButton from '../payment-button';
const Review = ({ cart }: { cart: any }) => {
const searchParams = useSearchParams()
const searchParams = useSearchParams();
const isOpen = searchParams.get("step") === "review"
const isOpen = searchParams.get('step') === 'review';
const paidByGiftcard =
cart?.gift_cards && cart?.gift_cards?.length > 0 && cart?.total === 0
cart?.gift_cards && cart?.gift_cards?.length > 0 && cart?.total === 0;
const previousStepsCompleted =
cart.shipping_address &&
cart.shipping_methods.length > 0 &&
(cart.payment_collection || paidByGiftcard)
(cart.payment_collection || paidByGiftcard);
return (
<div className="bg-white">
<div className="flex flex-row items-center justify-between mb-6">
<div className="mb-6 flex flex-row items-center justify-between">
<Heading
level="h2"
className={clx(
"flex flex-row text-3xl-regular gap-x-2 items-baseline",
'text-3xl-regular flex flex-row items-baseline gap-x-2',
{
"opacity-50 pointer-events-none select-none": !isOpen,
}
'pointer-events-none opacity-50 select-none': !isOpen,
},
)}
>
Review
@@ -35,7 +36,7 @@ const Review = ({ cart }: { cart: any }) => {
</div>
{isOpen && previousStepsCompleted && (
<>
<div className="flex items-start gap-x-1 w-full mb-6">
<div className="mb-6 flex w-full items-start gap-x-1">
<div className="w-full">
<Text className="txt-medium-plus text-ui-fg-base mb-1">
By clicking the Place Order button, you confirm that you have
@@ -49,7 +50,7 @@ const Review = ({ cart }: { cart: any }) => {
</>
)}
</div>
)
}
);
};
export default Review
export default Review;

View File

@@ -1,11 +1,13 @@
import { HttpTypes } from "@medusajs/types"
import { Container } from "@medusajs/ui"
import Checkbox from "@modules/common/components/checkbox"
import Input from "@modules/common/components/input"
import { mapKeys } from "lodash"
import React, { useEffect, useMemo, useState } from "react"
import AddressSelect from "../address-select"
import CountrySelect from "../country-select"
import React, { useEffect, useMemo, useState } from 'react';
import { HttpTypes } from '@medusajs/types';
import { Container } from '@medusajs/ui';
import Checkbox from '@modules/common/components/checkbox';
import Input from '@modules/common/components/input';
import { mapKeys } from 'lodash';
import AddressSelect from '../address-select';
import CountrySelect from '../country-select';
const ShippingAddress = ({
customer,
@@ -13,84 +15,84 @@ const ShippingAddress = ({
checked,
onChange,
}: {
customer: HttpTypes.StoreCustomer | null
cart: HttpTypes.StoreCart | null
checked: boolean
onChange: () => void
customer: HttpTypes.StoreCustomer | null;
cart: HttpTypes.StoreCart | null;
checked: boolean;
onChange: () => void;
}) => {
const [formData, setFormData] = useState<Record<string, any>>({
"shipping_address.first_name": cart?.shipping_address?.first_name || "",
"shipping_address.last_name": cart?.shipping_address?.last_name || "",
"shipping_address.address_1": cart?.shipping_address?.address_1 || "",
"shipping_address.company": cart?.shipping_address?.company || "",
"shipping_address.postal_code": cart?.shipping_address?.postal_code || "",
"shipping_address.city": cart?.shipping_address?.city || "",
"shipping_address.country_code": cart?.shipping_address?.country_code || "",
"shipping_address.province": cart?.shipping_address?.province || "",
"shipping_address.phone": cart?.shipping_address?.phone || "",
email: cart?.email || "",
})
'shipping_address.first_name': cart?.shipping_address?.first_name || '',
'shipping_address.last_name': cart?.shipping_address?.last_name || '',
'shipping_address.address_1': cart?.shipping_address?.address_1 || '',
'shipping_address.company': cart?.shipping_address?.company || '',
'shipping_address.postal_code': cart?.shipping_address?.postal_code || '',
'shipping_address.city': cart?.shipping_address?.city || '',
'shipping_address.country_code': cart?.shipping_address?.country_code || '',
'shipping_address.province': cart?.shipping_address?.province || '',
'shipping_address.phone': cart?.shipping_address?.phone || '',
email: cart?.email || '',
});
const countriesInRegion = useMemo(
() => cart?.region?.countries?.map((c) => c.iso_2),
[cart?.region]
)
[cart?.region],
);
// check if customer has saved addresses that are in the current region
const addressesInRegion = useMemo(
() =>
customer?.addresses.filter(
(a) => a.country_code && countriesInRegion?.includes(a.country_code)
(a) => a.country_code && countriesInRegion?.includes(a.country_code),
),
[customer?.addresses, countriesInRegion]
)
[customer?.addresses, countriesInRegion],
);
const setFormAddress = (
address?: HttpTypes.StoreCartAddress,
email?: string
email?: string,
) => {
address &&
setFormData((prevState: Record<string, any>) => ({
...prevState,
"shipping_address.first_name": address?.first_name || "",
"shipping_address.last_name": address?.last_name || "",
"shipping_address.address_1": address?.address_1 || "",
"shipping_address.company": address?.company || "",
"shipping_address.postal_code": address?.postal_code || "",
"shipping_address.city": address?.city || "",
"shipping_address.country_code": address?.country_code || "",
"shipping_address.province": address?.province || "",
"shipping_address.phone": address?.phone || "",
}))
'shipping_address.first_name': address?.first_name || '',
'shipping_address.last_name': address?.last_name || '',
'shipping_address.address_1': address?.address_1 || '',
'shipping_address.company': address?.company || '',
'shipping_address.postal_code': address?.postal_code || '',
'shipping_address.city': address?.city || '',
'shipping_address.country_code': address?.country_code || '',
'shipping_address.province': address?.province || '',
'shipping_address.phone': address?.phone || '',
}));
email &&
setFormData((prevState: Record<string, any>) => ({
...prevState,
email: email,
}))
}
}));
};
useEffect(() => {
// Ensure cart is not null and has a shipping_address before setting form data
if (cart && cart.shipping_address) {
setFormAddress(cart?.shipping_address, cart?.email)
setFormAddress(cart?.shipping_address, cart?.email);
}
if (cart && !cart.email && customer?.email) {
setFormAddress(undefined, customer.email)
setFormAddress(undefined, customer.email);
}
}, [cart]) // Add cart as a dependency
}, [cart]); // Add cart as a dependency
const handleChange = (
e: React.ChangeEvent<
HTMLInputElement | HTMLInputElement | HTMLSelectElement
>
>,
) => {
setFormData({
...formData,
[e.target.name]: e.target.value,
})
}
});
};
return (
<>
@@ -103,7 +105,7 @@ const ShippingAddress = ({
addresses={customer.addresses}
addressInput={
mapKeys(formData, (_, key) =>
key.replace("shipping_address.", "")
key.replace('shipping_address.', ''),
) as HttpTypes.StoreCartAddress
}
onSelect={setFormAddress}
@@ -115,7 +117,7 @@ const ShippingAddress = ({
label="First name"
name="shipping_address.first_name"
autoComplete="given-name"
value={formData["shipping_address.first_name"]}
value={formData['shipping_address.first_name']}
onChange={handleChange}
required
data-testid="shipping-first-name-input"
@@ -124,7 +126,7 @@ const ShippingAddress = ({
label="Last name"
name="shipping_address.last_name"
autoComplete="family-name"
value={formData["shipping_address.last_name"]}
value={formData['shipping_address.last_name']}
onChange={handleChange}
required
data-testid="shipping-last-name-input"
@@ -133,7 +135,7 @@ const ShippingAddress = ({
label="Address"
name="shipping_address.address_1"
autoComplete="address-line1"
value={formData["shipping_address.address_1"]}
value={formData['shipping_address.address_1']}
onChange={handleChange}
required
data-testid="shipping-address-input"
@@ -141,7 +143,7 @@ const ShippingAddress = ({
<Input
label="Company"
name="shipping_address.company"
value={formData["shipping_address.company"]}
value={formData['shipping_address.company']}
onChange={handleChange}
autoComplete="organization"
data-testid="shipping-company-input"
@@ -150,7 +152,7 @@ const ShippingAddress = ({
label="Postal code"
name="shipping_address.postal_code"
autoComplete="postal-code"
value={formData["shipping_address.postal_code"]}
value={formData['shipping_address.postal_code']}
onChange={handleChange}
required
data-testid="shipping-postal-code-input"
@@ -159,7 +161,7 @@ const ShippingAddress = ({
label="City"
name="shipping_address.city"
autoComplete="address-level2"
value={formData["shipping_address.city"]}
value={formData['shipping_address.city']}
onChange={handleChange}
required
data-testid="shipping-city-input"
@@ -168,7 +170,7 @@ const ShippingAddress = ({
name="shipping_address.country_code"
autoComplete="country"
region={cart?.region}
value={formData["shipping_address.country_code"]}
value={formData['shipping_address.country_code']}
onChange={handleChange}
required
data-testid="shipping-country-select"
@@ -177,7 +179,7 @@ const ShippingAddress = ({
label="State / Province"
name="shipping_address.province"
autoComplete="address-level1"
value={formData["shipping_address.province"]}
value={formData['shipping_address.province']}
onChange={handleChange}
data-testid="shipping-province-input"
/>
@@ -191,7 +193,7 @@ const ShippingAddress = ({
data-testid="billing-address-checkbox"
/>
</div>
<div className="grid grid-cols-2 gap-4 mb-4">
<div className="mb-4 grid grid-cols-2 gap-4">
<Input
label="Email"
name="email"
@@ -207,13 +209,13 @@ const ShippingAddress = ({
label="Phone"
name="shipping_address.phone"
autoComplete="tel"
value={formData["shipping_address.phone"]}
value={formData['shipping_address.phone']}
onChange={handleChange}
data-testid="shipping-phone-input"
/>
</div>
</>
)
}
);
};
export default ShippingAddress
export default ShippingAddress;

View File

@@ -1,164 +1,166 @@
"use client"
'use client';
import { RadioGroup, Radio } from "@headlessui/react"
import { setShippingMethod } from "@lib/data/cart"
import { calculatePriceForShippingOption } from "@lib/data/fulfillment"
import { convertToLocale } from "@lib/util/money"
import { CheckCircleSolid, Loader } from "@medusajs/icons"
import { HttpTypes } from "@medusajs/types"
import { Button, Heading, Text, clx } from "@medusajs/ui"
import ErrorMessage from "@modules/checkout/components/error-message"
import Divider from "@modules/common/components/divider"
import MedusaRadio from "@modules/common/components/radio"
import { usePathname, useRouter, useSearchParams } from "next/navigation"
import { useEffect, useState } from "react"
import { useEffect, useState } from 'react';
const PICKUP_OPTION_ON = "__PICKUP_ON"
const PICKUP_OPTION_OFF = "__PICKUP_OFF"
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { Radio, RadioGroup } from '@headlessui/react';
import { setShippingMethod } from '@lib/data/cart';
import { calculatePriceForShippingOption } from '@lib/data/fulfillment';
import { convertToLocale } from '@lib/util/money';
import { CheckCircleSolid, Loader } from '@medusajs/icons';
import { HttpTypes } from '@medusajs/types';
import { Button, Heading, Text, clx } from '@medusajs/ui';
import ErrorMessage from '@modules/checkout/components/error-message';
import Divider from '@modules/common/components/divider';
import MedusaRadio from '@modules/common/components/radio';
const PICKUP_OPTION_ON = '__PICKUP_ON';
const PICKUP_OPTION_OFF = '__PICKUP_OFF';
type ShippingProps = {
cart: HttpTypes.StoreCart
availableShippingMethods: HttpTypes.StoreCartShippingOption[] | null
}
cart: HttpTypes.StoreCart;
availableShippingMethods: HttpTypes.StoreCartShippingOption[] | null;
};
function formatAddress(address) {
if (!address) {
return ""
return '';
}
let ret = ""
let ret = '';
if (address.address_1) {
ret += ` ${address.address_1}`
ret += ` ${address.address_1}`;
}
if (address.address_2) {
ret += `, ${address.address_2}`
ret += `, ${address.address_2}`;
}
if (address.postal_code) {
ret += `, ${address.postal_code} ${address.city}`
ret += `, ${address.postal_code} ${address.city}`;
}
if (address.country_code) {
ret += `, ${address.country_code.toUpperCase()}`
ret += `, ${address.country_code.toUpperCase()}`;
}
return ret
return ret;
}
const Shipping: React.FC<ShippingProps> = ({
cart,
availableShippingMethods,
}) => {
const [isLoading, setIsLoading] = useState(false)
const [isLoadingPrices, setIsLoadingPrices] = useState(true)
const [isLoading, setIsLoading] = useState(false);
const [isLoadingPrices, setIsLoadingPrices] = useState(true);
const [showPickupOptions, setShowPickupOptions] =
useState<string>(PICKUP_OPTION_OFF)
useState<string>(PICKUP_OPTION_OFF);
const [calculatedPricesMap, setCalculatedPricesMap] = useState<
Record<string, number>
>({})
const [error, setError] = useState<string | null>(null)
>({});
const [error, setError] = useState<string | null>(null);
const [shippingMethodId, setShippingMethodId] = useState<string | null>(
cart.shipping_methods?.at(-1)?.shipping_option_id || null
)
cart.shipping_methods?.at(-1)?.shipping_option_id || null,
);
const searchParams = useSearchParams()
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams();
const router = useRouter();
const pathname = usePathname();
const isOpen = searchParams.get("step") === "delivery"
const isOpen = searchParams.get('step') === 'delivery';
const _shippingMethods = availableShippingMethods?.filter(
(sm) => sm.service_zone?.fulfillment_set?.type !== "pickup"
)
(sm) => sm.service_zone?.fulfillment_set?.type !== 'pickup',
);
const _pickupMethods = availableShippingMethods?.filter(
(sm) => sm.service_zone?.fulfillment_set?.type === "pickup"
)
(sm) => sm.service_zone?.fulfillment_set?.type === 'pickup',
);
const hasPickupOptions = !!_pickupMethods?.length
const hasPickupOptions = !!_pickupMethods?.length;
useEffect(() => {
setIsLoadingPrices(true)
setIsLoadingPrices(true);
if (_shippingMethods?.length) {
const promises = _shippingMethods
.filter((sm) => sm.price_type === "calculated")
.map((sm) => calculatePriceForShippingOption(sm.id, cart.id))
.filter((sm) => sm.price_type === 'calculated')
.map((sm) => calculatePriceForShippingOption(sm.id, cart.id));
if (promises.length) {
Promise.allSettled(promises).then((res) => {
const pricesMap: Record<string, number> = {}
const pricesMap: Record<string, number> = {};
res
.filter((r) => r.status === "fulfilled")
.forEach((p) => (pricesMap[p.value?.id || ""] = p.value?.amount!))
.filter((r) => r.status === 'fulfilled')
.forEach((p) => (pricesMap[p.value?.id || ''] = p.value?.amount!));
setCalculatedPricesMap(pricesMap)
setIsLoadingPrices(false)
})
setCalculatedPricesMap(pricesMap);
setIsLoadingPrices(false);
});
}
}
if (_pickupMethods?.find((m) => m.id === shippingMethodId)) {
setShowPickupOptions(PICKUP_OPTION_ON)
setShowPickupOptions(PICKUP_OPTION_ON);
}
}, [availableShippingMethods])
}, [availableShippingMethods]);
const handleEdit = () => {
router.push(pathname + "?step=delivery", { scroll: false })
}
router.push(pathname + '?step=delivery', { scroll: false });
};
const handleSubmit = () => {
router.push(pathname + "?step=payment", { scroll: false })
}
router.push(pathname + '?step=payment', { scroll: false });
};
const handleSetShippingMethod = async (
id: string,
variant: "shipping" | "pickup"
variant: 'shipping' | 'pickup',
) => {
setError(null)
setError(null);
if (variant === "pickup") {
setShowPickupOptions(PICKUP_OPTION_ON)
if (variant === 'pickup') {
setShowPickupOptions(PICKUP_OPTION_ON);
} else {
setShowPickupOptions(PICKUP_OPTION_OFF)
setShowPickupOptions(PICKUP_OPTION_OFF);
}
let currentId: string | null = null
setIsLoading(true)
let currentId: string | null = null;
setIsLoading(true);
setShippingMethodId((prev) => {
currentId = prev
return id
})
currentId = prev;
return id;
});
await setShippingMethod({ cartId: cart.id, shippingMethodId: id })
.catch((err) => {
setShippingMethodId(currentId)
setShippingMethodId(currentId);
setError(err.message)
setError(err.message);
})
.finally(() => {
setIsLoading(false)
})
}
setIsLoading(false);
});
};
useEffect(() => {
setError(null)
}, [isOpen])
setError(null);
}, [isOpen]);
return (
<div className="bg-white">
<div className="flex flex-row items-center justify-between mb-6">
<div className="mb-6 flex flex-row items-center justify-between">
<Heading
level="h2"
className={clx(
"flex flex-row text-3xl-regular gap-x-2 items-baseline",
'text-3xl-regular flex flex-row items-baseline gap-x-2',
{
"opacity-50 pointer-events-none select-none":
'pointer-events-none opacity-50 select-none':
!isOpen && cart.shipping_methods?.length === 0,
}
},
)}
>
Delivery
@@ -185,25 +187,25 @@ const Shipping: React.FC<ShippingProps> = ({
<>
<div className="grid">
<div className="flex flex-col">
<span className="font-medium txt-medium text-ui-fg-base">
<span className="txt-medium text-ui-fg-base font-medium">
Shipping method
</span>
<span className="mb-4 text-ui-fg-muted txt-medium">
<span className="text-ui-fg-muted txt-medium mb-4">
How would you like you order delivered
</span>
</div>
<div data-testid="delivery-options-container">
<div className="pb-8 md:pt-0 pt-2">
<div className="pt-2 pb-8 md:pt-0">
{hasPickupOptions && (
<RadioGroup
value={showPickupOptions}
onChange={(value) => {
const id = _pickupMethods.find(
(option) => !option.insufficient_inventory
)?.id
(option) => !option.insufficient_inventory,
)?.id;
if (id) {
handleSetShippingMethod(id, "pickup")
handleSetShippingMethod(id, 'pickup');
}
}}
>
@@ -211,11 +213,11 @@ const Shipping: React.FC<ShippingProps> = ({
value={PICKUP_OPTION_ON}
data-testid="delivery-option-radio"
className={clx(
"flex items-center justify-between text-small-regular cursor-pointer py-4 border rounded-rounded px-8 mb-2 hover:shadow-borders-interactive-with-active",
'text-small-regular rounded-rounded hover:shadow-borders-interactive-with-active mb-2 flex cursor-pointer items-center justify-between border px-8 py-4',
{
"border-ui-border-interactive":
'border-ui-border-interactive':
showPickupOptions === PICKUP_OPTION_ON,
}
},
)}
>
<div className="flex items-center gap-x-4">
@@ -226,7 +228,7 @@ const Shipping: React.FC<ShippingProps> = ({
Pick up your order
</span>
</div>
<span className="justify-self-end text-ui-fg-base">
<span className="text-ui-fg-base justify-self-end">
-
</span>
</Radio>
@@ -234,13 +236,13 @@ const Shipping: React.FC<ShippingProps> = ({
)}
<RadioGroup
value={shippingMethodId}
onChange={(v) => handleSetShippingMethod(v, "shipping")}
onChange={(v) => handleSetShippingMethod(v, 'shipping')}
>
{_shippingMethods?.map((option) => {
const isDisabled =
option.price_type === "calculated" &&
option.price_type === 'calculated' &&
!isLoadingPrices &&
typeof calculatedPricesMap[option.id] !== "number"
typeof calculatedPricesMap[option.id] !== 'number';
return (
<Radio
@@ -249,13 +251,13 @@ const Shipping: React.FC<ShippingProps> = ({
data-testid="delivery-option-radio"
disabled={isDisabled}
className={clx(
"flex items-center justify-between text-small-regular cursor-pointer py-4 border rounded-rounded px-8 mb-2 hover:shadow-borders-interactive-with-active",
'text-small-regular rounded-rounded hover:shadow-borders-interactive-with-active mb-2 flex cursor-pointer items-center justify-between border px-8 py-4',
{
"border-ui-border-interactive":
'border-ui-border-interactive':
option.id === shippingMethodId,
"hover:shadow-brders-none cursor-not-allowed":
'hover:shadow-brders-none cursor-not-allowed':
isDisabled,
}
},
)}
>
<div className="flex items-center gap-x-4">
@@ -266,8 +268,8 @@ const Shipping: React.FC<ShippingProps> = ({
{option.name}
</span>
</div>
<span className="justify-self-end text-ui-fg-base">
{option.price_type === "flat" ? (
<span className="text-ui-fg-base justify-self-end">
{option.price_type === 'flat' ? (
convertToLocale({
amount: option.amount!,
currency_code: cart?.currency_code,
@@ -280,11 +282,11 @@ const Shipping: React.FC<ShippingProps> = ({
) : isLoadingPrices ? (
<Loader />
) : (
"-"
'-'
)}
</span>
</Radio>
)
);
})}
</RadioGroup>
</div>
@@ -294,18 +296,18 @@ const Shipping: React.FC<ShippingProps> = ({
{showPickupOptions === PICKUP_OPTION_ON && (
<div className="grid">
<div className="flex flex-col">
<span className="font-medium txt-medium text-ui-fg-base">
<span className="txt-medium text-ui-fg-base font-medium">
Store
</span>
<span className="mb-4 text-ui-fg-muted txt-medium">
<span className="text-ui-fg-muted txt-medium mb-4">
Choose a store near you
</span>
</div>
<div data-testid="delivery-options-container">
<div className="pb-8 md:pt-0 pt-2">
<div className="pt-2 pb-8 md:pt-0">
<RadioGroup
value={shippingMethodId}
onChange={(v) => handleSetShippingMethod(v, "pickup")}
onChange={(v) => handleSetShippingMethod(v, 'pickup')}
>
{_pickupMethods?.map((option) => {
return (
@@ -315,13 +317,13 @@ const Shipping: React.FC<ShippingProps> = ({
disabled={option.insufficient_inventory}
data-testid="delivery-option-radio"
className={clx(
"flex items-center justify-between text-small-regular cursor-pointer py-4 border rounded-rounded px-8 mb-2 hover:shadow-borders-interactive-with-active",
'text-small-regular rounded-rounded hover:shadow-borders-interactive-with-active mb-2 flex cursor-pointer items-center justify-between border px-8 py-4',
{
"border-ui-border-interactive":
'border-ui-border-interactive':
option.id === shippingMethodId,
"hover:shadow-brders-none cursor-not-allowed":
'hover:shadow-brders-none cursor-not-allowed':
option.insufficient_inventory,
}
},
)}
>
<div className="flex items-start gap-x-4">
@@ -335,19 +337,19 @@ const Shipping: React.FC<ShippingProps> = ({
<span className="text-base-regular text-ui-fg-muted">
{formatAddress(
option.service_zone?.fulfillment_set?.location
?.address
?.address,
)}
</span>
</div>
</div>
<span className="justify-self-end text-ui-fg-base">
<span className="text-ui-fg-base justify-self-end">
{convertToLocale({
amount: option.amount!,
currency_code: cart?.currency_code,
})}
</span>
</Radio>
)
);
})}
</RadioGroup>
</div>
@@ -376,12 +378,12 @@ const Shipping: React.FC<ShippingProps> = ({
<div>
<div className="text-small-regular">
{cart && (cart.shipping_methods?.length ?? 0) > 0 && (
<div className="flex flex-col w-1/3">
<div className="flex w-1/3 flex-col">
<Text className="txt-medium-plus text-ui-fg-base mb-1">
Method
</Text>
<Text className="txt-medium text-ui-fg-subtle">
{cart.shipping_methods?.at(-1)?.name}{" "}
{cart.shipping_methods?.at(-1)?.name}{' '}
{convertToLocale({
amount: cart.shipping_methods.at(-1)?.amount!,
currency_code: cart?.currency_code,
@@ -394,7 +396,7 @@ const Shipping: React.FC<ShippingProps> = ({
)}
<Divider className="mt-8" />
</div>
)
}
);
};
export default Shipping
export default Shipping;

View File

@@ -1,21 +1,23 @@
"use client"
'use client';
import { Button } from "@medusajs/ui"
import React from "react"
import { useFormStatus } from "react-dom"
import React from 'react';
import { useFormStatus } from 'react-dom';
import { Button } from '@medusajs/ui';
export function SubmitButton({
children,
variant = "primary",
variant = 'primary',
className,
"data-testid": dataTestId,
'data-testid': dataTestId,
}: {
children: React.ReactNode
variant?: "primary" | "secondary" | "transparent" | "danger" | null
className?: string
"data-testid"?: string
children: React.ReactNode;
variant?: 'primary' | 'secondary' | 'transparent' | 'danger' | null;
className?: string;
'data-testid'?: string;
}) {
const { pending } = useFormStatus()
const { pending } = useFormStatus();
return (
<Button
@@ -23,10 +25,10 @@ export function SubmitButton({
className={className}
type="submit"
isLoading={pending}
variant={variant || "primary"}
variant={variant || 'primary'}
data-testid={dataTestId}
>
{children}
</Button>
)
);
}

View File

@@ -1,31 +1,31 @@
import { listCartShippingMethods } from "@lib/data/fulfillment"
import { listCartPaymentMethods } from "@lib/data/payment"
import { HttpTypes } from "@medusajs/types"
import Addresses from "@modules/checkout/components/addresses"
import Payment from "@modules/checkout/components/payment"
import Review from "@modules/checkout/components/review"
import Shipping from "@modules/checkout/components/shipping"
import { listCartShippingMethods } from '@lib/data/fulfillment';
import { listCartPaymentMethods } from '@lib/data/payment';
import { HttpTypes } from '@medusajs/types';
import Addresses from '@modules/checkout/components/addresses';
import Payment from '@modules/checkout/components/payment';
import Review from '@modules/checkout/components/review';
import Shipping from '@modules/checkout/components/shipping';
export default async function CheckoutForm({
cart,
customer,
}: {
cart: HttpTypes.StoreCart | null
customer: HttpTypes.StoreCustomer | null
cart: HttpTypes.StoreCart | null;
customer: HttpTypes.StoreCustomer | null;
}) {
if (!cart) {
return null
return null;
}
const shippingMethods = await listCartShippingMethods(cart.id)
const paymentMethods = await listCartPaymentMethods(cart.region?.id ?? "")
const shippingMethods = await listCartShippingMethods(cart.id);
const paymentMethods = await listCartPaymentMethods(cart.region?.id ?? '');
if (!shippingMethods || !paymentMethods) {
return null
return null;
}
return (
<div className="w-full grid grid-cols-1 gap-y-8">
<div className="grid w-full grid-cols-1 gap-y-8">
<Addresses cart={cart} customer={customer} />
<Shipping cart={cart} availableShippingMethods={shippingMethods} />
@@ -34,5 +34,5 @@ export default async function CheckoutForm({
<Review cart={cart} />
</div>
)
);
}

View File

@@ -1,18 +1,17 @@
import { Heading } from "@medusajs/ui"
import ItemsPreviewTemplate from "@modules/cart/templates/preview"
import DiscountCode from "@modules/checkout/components/discount-code"
import CartTotals from "@modules/common/components/cart-totals"
import Divider from "@modules/common/components/divider"
import { Heading } from '@medusajs/ui';
import ItemsPreviewTemplate from '@modules/cart/templates/preview';
import DiscountCode from '@modules/checkout/components/discount-code';
import CartTotals from '@modules/common/components/cart-totals';
import Divider from '@modules/common/components/divider';
const CheckoutSummary = ({ cart }: { cart: any }) => {
return (
<div className="sticky top-0 flex flex-col-reverse small:flex-col gap-y-8 py-8 small:py-0 ">
<div className="w-full bg-white flex flex-col">
<Divider className="my-6 small:hidden" />
<div className="small:flex-col small:py-0 sticky top-0 flex flex-col-reverse gap-y-8 py-8">
<div className="flex w-full flex-col bg-white">
<Divider className="small:hidden my-6" />
<Heading
level="h2"
className="flex flex-row text-3xl-regular items-baseline"
className="text-3xl-regular flex flex-row items-baseline"
>
In your Cart
</Heading>
@@ -24,7 +23,7 @@ const CheckoutSummary = ({ cart }: { cart: any }) => {
</div>
</div>
</div>
)
}
);
};
export default CheckoutSummary
export default CheckoutSummary;

View File

@@ -1,10 +1,10 @@
import { Suspense } from "react"
import { Suspense } from 'react';
import SkeletonProductGrid from "@modules/skeletons/templates/skeleton-product-grid"
import RefinementList from "@modules/store/components/refinement-list"
import { SortOptions } from "@modules/store/components/refinement-list/sort-products"
import PaginatedProducts from "@modules/store/templates/paginated-products"
import { HttpTypes } from "@medusajs/types"
import { HttpTypes } from '@medusajs/types';
import SkeletonProductGrid from '@modules/skeletons/templates/skeleton-product-grid';
import RefinementList from '@modules/store/components/refinement-list';
import { SortOptions } from '@modules/store/components/refinement-list/sort-products';
import PaginatedProducts from '@modules/store/templates/paginated-products';
export default function CollectionTemplate({
sortBy,
@@ -12,19 +12,19 @@ export default function CollectionTemplate({
page,
countryCode,
}: {
sortBy?: SortOptions
collection: HttpTypes.StoreCollection
page?: string
countryCode: string
sortBy?: SortOptions;
collection: HttpTypes.StoreCollection;
page?: string;
countryCode: string;
}) {
const pageNumber = page ? parseInt(page) : 1
const sort = sortBy || "created_at"
const pageNumber = page ? parseInt(page) : 1;
const sort = sortBy || 'created_at';
return (
<div className="flex flex-col small:flex-row small:items-start py-6 content-container">
<div className="small:flex-row small:items-start content-container flex flex-col py-6">
<RefinementList sortBy={sort} />
<div className="w-full">
<div className="mb-8 text-2xl-semi">
<div className="text-2xl-semi mb-8">
<h1>{collection.title}</h1>
</div>
<Suspense
@@ -43,5 +43,5 @@ export default function CollectionTemplate({
</Suspense>
</div>
</div>
)
);
}

View File

@@ -1,20 +1,21 @@
"use client"
'use client';
import { convertToLocale } from "@lib/util/money"
import React from "react"
import React from 'react';
import { convertToLocale } from '@lib/util/money';
type CartTotalsProps = {
totals: {
total?: number | null
subtotal?: number | null
tax_total?: number | null
shipping_total?: number | null
discount_total?: number | null
gift_card_total?: number | null
currency_code: string
shipping_subtotal?: number | null
}
}
total?: number | null;
subtotal?: number | null;
tax_total?: number | null;
shipping_total?: number | null;
discount_total?: number | null;
gift_card_total?: number | null;
currency_code: string;
shipping_subtotal?: number | null;
};
};
const CartTotals: React.FC<CartTotalsProps> = ({ totals }) => {
const {
@@ -25,13 +26,13 @@ const CartTotals: React.FC<CartTotalsProps> = ({ totals }) => {
discount_total,
gift_card_total,
shipping_subtotal,
} = totals
} = totals;
return (
<div>
<div className="flex flex-col gap-y-2 txt-medium text-ui-fg-subtle ">
<div className="txt-medium text-ui-fg-subtle flex flex-col gap-y-2">
<div className="flex items-center justify-between">
<span className="flex gap-x-1 items-center">
<span className="flex items-center gap-x-1">
Subtotal (excl. shipping and taxes)
</span>
<span data-testid="cart-subtotal" data-value={subtotal || 0}>
@@ -46,7 +47,7 @@ const CartTotals: React.FC<CartTotalsProps> = ({ totals }) => {
data-testid="cart-discount"
data-value={discount_total || 0}
>
-{" "}
-{' '}
{convertToLocale({ amount: discount_total ?? 0, currency_code })}
</span>
</div>
@@ -58,7 +59,7 @@ const CartTotals: React.FC<CartTotalsProps> = ({ totals }) => {
</span>
</div>
<div className="flex justify-between">
<span className="flex gap-x-1 items-center ">Taxes</span>
<span className="flex items-center gap-x-1">Taxes</span>
<span data-testid="cart-taxes" data-value={tax_total || 0}>
{convertToLocale({ amount: tax_total ?? 0, currency_code })}
</span>
@@ -71,14 +72,14 @@ const CartTotals: React.FC<CartTotalsProps> = ({ totals }) => {
data-testid="cart-gift-card-amount"
data-value={gift_card_total || 0}
>
-{" "}
-{' '}
{convertToLocale({ amount: gift_card_total ?? 0, currency_code })}
</span>
</div>
)}
</div>
<div className="h-px w-full border-b border-gray-200 my-4" />
<div className="flex items-center justify-between text-ui-fg-base mb-2 txt-medium ">
<div className="my-4 h-px w-full border-b border-gray-200" />
<div className="text-ui-fg-base txt-medium mb-2 flex items-center justify-between">
<span>Total</span>
<span
className="txt-xlarge-plus"
@@ -88,9 +89,9 @@ const CartTotals: React.FC<CartTotalsProps> = ({ totals }) => {
{convertToLocale({ amount: total ?? 0, currency_code })}
</span>
</div>
<div className="h-px w-full border-b border-gray-200 mt-4" />
<div className="mt-4 h-px w-full border-b border-gray-200" />
</div>
)
}
);
};
export default CartTotals
export default CartTotals;

View File

@@ -1,23 +1,24 @@
import { Checkbox, Label } from "@medusajs/ui"
import React from "react"
import React from 'react';
import { Checkbox, Label } from '@medusajs/ui';
type CheckboxProps = {
checked?: boolean
onChange?: () => void
label: string
name?: string
'data-testid'?: string
}
checked?: boolean;
onChange?: () => void;
label: string;
name?: string;
'data-testid'?: string;
};
const CheckboxWithLabel: React.FC<CheckboxProps> = ({
checked = true,
onChange,
label,
name,
'data-testid': dataTestId
'data-testid': dataTestId,
}) => {
return (
<div className="flex items-center space-x-2 ">
<div className="flex items-center space-x-2">
<Checkbox
className="text-base-regular flex items-center gap-x-2"
id="checkbox"
@@ -31,13 +32,13 @@ const CheckboxWithLabel: React.FC<CheckboxProps> = ({
/>
<Label
htmlFor="checkbox"
className="!transform-none !txt-medium"
className="!txt-medium !transform-none"
size="large"
>
{label}
</Label>
</div>
)
}
);
};
export default CheckboxWithLabel
export default CheckboxWithLabel;

View File

@@ -1,10 +1,12 @@
"use client";
'use client';
import { deleteLineItem } from "@lib/data/cart";
import { Spinner, Trash } from "@medusajs/icons";
import { clx } from "@medusajs/ui";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { deleteLineItem } from '@lib/data/cart';
import { Spinner, Trash } from '@medusajs/icons';
import { clx } from '@medusajs/ui';
const DeleteButton = ({
id,
@@ -36,15 +38,19 @@ const DeleteButton = ({
return (
<div
className={clx(
"flex items-center justify-between text-small-regular",
className
'text-small-regular flex items-center justify-between',
className,
)}
>
<button
className="flex gap-x-1 text-ui-fg-subtle hover:text-ui-fg-base cursor-pointer"
className="text-ui-fg-subtle hover:text-ui-fg-base flex cursor-pointer gap-x-1"
onClick={() => handleDelete(id)}
>
{isDeleting ? <Spinner className="animate-spin" /> : (Icon ?? <Trash />)}
{isDeleting ? (
<Spinner className="animate-spin" />
) : (
(Icon ?? <Trash />)
)}
<span>{children}</span>
</button>
</div>

View File

@@ -1,9 +1,9 @@
import { clx } from "@medusajs/ui"
import { clx } from '@medusajs/ui';
const Divider = ({ className }: { className?: string }) => (
<div
className={clx("h-px w-full border-b border-gray-200 mt-1", className)}
className={clx('mt-1 h-px w-full border-b border-gray-200', className)}
/>
)
);
export default Divider
export default Divider;

View File

@@ -1,49 +1,49 @@
import { EllipseMiniSolid } from "@medusajs/icons"
import { Label, RadioGroup, Text, clx } from "@medusajs/ui"
import { EllipseMiniSolid } from '@medusajs/icons';
import { Label, RadioGroup, Text, clx } from '@medusajs/ui';
type FilterRadioGroupProps = {
title: string
title: string;
items: {
value: string
label: string
}[]
value: any
handleChange: (...args: any[]) => void
"data-testid"?: string
}
value: string;
label: string;
}[];
value: any;
handleChange: (...args: any[]) => void;
'data-testid'?: string;
};
const FilterRadioGroup = ({
title,
items,
value,
handleChange,
"data-testid": dataTestId,
'data-testid': dataTestId,
}: FilterRadioGroupProps) => {
return (
<div className="flex gap-x-3 flex-col gap-y-3">
<div className="flex flex-col gap-x-3 gap-y-3">
<Text className="txt-compact-small-plus text-ui-fg-muted">{title}</Text>
<RadioGroup data-testid={dataTestId} onValueChange={handleChange}>
{items?.map((i) => (
<div
key={i.value}
className={clx("flex gap-x-2 items-center", {
"ml-[-23px]": i.value === value,
className={clx('flex items-center gap-x-2', {
'ml-[-23px]': i.value === value,
})}
>
{i.value === value && <EllipseMiniSolid />}
<RadioGroup.Item
checked={i.value === value}
className="hidden peer"
className="peer hidden"
id={i.value}
value={i.value}
/>
<Label
htmlFor={i.value}
className={clx(
"!txt-compact-small !transform-none text-ui-fg-subtle hover:cursor-pointer",
'!txt-compact-small text-ui-fg-subtle !transform-none hover:cursor-pointer',
{
"text-ui-fg-base": i.value === value,
}
'text-ui-fg-base': i.value === value,
},
)}
data-testid="radio-label"
data-active={i.value === value}
@@ -54,7 +54,7 @@ const FilterRadioGroup = ({
))}
</RadioGroup>
</div>
)
}
);
};
export default FilterRadioGroup
export default FilterRadioGroup;

View File

@@ -1,55 +1,55 @@
// cart-totals
export { default as CartTotals } from "./cart-totals";
export * from "./cart-totals";
export { default as CartTotals } from './cart-totals';
export * from './cart-totals';
// checkbox
export { default as Checkbox } from "./checkbox";
export * from "./checkbox";
export { default as Checkbox } from './checkbox';
export * from './checkbox';
// delete-button
export { default as DeleteButton } from "./delete-button";
export * from "./delete-button";
export { default as DeleteButton } from './delete-button';
export * from './delete-button';
// divider
export { default as Divider } from "./divider";
export * from "./divider";
export { default as Divider } from './divider';
export * from './divider';
// filter-radio-group
export { default as FilterRadioGroup } from "./filter-radio-group";
export * from "./filter-radio-group";
export { default as FilterRadioGroup } from './filter-radio-group';
export * from './filter-radio-group';
// input
export { default as Input } from "./input";
export * from "./input";
export { default as Input } from './input';
export * from './input';
// interactive-link
export { default as InteractiveLink } from "./interactive-link";
export * from "./interactive-link";
export { default as InteractiveLink } from './interactive-link';
export * from './interactive-link';
// line-item-options
export { default as LineItemOptions } from "./line-item-options";
export * from "./line-item-options";
export { default as LineItemOptions } from './line-item-options';
export * from './line-item-options';
// line-item-price
export { default as LineItemPrice } from "./line-item-price";
export * from "./line-item-price";
export { default as LineItemPrice } from './line-item-price';
export * from './line-item-price';
// line-item-unit-price
export { default as LineItemUnitPrice } from "./line-item-unit-price";
export * from "./line-item-unit-price";
export { default as LineItemUnitPrice } from './line-item-unit-price';
export * from './line-item-unit-price';
// localized-client-link
export { default as LocalizedClientLink } from "./localized-client-link";
export * from "./localized-client-link";
export { default as LocalizedClientLink } from './localized-client-link';
export * from './localized-client-link';
// modal
export { default as Modal } from "./modal";
export * from "./modal";
export { default as Modal } from './modal';
export * from './modal';
// native-select
export { default as NativeSelect } from "./native-select";
export * from "./native-select";
export { default as NativeSelect } from './native-select';
export * from './native-select';
// radio
export { default as Radio } from "./radio";
export * from "./radio";
export { default as Radio } from './radio';
export * from './radio';

View File

@@ -1,14 +1,14 @@
"use client";
'use client';
import { Label } from "@medusajs/ui";
import React, { useEffect, useImperativeHandle, useState } from "react";
import React, { useEffect, useImperativeHandle, useState } from 'react';
import Eye from "@modules/common/icons/eye";
import EyeOff from "@modules/common/icons/eye-off";
import { Label } from '@medusajs/ui';
import Eye from '@modules/common/icons/eye';
import EyeOff from '@modules/common/icons/eye-off';
type InputProps = Omit<
Omit<React.InputHTMLAttributes<HTMLInputElement>, "size">,
"placeholder"
Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'>,
'placeholder'
> & {
label: string;
errors?: Record<string, unknown>;
@@ -24,45 +24,45 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
const [inputType, setInputType] = useState(type);
useEffect(() => {
if (type === "password" && showPassword) {
setInputType("text");
if (type === 'password' && showPassword) {
setInputType('text');
}
if (type === "password" && !showPassword) {
setInputType("password");
if (type === 'password' && !showPassword) {
setInputType('password');
}
}, [type, showPassword]);
useImperativeHandle(ref, () => inputRef.current!);
return (
<div className="flex flex-col w-full">
<div className="flex w-full flex-col">
{topLabel && (
<Label className="mb-2 txt-compact-medium-plus">{topLabel}</Label>
<Label className="txt-compact-medium-plus mb-2">{topLabel}</Label>
)}
<div className="flex relative z-0 w-full txt-compact-medium">
<div className="txt-compact-medium relative z-0 flex w-full">
<input
type={inputType}
name={name}
placeholder=" "
required={required}
className="pt-4 pb-1 block w-full h-11 px-4 mt-0 bg-ui-bg-field border rounded-md appearance-none focus:outline-none focus:ring-0 focus:shadow-borders-interactive-with-active border-ui-border-base hover:bg-ui-bg-field-hover"
className="bg-ui-bg-field focus:shadow-borders-interactive-with-active border-ui-border-base hover:bg-ui-bg-field-hover mt-0 block h-11 w-full appearance-none rounded-md border px-4 pt-4 pb-1 focus:ring-0 focus:outline-none"
{...props}
ref={inputRef}
/>
<label
htmlFor={name}
onClick={() => inputRef.current?.focus()}
className="flex items-center justify-center mx-3 px-1 transition-all absolute duration-300 top-3 -z-1 origin-0 text-ui-fg-subtle"
className="origin-0 text-ui-fg-subtle absolute top-3 -z-1 mx-3 flex items-center justify-center px-1 transition-all duration-300"
>
{label}
{required && <span className="text-rose-500">*</span>}
</label>
{type === "password" && (
{type === 'password' && (
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="text-ui-fg-subtle px-4 focus:outline-none transition-all duration-150 outline-none focus:text-ui-fg-base absolute right-0 top-3"
className="text-ui-fg-subtle focus:text-ui-fg-base absolute top-3 right-0 px-4 transition-all duration-150 outline-none focus:outline-none"
>
{showPassword ? <Eye /> : <EyeOff />}
</button>
@@ -70,9 +70,9 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
</div>
</div>
);
}
},
);
Input.displayName = "Input";
Input.displayName = 'Input';
export default Input;

View File

@@ -1,12 +1,13 @@
import { ArrowUpRightMini } from "@medusajs/icons"
import { Text } from "@medusajs/ui"
import LocalizedClientLink from "../localized-client-link"
import { ArrowUpRightMini } from '@medusajs/icons';
import { Text } from '@medusajs/ui';
import LocalizedClientLink from '../localized-client-link';
type InteractiveLinkProps = {
href: string
children?: React.ReactNode
onClick?: () => void
}
href: string;
children?: React.ReactNode;
onClick?: () => void;
};
const InteractiveLink = ({
href,
@@ -16,18 +17,18 @@ const InteractiveLink = ({
}: InteractiveLinkProps) => {
return (
<LocalizedClientLink
className="flex gap-x-1 items-center group"
className="group flex items-center gap-x-1"
href={href}
onClick={onClick}
{...props}
>
<Text className="text-ui-fg-interactive">{children}</Text>
<ArrowUpRightMini
className="group-hover:rotate-45 ease-in-out duration-150"
className="duration-150 ease-in-out group-hover:rotate-45"
color="var(--fg-interactive)"
/>
</LocalizedClientLink>
)
}
);
};
export default InteractiveLink
export default InteractiveLink;

View File

@@ -1,26 +1,26 @@
import { HttpTypes } from "@medusajs/types"
import { Text } from "@medusajs/ui"
import { HttpTypes } from '@medusajs/types';
import { Text } from '@medusajs/ui';
type LineItemOptionsProps = {
variant: HttpTypes.StoreProductVariant | undefined
"data-testid"?: string
"data-value"?: HttpTypes.StoreProductVariant
}
variant: HttpTypes.StoreProductVariant | undefined;
'data-testid'?: string;
'data-value'?: HttpTypes.StoreProductVariant;
};
const LineItemOptions = ({
variant,
"data-testid": dataTestid,
"data-value": dataValue,
'data-testid': dataTestid,
'data-value': dataValue,
}: LineItemOptionsProps) => {
return (
<Text
data-testid={dataTestid}
data-value={dataValue}
className="inline-block txt-medium text-ui-fg-subtle w-full overflow-hidden text-ellipsis"
className="txt-medium text-ui-fg-subtle inline-block w-full overflow-hidden text-ellipsis"
>
Variant: {variant?.title}
</Text>
)
}
);
};
export default LineItemOptions
export default LineItemOptions;

View File

@@ -1,35 +1,35 @@
import { getPercentageDiff } from "@lib/util/get-precentage-diff"
import { convertToLocale } from "@lib/util/money"
import { HttpTypes } from "@medusajs/types"
import { clx } from "@medusajs/ui"
import { getPercentageDiff } from '@lib/util/get-precentage-diff';
import { convertToLocale } from '@lib/util/money';
import { HttpTypes } from '@medusajs/types';
import { clx } from '@medusajs/ui';
type LineItemPriceProps = {
item: HttpTypes.StoreCartLineItem | HttpTypes.StoreOrderLineItem
style?: "default" | "tight"
currencyCode: string
}
item: HttpTypes.StoreCartLineItem | HttpTypes.StoreOrderLineItem;
style?: 'default' | 'tight';
currencyCode: string;
};
const LineItemPrice = ({
item,
style = "default",
style = 'default',
currencyCode,
}: LineItemPriceProps) => {
const { total, original_total } = item
const originalPrice = original_total
const currentPrice = total
const hasReducedPrice = currentPrice < originalPrice
const { total, original_total } = item;
const originalPrice = original_total;
const currentPrice = total;
const hasReducedPrice = currentPrice < originalPrice;
return (
<div className="flex flex-col gap-x-2 text-ui-fg-subtle items-end">
<div className="text-ui-fg-subtle flex flex-col items-end gap-x-2">
<div className="text-left">
{hasReducedPrice && (
<>
<p>
{style === "default" && (
{style === 'default' && (
<span className="text-ui-fg-subtle">Original: </span>
)}
<span
className="line-through text-ui-fg-muted"
className="text-ui-fg-muted line-through"
data-testid="product-original-price"
>
{convertToLocale({
@@ -38,7 +38,7 @@ const LineItemPrice = ({
})}
</span>
</p>
{style === "default" && (
{style === 'default' && (
<span className="text-ui-fg-interactive">
-{getPercentageDiff(originalPrice, currentPrice || 0)}%
</span>
@@ -46,8 +46,8 @@ const LineItemPrice = ({
</>
)}
<span
className={clx("text-base-regular", {
"text-ui-fg-interactive": hasReducedPrice,
className={clx('text-base-regular', {
'text-ui-fg-interactive': hasReducedPrice,
})}
data-testid="product-price"
>
@@ -58,7 +58,7 @@ const LineItemPrice = ({
</span>
</div>
</div>
)
}
);
};
export default LineItemPrice
export default LineItemPrice;

View File

@@ -1,31 +1,31 @@
import { convertToLocale } from "@lib/util/money"
import { HttpTypes } from "@medusajs/types"
import { clx } from "@medusajs/ui"
import { convertToLocale } from '@lib/util/money';
import { HttpTypes } from '@medusajs/types';
import { clx } from '@medusajs/ui';
type LineItemUnitPriceProps = {
item: HttpTypes.StoreCartLineItem | HttpTypes.StoreOrderLineItem
style?: "default" | "tight"
currencyCode: string
}
item: HttpTypes.StoreCartLineItem | HttpTypes.StoreOrderLineItem;
style?: 'default' | 'tight';
currencyCode: string;
};
const LineItemUnitPrice = ({
item,
style = "default",
style = 'default',
currencyCode,
}: LineItemUnitPriceProps) => {
const { total, original_total } = item
const hasReducedPrice = total < original_total
const { total, original_total } = item;
const hasReducedPrice = total < original_total;
const percentage_diff = Math.round(
((original_total - total) / original_total) * 100
)
((original_total - total) / original_total) * 100,
);
return (
<div className="flex flex-col text-ui-fg-muted justify-center h-full">
<div className="text-ui-fg-muted flex h-full flex-col justify-center">
{hasReducedPrice && (
<>
<p>
{style === "default" && (
{style === 'default' && (
<span className="text-ui-fg-muted">Original: </span>
)}
<span
@@ -38,14 +38,14 @@ const LineItemUnitPrice = ({
})}
</span>
</p>
{style === "default" && (
{style === 'default' && (
<span className="text-ui-fg-interactive">-{percentage_diff}%</span>
)}
</>
)}
<span
className={clx("text-base-regular", {
"text-ui-fg-interactive": hasReducedPrice,
className={clx('text-base-regular', {
'text-ui-fg-interactive': hasReducedPrice,
})}
data-testid="product-unit-price"
>
@@ -55,7 +55,7 @@ const LineItemUnitPrice = ({
})}
</span>
</div>
)
}
);
};
export default LineItemUnitPrice
export default LineItemUnitPrice;

View File

@@ -1,8 +1,9 @@
"use client"
'use client';
import Link from "next/link"
import { useParams } from "next/navigation"
import React from "react"
import React from 'react';
import Link from 'next/link';
import { useParams } from 'next/navigation';
/**
* Use this component to create a Next.js `<Link />` that persists the current country code in the url,
@@ -13,20 +14,20 @@ const LocalizedClientLink = ({
href,
...props
}: {
children?: React.ReactNode
href: string
className?: string
onClick?: () => void
passHref?: true
[x: string]: any
children?: React.ReactNode;
href: string;
className?: string;
onClick?: () => void;
passHref?: true;
[x: string]: any;
}) => {
const { countryCode } = useParams()
const { countryCode } = useParams();
return (
<Link href={`/${countryCode}${href}`} {...props}>
{children}
</Link>
)
}
);
};
export default LocalizedClientLink
export default LocalizedClientLink;

View File

@@ -1,26 +1,26 @@
import { Dialog, Transition } from "@headlessui/react"
import { clx } from "@medusajs/ui"
import React, { Fragment } from "react"
import React, { Fragment } from 'react';
import { ModalProvider, useModal } from "@lib/context/modal-context"
import X from "@modules/common/icons/x"
import { Dialog, Transition } from '@headlessui/react';
import { ModalProvider, useModal } from '@lib/context/modal-context';
import { clx } from '@medusajs/ui';
import X from '@modules/common/icons/x';
type ModalProps = {
isOpen: boolean
close: () => void
size?: "small" | "medium" | "large"
search?: boolean
children: React.ReactNode
'data-testid'?: string
}
isOpen: boolean;
close: () => void;
size?: 'small' | 'medium' | 'large';
search?: boolean;
children: React.ReactNode;
'data-testid'?: string;
};
const Modal = ({
isOpen,
close,
size = "medium",
size = 'medium',
search = false,
children,
'data-testid': dataTestId
'data-testid': dataTestId,
}: ModalProps) => {
return (
<Transition appear show={isOpen} as={Fragment}>
@@ -34,17 +34,17 @@ const Modal = ({
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-opacity-75 backdrop-blur-md h-screen" />
<div className="bg-opacity-75 fixed inset-0 h-screen backdrop-blur-md" />
</Transition.Child>
<div className="fixed inset-0 overflow-y-hidden">
<div
className={clx(
"flex min-h-full h-full justify-center p-4 text-center",
'flex h-full min-h-full justify-center p-4 text-center',
{
"items-center": !search,
"items-start": search,
}
'items-center': !search,
'items-start': search,
},
)}
>
<Transition.Child
@@ -59,14 +59,14 @@ const Modal = ({
<Dialog.Panel
data-testid={dataTestId}
className={clx(
"flex flex-col justify-start w-full transform p-5 text-left align-middle transition-all max-h-[75vh] h-fit",
'flex h-fit max-h-[75vh] w-full transform flex-col justify-start p-5 text-left align-middle transition-all',
{
"max-w-md": size === "small",
"max-w-xl": size === "medium",
"max-w-3xl": size === "large",
"bg-transparent shadow-none": search,
"bg-white shadow-xl border rounded-rounded": !search,
}
'max-w-md': size === 'small',
'max-w-xl': size === 'medium',
'max-w-3xl': size === 'large',
'bg-transparent shadow-none': search,
'rounded-rounded border bg-white shadow-xl': !search,
},
)}
>
<ModalProvider close={close}>{children}</ModalProvider>
@@ -76,11 +76,11 @@ const Modal = ({
</div>
</Dialog>
</Transition>
)
}
);
};
const Title: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { close } = useModal()
const { close } = useModal();
return (
<Dialog.Title className="flex items-center justify-between">
@@ -91,28 +91,30 @@ const Title: React.FC<{ children: React.ReactNode }> = ({ children }) => {
</button>
</div>
</Dialog.Title>
)
}
);
};
const Description: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return (
<Dialog.Description className="flex text-small-regular text-ui-fg-base items-center justify-center pt-2 pb-4 h-full">
<Dialog.Description className="text-small-regular text-ui-fg-base flex h-full items-center justify-center pt-2 pb-4">
{children}
</Dialog.Description>
)
}
);
};
const Body: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return <div className="flex justify-center">{children}</div>
}
return <div className="flex justify-center">{children}</div>;
};
const Footer: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return <div className="flex items-center justify-end gap-x-4">{children}</div>
}
return (
<div className="flex items-center justify-end gap-x-4">{children}</div>
);
};
Modal.Title = Title
Modal.Description = Description
Modal.Body = Body
Modal.Footer = Footer
Modal.Title = Title;
Modal.Description = Description;
Modal.Body = Body;
Modal.Footer = Footer;
export default Modal
export default Modal;

View File

@@ -1,7 +1,5 @@
"use client";
'use client';
import { ChevronUpDown } from "@medusajs/icons";
import { clx } from "@medusajs/ui";
import {
SelectHTMLAttributes,
forwardRef,
@@ -9,7 +7,10 @@ import {
useImperativeHandle,
useRef,
useState,
} from "react";
} from 'react';
import { ChevronUpDown } from '@medusajs/icons';
import { clx } from '@medusajs/ui';
export type NativeSelectProps = {
placeholder?: string;
@@ -19,19 +20,19 @@ export type NativeSelectProps = {
const NativeSelect = forwardRef<HTMLSelectElement, NativeSelectProps>(
(
{ placeholder = "Select...", defaultValue, className, children, ...props },
ref
{ placeholder = 'Select...', defaultValue, className, children, ...props },
ref,
) => {
const innerRef = useRef<HTMLSelectElement>(null);
const [isPlaceholder, setIsPlaceholder] = useState(false);
useImperativeHandle<HTMLSelectElement | null, HTMLSelectElement | null>(
ref,
() => innerRef.current
() => innerRef.current,
);
useEffect(() => {
if (innerRef.current && innerRef.current.value === "") {
if (innerRef.current && innerRef.current.value === '') {
setIsPlaceholder(true);
} else {
setIsPlaceholder(false);
@@ -44,33 +45,33 @@ const NativeSelect = forwardRef<HTMLSelectElement, NativeSelectProps>(
onFocus={() => innerRef.current?.focus()}
onBlur={() => innerRef.current?.blur()}
className={clx(
"relative flex items-center text-base-regular border border-ui-border-base bg-ui-bg-subtle rounded-md hover:bg-ui-bg-field-hover",
'text-base-regular border-ui-border-base bg-ui-bg-subtle hover:bg-ui-bg-field-hover relative flex items-center rounded-md border',
className,
{
"text-ui-fg-muted": isPlaceholder,
}
'text-ui-fg-muted': isPlaceholder,
},
)}
>
<select
ref={innerRef}
defaultValue={defaultValue}
{...props}
className="appearance-none flex-1 bg-transparent border-none px-4 py-2.5 transition-colors duration-150 outline-none "
className="flex-1 appearance-none border-none bg-transparent px-4 py-2.5 transition-colors duration-150 outline-none"
>
<option disabled value="">
{placeholder}
</option>
{children}
</select>
<span className="absolute right-4 inset-y-0 flex items-center pointer-events-none ">
<span className="pointer-events-none absolute inset-y-0 right-4 flex items-center">
<ChevronUpDown />
</span>
</div>
</div>
);
}
},
);
NativeSelect.displayName = "NativeSelect";
NativeSelect.displayName = 'NativeSelect';
export default NativeSelect;

View File

@@ -1,27 +1,33 @@
const Radio = ({ checked, 'data-testid': dataTestId }: { checked: boolean, 'data-testid'?: string }) => {
const Radio = ({
checked,
'data-testid': dataTestId,
}: {
checked: boolean;
'data-testid'?: string;
}) => {
return (
<>
<button
type="button"
role="radio"
aria-checked="true"
data-state={checked ? "checked" : "unchecked"}
data-state={checked ? 'checked' : 'unchecked'}
className="group relative flex h-5 w-5 items-center justify-center outline-none"
data-testid={dataTestId || 'radio-button'}
>
<div className="shadow-borders-base group-hover:shadow-borders-strong-with-shadow bg-ui-bg-base group-data-[state=checked]:bg-ui-bg-interactive group-data-[state=checked]:shadow-borders-interactive group-focus:!shadow-borders-interactive-with-focus group-disabled:!bg-ui-bg-disabled group-disabled:!shadow-borders-base flex h-[14px] w-[14px] items-center justify-center rounded-full transition-all">
{checked && (
<span
data-state={checked ? "checked" : "unchecked"}
data-state={checked ? 'checked' : 'unchecked'}
className="group flex items-center justify-center"
>
<div className="bg-ui-bg-base shadow-details-contrast-on-bg-interactive group-disabled:bg-ui-fg-disabled rounded-full group-disabled:shadow-none h-1.5 w-1.5"></div>
<div className="bg-ui-bg-base shadow-details-contrast-on-bg-interactive group-disabled:bg-ui-fg-disabled h-1.5 w-1.5 rounded-full group-disabled:shadow-none"></div>
</span>
)}
</div>
</button>
</>
)
}
);
};
export default Radio
export default Radio;

View File

@@ -1,10 +1,10 @@
import React from "react"
import React from 'react';
import { IconProps } from "types/icon"
import { IconProps } from 'types/icon';
const Back: React.FC<IconProps> = ({
size = "16",
color = "currentColor",
size = '16',
color = 'currentColor',
...attributes
}) => {
return (
@@ -31,7 +31,7 @@ const Back: React.FC<IconProps> = ({
strokeLinejoin="round"
/>
</svg>
)
}
);
};
export default Back
export default Back;

View File

@@ -1,10 +1,10 @@
import React from "react"
import React from 'react';
import { IconProps } from "types/icon"
import { IconProps } from 'types/icon';
const Ideal: React.FC<IconProps> = ({
size = "20",
color = "currentColor",
size = '20',
color = 'currentColor',
...attributes
}) => {
return (
@@ -20,7 +20,7 @@ const Ideal: React.FC<IconProps> = ({
<title>Bancontact icon</title>
<path d="M21.385 9.768h-7.074l-4.293 5.022H1.557L3.84 12.1H1.122C.505 12.1 0 12.616 0 13.25v2.428c0 .633.505 1.15 1.122 1.15h12.933c.617 0 1.46-.384 1.874-.854l1.956-2.225 3.469-3.946.031-.035zm-1.123 1.279l-.751.855.75-.855zm2.616-3.875H9.982c-.617 0-1.462.384-1.876.853l-5.49 6.208h7.047l4.368-5.02h8.424l-2.263 2.689h2.686c.617 0 1.122-.518 1.122-1.151V8.323c0-.633-.505-1.15-1.122-1.15zm-1.87 3.024l-.374.427-.1.114.474-.54z" />
</svg>
)
}
);
};
export default Ideal
export default Ideal;

View File

@@ -1,9 +1,10 @@
import React from "react";
import { IconProps } from "../../../types/icon";
import React from 'react';
import { IconProps } from '../../../types/icon';
const ChevronDown: React.FC<IconProps> = ({
size = "16",
color = "currentColor",
size = '16',
color = 'currentColor',
...attributes
}) => {
return (

View File

@@ -1,10 +1,10 @@
import React from "react"
import React from 'react';
import { IconProps } from "types/icon"
import { IconProps } from 'types/icon';
const EyeOff: React.FC<IconProps> = ({
size = "20",
color = "currentColor",
size = '20',
color = 'currentColor',
...attributes
}) => {
return (
@@ -31,7 +31,7 @@ const EyeOff: React.FC<IconProps> = ({
strokeLinejoin="round"
/>
</svg>
)
}
);
};
export default EyeOff
export default EyeOff;

View File

@@ -1,10 +1,10 @@
import React from "react"
import React from 'react';
import { IconProps } from "types/icon"
import { IconProps } from 'types/icon';
const Eye: React.FC<IconProps> = ({
size = "20",
color = "currentColor",
size = '20',
color = 'currentColor',
...attributes
}) => {
return (
@@ -31,7 +31,7 @@ const Eye: React.FC<IconProps> = ({
strokeLinejoin="round"
/>
</svg>
)
}
);
};
export default Eye
export default Eye;

View File

@@ -1,10 +1,10 @@
import React from "react"
import React from 'react';
import { IconProps } from "types/icon"
import { IconProps } from 'types/icon';
const FastDelivery: React.FC<IconProps> = ({
size = "16",
color = "currentColor",
size = '16',
color = 'currentColor',
...attributes
}) => {
return (
@@ -59,7 +59,7 @@ const FastDelivery: React.FC<IconProps> = ({
strokeLinejoin="round"
/>
</svg>
)
}
);
};
export default FastDelivery
export default FastDelivery;

View File

@@ -1,10 +1,10 @@
import React from "react"
import React from 'react';
import { IconProps } from "types/icon"
import { IconProps } from 'types/icon';
const Ideal: React.FC<IconProps> = ({
size = "20",
color = "currentColor",
size = '20',
color = 'currentColor',
...attributes
}) => {
return (
@@ -20,7 +20,7 @@ const Ideal: React.FC<IconProps> = ({
<title>iDEAL icon</title>
<path d="M.975 2.61v18.782h11.411c6.89 0 10.64-3.21 10.64-9.415 0-6.377-4.064-9.367-10.64-9.367H.975zm11.411-.975C22.491 1.635 24 8.115 24 11.977c0 6.7-4.124 10.39-11.614 10.39H0V1.635h12.386z M2.506 13.357h3.653v6.503H2.506z M6.602 10.082a2.27 2.27 0 1 1-4.54 0 2.27 2.27 0 0 1 4.54 0m1.396-1.057v2.12h.65c.45 0 .867-.13.867-1.077 0-.924-.463-1.043-.867-1.043h-.65zm10.85-1.054h1.053v3.174h1.56c-.428-5.758-4.958-7.002-9.074-7.002H7.999v3.83h.65c1.183 0 1.92.803 1.92 2.095 0 1.333-.719 2.129-1.92 2.129h-.65v7.665h4.388c6.692 0 9.021-3.107 9.103-7.665h-2.64V7.97zm-2.93 2.358h.76l-.348-1.195h-.063l-.35 1.195zm-1.643 1.87l1.274-4.228h1.497l1.274 4.227h-1.095l-.239-.818H15.61l-.24.818h-1.095zm-.505-1.054v1.052h-2.603V7.973h2.519v1.052h-1.467v.49h1.387v1.05H12.22v.58h1.55z" />
</svg>
)
}
);
};
export default Ideal
export default Ideal;

View File

@@ -1,71 +1,71 @@
// back
export { default as BackIcon } from "./back";
export * from "./back";
export { default as BackIcon } from './back';
export * from './back';
// bancontact
export { default as BancontactIcon } from "./bancontact";
export * from "./bancontact";
export { default as BancontactIcon } from './bancontact';
export * from './bancontact';
// chevron-down
export { default as ChevronDownIcon } from "./chevron-down";
export * from "./chevron-down";
export { default as ChevronDownIcon } from './chevron-down';
export * from './chevron-down';
// eye-off
export { default as EyeOffIcon } from "./eye-off";
export * from "./eye-off";
export { default as EyeOffIcon } from './eye-off';
export * from './eye-off';
// eye
export { default as EyeIcon } from "./eye";
export * from "./eye";
export { default as EyeIcon } from './eye';
export * from './eye';
// fast-delivery
export { default as FastDeliveryIcon } from "./fast-delivery";
export * from "./fast-delivery";
export { default as FastDeliveryIcon } from './fast-delivery';
export * from './fast-delivery';
// ideal
export { default as IdealIcon } from "./ideal";
export * from "./ideal";
export { default as IdealIcon } from './ideal';
export * from './ideal';
// map-pin
export { default as MapPinIcon } from "./map-pin";
export * from "./map-pin";
export { default as MapPinIcon } from './map-pin';
export * from './map-pin';
// medusa
export { default as MedusaIcon } from "./medusa";
export * from "./medusa";
export { default as MedusaIcon } from './medusa';
export * from './medusa';
// nextjs
export { default as NextjsIcon } from "./nextjs";
export * from "./nextjs";
export { default as NextjsIcon } from './nextjs';
export * from './nextjs';
// package
export { default as PackageIcon } from "./package";
export * from "./package";
export { default as PackageIcon } from './package';
export * from './package';
// paypal
export { default as PaypalIcon } from "./paypal";
export * from "./paypal";
export { default as PaypalIcon } from './paypal';
export * from './paypal';
// placeholder-image
export { default as PlaceholderImageIcon } from "./placeholder-image";
export * from "./placeholder-image";
export { default as PlaceholderImageIcon } from './placeholder-image';
export * from './placeholder-image';
// refresh
export { default as RefreshIcon } from "./refresh";
export * from "./refresh";
export { default as RefreshIcon } from './refresh';
export * from './refresh';
// spinner
export { default as SpinnerIcon } from "./spinner";
export * from "./spinner";
export { default as SpinnerIcon } from './spinner';
export * from './spinner';
// trash
export { default as TrashIcon } from "./trash";
export * from "./trash";
export { default as TrashIcon } from './trash';
export * from './trash';
// user
export { default as UserIcon } from "./user";
export * from "./user";
export { default as UserIcon } from './user';
export * from './user';
// x
export { default as XIcon } from "./x";
export * from "./x";
export { default as XIcon } from './x';
export * from './x';

Some files were not shown because too many files have changed in this diff Show More