diff --git a/.env b/.env index 164e333..5808df9 100644 --- a/.env +++ b/.env @@ -49,4 +49,7 @@ LOGGER=pino NEXT_PUBLIC_DEFAULT_LOCALE=et NEXT_PUBLIC_TEAM_NAVIGATION_STYLE=custom -NEXT_PUBLIC_USER_NAVIGATION_STYLE=custom \ No newline at end of file +NEXT_PUBLIC_USER_NAVIGATION_STYLE=custom + +# MEDUSA +NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY= \ No newline at end of file diff --git a/.env.example b/.env.example index 2a3d662..1b78f4e 100644 --- a/.env.example +++ b/.env.example @@ -16,4 +16,6 @@ EMAIL_USER= # refer to your email provider's documentation EMAIL_PASSWORD= # refer to your email provider's documentation EMAIL_HOST= # refer to your email provider's documentation EMAIL_PORT= # or 465 for SSL -EMAIL_TLS= # or false for SSL (see provider documentation) \ No newline at end of file +EMAIL_TLS= # or false for SSL (see provider documentation) + +NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY= \ No newline at end of file diff --git a/README.md b/README.md index dde3485..21d54ba 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,8 @@ pnpm clean pnpm i ``` +if you get missing dependency error do `pnpm i --force` + ## Adding new dependency ```bash @@ -70,3 +72,9 @@ To update database types run: ```bash npm run supabase:typegen:app ``` + +## Medusa store + +To get medusa store working you need to update the env's to your running medusa app and migrate the tables from medusa project to your supabase project + +You can get `NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY` from your medusa app settings diff --git a/app/store/[countryCode]/(checkout)/checkout/page.tsx b/app/store/[countryCode]/(checkout)/checkout/page.tsx new file mode 100644 index 0000000..6244a1d --- /dev/null +++ b/app/store/[countryCode]/(checkout)/checkout/page.tsx @@ -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 ( +
+ + + + +
+ ) +} diff --git a/app/store/[countryCode]/(checkout)/layout.tsx b/app/store/[countryCode]/(checkout)/layout.tsx new file mode 100644 index 0000000..dcbf3ff --- /dev/null +++ b/app/store/[countryCode]/(checkout)/layout.tsx @@ -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 ( +
+
+ +
+
+ {children} +
+
+ +
+
+ ); +} diff --git a/app/store/[countryCode]/(checkout)/not-found.tsx b/app/store/[countryCode]/(checkout)/not-found.tsx new file mode 100644 index 0000000..70a1c24 --- /dev/null +++ b/app/store/[countryCode]/(checkout)/not-found.tsx @@ -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 ( +
+

Page not found

+

+ The page you tried to access does not exist. +

+ Go to frontpage +
+ ); +} diff --git a/app/store/[countryCode]/(main)/account/@dashboard/addresses/page.tsx b/app/store/[countryCode]/(main)/account/@dashboard/addresses/page.tsx new file mode 100644 index 0000000..c010411 --- /dev/null +++ b/app/store/[countryCode]/(main)/account/@dashboard/addresses/page.tsx @@ -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 ( +
+
+

Shipping Addresses

+

+ View and update your shipping addresses, you can add as many as you + like. Saving your addresses will make them available during checkout. +

+
+ +
+ ); +} diff --git a/app/store/[countryCode]/(main)/account/@dashboard/loading.tsx b/app/store/[countryCode]/(main)/account/@dashboard/loading.tsx new file mode 100644 index 0000000..207fc67 --- /dev/null +++ b/app/store/[countryCode]/(main)/account/@dashboard/loading.tsx @@ -0,0 +1,9 @@ +import Spinner from '~/medusa/modules/common/icons/spinner'; + +export default function Loading() { + return ( +
+ +
+ ); +} diff --git a/app/store/[countryCode]/(main)/account/@dashboard/orders/details/[id]/page.tsx b/app/store/[countryCode]/(main)/account/@dashboard/orders/details/[id]/page.tsx new file mode 100644 index 0000000..f51ac84 --- /dev/null +++ b/app/store/[countryCode]/(main)/account/@dashboard/orders/details/[id]/page.tsx @@ -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 { + 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 ; +} diff --git a/app/store/[countryCode]/(main)/account/@dashboard/orders/page.tsx b/app/store/[countryCode]/(main)/account/@dashboard/orders/page.tsx new file mode 100644 index 0000000..7e30118 --- /dev/null +++ b/app/store/[countryCode]/(main)/account/@dashboard/orders/page.tsx @@ -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 ( +
+
+

Orders

+

+ View your previous orders and their status. You can also create + returns or exchanges for your orders if needed. +

+
+
+ + + +
+
+ ); +} diff --git a/app/store/[countryCode]/(main)/account/@dashboard/page.tsx b/app/store/[countryCode]/(main)/account/@dashboard/page.tsx new file mode 100644 index 0000000..9af729b --- /dev/null +++ b/app/store/[countryCode]/(main)/account/@dashboard/page.tsx @@ -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 ; +} diff --git a/app/store/[countryCode]/(main)/account/@dashboard/profile/page.tsx b/app/store/[countryCode]/(main)/account/@dashboard/profile/page.tsx new file mode 100644 index 0000000..d140f03 --- /dev/null +++ b/app/store/[countryCode]/(main)/account/@dashboard/profile/page.tsx @@ -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 ( +
+
+

Profile

+

+ View and update your profile information, including your name, email, + and phone number. You can also update your billing address, or change + your password. +

+
+
+ + + + + + + {/* + */} + +
+
+ ); +} + +const Divider = () => { + return
; +}; +``; diff --git a/app/store/[countryCode]/(main)/account/@login/page.tsx b/app/store/[countryCode]/(main)/account/@login/page.tsx new file mode 100644 index 0000000..405b31b --- /dev/null +++ b/app/store/[countryCode]/(main)/account/@login/page.tsx @@ -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 ; +} diff --git a/app/store/[countryCode]/(main)/account/layout.tsx b/app/store/[countryCode]/(main)/account/layout.tsx new file mode 100644 index 0000000..04d05e4 --- /dev/null +++ b/app/store/[countryCode]/(main)/account/layout.tsx @@ -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 ( + + {customer ? dashboard : login} + + + ); +} diff --git a/app/store/[countryCode]/(main)/account/loading.tsx b/app/store/[countryCode]/(main)/account/loading.tsx new file mode 100644 index 0000000..207fc67 --- /dev/null +++ b/app/store/[countryCode]/(main)/account/loading.tsx @@ -0,0 +1,9 @@ +import Spinner from '~/medusa/modules/common/icons/spinner'; + +export default function Loading() { + return ( +
+ +
+ ); +} diff --git a/app/store/[countryCode]/(main)/cart/loading.tsx b/app/store/[countryCode]/(main)/cart/loading.tsx new file mode 100644 index 0000000..f093295 --- /dev/null +++ b/app/store/[countryCode]/(main)/cart/loading.tsx @@ -0,0 +1,5 @@ +import SkeletonCartPage from '~/medusa/modules/skeletons/templates/skeleton-cart-page'; + +export default function Loading() { + return ; +} diff --git a/app/store/[countryCode]/(main)/cart/not-found.tsx b/app/store/[countryCode]/(main)/cart/not-found.tsx new file mode 100644 index 0000000..a2f02bd --- /dev/null +++ b/app/store/[countryCode]/(main)/cart/not-found.tsx @@ -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 ( +
+

Page not found

+

+ The cart you tried to access does not exist. Clear your cookies and try + again. +

+ Go to frontpage +
+ ); +} diff --git a/app/store/[countryCode]/(main)/cart/page.tsx b/app/store/[countryCode]/(main)/cart/page.tsx new file mode 100644 index 0000000..72fc72b --- /dev/null +++ b/app/store/[countryCode]/(main)/cart/page.tsx @@ -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 ; +} diff --git a/app/store/[countryCode]/(main)/categories/[...category]/page.tsx b/app/store/[countryCode]/(main)/categories/[...category]/page.tsx new file mode 100644 index 0000000..ca873ab --- /dev/null +++ b/app/store/[countryCode]/(main)/categories/[...category]/page.tsx @@ -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 { + 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 ( + + ); +} diff --git a/app/store/[countryCode]/(main)/collections/[handle]/page.tsx b/app/store/[countryCode]/(main)/collections/[handle]/page.tsx new file mode 100644 index 0000000..1b01743 --- /dev/null +++ b/app/store/[countryCode]/(main)/collections/[handle]/page.tsx @@ -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 { + 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 ( + + ); +} diff --git a/app/store/[countryCode]/(main)/layout.tsx b/app/store/[countryCode]/(main)/layout.tsx new file mode 100644 index 0000000..31d1512 --- /dev/null +++ b/app/store/[countryCode]/(main)/layout.tsx @@ -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 ( + <> +