create-cartbase 0.0.1 → 0.1.1

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 (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +9 -3
  3. package/dist/index.js +94 -0
  4. package/package.json +18 -4
  5. package/template/app/CLAUDE.md +18 -0
  6. package/template/app/docs/BUILD-A-STOREFRONT.md +216 -0
  7. package/template/app/docs/README.md +77 -0
  8. package/template/app/docs/auth.md +105 -0
  9. package/template/app/docs/carts.md +376 -0
  10. package/template/app/docs/categories.md +194 -0
  11. package/template/app/docs/checkout.md +611 -0
  12. package/template/app/docs/collections.md +167 -0
  13. package/template/app/docs/components.md +1090 -0
  14. package/template/app/docs/consent.md +81 -0
  15. package/template/app/docs/content.md +126 -0
  16. package/template/app/docs/customers.md +269 -0
  17. package/template/app/docs/deploy.md +192 -0
  18. package/template/app/docs/gift-cards.md +153 -0
  19. package/template/app/docs/integrations.md +137 -0
  20. package/template/app/docs/menus.md +73 -0
  21. package/template/app/docs/metaobjects.md +126 -0
  22. package/template/app/docs/orders.md +221 -0
  23. package/template/app/docs/platform.md +126 -0
  24. package/template/app/docs/products.md +300 -0
  25. package/template/app/docs/redirects.md +50 -0
  26. package/template/app/docs/regions.md +206 -0
  27. package/template/app/docs/reviews.md +223 -0
  28. package/template/app/docs/search.md +218 -0
  29. package/template/app/docs/subscriptions.md +148 -0
  30. package/template/app/next.config.ts +34 -0
  31. package/template/app/package.json +25 -0
  32. package/template/app/postcss.config.cjs +6 -0
  33. package/template/app/smoke.mjs +158 -0
  34. package/template/app/src/app/checkout/checkout-page-client.tsx +66 -0
  35. package/template/app/src/app/checkout/page.tsx +51 -0
  36. package/template/app/src/app/globals.css +42 -0
  37. package/template/app/src/app/layout.tsx +113 -0
  38. package/template/app/src/app/order/[id]/confirmed/page.tsx +77 -0
  39. package/template/app/src/app/page.tsx +25 -0
  40. package/template/app/src/app/products/[handle]/page.tsx +58 -0
  41. package/template/app/src/app/providers.tsx +54 -0
  42. package/template/app/src/app/search/page.tsx +20 -0
  43. package/template/app/src/lib/browser-client.ts +35 -0
  44. package/template/app/src/lib/cart-actions.ts +47 -0
  45. package/template/app/src/lib/config.ts +21 -0
  46. package/template/app/src/lib/server-client.ts +25 -0
  47. package/template/app/tailwind.config.cjs +9 -0
  48. package/template/app/tsconfig.json +41 -0
  49. package/template/app/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,51 @@
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 { getIntegrationsConfig } from "@cartbase/storefront/api/integrations"
9
+ import { readCartCookie } from "@/lib/config"
10
+ import { getServerClient } from "@/lib/server-client"
11
+ import { CheckoutPageClient } from "./checkout-page-client"
12
+
13
+ /**
14
+ * Checkout (runbook step 8 / checkout.md): list shipping options and
15
+ * payment providers WITH `cart_id` (server-side rule filtering), fetch the
16
+ * integrations `cod` block, and hand everything to the orchestrated
17
+ * client layout. Redirect completed/missing carts server-side
18
+ * (checkout-client mount rule).
19
+ */
20
+ export default async function CheckoutPage() {
21
+ const jar = await cookies()
22
+ // Back-compat: prefer `_cartbase_cart`, fall back to the legacy
23
+ // `_barter_cart_id` name (platform-fingerprints card).
24
+ const cartId = readCartCookie((name) => jar.get(name)?.value)
25
+ if (!cartId) redirect("/")
26
+
27
+ const client = await getServerClient()
28
+ const cart = await retrieveCart(client, cartId)
29
+ .then((res) => res.cart)
30
+ .catch(() => null)
31
+ if (!cart || (cart.items ?? []).length === 0) redirect("/")
32
+ if (cart.completed_at) redirect("/")
33
+
34
+ const [shippingOptions, paymentProviders, integrations] = await Promise.all([
35
+ listShippingOptions(client, { cart_id: cart.id }),
36
+ listPaymentProviders(client, {
37
+ cart_id: cart.id,
38
+ region_id: cart.region_id ?? undefined,
39
+ }),
40
+ getIntegrationsConfig(client),
41
+ ])
42
+
43
+ return (
44
+ <CheckoutPageClient
45
+ cart={cart}
46
+ shippingOptions={shippingOptions.shipping_options}
47
+ paymentProviders={paymentProviders.payment_providers}
48
+ codConfig={integrations.cod}
49
+ />
50
+ )
51
+ }
@@ -0,0 +1,42 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ /*
6
+ * Storefront theme tokens — shadcn-standard names, raw oklch channels
7
+ * (the @cartbase/storefront tailwind-preset wraps them in oklch(...)).
8
+ * Neutral light theme for the reference app.
9
+ */
10
+ :root {
11
+ --background: 1 0 0;
12
+ --foreground: 0.145 0 0;
13
+ --card: 1 0 0;
14
+ --card-foreground: 0.145 0 0;
15
+ --popover: 1 0 0;
16
+ --popover-foreground: 0.145 0 0;
17
+ --primary: 0.205 0 0;
18
+ --primary-foreground: 0.985 0 0;
19
+ --secondary: 0.97 0 0;
20
+ --secondary-foreground: 0.205 0 0;
21
+ --muted: 0.97 0 0;
22
+ --muted-foreground: 0.556 0 0;
23
+ --accent: 0.97 0 0;
24
+ --accent-foreground: 0.205 0 0;
25
+ --destructive: 0.577 0.245 27.325;
26
+ --destructive-foreground: 0.985 0 0;
27
+ --border: 0.922 0 0;
28
+ --input: 0.922 0 0;
29
+ --ring: 0.708 0 0;
30
+ --success: 0.648 0.15 160;
31
+ --success-foreground: 0.985 0 0;
32
+ --warning: 0.828 0.189 84.429;
33
+ --warning-foreground: 0.279 0.077 45.635;
34
+ --radius: 0.5rem;
35
+ --font-display: ui-serif;
36
+ --font-body: system-ui;
37
+ --font-mono: ui-monospace;
38
+ }
39
+
40
+ body {
41
+ @apply bg-background text-foreground font-body;
42
+ }
@@ -0,0 +1,113 @@
1
+ import type { Metadata } from "next"
2
+ import Link from "next/link"
3
+ import { cookies } from "next/headers"
4
+ import { retrieveCart, type Cart } from "@cartbase/storefront/api/carts"
5
+ import { getConsent } from "@cartbase/storefront/api/consent"
6
+ import { getMenu, type Menu } from "@cartbase/storefront/api/menus"
7
+ import { ConsentInit } from "@cartbase/storefront/tracking/consent-init"
8
+ import { CartButtonClient } from "@cartbase/storefront/common/cart-button-client"
9
+ import { createStorefrontMetadata, PlatformInit } from "@cartbase/storefront/platform"
10
+ import { BARTER_CLIENT_ID, readCartCookie } from "@/lib/config"
11
+ import { getServerClient } from "@/lib/server-client"
12
+ import { Providers } from "./providers"
13
+ import "./globals.css"
14
+
15
+ // createStorefrontMetadata (platform-fingerprints card) stamps
16
+ // `<meta name="generator" content="Cartbase" />` on every page — never
17
+ // hand-write `generator` here.
18
+ export const metadata: Metadata = createStorefrontMetadata({
19
+ title: "Barter Example Store",
20
+ description: "Reference storefront built on @cartbase/storefront",
21
+ })
22
+
23
+ /**
24
+ * Runbook steps 3–4: bootstrap store config at layout level, then mount
25
+ * order inside <body>: ConsentInit FIRST (static, synchronous), then the
26
+ * consent-gated UI, then navigation. A store with no menus 404s every
27
+ * handle — render no nav, never crash (menus.md).
28
+ */
29
+ async function fetchMenu(handle: string): Promise<Menu | null> {
30
+ const client = await getServerClient()
31
+ try {
32
+ const { menu } = await getMenu(client, handle)
33
+ return menu
34
+ } catch {
35
+ return null // unknown/deleted handle (404) → graceful "no nav"
36
+ }
37
+ }
38
+
39
+ function MenuNav({ menu }: { menu: Menu | null }) {
40
+ if (!menu || menu.items.length === 0) return null
41
+ return (
42
+ <nav className="flex items-center gap-4">
43
+ {menu.items.map((item) => (
44
+ <Link
45
+ key={`${item.title}-${item.url}`}
46
+ href={item.url}
47
+ className="text-sm text-muted-foreground hover:text-foreground"
48
+ >
49
+ {item.title}
50
+ </Link>
51
+ ))}
52
+ </nav>
53
+ )
54
+ }
55
+
56
+ export default async function RootLayout({
57
+ children,
58
+ }: {
59
+ children: React.ReactNode
60
+ }) {
61
+ const client = await getServerClient()
62
+ const jar = await cookies()
63
+ // Back-compat: prefer the new `_cartbase_cart` cookie, fall back to the
64
+ // legacy `_barter_cart_id` name so an existing visitor's cart survives
65
+ // the rename (platform-fingerprints card).
66
+ const cartId = readCartCookie((name) => jar.get(name)?.value) ?? null
67
+
68
+ const [consentRes, mainMenu, footerMenu, cart] = await Promise.all([
69
+ getConsent(client),
70
+ fetchMenu("main-menu"),
71
+ fetchMenu("footer"),
72
+ cartId
73
+ ? retrieveCart(client, cartId)
74
+ .then((res): Cart | null => res.cart)
75
+ .catch(() => null) // stale cookie → empty cart state
76
+ : Promise.resolve(null),
77
+ ])
78
+
79
+ return (
80
+ <html lang="en">
81
+ <body>
82
+ <ConsentInit />
83
+ <PlatformInit storeId={BARTER_CLIENT_ID} />
84
+ <Providers cart={cart} consent={consentRes.consent}>
85
+ <header className="border-b border-border">
86
+ <div className="max-w-7xl mx-auto px-4 h-16 flex items-center justify-between gap-6">
87
+ <Link href="/" className="font-semibold text-lg">
88
+ Barter Example Store
89
+ </Link>
90
+ <div className="flex items-center gap-6">
91
+ <MenuNav menu={mainMenu} />
92
+ <Link
93
+ href="/search"
94
+ className="text-sm text-muted-foreground hover:text-foreground"
95
+ >
96
+ Search
97
+ </Link>
98
+ <CartButtonClient cart={cart} />
99
+ </div>
100
+ </div>
101
+ </header>
102
+ <main>{children}</main>
103
+ <footer className="border-t border-border mt-12">
104
+ <div className="max-w-7xl mx-auto px-4 py-8 text-sm text-muted-foreground flex items-center justify-between">
105
+ <span>Barter Example Store</span>
106
+ <MenuNav menu={footerMenu} />
107
+ </div>
108
+ </footer>
109
+ </Providers>
110
+ </body>
111
+ </html>
112
+ )
113
+ }
@@ -0,0 +1,77 @@
1
+ "use client"
2
+
3
+ import { useEffect, useState } from "react"
4
+ import Link from "next/link"
5
+ import type { CompletedOrder, CartLineItem } from "@cartbase/storefront/api/carts"
6
+ import {
7
+ OrderCompletedTemplate,
8
+ } from "@cartbase/storefront/order/order-completed-template"
9
+ import {
10
+ displayItemFromCartLine,
11
+ } from "@cartbase/storefront/order/order-item"
12
+ import {
13
+ orderTotalsFromSummary,
14
+ } from "@cartbase/storefront/order/order-totals"
15
+ import { LAST_ORDER_STORAGE_KEY } from "../../../checkout/checkout-page-client"
16
+
17
+ /**
18
+ * Order confirmation (runbook step 8.6 + orders.md): guests have NO
19
+ * anonymous order read — the `completeCart()` response is the only order
20
+ * handle, so the checkout page stashes it in sessionStorage and this page
21
+ * renders it through the order family. Totals come from the order's
22
+ * summary snapshot (`orderTotalsFromSummary`); items keep the decorated
23
+ * cart lines' server-computed totals (`displayItemFromCartLine`).
24
+ */
25
+ type Stash = { order: CompletedOrder; cartItems: CartLineItem[] }
26
+
27
+ export default function OrderConfirmedPage() {
28
+ const [stash, setStash] = useState<Stash | null | "missing">(null)
29
+
30
+ useEffect(() => {
31
+ try {
32
+ const raw = sessionStorage.getItem(LAST_ORDER_STORAGE_KEY)
33
+ setStash(raw ? (JSON.parse(raw) as Stash) : "missing")
34
+ } catch {
35
+ setStash("missing")
36
+ }
37
+ }, [])
38
+
39
+ if (stash === null) return null // first paint before sessionStorage read
40
+
41
+ if (stash === "missing") {
42
+ return (
43
+ <div className="max-w-xl mx-auto px-4 py-16 text-center">
44
+ <h1 className="text-xl font-semibold mb-2">Order placed</h1>
45
+ <p className="text-muted-foreground mb-6">
46
+ Your order was placed successfully. A confirmation email is on its
47
+ way.
48
+ </p>
49
+ <Link href="/" className="underline">
50
+ Continue shopping
51
+ </Link>
52
+ </div>
53
+ )
54
+ }
55
+
56
+ const { order, cartItems } = stash
57
+ const totals = orderTotalsFromSummary(order.summary) ?? {
58
+ total: null,
59
+ }
60
+ // The completeCart() order carries FLATTENED items (checkout.md), not the
61
+ // order-detail pivot shape the template's default converter expects —
62
+ // pass the normalized cart-line items instead and drop `order.items`.
63
+ const orderForTemplate = { ...order, items: undefined }
64
+
65
+ return (
66
+ <OrderCompletedTemplate
67
+ order={orderForTemplate}
68
+ totals={totals}
69
+ items={cartItems.map(displayItemFromCartLine)}
70
+ // This reference store enables exactly one provider (pp_manual); a
71
+ // multi-provider store should stash the chosen provider id from its
72
+ // checkout state alongside the order.
73
+ paymentProviderId={"pp_manual"}
74
+ storeHref="/"
75
+ />
76
+ )
77
+ }
@@ -0,0 +1,25 @@
1
+ import { StoreTemplate } from "@cartbase/storefront/store/store-template"
2
+ import type { SortOptions } from "@cartbase/storefront/lib/sort-products"
3
+ import { PRICING_CONTEXT } 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
+ />
24
+ )
25
+ }
@@ -0,0 +1,58 @@
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 { addToCartAction } from "@/lib/cart-actions"
6
+ import { PRICING_CONTEXT } from "@/lib/config"
7
+ import { getServerClient } from "@/lib/server-client"
8
+
9
+ /**
10
+ * PDP (runbook step 5): fetch by handle WITH the pricing context, render
11
+ * the full product template. `addToCart` is the app-owned seam — a server
12
+ * action that owns the cart cookie (products family contract).
13
+ */
14
+ export default async function ProductPage({
15
+ params,
16
+ }: {
17
+ params: Promise<{ handle: string }>
18
+ }) {
19
+ const { handle } = await params
20
+ const client = await getServerClient()
21
+
22
+ let product
23
+ try {
24
+ const res = await retrieveProduct(client, handle, PRICING_CONTEXT)
25
+ product = res.product
26
+ } catch (e) {
27
+ if (e instanceof StoreApiError && e.status === 404) notFound()
28
+ throw e
29
+ }
30
+
31
+ return (
32
+ <ProductTemplate
33
+ client={client}
34
+ product={product}
35
+ pricingContext={PRICING_CONTEXT}
36
+ addToCart={addToCartAction}
37
+ />
38
+ )
39
+ }
40
+
41
+ export async function generateMetadata({
42
+ params,
43
+ }: {
44
+ params: Promise<{ handle: string }>
45
+ }) {
46
+ const { handle } = await params
47
+ const client = await getServerClient()
48
+ try {
49
+ const { product } = await retrieveProduct(client, handle)
50
+ // SEO fields with title/description fallbacks (runbook step 5).
51
+ return {
52
+ title: product.seo_title ?? product.title,
53
+ description: product.seo_description ?? product.description ?? undefined,
54
+ }
55
+ } catch {
56
+ return {}
57
+ }
58
+ }
@@ -0,0 +1,54 @@
1
+ "use client"
2
+
3
+ import type { Cart } from "@cartbase/storefront/api/carts"
4
+ import { CartDrawerProvider } from "@cartbase/storefront/cart-drawer/context"
5
+ import { CartDrawerTemplate } from "@cartbase/storefront/cart-drawer/template"
6
+ import { ConsentBanner } from "@cartbase/storefront/tracking/consent-banner"
7
+ import {
8
+ pickConsentCopy,
9
+ shouldRenderBanner,
10
+ type ConsentSettings,
11
+ } from "@cartbase/storefront/tracking/consent"
12
+ import { CART_COOKIE, CART_COOKIE_MAX_AGE } from "@/lib/config"
13
+ import { browserClient } from "@/lib/browser-client"
14
+
15
+ /**
16
+ * Client-side shell: ONE CartDrawerProvider per app (cart-drawer family
17
+ * mount rule) + the consent banner (consent.md: render the builtin banner
18
+ * only when `enabled && mode === "builtin"`; ConsentInit stays in the
19
+ * server layout as the first child of <body>).
20
+ */
21
+ export function Providers({
22
+ cart,
23
+ consent,
24
+ children,
25
+ }: {
26
+ cart: Cart | null
27
+ consent: ConsentSettings
28
+ children: React.ReactNode
29
+ }) {
30
+ const copy = pickConsentCopy(consent.copy, "en")
31
+ return (
32
+ <CartDrawerProvider
33
+ cart={cart}
34
+ client={browserClient}
35
+ onCartChange={(next) => {
36
+ // Persist the cart id (carts.md: the app owns the cart cookie).
37
+ document.cookie = `${CART_COOKIE}=${encodeURIComponent(
38
+ next.id
39
+ )};path=/;max-age=${CART_COOKIE_MAX_AGE};samesite=lax`
40
+ }}
41
+ >
42
+ {children}
43
+ <CartDrawerTemplate />
44
+ {shouldRenderBanner(consent) && copy && (
45
+ <ConsentBanner
46
+ copy={copy}
47
+ layout={consent.layout}
48
+ privacyHref={consent.privacy_href}
49
+ rejectOnFirstLayer={consent.reject_on_first_layer}
50
+ />
51
+ )}
52
+ </CartDrawerProvider>
53
+ )
54
+ }
@@ -0,0 +1,20 @@
1
+ import { SearchTemplate } from "@cartbase/storefront/store/search-template"
2
+ import { PRICING_CONTEXT } from "@/lib/config"
3
+ import { getServerClient } from "@/lib/server-client"
4
+
5
+ /** Search page (runbook step 5) — fully URL-state driven search-template. */
6
+ export default async function SearchPage({
7
+ searchParams,
8
+ }: {
9
+ searchParams: Promise<Record<string, string | string[] | undefined>>
10
+ }) {
11
+ const params = await searchParams
12
+ const client = await getServerClient()
13
+ return (
14
+ <SearchTemplate
15
+ client={client}
16
+ searchParams={params}
17
+ pricingContext={PRICING_CONTEXT}
18
+ />
19
+ )
20
+ }
@@ -0,0 +1,35 @@
1
+ "use client"
2
+
3
+ import { StorefrontClient } from "@cartbase/storefront/api/http"
4
+ import {
5
+ BARTER_CLIENT_ID,
6
+ BARTER_PUBLISHABLE_KEY,
7
+ BARTER_URL,
8
+ LOCALE_COOKIE,
9
+ } from "./config"
10
+
11
+ function readCookie(name: string): string | null {
12
+ if (typeof document === "undefined") return null
13
+ const match = document.cookie
14
+ .split("; ")
15
+ .find((row) => row.startsWith(`${name}=`))
16
+ return match ? decodeURIComponent(match.slice(name.length + 1)) : null
17
+ }
18
+
19
+ /**
20
+ * Browser-scope StorefrontClient (runbook step 2): constructed once per
21
+ * app. Guest-only reference app — no auth token store.
22
+ *
23
+ * baseUrl is the APP origin, not the API origin: the store API ships no
24
+ * CORS headers, so browser calls ride the same-origin `/api/store/*`
25
+ * rewrite (next.config.ts). During SSR of client components no fetches
26
+ * run — the API origin stands in only to satisfy the constructor.
27
+ */
28
+ export const browserClient = new StorefrontClient({
29
+ baseUrl:
30
+ typeof window !== "undefined" ? window.location.origin : BARTER_URL,
31
+ clientId: BARTER_CLIENT_ID,
32
+ publishableKey: BARTER_PUBLISHABLE_KEY,
33
+ getAuthToken: () => null,
34
+ getLocale: () => readCookie(LOCALE_COOKIE),
35
+ })
@@ -0,0 +1,47 @@
1
+ "use server"
2
+
3
+ import { revalidatePath } from "next/cache"
4
+ import { cookies } from "next/headers"
5
+ import { addLineItem, createCart } from "@cartbase/storefront/api/carts"
6
+ import { CART_COOKIE, CART_COOKIE_MAX_AGE, readCartCookie } from "./config"
7
+ import { getServerClient } from "./server-client"
8
+
9
+ /**
10
+ * PDP add-to-cart server action (carts.md: create the cart lazily on the
11
+ * first add — region falls back to the store default; persist `cart.id`
12
+ * in a cookie). Passed into `ProductTemplate`'s `addToCart` seam.
13
+ *
14
+ * Reads via `readCartCookie` (new `_cartbase_cart` name, falls back to the
15
+ * legacy `_barter_cart_id` — platform-fingerprints card); every WRITE uses
16
+ * the new name only, so a visitor's cart survives the rename.
17
+ */
18
+ export async function addToCartAction(input: {
19
+ variantId: string
20
+ quantity: number
21
+ }): Promise<void> {
22
+ const client = await getServerClient()
23
+ const jar = await cookies()
24
+ const cartId = readCartCookie((name) => jar.get(name)?.value)
25
+
26
+ if (cartId) {
27
+ await addLineItem(client, cartId, {
28
+ variant_id: input.variantId,
29
+ quantity: input.quantity,
30
+ })
31
+ } else {
32
+ const { cart } = await createCart(client, {
33
+ currency_code: "eur",
34
+ items: [{ variant_id: input.variantId, quantity: input.quantity }],
35
+ })
36
+ jar.set(CART_COOKIE, cart.id, {
37
+ path: "/",
38
+ sameSite: "lax",
39
+ httpOnly: false, // the browser-side drawer persists the same cookie
40
+ maxAge: CART_COOKIE_MAX_AGE,
41
+ })
42
+ }
43
+
44
+ // Layout refetches the cart snapshot → CartDrawerProvider sees the new
45
+ // product-line count and auto-opens the drawer.
46
+ revalidatePath("/", "layout")
47
+ }
@@ -0,0 +1,21 @@
1
+ /** Shared storefront configuration (BUILD-A-STOREFRONT.md — inputs table). */
2
+
3
+ export const BARTER_URL = process.env.NEXT_PUBLIC_CARTBASE_URL ?? ""
4
+ export const BARTER_CLIENT_ID = process.env.NEXT_PUBLIC_CARTBASE_CLIENT_ID ?? ""
5
+ export const BARTER_PUBLISHABLE_KEY =
6
+ process.env.NEXT_PUBLIC_CARTBASE_PUBLISHABLE_KEY || undefined
7
+
8
+ /**
9
+ * Cart-id cookie — owned by the app (the SDK never persists the cart);
10
+ * the NAME is owned by the library so every consumer emits the same wire
11
+ * fingerprint (platform-fingerprints card). `readCartCookie` is the
12
+ * backward-compat reader for the legacy `_barter_cart_id` name.
13
+ */
14
+ export { CART_COOKIE, readCartCookie } from "@cartbase/storefront/lib/cookie-names"
15
+ export const CART_COOKIE_MAX_AGE = 60 * 60 * 24 * 30 // 30 days
16
+
17
+ /** Locale cookie read by the clients' `getLocale`. */
18
+ export const LOCALE_COOKIE = "_barter_locale"
19
+
20
+ /** Pricing context for every catalog surface (runbook step 5). EUR only. */
21
+ export const PRICING_CONTEXT = { currency_code: "eur" } as const
@@ -0,0 +1,25 @@
1
+ import { cookies } from "next/headers"
2
+ import { StorefrontClient } from "@cartbase/storefront/api/http"
3
+ import {
4
+ BARTER_CLIENT_ID,
5
+ BARTER_PUBLISHABLE_KEY,
6
+ BARTER_URL,
7
+ LOCALE_COOKIE,
8
+ } from "./config"
9
+
10
+ /**
11
+ * Server-scope StorefrontClient (runbook step 2): construct per request;
12
+ * `getLocale` reads the locale cookie. No customer accounts in this
13
+ * reference app, so `getAuthToken` always answers null (guest).
14
+ */
15
+ export async function getServerClient(): Promise<StorefrontClient> {
16
+ const jar = await cookies()
17
+ const locale = jar.get(LOCALE_COOKIE)?.value ?? null
18
+ return new StorefrontClient({
19
+ baseUrl: BARTER_URL,
20
+ clientId: BARTER_CLIENT_ID,
21
+ publishableKey: BARTER_PUBLISHABLE_KEY,
22
+ getAuthToken: () => null,
23
+ getLocale: () => locale,
24
+ })
25
+ }
@@ -0,0 +1,9 @@
1
+ /** @type {import('tailwindcss').Config} */
2
+ module.exports = {
3
+ presets: [require("@cartbase/storefront/tailwind-preset")],
4
+ content: [
5
+ "./src/**/*.{ts,tsx}",
6
+ // The component library ships source — scan it so its classes exist.
7
+ "../../packages/storefront/src/**/*.{ts,tsx}",
8
+ ],
9
+ }
@@ -0,0 +1,41 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "lib": [
5
+ "dom",
6
+ "dom.iterable",
7
+ "esnext"
8
+ ],
9
+ "allowJs": true,
10
+ "skipLibCheck": true,
11
+ "strict": true,
12
+ "noEmit": true,
13
+ "esModuleInterop": true,
14
+ "module": "esnext",
15
+ "moduleResolution": "bundler",
16
+ "resolveJsonModule": true,
17
+ "isolatedModules": true,
18
+ "jsx": "react-jsx",
19
+ "incremental": true,
20
+ "plugins": [
21
+ {
22
+ "name": "next"
23
+ }
24
+ ],
25
+ "paths": {
26
+ "@/*": [
27
+ "./src/*"
28
+ ]
29
+ }
30
+ },
31
+ "include": [
32
+ "next-env.d.ts",
33
+ "**/*.ts",
34
+ "**/*.tsx",
35
+ ".next/types/**/*.ts",
36
+ ".next/dev/types/**/*.ts"
37
+ ],
38
+ "exclude": [
39
+ "node_modules"
40
+ ]
41
+ }