create-cartbase 0.1.16 → 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +25 -25
- package/dist/index.js +20 -20
- package/package.json +1 -1
- package/template/app/docs/auth.md +105 -105
- package/template/app/docs/carts.md +376 -376
- package/template/app/docs/categories.md +194 -194
- package/template/app/docs/checkout.md +714 -714
- package/template/app/docs/components.md +55 -11
- package/template/app/docs/consent.md +91 -91
- package/template/app/docs/deploy.md +197 -197
- package/template/app/docs/gift-cards.md +153 -153
- package/template/app/docs/metaobjects.md +126 -126
- package/template/app/docs/orders.md +221 -221
- package/template/app/docs/products.md +51 -2
- package/template/app/docs/regions.md +269 -269
- package/template/app/docs/reviews.md +223 -223
- package/template/app/docs/search.md +227 -227
- package/template/app/docs/store.md +47 -47
- package/template/app/docs/subscriptions.md +148 -148
- package/template/app/docs/variables.md +315 -315
- package/template/app/package.json +1 -1
- package/template/app/postcss.config.cjs +11 -11
- package/template/app/src/app/checkout/checkout-page-client.tsx +73 -73
- package/template/app/src/app/checkout/page.tsx +48 -48
- package/template/app/src/app/globals.css +26 -26
- package/template/app/src/app/page.tsx +28 -28
- package/template/app/src/app/products/[handle]/page.tsx +87 -87
- package/template/app/src/app/providers.tsx +68 -64
- package/template/app/src/app/search/page.tsx +23 -23
- package/template/app/src/lib/browser-client.ts +35 -35
- package/template/app/src/lib/config.ts +41 -41
- package/template/app/src/lib/server-client.ts +25 -25
- package/template/app/src/lib/cart-actions.ts +0 -47
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Tailwind 4 ships its own PostCSS plugin and handles vendor prefixing
|
|
3
|
-
* internally, so the v3 pairing of `tailwindcss` + `autoprefixer` is gone.
|
|
4
|
-
* There is no tailwind.config file either: the theme is defined in CSS
|
|
5
|
-
* (see src/app/globals.css and the theme shipped by @cartbase/storefront).
|
|
6
|
-
*/
|
|
7
|
-
module.exports = {
|
|
8
|
-
plugins: {
|
|
9
|
-
"@tailwindcss/postcss": {},
|
|
10
|
-
},
|
|
11
|
-
}
|
|
1
|
+
/*
|
|
2
|
+
* Tailwind 4 ships its own PostCSS plugin and handles vendor prefixing
|
|
3
|
+
* internally, so the v3 pairing of `tailwindcss` + `autoprefixer` is gone.
|
|
4
|
+
* There is no tailwind.config file either: the theme is defined in CSS
|
|
5
|
+
* (see src/app/globals.css and the theme shipped by @cartbase/storefront).
|
|
6
|
+
*/
|
|
7
|
+
module.exports = {
|
|
8
|
+
plugins: {
|
|
9
|
+
"@tailwindcss/postcss": {},
|
|
10
|
+
},
|
|
11
|
+
}
|
|
@@ -1,73 +1,73 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import { useRouter } from "next/navigation"
|
|
4
|
-
import type { Cart } from "@cartbase/storefront/api/carts"
|
|
5
|
-
import type { StoreShippingOption } from "@cartbase/storefront/api/checkout"
|
|
6
|
-
import type { StorePaymentEntry } from "@cartbase/storefront/api/checkout"
|
|
7
|
-
import { CheckoutProvider } from "@cartbase/storefront/checkout/context"
|
|
8
|
-
import { CheckoutClient } from "@cartbase/storefront/checkout/checkout-client"
|
|
9
|
-
import { browserClient } from "@/lib/browser-client"
|
|
10
|
-
|
|
11
|
-
/** sessionStorage key the confirmation page reads (guests have no order
|
|
12
|
-
* read endpoint — the completeCart() response is the only order handle,
|
|
13
|
-
* orders.md). */
|
|
14
|
-
export const LAST_ORDER_STORAGE_KEY = "barter:last-order"
|
|
15
|
-
|
|
16
|
-
export function CheckoutPageClient({
|
|
17
|
-
cart,
|
|
18
|
-
shippingOptions,
|
|
19
|
-
paymentProviders,
|
|
20
|
-
}: {
|
|
21
|
-
cart: Cart
|
|
22
|
-
shippingOptions: StoreShippingOption[]
|
|
23
|
-
paymentProviders: StorePaymentEntry[]
|
|
24
|
-
}) {
|
|
25
|
-
const router = useRouter()
|
|
26
|
-
|
|
27
|
-
return (
|
|
28
|
-
<CheckoutProvider orderConfirmedPath="/order/{id}/confirmed">
|
|
29
|
-
<CheckoutClient
|
|
30
|
-
client={browserClient}
|
|
31
|
-
cart={cart}
|
|
32
|
-
customer={null}
|
|
33
|
-
availableShippingMethods={shippingOptions}
|
|
34
|
-
availablePaymentMethods={paymentProviders}
|
|
35
|
-
// NO COUNTRY LIST HERE, on purpose (2026-09-13). This file is the
|
|
36
|
-
// seed for every scaffolded store, and it used to pass
|
|
37
|
-
// `countries={[{iso_2:"bg", display_name:"Bulgaria"}]}` with
|
|
38
|
-
// `countryCode="bg"`, so every store on earth shipped a checkout
|
|
39
|
-
// offering one country, Bulgaria, in a box the shopper could not
|
|
40
|
-
// change. The list belongs to the store, not to this file: omit the
|
|
41
|
-
// prop and the hook reads GET /api/store/countries, which answers
|
|
42
|
-
// with the countries the store's Markets declare, or with the whole
|
|
43
|
-
// world when it has declared none.
|
|
44
|
-
//
|
|
45
|
-
// A single-market store adds `countryCode="xx"` here to preselect
|
|
46
|
-
// its country, and a store that sells to exactly one country gets
|
|
47
|
-
// the read-only field automatically, because its Markets say so.
|
|
48
|
-
// Per-store rule (the documented paymentMethodFilter seam): this
|
|
49
|
-
// reference store checks out offline via its merchant methods only
|
|
50
|
-
// (pp_* kill: method entries carry payment_method_id, processors
|
|
51
|
-
// carry id) — the filter drops any connected processor.
|
|
52
|
-
paymentMethodFilter={(methods) =>
|
|
53
|
-
methods?.filter((m) => "payment_method_id" in m) ?? null
|
|
54
|
-
}
|
|
55
|
-
onOrderPlaced={(order) => {
|
|
56
|
-
// Guest order handle = the completeCart() response (orders.md).
|
|
57
|
-
// Stash it (+ the decorated cart lines for the items list) for
|
|
58
|
-
// the confirmation page, then navigate.
|
|
59
|
-
try {
|
|
60
|
-
sessionStorage.setItem(
|
|
61
|
-
LAST_ORDER_STORAGE_KEY,
|
|
62
|
-
JSON.stringify({ order, cartItems: cart.items ?? [] })
|
|
63
|
-
)
|
|
64
|
-
} catch {
|
|
65
|
-
// best-effort — the confirmation page has a fallback state
|
|
66
|
-
}
|
|
67
|
-
router.push(`/order/${order.id}/confirmed`)
|
|
68
|
-
}}
|
|
69
|
-
onCartChange={() => router.refresh()}
|
|
70
|
-
/>
|
|
71
|
-
</CheckoutProvider>
|
|
72
|
-
)
|
|
73
|
-
}
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useRouter } from "next/navigation"
|
|
4
|
+
import type { Cart } from "@cartbase/storefront/api/carts"
|
|
5
|
+
import type { StoreShippingOption } from "@cartbase/storefront/api/checkout"
|
|
6
|
+
import type { StorePaymentEntry } from "@cartbase/storefront/api/checkout"
|
|
7
|
+
import { CheckoutProvider } from "@cartbase/storefront/checkout/context"
|
|
8
|
+
import { CheckoutClient } from "@cartbase/storefront/checkout/checkout-client"
|
|
9
|
+
import { browserClient } from "@/lib/browser-client"
|
|
10
|
+
|
|
11
|
+
/** sessionStorage key the confirmation page reads (guests have no order
|
|
12
|
+
* read endpoint — the completeCart() response is the only order handle,
|
|
13
|
+
* orders.md). */
|
|
14
|
+
export const LAST_ORDER_STORAGE_KEY = "barter:last-order"
|
|
15
|
+
|
|
16
|
+
export function CheckoutPageClient({
|
|
17
|
+
cart,
|
|
18
|
+
shippingOptions,
|
|
19
|
+
paymentProviders,
|
|
20
|
+
}: {
|
|
21
|
+
cart: Cart
|
|
22
|
+
shippingOptions: StoreShippingOption[]
|
|
23
|
+
paymentProviders: StorePaymentEntry[]
|
|
24
|
+
}) {
|
|
25
|
+
const router = useRouter()
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<CheckoutProvider orderConfirmedPath="/order/{id}/confirmed">
|
|
29
|
+
<CheckoutClient
|
|
30
|
+
client={browserClient}
|
|
31
|
+
cart={cart}
|
|
32
|
+
customer={null}
|
|
33
|
+
availableShippingMethods={shippingOptions}
|
|
34
|
+
availablePaymentMethods={paymentProviders}
|
|
35
|
+
// NO COUNTRY LIST HERE, on purpose (2026-09-13). This file is the
|
|
36
|
+
// seed for every scaffolded store, and it used to pass
|
|
37
|
+
// `countries={[{iso_2:"bg", display_name:"Bulgaria"}]}` with
|
|
38
|
+
// `countryCode="bg"`, so every store on earth shipped a checkout
|
|
39
|
+
// offering one country, Bulgaria, in a box the shopper could not
|
|
40
|
+
// change. The list belongs to the store, not to this file: omit the
|
|
41
|
+
// prop and the hook reads GET /api/store/countries, which answers
|
|
42
|
+
// with the countries the store's Markets declare, or with the whole
|
|
43
|
+
// world when it has declared none.
|
|
44
|
+
//
|
|
45
|
+
// A single-market store adds `countryCode="xx"` here to preselect
|
|
46
|
+
// its country, and a store that sells to exactly one country gets
|
|
47
|
+
// the read-only field automatically, because its Markets say so.
|
|
48
|
+
// Per-store rule (the documented paymentMethodFilter seam): this
|
|
49
|
+
// reference store checks out offline via its merchant methods only
|
|
50
|
+
// (pp_* kill: method entries carry payment_method_id, processors
|
|
51
|
+
// carry id) — the filter drops any connected processor.
|
|
52
|
+
paymentMethodFilter={(methods) =>
|
|
53
|
+
methods?.filter((m) => "payment_method_id" in m) ?? null
|
|
54
|
+
}
|
|
55
|
+
onOrderPlaced={(order) => {
|
|
56
|
+
// Guest order handle = the completeCart() response (orders.md).
|
|
57
|
+
// Stash it (+ the decorated cart lines for the items list) for
|
|
58
|
+
// the confirmation page, then navigate.
|
|
59
|
+
try {
|
|
60
|
+
sessionStorage.setItem(
|
|
61
|
+
LAST_ORDER_STORAGE_KEY,
|
|
62
|
+
JSON.stringify({ order, cartItems: cart.items ?? [] })
|
|
63
|
+
)
|
|
64
|
+
} catch {
|
|
65
|
+
// best-effort — the confirmation page has a fallback state
|
|
66
|
+
}
|
|
67
|
+
router.push(`/order/${order.id}/confirmed`)
|
|
68
|
+
}}
|
|
69
|
+
onCartChange={() => router.refresh()}
|
|
70
|
+
/>
|
|
71
|
+
</CheckoutProvider>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
@@ -1,48 +1,48 @@
|
|
|
1
|
-
import { cookies } from "next/headers"
|
|
2
|
-
import { redirect } from "next/navigation"
|
|
3
|
-
import { retrieveCart } from "@cartbase/storefront/api/carts"
|
|
4
|
-
import {
|
|
5
|
-
listPaymentProviders,
|
|
6
|
-
listShippingOptions,
|
|
7
|
-
} from "@cartbase/storefront/api/checkout"
|
|
8
|
-
import { readCartCookie } from "@/lib/config"
|
|
9
|
-
import { getServerClient } from "@/lib/server-client"
|
|
10
|
-
import { CheckoutPageClient } from "./checkout-page-client"
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Checkout (runbook step 8 / checkout.md): list shipping options and
|
|
14
|
-
* payment providers WITH `cart_id` (server-side rule filtering), fetch the
|
|
15
|
-
* integrations `cod` block, and hand everything to the orchestrated
|
|
16
|
-
* client layout. Redirect completed/missing carts server-side
|
|
17
|
-
* (checkout-client mount rule).
|
|
18
|
-
*/
|
|
19
|
-
export default async function CheckoutPage() {
|
|
20
|
-
const jar = await cookies()
|
|
21
|
-
// Back-compat: prefer `_cartbase_cart`, fall back to the legacy
|
|
22
|
-
// `_barter_cart_id` name (platform-fingerprints card).
|
|
23
|
-
const cartId = readCartCookie((name) => jar.get(name)?.value)
|
|
24
|
-
if (!cartId) redirect("/")
|
|
25
|
-
|
|
26
|
-
const client = await getServerClient()
|
|
27
|
-
const cart = await retrieveCart(client, cartId)
|
|
28
|
-
.then((res) => res.cart)
|
|
29
|
-
.catch(() => null)
|
|
30
|
-
if (!cart || (cart.items ?? []).length === 0) redirect("/")
|
|
31
|
-
if (cart.completed_at) redirect("/")
|
|
32
|
-
|
|
33
|
-
const [shippingOptions, paymentProviders] = await Promise.all([
|
|
34
|
-
listShippingOptions(client, { cart_id: cart.id }),
|
|
35
|
-
listPaymentProviders(client, {
|
|
36
|
-
cart_id: cart.id,
|
|
37
|
-
region_id: cart.region_id ?? undefined,
|
|
38
|
-
}),
|
|
39
|
-
])
|
|
40
|
-
|
|
41
|
-
return (
|
|
42
|
-
<CheckoutPageClient
|
|
43
|
-
cart={cart}
|
|
44
|
-
shippingOptions={shippingOptions.shipping_options}
|
|
45
|
-
paymentProviders={paymentProviders.payment_providers}
|
|
46
|
-
/>
|
|
47
|
-
)
|
|
48
|
-
}
|
|
1
|
+
import { cookies } from "next/headers"
|
|
2
|
+
import { redirect } from "next/navigation"
|
|
3
|
+
import { retrieveCart } from "@cartbase/storefront/api/carts"
|
|
4
|
+
import {
|
|
5
|
+
listPaymentProviders,
|
|
6
|
+
listShippingOptions,
|
|
7
|
+
} from "@cartbase/storefront/api/checkout"
|
|
8
|
+
import { readCartCookie } from "@/lib/config"
|
|
9
|
+
import { getServerClient } from "@/lib/server-client"
|
|
10
|
+
import { CheckoutPageClient } from "./checkout-page-client"
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Checkout (runbook step 8 / checkout.md): list shipping options and
|
|
14
|
+
* payment providers WITH `cart_id` (server-side rule filtering), fetch the
|
|
15
|
+
* integrations `cod` block, and hand everything to the orchestrated
|
|
16
|
+
* client layout. Redirect completed/missing carts server-side
|
|
17
|
+
* (checkout-client mount rule).
|
|
18
|
+
*/
|
|
19
|
+
export default async function CheckoutPage() {
|
|
20
|
+
const jar = await cookies()
|
|
21
|
+
// Back-compat: prefer `_cartbase_cart`, fall back to the legacy
|
|
22
|
+
// `_barter_cart_id` name (platform-fingerprints card).
|
|
23
|
+
const cartId = readCartCookie((name) => jar.get(name)?.value)
|
|
24
|
+
if (!cartId) redirect("/")
|
|
25
|
+
|
|
26
|
+
const client = await getServerClient()
|
|
27
|
+
const cart = await retrieveCart(client, cartId)
|
|
28
|
+
.then((res) => res.cart)
|
|
29
|
+
.catch(() => null)
|
|
30
|
+
if (!cart || (cart.items ?? []).length === 0) redirect("/")
|
|
31
|
+
if (cart.completed_at) redirect("/")
|
|
32
|
+
|
|
33
|
+
const [shippingOptions, paymentProviders] = await Promise.all([
|
|
34
|
+
listShippingOptions(client, { cart_id: cart.id }),
|
|
35
|
+
listPaymentProviders(client, {
|
|
36
|
+
cart_id: cart.id,
|
|
37
|
+
region_id: cart.region_id ?? undefined,
|
|
38
|
+
}),
|
|
39
|
+
])
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<CheckoutPageClient
|
|
43
|
+
cart={cart}
|
|
44
|
+
shippingOptions={shippingOptions.shipping_options}
|
|
45
|
+
paymentProviders={paymentProviders.payment_providers}
|
|
46
|
+
/>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* The whole styling setup for a Cartbase storefront: two imports.
|
|
3
|
-
*
|
|
4
|
-
* Tailwind 4 is CSS-first, so there is no config file and no preset to
|
|
5
|
-
* register. The library ships its theme filled, so this app renders as a
|
|
6
|
-
* designed store without choosing a single value.
|
|
7
|
-
*
|
|
8
|
-
* To make the store yours, override the token values below the imports.
|
|
9
|
-
* Never redefine the names — components reference them, and the library
|
|
10
|
-
* repaints from the values alone.
|
|
11
|
-
*/
|
|
12
|
-
@import "tailwindcss";
|
|
13
|
-
@import "@cartbase/storefront/theme";
|
|
14
|
-
|
|
15
|
-
/*
|
|
16
|
-
* The library ships as TypeScript source rather than a build, so Tailwind has
|
|
17
|
-
* to scan it to know which classes exist. Without this the components render
|
|
18
|
-
* with no styles at all — the classes are in the markup but never generated.
|
|
19
|
-
*/
|
|
20
|
-
@source "../../node_modules/@cartbase/storefront/src";
|
|
21
|
-
|
|
22
|
-
/*
|
|
23
|
-
* Dark mode is class-based: put `.dark` on <html> and every token swaps.
|
|
24
|
-
* Tailwind 4 needs this declared explicitly.
|
|
25
|
-
*/
|
|
26
|
-
@custom-variant dark (&:where(.dark, .dark *));
|
|
1
|
+
/*
|
|
2
|
+
* The whole styling setup for a Cartbase storefront: two imports.
|
|
3
|
+
*
|
|
4
|
+
* Tailwind 4 is CSS-first, so there is no config file and no preset to
|
|
5
|
+
* register. The library ships its theme filled, so this app renders as a
|
|
6
|
+
* designed store without choosing a single value.
|
|
7
|
+
*
|
|
8
|
+
* To make the store yours, override the token values below the imports.
|
|
9
|
+
* Never redefine the names — components reference them, and the library
|
|
10
|
+
* repaints from the values alone.
|
|
11
|
+
*/
|
|
12
|
+
@import "tailwindcss";
|
|
13
|
+
@import "@cartbase/storefront/theme";
|
|
14
|
+
|
|
15
|
+
/*
|
|
16
|
+
* The library ships as TypeScript source rather than a build, so Tailwind has
|
|
17
|
+
* to scan it to know which classes exist. Without this the components render
|
|
18
|
+
* with no styles at all — the classes are in the markup but never generated.
|
|
19
|
+
*/
|
|
20
|
+
@source "../../node_modules/@cartbase/storefront/src";
|
|
21
|
+
|
|
22
|
+
/*
|
|
23
|
+
* Dark mode is class-based: put `.dark` on <html> and every token swaps.
|
|
24
|
+
* Tailwind 4 needs this declared explicitly.
|
|
25
|
+
*/
|
|
26
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
import { StoreTemplate } from "@cartbase/storefront/store/store-template"
|
|
2
|
-
import type { SortOptions } from "@cartbase/storefront/lib/sort-products"
|
|
3
|
-
import { PRICING_CONTEXT, STORE_LOCALE } from "@/lib/config"
|
|
4
|
-
import { getServerClient } from "@/lib/server-client"
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Home = the all-products listing (runbook step 5: always pass a pricing
|
|
8
|
-
* context or prices come back undecorated).
|
|
9
|
-
*/
|
|
10
|
-
export default async function HomePage({
|
|
11
|
-
searchParams,
|
|
12
|
-
}: {
|
|
13
|
-
searchParams: Promise<{ sortBy?: string; page?: string }>
|
|
14
|
-
}) {
|
|
15
|
-
const { sortBy, page } = await searchParams
|
|
16
|
-
const client = await getServerClient()
|
|
17
|
-
return (
|
|
18
|
-
<StoreTemplate
|
|
19
|
-
client={client}
|
|
20
|
-
sortBy={sortBy as SortOptions | undefined}
|
|
21
|
-
page={page}
|
|
22
|
-
pricingContext={PRICING_CONTEXT}
|
|
23
|
-
// Server-rendered: the locale provider cannot reach it, so the pack
|
|
24
|
-
// comes as a prop, and the prop is required.
|
|
25
|
-
labels={STORE_LOCALE.store}
|
|
26
|
-
/>
|
|
27
|
-
)
|
|
28
|
-
}
|
|
1
|
+
import { StoreTemplate } from "@cartbase/storefront/store/store-template"
|
|
2
|
+
import type { SortOptions } from "@cartbase/storefront/lib/sort-products"
|
|
3
|
+
import { PRICING_CONTEXT, STORE_LOCALE } from "@/lib/config"
|
|
4
|
+
import { getServerClient } from "@/lib/server-client"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Home = the all-products listing (runbook step 5: always pass a pricing
|
|
8
|
+
* context or prices come back undecorated).
|
|
9
|
+
*/
|
|
10
|
+
export default async function HomePage({
|
|
11
|
+
searchParams,
|
|
12
|
+
}: {
|
|
13
|
+
searchParams: Promise<{ sortBy?: string; page?: string }>
|
|
14
|
+
}) {
|
|
15
|
+
const { sortBy, page } = await searchParams
|
|
16
|
+
const client = await getServerClient()
|
|
17
|
+
return (
|
|
18
|
+
<StoreTemplate
|
|
19
|
+
client={client}
|
|
20
|
+
sortBy={sortBy as SortOptions | undefined}
|
|
21
|
+
page={page}
|
|
22
|
+
pricingContext={PRICING_CONTEXT}
|
|
23
|
+
// Server-rendered: the locale provider cannot reach it, so the pack
|
|
24
|
+
// comes as a prop, and the prop is required.
|
|
25
|
+
labels={STORE_LOCALE.store}
|
|
26
|
+
/>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
@@ -1,87 +1,87 @@
|
|
|
1
|
-
import { notFound } from "next/navigation"
|
|
2
|
-
import { retrieveProduct } from "@cartbase/storefront/api/products"
|
|
3
|
-
import { StoreApiError } from "@cartbase/storefront/api/types"
|
|
4
|
-
import { ProductTemplate } from "@cartbase/storefront/products/product-template"
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
*
|
|
11
|
-
* the
|
|
12
|
-
*
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
// The product page contract: the address names the variant
|
|
40
|
-
// (`?variant=<digits>`, Shopify's parameter), and the page hands it to
|
|
41
|
-
// the template so the FIRST paint is that variant's price, code and
|
|
42
|
-
// stock. Switching a variant rewrites the address in the browser
|
|
43
|
-
// without asking the server.
|
|
44
|
-
initialVariantId={variant}
|
|
45
|
-
// NO `promises` here, deliberately. Until 0.13.0 the library carried
|
|
46
|
-
// delivery, exchange and return promises as label DEFAULTS, so every
|
|
47
|
-
// store scaffolded from this file told shoppers "your package will
|
|
48
|
-
// arrive in 3-5 business days" and "we'll refund your money" without
|
|
49
|
-
// anyone having decided that. Those are YOUR commitments, so you write
|
|
50
|
-
// them:
|
|
51
|
-
//
|
|
52
|
-
// promises={[
|
|
53
|
-
// { icon: "delivery", title: "Fast delivery",
|
|
54
|
-
// body: "Your order arrives in 2 working days." },
|
|
55
|
-
// ]}
|
|
56
|
-
//
|
|
57
|
-
// Pass nothing and the section does not exist, which is the right
|
|
58
|
-
// default for a store that has not decided yet. The physical-facts
|
|
59
|
-
// section appears on its own for products that HAVE facts, and is
|
|
60
|
-
// skipped for those that do not.
|
|
61
|
-
//
|
|
62
|
-
// The pack has to be handed over here, not just mounted at the root:
|
|
63
|
-
// the related-products strip is a server component and cannot read
|
|
64
|
-
// the locale provider, so its heading would stay English. Required.
|
|
65
|
-
labels={STORE_LOCALE.products}
|
|
66
|
-
/>
|
|
67
|
-
)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export async function generateMetadata({
|
|
71
|
-
params,
|
|
72
|
-
}: {
|
|
73
|
-
params: Promise<{ handle: string }>
|
|
74
|
-
}) {
|
|
75
|
-
const { handle } = await params
|
|
76
|
-
const client = await getServerClient()
|
|
77
|
-
try {
|
|
78
|
-
const { product } = await retrieveProduct(client, handle)
|
|
79
|
-
// SEO fields with title/description fallbacks (runbook step 5).
|
|
80
|
-
return {
|
|
81
|
-
title: product.seo_title ?? product.title,
|
|
82
|
-
description: product.seo_description ?? product.description ?? undefined,
|
|
83
|
-
}
|
|
84
|
-
} catch {
|
|
85
|
-
return {}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
1
|
+
import { notFound } from "next/navigation"
|
|
2
|
+
import { retrieveProduct } from "@cartbase/storefront/api/products"
|
|
3
|
+
import { StoreApiError } from "@cartbase/storefront/api/types"
|
|
4
|
+
import { ProductTemplate } from "@cartbase/storefront/products/product-template"
|
|
5
|
+
import { PRICING_CONTEXT, STORE_LOCALE } from "@/lib/config"
|
|
6
|
+
import { getServerClient } from "@/lib/server-client"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* PDP (runbook step 5): fetch by handle WITH the pricing context, render
|
|
10
|
+
* the full product template. No `addToCart` is passed: the add goes
|
|
11
|
+
* through the cart drawer mounted in `providers.tsx`, so the drawer opens
|
|
12
|
+
* at the click with the product in it and no page render runs (the product
|
|
13
|
+
* page contract, rule 6). The drawer's `onCartChange` owns the cart cookie.
|
|
14
|
+
*/
|
|
15
|
+
export default async function ProductPage({
|
|
16
|
+
params,
|
|
17
|
+
searchParams,
|
|
18
|
+
}: {
|
|
19
|
+
params: Promise<{ handle: string }>
|
|
20
|
+
searchParams: Promise<{ variant?: string }>
|
|
21
|
+
}) {
|
|
22
|
+
const [{ handle }, { variant }] = await Promise.all([params, searchParams])
|
|
23
|
+
const client = await getServerClient()
|
|
24
|
+
|
|
25
|
+
let product
|
|
26
|
+
try {
|
|
27
|
+
const res = await retrieveProduct(client, handle, PRICING_CONTEXT)
|
|
28
|
+
product = res.product
|
|
29
|
+
} catch (e) {
|
|
30
|
+
if (e instanceof StoreApiError && e.status === 404) notFound()
|
|
31
|
+
throw e
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<ProductTemplate
|
|
36
|
+
client={client}
|
|
37
|
+
product={product}
|
|
38
|
+
pricingContext={PRICING_CONTEXT}
|
|
39
|
+
// The product page contract: the address names the variant
|
|
40
|
+
// (`?variant=<digits>`, Shopify's parameter), and the page hands it to
|
|
41
|
+
// the template so the FIRST paint is that variant's price, code and
|
|
42
|
+
// stock. Switching a variant rewrites the address in the browser
|
|
43
|
+
// without asking the server.
|
|
44
|
+
initialVariantId={variant}
|
|
45
|
+
// NO `promises` here, deliberately. Until 0.13.0 the library carried
|
|
46
|
+
// delivery, exchange and return promises as label DEFAULTS, so every
|
|
47
|
+
// store scaffolded from this file told shoppers "your package will
|
|
48
|
+
// arrive in 3-5 business days" and "we'll refund your money" without
|
|
49
|
+
// anyone having decided that. Those are YOUR commitments, so you write
|
|
50
|
+
// them:
|
|
51
|
+
//
|
|
52
|
+
// promises={[
|
|
53
|
+
// { icon: "delivery", title: "Fast delivery",
|
|
54
|
+
// body: "Your order arrives in 2 working days." },
|
|
55
|
+
// ]}
|
|
56
|
+
//
|
|
57
|
+
// Pass nothing and the section does not exist, which is the right
|
|
58
|
+
// default for a store that has not decided yet. The physical-facts
|
|
59
|
+
// section appears on its own for products that HAVE facts, and is
|
|
60
|
+
// skipped for those that do not.
|
|
61
|
+
//
|
|
62
|
+
// The pack has to be handed over here, not just mounted at the root:
|
|
63
|
+
// the related-products strip is a server component and cannot read
|
|
64
|
+
// the locale provider, so its heading would stay English. Required.
|
|
65
|
+
labels={STORE_LOCALE.products}
|
|
66
|
+
/>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function generateMetadata({
|
|
71
|
+
params,
|
|
72
|
+
}: {
|
|
73
|
+
params: Promise<{ handle: string }>
|
|
74
|
+
}) {
|
|
75
|
+
const { handle } = await params
|
|
76
|
+
const client = await getServerClient()
|
|
77
|
+
try {
|
|
78
|
+
const { product } = await retrieveProduct(client, handle)
|
|
79
|
+
// SEO fields with title/description fallbacks (runbook step 5).
|
|
80
|
+
return {
|
|
81
|
+
title: product.seo_title ?? product.title,
|
|
82
|
+
description: product.seo_description ?? product.description ?? undefined,
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
return {}
|
|
86
|
+
}
|
|
87
|
+
}
|