create-cartbase 0.1.20 → 0.1.22

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.
Files changed (35) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +25 -25
  3. package/dist/index.js +20 -20
  4. package/package.json +24 -24
  5. package/template/app/docs/auth.md +105 -105
  6. package/template/app/docs/carts.md +23 -4
  7. package/template/app/docs/categories.md +194 -194
  8. package/template/app/docs/checkout.md +714 -714
  9. package/template/app/docs/components.md +270 -24
  10. package/template/app/docs/consent.md +91 -91
  11. package/template/app/docs/deploy.md +197 -197
  12. package/template/app/docs/gift-cards.md +153 -153
  13. package/template/app/docs/metaobjects.md +126 -126
  14. package/template/app/docs/orders.md +221 -221
  15. package/template/app/docs/regions.md +269 -269
  16. package/template/app/docs/reviews.md +258 -238
  17. package/template/app/docs/search.md +227 -227
  18. package/template/app/docs/store.md +23 -1
  19. package/template/app/docs/subscriptions.md +148 -148
  20. package/template/app/docs/variables.md +331 -315
  21. package/template/app/package.json +1 -1
  22. package/template/app/postcss.config.cjs +11 -11
  23. package/template/app/src/app/checkout/checkout-empty.tsx +36 -0
  24. package/template/app/src/app/checkout/checkout-page-client.tsx +80 -73
  25. package/template/app/src/app/checkout/error.tsx +23 -0
  26. package/template/app/src/app/checkout/page.tsx +16 -7
  27. package/template/app/src/app/globals.css +26 -26
  28. package/template/app/src/app/layout.tsx +126 -126
  29. package/template/app/src/app/page.tsx +37 -37
  30. package/template/app/src/app/products/[handle]/page.tsx +89 -89
  31. package/template/app/src/app/search/page.tsx +33 -33
  32. package/template/app/src/lib/browser-client.ts +35 -35
  33. package/template/app/src/lib/catalog.ts +120 -120
  34. package/template/app/src/lib/server-client.ts +25 -25
  35. package/template/app/src/lib/store-client.ts +16 -16
@@ -9,7 +9,7 @@
9
9
  "typecheck": "tsc --noEmit"
10
10
  },
