Merge pull request #31 from MR-medreport/B2B-26
B2B-26: move selfservice tables to medreport schema and add medusa store
This commit is contained in:
30
app/store/[countryCode]/(checkout)/checkout/page.tsx
Normal file
30
app/store/[countryCode]/(checkout)/checkout/page.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { retrieveCart } from "@lib/data/cart"
|
||||
import { retrieveCustomer } from "@lib/data/customer"
|
||||
import PaymentWrapper from "@modules/checkout/components/payment-wrapper"
|
||||
import CheckoutForm from "@modules/checkout/templates/checkout-form"
|
||||
import CheckoutSummary from "@modules/checkout/templates/checkout-summary"
|
||||
import { Metadata } from "next"
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Checkout",
|
||||
}
|
||||
|
||||
export default async function Checkout() {
|
||||
const cart = await retrieveCart()
|
||||
|
||||
if (!cart) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
const customer = await retrieveCustomer()
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 small:grid-cols-[1fr_416px] content-container gap-x-40 py-12">
|
||||
<PaymentWrapper cart={cart}>
|
||||
<CheckoutForm cart={cart} customer={customer} />
|
||||
</PaymentWrapper>
|
||||
<CheckoutSummary cart={cart} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
45
app/store/[countryCode]/(checkout)/layout.tsx
Normal file
45
app/store/[countryCode]/(checkout)/layout.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { LocalizedClientLink } from '~/medusa/modules/common/components';
|
||||
import { ChevronDownIcon } from '~/medusa/modules/common/icons';
|
||||
import { MedusaCTA } from '~/medusa/modules/layout/components';
|
||||
|
||||
export default function CheckoutLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="small:min-h-screen relative w-full bg-white">
|
||||
<div className="h-16 border-b bg-white">
|
||||
<nav className="content-container flex h-full items-center justify-between">
|
||||
<LocalizedClientLink
|
||||
href="/cart"
|
||||
className="text-small-semi text-ui-fg-base flex flex-1 basis-0 items-center gap-x-2 uppercase"
|
||||
data-testid="back-to-cart-link"
|
||||
>
|
||||
<ChevronDownIcon className="rotate-90" size={16} />
|
||||
<span className="small:block txt-compact-plus text-ui-fg-subtle hover:text-ui-fg-base mt-px hidden">
|
||||
Back to shopping cart
|
||||
</span>
|
||||
<span className="small:hidden txt-compact-plus text-ui-fg-subtle hover:text-ui-fg-base mt-px block">
|
||||
Back
|
||||
</span>
|
||||
</LocalizedClientLink>
|
||||
<LocalizedClientLink
|
||||
href="/"
|
||||
className="txt-compact-xlarge-plus text-ui-fg-subtle hover:text-ui-fg-base uppercase"
|
||||
data-testid="store-link"
|
||||
>
|
||||
Medusa Store
|
||||
</LocalizedClientLink>
|
||||
<div className="flex-1 basis-0" />
|
||||
</nav>
|
||||
</div>
|
||||
<div className="relative" data-testid="checkout-container">
|
||||
{children}
|
||||
</div>
|
||||
<div className="flex w-full items-center justify-center py-4">
|
||||
<MedusaCTA />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
20
app/store/[countryCode]/(checkout)/not-found.tsx
Normal file
20
app/store/[countryCode]/(checkout)/not-found.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Metadata } from 'next';
|
||||
|
||||
import InteractiveLink from '~/medusa/modules/common/components/interactive-link';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: '404',
|
||||
description: 'Something went wrong',
|
||||
};
|
||||
|
||||
export default async function NotFound() {
|
||||
return (
|
||||
<div className="flex min-h-[calc(100vh-64px)] flex-col items-center justify-center gap-4">
|
||||
<h1 className="text-2xl-semi text-ui-fg-base">Page not found</h1>
|
||||
<p className="text-small-regular text-ui-fg-base">
|
||||
The page you tried to access does not exist.
|
||||
</p>
|
||||
<InteractiveLink href="/">Go to frontpage</InteractiveLink>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Metadata } from 'next';
|
||||
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { retrieveCustomer } from '~/medusa/lib/data/customer';
|
||||
import { getRegion } from '~/medusa/lib/data/regions';
|
||||
import AddressBook from '~/medusa/modules/account/components/address-book';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Addresses',
|
||||
description: 'View your addresses',
|
||||
};
|
||||
|
||||
export default async function Addresses(props: {
|
||||
params: Promise<{ countryCode: string }>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const { countryCode } = params;
|
||||
const customer = await retrieveCustomer();
|
||||
const region = await getRegion(countryCode);
|
||||
|
||||
if (!customer || !region) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full" data-testid="addresses-page-wrapper">
|
||||
<div className="mb-8 flex flex-col gap-y-4">
|
||||
<h1 className="text-2xl-semi">Shipping Addresses</h1>
|
||||
<p className="text-base-regular">
|
||||
View and update your shipping addresses, you can add as many as you
|
||||
like. Saving your addresses will make them available during checkout.
|
||||
</p>
|
||||
</div>
|
||||
<AddressBook customer={customer} region={region} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import Spinner from '~/medusa/modules/common/icons/spinner';
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="text-ui-fg-base flex h-full w-full items-center justify-center">
|
||||
<Spinner size={36} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Metadata } from 'next';
|
||||
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { retrieveOrder } from '~/medusa/lib/data/orders';
|
||||
import OrderDetailsTemplate from '~/medusa/modules/order/templates/order-details-template';
|
||||
|
||||
type Props = {
|
||||
params: Promise<{ id: string }>;
|
||||
};
|
||||
|
||||
export async function generateMetadata(props: Props): Promise<Metadata> {
|
||||
const params = await props.params;
|
||||
const order = await retrieveOrder(params.id).catch(() => null);
|
||||
|
||||
if (!order) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return {
|
||||
title: `Order #${order.display_id}`,
|
||||
description: `View your order`,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function OrderDetailPage(props: Props) {
|
||||
const params = await props.params;
|
||||
const order = await retrieveOrder(params.id).catch(() => null);
|
||||
|
||||
if (!order) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return <OrderDetailsTemplate order={order} />;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Metadata } from 'next';
|
||||
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { listOrders } from '~/medusa/lib/data/orders';
|
||||
import OrderOverview from '~/medusa/modules/account/components/order-overview';
|
||||
import TransferRequestForm from '~/medusa/modules/account/components/transfer-request-form';
|
||||
import Divider from '~/medusa/modules/common/components/divider';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Orders',
|
||||
description: 'Overview of your previous orders.',
|
||||
};
|
||||
|
||||
export default async function Orders() {
|
||||
const orders = await listOrders();
|
||||
|
||||
if (!orders) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full" data-testid="orders-page-wrapper">
|
||||
<div className="mb-8 flex flex-col gap-y-4">
|
||||
<h1 className="text-2xl-semi">Orders</h1>
|
||||
<p className="text-base-regular">
|
||||
View your previous orders and their status. You can also create
|
||||
returns or exchanges for your orders if needed.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<OrderOverview orders={orders} />
|
||||
<Divider className="my-16" />
|
||||
<TransferRequestForm />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
23
app/store/[countryCode]/(main)/account/@dashboard/page.tsx
Normal file
23
app/store/[countryCode]/(main)/account/@dashboard/page.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Metadata } from 'next';
|
||||
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { retrieveCustomer } from '~/medusa/lib/data/customer';
|
||||
import { listOrders } from '~/medusa/lib/data/orders';
|
||||
import Overview from '~/medusa/modules/account/components/overview';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Account',
|
||||
description: 'Overview of your account activity.',
|
||||
};
|
||||
|
||||
export default async function OverviewTemplate() {
|
||||
const customer = await retrieveCustomer().catch(() => null);
|
||||
const orders = (await listOrders().catch(() => null)) || null;
|
||||
|
||||
if (!customer) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return <Overview customer={customer} orders={orders} />;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Metadata } from 'next';
|
||||
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { retrieveCustomer } from '~/medusa/lib/data/customer';
|
||||
import { listRegions } from '~/medusa/lib/data/regions';
|
||||
import ProfilePhone from '~/medusa/modules/account//components/profile-phone';
|
||||
import ProfileBillingAddress from '~/medusa/modules/account/components/profile-billing-address';
|
||||
import ProfileEmail from '~/medusa/modules/account/components/profile-email';
|
||||
import ProfileName from '~/medusa/modules/account/components/profile-name';
|
||||
import ProfilePassword from '~/medusa/modules/account/components/profile-password';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Profile',
|
||||
description: 'View and edit your Medusa Store profile.',
|
||||
};
|
||||
|
||||
export default async function Profile() {
|
||||
const customer = await retrieveCustomer();
|
||||
const regions = await listRegions();
|
||||
|
||||
if (!customer || !regions) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full" data-testid="profile-page-wrapper">
|
||||
<div className="mb-8 flex flex-col gap-y-4">
|
||||
<h1 className="text-2xl-semi">Profile</h1>
|
||||
<p className="text-base-regular">
|
||||
View and update your profile information, including your name, email,
|
||||
and phone number. You can also update your billing address, or change
|
||||
your password.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex w-full flex-col gap-y-8">
|
||||
<ProfileName customer={customer} />
|
||||
<Divider />
|
||||
<ProfileEmail customer={customer} />
|
||||
<Divider />
|
||||
<ProfilePhone customer={customer} />
|
||||
<Divider />
|
||||
{/* <ProfilePassword customer={customer} />
|
||||
<Divider /> */}
|
||||
<ProfileBillingAddress customer={customer} regions={regions} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Divider = () => {
|
||||
return <div className="h-px w-full bg-gray-200" />;
|
||||
};
|
||||
``;
|
||||
12
app/store/[countryCode]/(main)/account/@login/page.tsx
Normal file
12
app/store/[countryCode]/(main)/account/@login/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Metadata } from 'next';
|
||||
|
||||
import LoginTemplate from '~/medusa/modules/account/templates/login-template';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Sign in',
|
||||
description: 'Sign in to your Medusa Store account.',
|
||||
};
|
||||
|
||||
export default function Login() {
|
||||
return <LoginTemplate />;
|
||||
}
|
||||
21
app/store/[countryCode]/(main)/account/layout.tsx
Normal file
21
app/store/[countryCode]/(main)/account/layout.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Toaster } from '@medusajs/ui';
|
||||
|
||||
import { retrieveCustomer } from '~/medusa/lib/data';
|
||||
import { AccountLayout } from '~/medusa/modules/account/templates';
|
||||
|
||||
export default async function AccountPageLayout({
|
||||
dashboard,
|
||||
login,
|
||||
}: {
|
||||
dashboard?: React.ReactNode;
|
||||
login?: React.ReactNode;
|
||||
}) {
|
||||
const customer = await retrieveCustomer().catch(() => null);
|
||||
|
||||
return (
|
||||
<AccountLayout customer={customer}>
|
||||
{customer ? dashboard : login}
|
||||
<Toaster />
|
||||
</AccountLayout>
|
||||
);
|
||||
}
|
||||
9
app/store/[countryCode]/(main)/account/loading.tsx
Normal file
9
app/store/[countryCode]/(main)/account/loading.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import Spinner from '~/medusa/modules/common/icons/spinner';
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="text-ui-fg-base flex h-full w-full items-center justify-center">
|
||||
<Spinner size={36} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
5
app/store/[countryCode]/(main)/cart/loading.tsx
Normal file
5
app/store/[countryCode]/(main)/cart/loading.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import SkeletonCartPage from '~/medusa/modules/skeletons/templates/skeleton-cart-page';
|
||||
|
||||
export default function Loading() {
|
||||
return <SkeletonCartPage />;
|
||||
}
|
||||
21
app/store/[countryCode]/(main)/cart/not-found.tsx
Normal file
21
app/store/[countryCode]/(main)/cart/not-found.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Metadata } from 'next';
|
||||
|
||||
import InteractiveLink from '~/medusa/modules/common/components/interactive-link';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: '404',
|
||||
description: 'Something went wrong',
|
||||
};
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div className="flex min-h-[calc(100vh-64px)] flex-col items-center justify-center">
|
||||
<h1 className="text-2xl-semi text-ui-fg-base">Page not found</h1>
|
||||
<p className="text-small-regular text-ui-fg-base">
|
||||
The cart you tried to access does not exist. Clear your cookies and try
|
||||
again.
|
||||
</p>
|
||||
<InteractiveLink href="/">Go to frontpage</InteractiveLink>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
23
app/store/[countryCode]/(main)/cart/page.tsx
Normal file
23
app/store/[countryCode]/(main)/cart/page.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Metadata } from 'next';
|
||||
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { retrieveCart } from '~/medusa/lib/data/cart';
|
||||
import { retrieveCustomer } from '~/medusa/lib/data/customer';
|
||||
import CartTemplate from '~/medusa/modules/cart/templates';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Cart',
|
||||
description: 'View your cart',
|
||||
};
|
||||
|
||||
export default async function Cart() {
|
||||
const cart = await retrieveCart().catch((error) => {
|
||||
console.error(error);
|
||||
return notFound();
|
||||
});
|
||||
|
||||
const customer = await retrieveCustomer();
|
||||
|
||||
return <CartTemplate cart={cart} customer={customer} />;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Metadata } from 'next';
|
||||
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { StoreRegion } from '@medusajs/types';
|
||||
|
||||
import {
|
||||
getCategoryByHandle,
|
||||
listCategories,
|
||||
} from '~/medusa/lib/data/categories';
|
||||
import { listRegions } from '~/medusa/lib/data/regions';
|
||||
import CategoryTemplate from '~/medusa/modules/categories/templates';
|
||||
import { SortOptions } from '~/medusa/modules/store/components/refinement-list/sort-products';
|
||||
|
||||
type Props = {
|
||||
params: Promise<{ category: string[]; countryCode: string }>;
|
||||
searchParams: Promise<{
|
||||
sortBy?: SortOptions;
|
||||
page?: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const product_categories = await listCategories();
|
||||
|
||||
if (!product_categories) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const countryCodes = await listRegions().then((regions: StoreRegion[]) =>
|
||||
regions?.map((r) => r.countries?.map((c) => c.iso_2)).flat(),
|
||||
);
|
||||
|
||||
const categoryHandles = product_categories.map(
|
||||
(category: any) => category.handle,
|
||||
);
|
||||
|
||||
const staticParams = countryCodes
|
||||
?.map((countryCode: string | undefined) =>
|
||||
categoryHandles.map((handle: any) => ({
|
||||
countryCode,
|
||||
category: [handle],
|
||||
})),
|
||||
)
|
||||
.flat();
|
||||
|
||||
return staticParams;
|
||||
}
|
||||
|
||||
export async function generateMetadata(props: Props): Promise<Metadata> {
|
||||
const params = await props.params;
|
||||
try {
|
||||
const productCategory = await getCategoryByHandle(params.category);
|
||||
|
||||
const title = productCategory.name + ' | Medusa Store';
|
||||
|
||||
const description = productCategory.description ?? `${title} category.`;
|
||||
|
||||
return {
|
||||
title: `${title} | Medusa Store`,
|
||||
description,
|
||||
alternates: {
|
||||
canonical: `${params.category.join('/')}`,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
notFound();
|
||||
}
|
||||
}
|
||||
|
||||
export default async function CategoryPage(props: Props) {
|
||||
const searchParams = await props.searchParams;
|
||||
const params = await props.params;
|
||||
const { sortBy, page } = searchParams;
|
||||
|
||||
const productCategory = await getCategoryByHandle(params.category);
|
||||
|
||||
if (!productCategory) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<CategoryTemplate
|
||||
category={productCategory}
|
||||
sortBy={sortBy}
|
||||
page={page}
|
||||
countryCode={params.countryCode}
|
||||
/>
|
||||
);
|
||||
}
|
||||
95
app/store/[countryCode]/(main)/collections/[handle]/page.tsx
Normal file
95
app/store/[countryCode]/(main)/collections/[handle]/page.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import { Metadata } from 'next';
|
||||
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { StoreCollection, StoreRegion } from '@medusajs/types';
|
||||
|
||||
import {
|
||||
getCollectionByHandle,
|
||||
listCollections,
|
||||
} from '~/medusa/lib/data/collections';
|
||||
import { listRegions } from '~/medusa/lib/data/regions';
|
||||
import CollectionTemplate from '~/medusa/modules/collections/templates';
|
||||
import { SortOptions } from '~/medusa/modules/store/components/refinement-list/sort-products';
|
||||
|
||||
type Props = {
|
||||
params: Promise<{ handle: string; countryCode: string }>;
|
||||
searchParams: Promise<{
|
||||
page?: string;
|
||||
sortBy?: SortOptions;
|
||||
}>;
|
||||
};
|
||||
|
||||
export const PRODUCT_LIMIT = 12;
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const { collections } = await listCollections({
|
||||
fields: '*products',
|
||||
});
|
||||
|
||||
if (!collections) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const countryCodes = await listRegions().then(
|
||||
(regions: StoreRegion[]) =>
|
||||
regions
|
||||
?.map((r) => r.countries?.map((c) => c.iso_2))
|
||||
.flat()
|
||||
.filter(Boolean) as string[],
|
||||
);
|
||||
|
||||
const collectionHandles = collections.map(
|
||||
(collection: StoreCollection) => collection.handle,
|
||||
);
|
||||
|
||||
const staticParams = countryCodes
|
||||
?.map((countryCode: string) =>
|
||||
collectionHandles.map((handle: string | undefined) => ({
|
||||
countryCode,
|
||||
handle,
|
||||
})),
|
||||
)
|
||||
.flat();
|
||||
|
||||
return staticParams;
|
||||
}
|
||||
|
||||
export async function generateMetadata(props: Props): Promise<Metadata> {
|
||||
const params = await props.params;
|
||||
const collection = await getCollectionByHandle(params.handle);
|
||||
|
||||
if (!collection) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const metadata = {
|
||||
title: `${collection.title} | Medusa Store`,
|
||||
description: `${collection.title} collection`,
|
||||
} as Metadata;
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
export default async function CollectionPage(props: Props) {
|
||||
const searchParams = await props.searchParams;
|
||||
const params = await props.params;
|
||||
const { sortBy, page } = searchParams;
|
||||
|
||||
const collection = await getCollectionByHandle(params.handle).then(
|
||||
(collection: StoreCollection) => collection,
|
||||
);
|
||||
|
||||
if (!collection) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<CollectionTemplate
|
||||
collection={collection}
|
||||
page={page}
|
||||
sortBy={sortBy}
|
||||
countryCode={params.countryCode}
|
||||
/>
|
||||
);
|
||||
}
|
||||
46
app/store/[countryCode]/(main)/layout.tsx
Normal file
46
app/store/[countryCode]/(main)/layout.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Metadata } from 'next';
|
||||
|
||||
import { StoreCartShippingOption } from '@medusajs/types';
|
||||
|
||||
import { listCartOptions, retrieveCart } from '~/medusa/lib/data/cart';
|
||||
import { retrieveCustomer } from '~/medusa/lib/data/customer';
|
||||
import { getBaseURL } from '~/medusa/lib/util/env';
|
||||
import CartMismatchBanner from '~/medusa/modules/layout/components/cart-mismatch-banner';
|
||||
import Footer from '~/medusa/modules/layout/templates/footer';
|
||||
import Nav from '~/medusa/modules/layout/templates/nav';
|
||||
import FreeShippingPriceNudge from '~/medusa/modules/shipping/components/free-shipping-price-nudge';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL(getBaseURL()),
|
||||
};
|
||||
|
||||
export default async function PageLayout(props: { children: React.ReactNode }) {
|
||||
const customer = await retrieveCustomer();
|
||||
const cart = await retrieveCart();
|
||||
let shippingOptions: StoreCartShippingOption[] = [];
|
||||
|
||||
if (cart) {
|
||||
const { shipping_options } = await listCartOptions();
|
||||
|
||||
shippingOptions = shipping_options;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Nav />
|
||||
{customer && cart && (
|
||||
<CartMismatchBanner customer={customer} cart={cart} />
|
||||
)}
|
||||
|
||||
{cart && (
|
||||
<FreeShippingPriceNudge
|
||||
variant="popup"
|
||||
cart={cart}
|
||||
shippingOptions={shippingOptions}
|
||||
/>
|
||||
)}
|
||||
{props.children}
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
20
app/store/[countryCode]/(main)/not-found.tsx
Normal file
20
app/store/[countryCode]/(main)/not-found.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Metadata } from 'next';
|
||||
|
||||
import InteractiveLink from '~/medusa/modules/common/components/interactive-link';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: '404',
|
||||
description: 'Something went wrong',
|
||||
};
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div className="flex min-h-[calc(100vh-64px)] flex-col items-center justify-center gap-4">
|
||||
<h1 className="text-2xl-semi text-ui-fg-base">Page not found</h1>
|
||||
<p className="text-small-regular text-ui-fg-base">
|
||||
The page you tried to access does not exist.
|
||||
</p>
|
||||
<InteractiveLink href="/">Go to frontpage</InteractiveLink>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import SkeletonOrderConfirmed from '~/medusa/modules/skeletons/templates/skeleton-order-confirmed';
|
||||
|
||||
export default function Loading() {
|
||||
return <SkeletonOrderConfirmed />;
|
||||
}
|
||||
25
app/store/[countryCode]/(main)/order/[id]/confirmed/page.tsx
Normal file
25
app/store/[countryCode]/(main)/order/[id]/confirmed/page.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Metadata } from 'next';
|
||||
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { retrieveOrder } from '~/medusa/lib/data/orders';
|
||||
import OrderCompletedTemplate from '~/medusa/modules/order/templates/order-completed-template';
|
||||
|
||||
type Props = {
|
||||
params: Promise<{ id: string }>;
|
||||
};
|
||||
export const metadata: Metadata = {
|
||||
title: 'Order Confirmed',
|
||||
description: 'You purchase was successful',
|
||||
};
|
||||
|
||||
export default async function OrderConfirmedPage(props: Props) {
|
||||
const params = await props.params;
|
||||
const order = await retrieveOrder(params.id).catch(() => null);
|
||||
|
||||
if (!order) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
return <OrderCompletedTemplate order={order} />;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Heading, Text } from '@medusajs/ui';
|
||||
|
||||
import { acceptTransferRequest } from '~/medusa/lib/data/orders';
|
||||
import TransferImage from '~/medusa/modules/order/components/transfer-image';
|
||||
|
||||
export default async function TransferPage({
|
||||
params,
|
||||
}: {
|
||||
params: { id: string; token: string };
|
||||
}) {
|
||||
const { id, token } = params;
|
||||
|
||||
const { success, error } = await acceptTransferRequest(id, token);
|
||||
|
||||
return (
|
||||
<div className="mx-auto mt-10 mb-20 flex w-2/5 flex-col items-start gap-y-4">
|
||||
<TransferImage />
|
||||
<div className="flex flex-col gap-y-6">
|
||||
{success && (
|
||||
<>
|
||||
<Heading level="h1" className="text-xl text-zinc-900">
|
||||
Order transfered!
|
||||
</Heading>
|
||||
<Text className="text-zinc-600">
|
||||
Order {id} has been successfully transfered to the new owner.
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
{!success && (
|
||||
<>
|
||||
<Text className="text-zinc-600">
|
||||
There was an error accepting the transfer. Please try again.
|
||||
</Text>
|
||||
{error && (
|
||||
<Text className="text-red-500">Error message: {error}</Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Heading, Text } from '@medusajs/ui';
|
||||
|
||||
import { declineTransferRequest } from '~/medusa/lib/data/orders';
|
||||
import TransferImage from '~/medusa/modules/order/components/transfer-image';
|
||||
|
||||
export default async function TransferPage({
|
||||
params,
|
||||
}: {
|
||||
params: { id: string; token: string };
|
||||
}) {
|
||||
const { id, token } = params;
|
||||
|
||||
const { success, error } = await declineTransferRequest(id, token);
|
||||
|
||||
return (
|
||||
<div className="mx-auto mt-10 mb-20 flex w-2/5 flex-col items-start gap-y-4">
|
||||
<TransferImage />
|
||||
<div className="flex flex-col gap-y-6">
|
||||
{success && (
|
||||
<>
|
||||
<Heading level="h1" className="text-xl text-zinc-900">
|
||||
Order transfer declined!
|
||||
</Heading>
|
||||
<Text className="text-zinc-600">
|
||||
Transfer of order {id} has been successfully declined.
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
{!success && (
|
||||
<>
|
||||
<Text className="text-zinc-600">
|
||||
There was an error declining the transfer. Please try again.
|
||||
</Text>
|
||||
{error && (
|
||||
<Text className="text-red-500">Error message: {error}</Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Heading, Text } from '@medusajs/ui';
|
||||
|
||||
import TransferActions from '~/medusa/modules/order/components/transfer-actions';
|
||||
import TransferImage from '~/medusa/modules/order/components/transfer-image';
|
||||
|
||||
export default async function TransferPage({
|
||||
params,
|
||||
}: {
|
||||
params: { id: string; token: string };
|
||||
}) {
|
||||
const { id, token } = params;
|
||||
|
||||
return (
|
||||
<div className="mx-auto mt-10 mb-20 flex w-2/5 flex-col items-start gap-y-4">
|
||||
<TransferImage />
|
||||
<div className="flex flex-col gap-y-6">
|
||||
<Heading level="h1" className="text-xl text-zinc-900">
|
||||
Transfer request for order {id}
|
||||
</Heading>
|
||||
<Text className="text-zinc-600">
|
||||
You've received a request to transfer ownership of your order (
|
||||
{id}). If you agree to this request, you can approve the transfer by
|
||||
clicking the button below.
|
||||
</Text>
|
||||
<div className="h-px w-full bg-zinc-200" />
|
||||
<Text className="text-zinc-600">
|
||||
If you accept, the new owner will take over all responsibilities and
|
||||
permissions associated with this order.
|
||||
</Text>
|
||||
<Text className="text-zinc-600">
|
||||
If you do not recognize this request or wish to retain ownership, no
|
||||
further action is required.
|
||||
</Text>
|
||||
<div className="h-px w-full bg-zinc-200" />
|
||||
<TransferActions id={id} token={token} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
40
app/store/[countryCode]/(main)/page.tsx
Normal file
40
app/store/[countryCode]/(main)/page.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Metadata } from 'next';
|
||||
|
||||
import { getRegion, listCollections } from '~/medusa/lib/data';
|
||||
import FeaturedProducts from '~/medusa/modules/home/components/featured-products';
|
||||
import Hero from '~/medusa/modules/home/components/hero';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Medusa Next.js Starter Template',
|
||||
description:
|
||||
'A performant frontend ecommerce starter template with Next.js 15 and Medusa.',
|
||||
};
|
||||
|
||||
export default async function Home(props: {
|
||||
params: Promise<{ countryCode: string }>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
|
||||
const { countryCode } = params;
|
||||
|
||||
const region = await getRegion(countryCode);
|
||||
|
||||
const { collections } = await listCollections({
|
||||
fields: 'id, handle, title',
|
||||
});
|
||||
|
||||
if (!collections || !region) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Hero />
|
||||
<div className="py-12">
|
||||
<ul className="flex flex-col gap-x-6">
|
||||
<FeaturedProducts collections={collections} region={region} />
|
||||
</ul>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
108
app/store/[countryCode]/(main)/products/[handle]/page.tsx
Normal file
108
app/store/[countryCode]/(main)/products/[handle]/page.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import { Metadata } from 'next';
|
||||
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { listProducts } from '~/medusa/lib/data/products';
|
||||
import { getRegion, listRegions } from '~/medusa/lib/data/regions';
|
||||
import ProductTemplate from '~/medusa/modules/products/templates';
|
||||
|
||||
type Props = {
|
||||
params: Promise<{ countryCode: string; handle: string }>;
|
||||
};
|
||||
|
||||
export async function generateStaticParams() {
|
||||
try {
|
||||
const countryCodes = await listRegions().then((regions) =>
|
||||
regions?.map((r) => r.countries?.map((c) => c.iso_2)).flat(),
|
||||
);
|
||||
|
||||
if (!countryCodes) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const promises = countryCodes.map(async (country) => {
|
||||
const { response } = await listProducts({
|
||||
countryCode: country,
|
||||
queryParams: { limit: 100, fields: 'handle' },
|
||||
});
|
||||
|
||||
return {
|
||||
country,
|
||||
products: response.products,
|
||||
};
|
||||
});
|
||||
|
||||
const countryProducts = await Promise.all(promises);
|
||||
|
||||
return countryProducts
|
||||
.flatMap((countryData) =>
|
||||
countryData.products.map((product) => ({
|
||||
countryCode: countryData.country,
|
||||
handle: product.handle,
|
||||
})),
|
||||
)
|
||||
.filter((param) => param.handle);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to generate static paths for product pages: ${
|
||||
error instanceof Error ? error.message : 'Unknown error'
|
||||
}.`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateMetadata(props: Props): Promise<Metadata> {
|
||||
const params = await props.params;
|
||||
const { handle } = params;
|
||||
const region = await getRegion(params.countryCode);
|
||||
|
||||
if (!region) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const product = await listProducts({
|
||||
countryCode: params.countryCode,
|
||||
queryParams: { handle },
|
||||
}).then(({ response }) => response.products[0]);
|
||||
|
||||
if (!product) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return {
|
||||
title: `${product.title} | Medusa Store`,
|
||||
description: `${product.title}`,
|
||||
openGraph: {
|
||||
title: `${product.title} | Medusa Store`,
|
||||
description: `${product.title}`,
|
||||
images: product.thumbnail ? [product.thumbnail] : [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function ProductPage(props: Props) {
|
||||
const params = await props.params;
|
||||
const region = await getRegion(params.countryCode);
|
||||
|
||||
if (!region) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const pricedProduct = await listProducts({
|
||||
countryCode: params.countryCode,
|
||||
queryParams: { handle: params.handle },
|
||||
}).then(({ response }) => response.products[0]);
|
||||
|
||||
if (!pricedProduct) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<ProductTemplate
|
||||
product={pricedProduct}
|
||||
region={region}
|
||||
countryCode={params.countryCode}
|
||||
/>
|
||||
);
|
||||
}
|
||||
33
app/store/[countryCode]/(main)/store/page.tsx
Normal file
33
app/store/[countryCode]/(main)/store/page.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Metadata } from 'next';
|
||||
|
||||
import { SortOptions } from '~/medusa/modules/store/components/refinement-list/sort-products';
|
||||
import StoreTemplate from '~/medusa/modules/store/templates';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Store',
|
||||
description: 'Explore all of our products.',
|
||||
};
|
||||
|
||||
type Params = {
|
||||
searchParams: Promise<{
|
||||
sortBy?: SortOptions;
|
||||
page?: string;
|
||||
}>;
|
||||
params: Promise<{
|
||||
countryCode: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
export default async function StorePage(props: Params) {
|
||||
const params = await props.params;
|
||||
const searchParams = await props.searchParams;
|
||||
const { sortBy, page } = searchParams;
|
||||
|
||||
return (
|
||||
<StoreTemplate
|
||||
sortBy={sortBy}
|
||||
page={page}
|
||||
countryCode={params.countryCode}
|
||||
/>
|
||||
);
|
||||
}
|
||||
29
app/store/not-found.tsx
Normal file
29
app/store/not-found.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Metadata } from 'next';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import { ArrowUpRightMini } from '@medusajs/icons';
|
||||
import { Text } from '@medusajs/ui';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: '404',
|
||||
description: 'Something went wrong',
|
||||
};
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div className="flex min-h-[calc(100vh-64px)] flex-col items-center justify-center gap-4">
|
||||
<h1 className="text-2xl-semi text-ui-fg-base">Page not found</h1>
|
||||
<p className="text-small-regular text-ui-fg-base">
|
||||
The page you tried to access does not exist.
|
||||
</p>
|
||||
<Link className="group flex items-center gap-x-1" href="/">
|
||||
<Text className="text-ui-fg-interactive">Go to frontpage</Text>
|
||||
<ArrowUpRightMini
|
||||
className="duration-150 ease-in-out group-hover:rotate-45"
|
||||
color="var(--fg-interactive)"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
BIN
app/store/opengraph-image.jpg
Normal file
BIN
app/store/opengraph-image.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 229 KiB |
BIN
app/store/twitter-image.jpg
Normal file
BIN
app/store/twitter-image.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 229 KiB |
Reference in New Issue
Block a user