diff --git a/src/app/(main)/products/loading.tsx b/src/app/[channel]/(main)/products/loading.tsx
similarity index 100%
rename from src/app/(main)/products/loading.tsx
rename to src/app/[channel]/(main)/products/loading.tsx
diff --git a/src/app/(main)/products/page.tsx b/src/app/[channel]/(main)/products/page.tsx
similarity index 86%
rename from src/app/(main)/products/page.tsx
rename to src/app/[channel]/(main)/products/page.tsx
index 41c66d1d1..d89e9c65d 100644
--- a/src/app/(main)/products/page.tsx
+++ b/src/app/[channel]/(main)/products/page.tsx
@@ -3,7 +3,6 @@ import { ProductListPaginatedDocument } from "@/gql/graphql";
import { executeGraphQL } from "@/lib/graphql";
import { Pagination } from "@/ui/components/Pagination";
import { ProductList } from "@/ui/components/ProductList";
-import { DEFAULT_CHANNEL } from "@/checkout/lib/regions";
import { ProductsPerPage } from "@/app/config";
export const metadata = {
@@ -11,20 +10,22 @@ export const metadata = {
description: "All products in Saleor Storefront example",
};
-type Props = {
+export default async function Page({
+ params,
+ searchParams,
+}: {
+ params: { channel: string };
searchParams: {
cursor: string | string[] | undefined;
};
-};
-
-export default async function Page({ searchParams }: Props) {
+}) {
const cursor = typeof searchParams.cursor === "string" ? searchParams.cursor : null;
const { products } = await executeGraphQL(ProductListPaginatedDocument, {
variables: {
first: ProductsPerPage,
after: cursor,
- channel: DEFAULT_CHANNEL,
+ channel: params.channel,
},
revalidate: 60,
});
@@ -44,7 +45,7 @@ export default async function Page({ searchParams }: Props) {
diff --git a/src/app/(main)/search/page.tsx b/src/app/[channel]/(main)/search/page.tsx
similarity index 90%
rename from src/app/(main)/search/page.tsx
rename to src/app/[channel]/(main)/search/page.tsx
index 9cd6aad5c..214ec83fe 100644
--- a/src/app/(main)/search/page.tsx
+++ b/src/app/[channel]/(main)/search/page.tsx
@@ -3,7 +3,6 @@ import { OrderDirection, ProductOrderField, SearchProductsDocument } from "@/gql
import { executeGraphQL } from "@/lib/graphql";
import { Pagination } from "@/ui/components/Pagination";
import { ProductList } from "@/ui/components/ProductList";
-import { DEFAULT_CHANNEL } from "@/checkout/lib/regions";
import { ProductsPerPage } from "@/app/config";
export const metadata = {
@@ -11,11 +10,13 @@ export const metadata = {
description: "Search products in Saleor Storefront example",
};
-type Props = {
+export default async function Page({
+ searchParams,
+ params,
+}: {
searchParams: Record<"query" | "cursor", string | string[] | undefined>;
-};
-
-export default async function Page({ searchParams }: Props) {
+ params: { channel: string };
+}) {
const cursor = typeof searchParams.cursor === "string" ? searchParams.cursor : null;
const searchValue = searchParams.query;
@@ -38,7 +39,7 @@ export default async function Page({ searchParams }: Props) {
after: cursor,
sortBy: ProductOrderField.Rating,
sortDirection: OrderDirection.Asc,
- channel: DEFAULT_CHANNEL,
+ channel: params.channel,
},
revalidate: 60,
});
@@ -61,7 +62,7 @@ export default async function Page({ searchParams }: Props) {
diff --git a/src/app/[channel]/layout.tsx b/src/app/[channel]/layout.tsx
new file mode 100644
index 000000000..ab40ec01b
--- /dev/null
+++ b/src/app/[channel]/layout.tsx
@@ -0,0 +1,29 @@
+import { type ReactNode } from "react";
+import { executeGraphQL } from "@/lib/graphql";
+import { ChannelsListDocument } from "@/gql/graphql";
+
+export const generateStaticParams = async () => {
+ // the `channels` query is protected
+ // you can either hardcode the channels or use an app token to fetch the channel list here
+
+ if (process.env.SALEOR_APP_TOKEN) {
+ const channels = await executeGraphQL(ChannelsListDocument, {
+ withAuth: false, // disable cookie-based auth for this call
+ headers: {
+ // and use app token instead
+ Authorization: `Bearer ${process.env.SALEOR_APP_TOKEN}`,
+ },
+ });
+ return (
+ channels.channels
+ ?.filter((channel) => channel.isActive)
+ .map((channel) => ({ channel: channel.slug })) ?? []
+ );
+ } else {
+ return [{ channel: "default-channel" }];
+ }
+};
+
+export default function ChannelLayout({ children }: { children: ReactNode }) {
+ return children;
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
new file mode 100644
index 000000000..722f46db6
--- /dev/null
+++ b/src/app/page.tsx
@@ -0,0 +1,5 @@
+import { redirect } from "next/navigation";
+
+export default function EmptyPage() {
+ redirect("/default-channel");
+}
diff --git a/src/checkout/lib/regions.ts b/src/checkout/lib/regions.ts
index 8267898d8..2b6c5f70d 100644
--- a/src/checkout/lib/regions.ts
+++ b/src/checkout/lib/regions.ts
@@ -2,6 +2,4 @@ export const locales = ["en-US"] as const;
export const DEFAULT_LOCALE = "en-US";
-export const DEFAULT_CHANNEL = "default-channel";
-
export type Locale = (typeof locales)[number];
diff --git a/src/checkout/lib/utils/url.ts b/src/checkout/lib/utils/url.ts
index f1c82cd75..1a78bd11c 100644
--- a/src/checkout/lib/utils/url.ts
+++ b/src/checkout/lib/utils/url.ts
@@ -1,5 +1,4 @@
import queryString from "query-string";
-import { DEFAULT_CHANNEL } from "@/checkout/lib/regions";
import { type CountryCode } from "@/checkout/graphql";
import { type MightNotExist } from "@/checkout/lib/globalTypes";
@@ -38,10 +37,6 @@ type RawQueryParams = Record
& CustomTypedQ
export type QueryParams = Record & CustomTypedQueryParams;
-const defaultParams: Partial = {
- channel: DEFAULT_CHANNEL,
-};
-
// this is intentional, we know what we'll get from the query but
// queryString has no way to type this in such a specific way
export const getRawQueryParams = () => queryString.parse(location.search) as unknown as RawQueryParams;
@@ -52,7 +47,7 @@ export const getQueryParams = (): QueryParams => {
return Object.entries(params).reduce((result, entry) => {
const [paramName, paramValue] = entry as [UnmappedQueryParam, ParamBasicValue];
const mappedParamName = queryParamsMap[paramName];
- const mappedParamValue = paramValue || defaultParams[paramName];
+ const mappedParamValue = paramValue;
return {
...result,
diff --git a/src/graphql/ChannelsList.graphql b/src/graphql/ChannelsList.graphql
new file mode 100644
index 000000000..299e365e8
--- /dev/null
+++ b/src/graphql/ChannelsList.graphql
@@ -0,0 +1,13 @@
+query ChannelsList {
+ channels {
+ id
+ name
+ slug
+ isActive
+ currencyCode
+ countries {
+ country
+ code
+ }
+ }
+}
diff --git a/src/lib/checkout.ts b/src/lib/checkout.ts
index 08432a7e6..0c1e30b74 100644
--- a/src/lib/checkout.ts
+++ b/src/lib/checkout.ts
@@ -1,7 +1,23 @@
-import { DEFAULT_CHANNEL } from "@/checkout/lib/regions";
+import { cookies } from "next/headers";
import { CheckoutCreateDocument, CheckoutFindDocument } from "@/gql/graphql";
import { executeGraphQL } from "@/lib/graphql";
+export function getIdFromCookies(channel: string) {
+ const cookieName = `checkoutId-${channel}`;
+ const checkoutId = cookies().get(cookieName)?.value || "";
+ return checkoutId;
+}
+
+export function saveIdToCookie(channel: string, checkoutId: string) {
+ const shouldUseHttps =
+ process.env.NEXT_PUBLIC_STOREFRONT_URL?.startsWith("https") || !!process.env.NEXT_PUBLIC_VERCEL_URL;
+ const cookieName = `checkoutId-${channel}`;
+ cookies().set(cookieName, checkoutId, {
+ sameSite: "lax",
+ secure: shouldUseHttps,
+ });
+}
+
export async function find(checkoutId: string) {
try {
const { checkout } = checkoutId
@@ -19,13 +35,13 @@ export async function find(checkoutId: string) {
}
}
-export async function findOrCreate(checkoutId?: string) {
+export async function findOrCreate({ channel, checkoutId }: { checkoutId?: string; channel: string }) {
if (!checkoutId) {
- return (await create()).checkoutCreate?.checkout;
+ return (await create({ channel })).checkoutCreate?.checkout;
}
const checkout = await find(checkoutId);
- return checkout || (await create()).checkoutCreate?.checkout;
+ return checkout || (await create({ channel })).checkoutCreate?.checkout;
}
-export const create = () =>
- executeGraphQL(CheckoutCreateDocument, { cache: "no-cache", variables: { channel: DEFAULT_CHANNEL } });
+export const create = ({ channel }: { channel: string }) =>
+ executeGraphQL(CheckoutCreateDocument, { cache: "no-cache", variables: { channel } });
diff --git a/src/lib/graphql.ts b/src/lib/graphql.ts
index 09d3edbc0..6ede85b4b 100644
--- a/src/lib/graphql.ts
+++ b/src/lib/graphql.ts
@@ -48,6 +48,7 @@ export async function executeGraphQL(
return "";
}
})();
+ console.error(input.body);
throw new HTTPError(response, body);
}
diff --git a/src/ui/atoms/LinkWithChannel.tsx b/src/ui/atoms/LinkWithChannel.tsx
new file mode 100644
index 000000000..484d66704
--- /dev/null
+++ b/src/ui/atoms/LinkWithChannel.tsx
@@ -0,0 +1,19 @@
+"use client";
+import Link from "next/link";
+import { useParams } from "next/navigation";
+import { type ComponentProps } from "react";
+
+export const LinkWithChannel = ({
+ href,
+ ...props
+}: Omit, "href"> & { href: string }) => {
+ const { channel } = useParams<{ channel?: string }>();
+
+ if (!href.startsWith("/")) {
+ return ;
+ }
+
+ const encodedChannel = encodeURIComponent(channel ?? "");
+ const hrefWithChannel = `/${encodedChannel}${href}`;
+ return ;
+};
diff --git a/src/ui/components/Footer.tsx b/src/ui/components/Footer.tsx
index 864e24744..f5d9c1b64 100644
--- a/src/ui/components/Footer.tsx
+++ b/src/ui/components/Footer.tsx
@@ -1,11 +1,10 @@
-import Link from "next/link";
+import { LinkWithChannel } from "../atoms/LinkWithChannel";
import { MenuGetBySlugDocument } from "@/gql/graphql";
import { executeGraphQL } from "@/lib/graphql";
-import { DEFAULT_CHANNEL } from "@/checkout/lib/regions";
-export async function Footer() {
+export async function Footer({ channel }: { channel: string }) {
const footerLinks = await executeGraphQL(MenuGetBySlugDocument, {
- variables: { slug: "footer", channel: DEFAULT_CHANNEL },
+ variables: { slug: "footer", channel },
revalidate: 60 * 60 * 24,
});
const currentYear = new Date().getFullYear();
@@ -23,28 +22,34 @@ export async function Footer() {
if (child.category) {
return (
- {child.category.name}
+
+ {child.category.name}
+
);
}
if (child.collection) {
return (
- {child.collection.name}
+
+ {child.collection.name}
+
);
}
if (child.page) {
return (
- {child.page.title}
+
+ {child.page.title}
+
);
}
if (child.url) {
return (
- {child.name}
+ {child.name}
);
}
diff --git a/src/ui/components/Header.tsx b/src/ui/components/Header.tsx
index bfa5f42ed..930143a49 100644
--- a/src/ui/components/Header.tsx
+++ b/src/ui/components/Header.tsx
@@ -1,13 +1,13 @@
import { Logo } from "./Logo";
import { Nav } from "./nav/Nav";
-export function Header() {
+export function Header({ channel }: { channel: string }) {
return (
diff --git a/src/ui/components/Logo.tsx b/src/ui/components/Logo.tsx
index c318cc26d..321a5021e 100644
--- a/src/ui/components/Logo.tsx
+++ b/src/ui/components/Logo.tsx
@@ -1,7 +1,7 @@
"use client";
-import Link from "next/link";
import { usePathname } from "next/navigation";
+import { LinkWithChannel } from "../atoms/LinkWithChannel";
const companyName = "ACME";
@@ -17,9 +17,9 @@ export const Logo = () => {
}
return (
-
+
{companyName}
-
+
);
};
diff --git a/src/ui/components/OrderListItem.tsx b/src/ui/components/OrderListItem.tsx
index 1bb2c155a..2b6a8cd82 100644
--- a/src/ui/components/OrderListItem.tsx
+++ b/src/ui/components/OrderListItem.tsx
@@ -1,5 +1,5 @@
import Image from "next/image";
-import Link from "next/link";
+import { LinkWithChannel } from "../atoms/LinkWithChannel";
import { formatDate, formatMoney, getHrefForVariant } from "@/lib/utils";
import { type OrderDetailsFragment } from "@/gql/graphql";
import { PaymentStatus } from "@/ui/components/PaymentStatus";
@@ -32,12 +32,12 @@ export const OrderListItem = ({ order }: Props) => {
{/* TODO: Reveal after implementing the order details page. */}
{/*
-
View Order
-
+
*/}
@@ -76,7 +76,7 @@ export const OrderListItem = ({ order }: Props) => {