11
11
  "dependencies": {
12
- "@cartbase/storefront": "^0.20.0",
12
+ "@cartbase/storefront": "^0.22.0",
13
13
  "next": "16.2.4",
14
14
  "react": "19.2.4",
15
15
  "react-dom": "19.2.4"
@@ -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
+ }
@@ -0,0 +1,36 @@
1
+ import Link from "next/link"
2
+ import { defaultCheckoutLabels } from "@cartbase/storefront/checkout/labels"
3
+
4
+ /**
5
+ * The checkout with nothing to check out.
6
+ *
7
+ * A shopper who lands here with an empty cart used to be redirected to the
8
+ * home page without a word, which reads as the store losing their order.
9
+ * The page says it instead and offers the way back, which matters more here
10
+ * than anywhere: a checkout shell carries no header and no cart drawer, so
11
+ * the notice is the only door.
12
+ *
13
+ * The copy is the label pack's, so a store that mounts a locale gets it in
14
+ * its own language; this route renders on the server, above the provider,
15
+ * so it reads the defaults directly.
16
+ */
17
+ export function CheckoutEmpty() {
18
+ const labels = defaultCheckoutLabels
19
+
20
+ return (
21
+ <div className="mx-auto flex max-w-md flex-col items-center px-5 py-20 text-center">
22
+ <h1 className="text-xl font-semibold text-foreground">
23
+ {labels.cartEmptyTitle}
24
+ </h1>
25
+ <p className="mt-2 text-sm text-muted-foreground">
26
+ {labels.cartEmptyText}
27
+ </p>
28
+ <Link
29
+ href="/"
30
+ className="mt-6 inline-flex h-12 items-center justify-center rounded-xl bg-foreground px-8 text-sm font-semibold text-card transition-colors hover:bg-foreground/90"
31
+ >
32
+ {labels.cartEmptyBack}
33
+ </Link>
34
+ </div>
35
+ )
36
+ }
@@ -1,73 +1,80 @@
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
+ //
49
+ // NO PAYMENT FILTER HERE, on purpose (2026-09-18), for the same
50
+ // reason as the country list. This file used to pass a
51
+ // `paymentMethodFilter` that kept merchant methods and dropped every
52
+ // connected processor, so every scaffolded store was born ignoring
53
+ // its own card: a merchant could connect Stripe in the admin, see it
54
+ // reported as connected, and still be offered nothing but cash on
55
+ // delivery at checkout, with no error anywhere to explain it. What a
56
+ // store can be paid with is the store's answer, and the listing
57
+ // already gives it: processors it has connected and linked to the
58
+ // market, plus its enabled methods, rule-filtered server-side. The
59
+ // `paymentMethodFilter` seam stays for a store with a real reason to
60
+ // narrow it further, and a store with no processor connected needs
61
+ // no filter to hide one.
62
+ onOrderPlaced={(order) => {
63
+ // Guest order handle = the completeCart() response (orders.md).
64
+ // Stash it (+ the decorated cart lines for the items list) for
65
+ // the confirmation page, then navigate.
66
+ try {
67
+ sessionStorage.setItem(
68
+ LAST_ORDER_STORAGE_KEY,
69
+ JSON.stringify({ order, cartItems: cart.items ?? [] })
70
+ )
71
+ } catch {
72
+ // best-effort — the confirmation page has a fallback state
73
+ }
74
+ router.push(`/order/${order.id}/confirmed`)
75
+ }}
76
+ onCartChange={() => router.refresh()}
77
+ />
78
+ </CheckoutProvider>
79
+ )
80
+ }
@@ -0,0 +1,23 @@
1
+ "use client"
2
+
3
+ import { CheckoutErrorScreen } from "@cartbase/storefront/checkout/checkout-error-screen"
4
+ import { browserClient } from "@/lib/browser-client"
5
+
6
+ /**
7
+ * The checkout's error boundary. Next renders this in place of the page
8
+ * when anything in it throws, a server component included, so a shopper at
9
+ * the moment of paying sees the store's own words instead of the
10
+ * framework's error page, and the failure reaches the merchant's checkout
11
+ * error log rather than nobody.
12
+ */
13
+ export default function CheckoutError({
14
+ error,
15
+ reset,
16
+ }: {
17
+ error: Error & { digest?: string }
18
+ reset: () => void
19
+ }) {
20
+ return (
21
+ <CheckoutErrorScreen error={error} reset={reset} client={browserClient} />
22
+ )
23
+ }
@@ -8,6 +8,7 @@ import {
8
8
  } from "@cartbase/storefront/api/checkout"
9
9
  import { readCartCookie } from "@/lib/config"
10
10
  import { getServerClient } from "@/lib/server-client"
11
+ import { CheckoutEmpty } from "./checkout-empty"
11
12
  import { CheckoutPageClient } from "./checkout-page-client"
12
13
 
13
14
  /**
@@ -32,14 +33,22 @@ async function Checkout() {
32
33
  // Back-compat: prefer `_cartbase_cart`, fall back to the legacy
33
34
  // `_barter_cart_id` name (platform-fingerprints card).
34
35
  const cartId = readCartCookie((name) => jar.get(name)?.value)
35
- if (!cartId) redirect("/")
36
36
 
37
- const client = await getServerClient()
38
- const cart = await retrieveCart(client, cartId)
39
- .then((res) => res.cart)
40
- .catch(() => null)
41
- if (!cart || (cart.items ?? []).length === 0) redirect("/")
42
- if (cart.completed_at) redirect("/")
37
+ const client = cartId ? await getServerClient() : null
38
+ const cart =
39
+ client && cartId
40
+ ? await retrieveCart(client, cartId)
41
+ .then((res) => res.cart)
42
+ .catch(() => null)
43
+ : null
44
+
45
+ // A placed order still redirects: it has its own confirmation page.
46
+ if (cart?.completed_at) redirect("/")
47
+
48
+ // Nothing to check out, so the page SAYS so rather than bouncing the
49
+ // shopper home without a word (2026-09-18). A checkout shell carries no
50
+ // header and no drawer, so this notice is also the only way back.
51
+ if (!client || !cart || (cart.items ?? []).length === 0) return <CheckoutEmpty />
43
52
 
44
53
  const [shippingOptions, paymentProviders] = await Promise.all([
45
54
  listShippingOptions(client, { cart_id: cart.id }),
@@ -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,126 +1,126 @@
1
- import type { Metadata } from "next"
2
- import Link from "next/link"
3
- import type { Menu } from "@cartbase/storefront/api/menus"
4
- import { ConsentInit } from "@cartbase/storefront/tracking/consent-init"
5
- import { StorefrontTags } from "@cartbase/storefront/tracking/storefront-tags"
6
- import { TrackInit } from "@cartbase/storefront/tracking/track-init"
7
- import { CartButtonClient } from "@cartbase/storefront/common/cart-button-client"
8
- import { createStorefrontMetadata, PlatformInit } from "@cartbase/storefront/platform"
9
- import { STORE_LOCALE } from "@/lib/config"
10
- import { getConsentSettings, getMenuSafe, getStoreIdentity, getTracking } from "@/lib/catalog"
11
- import { Providers } from "./providers"
12
- import "./globals.css"
13
-
14
- // createStorefrontMetadata (platform-fingerprints card) stamps
15
- // `<meta name="generator" content="Cartbase" />` on every page — never
16
- // hand-write `generator` here.
17
- export async function generateMetadata(): Promise<Metadata> {
18
- const store = await getStoreIdentity()
19
- return createStorefrontMetadata({
20
- title: store.name,
21
- description: store.brand.slogan ?? undefined,
22
- })
23
- }
24
-
25
- function MenuNav({ menu }: { menu: Menu | null }) {
26
- if (!menu || menu.items.length === 0) return null
27
- return (
28
- <nav className="flex items-center gap-4">
29
- {menu.items.map((item) => (
30
- <Link
31
- key={`${item.title}-${item.url}`}
32
- href={item.url}
33
- className="text-sm text-muted-foreground hover:text-foreground"
34
- >
35
- {item.title}
36
- </Link>
37
- ))}
38
- </nav>
39
- )
40
- }
41
-
42
- /**
43
- * Runbook steps 3–4: bootstrap store config at layout level, then mount
44
- * order inside <body>: ConsentInit FIRST (static, synchronous), then the
45
- * consent-gated UI, then navigation. A store with no menus 404s every
46
- * handle — render no nav, never crash (menus.md).
47
- *
48
- * Nothing here reads the request. The store, the consent settings, the tags
49
- * and the menus are cached reads (lib/catalog.ts), so the shell is
50
- * prerendered and every page is served from the CDN (Cache Components). The
51
- * cart is the visitor's: the drawer reads its id from the cookie in the
52
- * browser and the cart after it (providers.tsx), and the cart button counts
53
- * the drawer's cart, so no page waits for the cart.
54
- */
55
- export default async function RootLayout({
56
- children,
57
- }: {
58
- children: React.ReactNode
59
- }) {
60
- const [store, consent, tracking, mainMenu, footerMenu] = await Promise.all([
61
- getStoreIdentity(),
62
- getConsentSettings(),
63
- getTracking(),
64
- getMenuSafe("main-menu"),
65
- getMenuSafe("footer"),
66
- ])
67
-
68
- return (
69
- <html lang={STORE_LOCALE.code}>
70
- <body>
71
- {/* The store's own consent switch decides the default: a store
72
- that collects consent starts every visitor denied until they
73
- choose, a store with the banner off starts them granted. The
74
- prop is required, so a layout cannot leave it out. */}
75
- <ConsentInit required={consent.enabled} />
76
- {/* Every marketing tag the store configured in the admin, mounted
77
- from its own config: Meta, TikTok, ChatGPT, Google (GA4 + Ads
78
- on one tag), GTM. Nothing to wire per vendor: saving the ids
79
- under Settings, Integrations is the whole merchant-side act.
80
- Order matters: ConsentInit sets the Consent Mode defaults
81
- synchronously ABOVE this, so every tag below inherits the
82
- gate. The config is a cached read handed over, so the tags
83
- cost the page nothing at request time. */}
84
- {tracking ? <StorefrontTags config={tracking} /> : null}
85
- {/* Captures UTMs and the ad-click ids (ttclid / gclid / gbraid /
86
- wbraid) that exist ONLY on an ad's landing URL — miss them
87
- here and no platform can attribute the order to the click. */}
88
- <TrackInit />
89
- <PlatformInit storeId={store.id} />
90
- <Providers consent={consent}>
91
- <header className="border-b border-border">
92
- <div className="max-w-7xl mx-auto px-4 h-16 flex items-center justify-between gap-6">
93
- <Link href="/" className="font-semibold text-lg">
94
- {store.name}
95
- </Link>
96
- <div className="flex items-center gap-6">
97
- <MenuNav menu={mainMenu} />
98
- <Link
99
- href="/search"
100
- className="text-sm text-muted-foreground hover:text-foreground"
101
- >
102
- Search
103
- </Link>
104
- {/*
105
- The component workbench lives at /gallery in THIS app only
106
- (the seed excludes it, src/lib/storefront/seed.ts), so the
107
- shop's own nav never links to it: a merchant's store must
108
- not carry a dead link.
109
- */}
110
- {/* No `cart` prop: the badge counts the drawer's own cart. */}
111
- <CartButtonClient />
112
- </div>
113
- </div>
114
- </header>
115
- <main>{children}</main>
116
- <footer className="border-t border-border mt-12">
117
- <div className="max-w-7xl mx-auto px-4 py-8 text-sm text-muted-foreground flex items-center justify-between">
118
- <span>{store.name}</span>
119
- <MenuNav menu={footerMenu} />
120
- </div>
121
- </footer>
122
- </Providers>
123
- </body>
124
- </html>
125
- )
126
- }
1
+ import type { Metadata } from "next"
2
+ import Link from "next/link"
3
+ import type { Menu } from "@cartbase/storefront/api/menus"
4
+ import { ConsentInit } from "@cartbase/storefront/tracking/consent-init"
5
+ import { StorefrontTags } from "@cartbase/storefront/tracking/storefront-tags"
6
+ import { TrackInit } from "@cartbase/storefront/tracking/track-init"
7
+ import { CartButtonClient } from "@cartbase/storefront/common/cart-button-client"
8
+ import { createStorefrontMetadata, PlatformInit } from "@cartbase/storefront/platform"
9
+ import { STORE_LOCALE } from "@/lib/config"
10
+ import { getConsentSettings, getMenuSafe, getStoreIdentity, getTracking } from "@/lib/catalog"
11
+ import { Providers } from "./providers"
12
+ import "./globals.css"
13
+
14
+ // createStorefrontMetadata (platform-fingerprints card) stamps
15
+ // `<meta name="generator" content="Cartbase" />` on every page — never
16
+ // hand-write `generator` here.
17
+ export async function generateMetadata(): Promise<Metadata> {
18
+ const store = await getStoreIdentity()
19
+ return createStorefrontMetadata({
20
+ title: store.name,
21
+ description: store.brand.slogan ?? undefined,
22
+ })
23
+ }
24
+
25
+ function MenuNav({ menu }: { menu: Menu | null }) {
26
+ if (!menu || menu.items.length === 0) return null
27
+ return (
28
+ <nav className="flex items-center gap-4">
29
+ {menu.items.map((item) => (
30
+ <Link
31
+ key={`${item.title}-${item.url}`}
32
+ href={item.url}
33
+ className="text-sm text-muted-foreground hover:text-foreground"
34
+ >
35
+ {item.title}
36
+ </Link>
37
+ ))}
38
+ </nav>
39
+ )
40
+ }
41
+
42
+ /**
43
+ * Runbook steps 3–4: bootstrap store config at layout level, then mount
44
+ * order inside <body>: ConsentInit FIRST (static, synchronous), then the
45
+ * consent-gated UI, then navigation. A store with no menus 404s every
46
+ * handle — render no nav, never crash (menus.md).
47
+ *
48
+ * Nothing here reads the request. The store, the consent settings, the tags
49
+ * and the menus are cached reads (lib/catalog.ts), so the shell is
50
+ * prerendered and every page is served from the CDN (Cache Components). The
51
+ * cart is the visitor's: the drawer reads its id from the cookie in the
52
+ * browser and the cart after it (providers.tsx), and the cart button counts
53
+ * the drawer's cart, so no page waits for the cart.
54
+ */
55
+ export default async function RootLayout({
56
+ children,
57
+ }: {
58
+ children: React.ReactNode
59
+ }) {
60
+ const [store, consent, tracking, mainMenu, footerMenu] = await Promise.all([
61
+ getStoreIdentity(),
62
+ getConsentSettings(),
63
+ getTracking(),
64
+ getMenuSafe("main-menu"),
65
+ getMenuSafe("footer"),
66
+ ])
67
+
68
+ return (
69
+ <html lang={STORE_LOCALE.code}>
70
+ <body>
71
+ {/* The store's own consent switch decides the default: a store
72
+ that collects consent starts every visitor denied until they
73
+ choose, a store with the banner off starts them granted. The
74
+ prop is required, so a layout cannot leave it out. */}
75
+ <ConsentInit required={consent.enabled} />
76
+ {/* Every marketing tag the store configured in the admin, mounted
77
+ from its own config: Meta, TikTok, ChatGPT, Google (GA4 + Ads
78
+ on one tag), GTM. Nothing to wire per vendor: saving the ids
79
+ under Settings, Integrations is the whole merchant-side act.
80
+ Order matters: ConsentInit sets the Consent Mode defaults
81
+ synchronously ABOVE this, so every tag below inherits the
82
+ gate. The config is a cached read handed over, so the tags
83
+ cost the page nothing at request time. */}
84
+ {tracking ? <StorefrontTags config={tracking} /> : null}
85
+ {/* Captures UTMs and the ad-click ids (ttclid / gclid / gbraid /
86
+ wbraid) that exist ONLY on an ad's landing URL — miss them
87
+ here and no platform can attribute the order to the click. */}
88
+ <TrackInit />
89
+ <PlatformInit storeId={store.id} />
90
+ <Providers consent={consent}>
91
+ <header className="border-b border-border">
92
+ <div className="max-w-7xl mx-auto px-4 h-16 flex items-center justify-between gap-6">
93
+ <Link href="/" className="font-semibold text-lg">
94
+ {store.name}
95
+ </Link>
96
+ <div className="flex items-center gap-6">
97
+ <MenuNav menu={mainMenu} />
98
+ <Link
99
+ href="/search"
100
+ className="text-sm text-muted-foreground hover:text-foreground"
101
+ >
102
+ Search
103
+ </Link>
104
+ {/*
105
+ The component workbench lives at /gallery in THIS app only
106
+ (the seed excludes it, src/lib/storefront/seed.ts), so the
107
+ shop's own nav never links to it: a merchant's store must
108
+ not carry a dead link.
109
+ */}
110
+ {/* No `cart` prop: the badge counts the drawer's own cart. */}
111
+ <CartButtonClient />
112
+ </div>
113
+ </div>
114
+ </header>
115
+ <main>{children}</main>
116
+ <footer className="border-t border-border mt-12">
117
+ <div className="max-w-7xl mx-auto px-4 py-8 text-sm text-muted-foreground flex items-center justify-between">
118
+ <span>{store.name}</span>
119
+ <MenuNav menu={footerMenu} />
120
+ </div>
121
+ </footer>
122
+ </Providers>
123
+ </body>
124
+ </html>
125
+ )
126
+ }