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.
- package/LICENSE +21 -21
- package/README.md +25 -25
- package/dist/index.js +20 -20
- package/package.json +24 -24
- package/template/app/docs/auth.md +105 -105
- package/template/app/docs/carts.md +23 -4
- package/template/app/docs/categories.md +194 -194
- package/template/app/docs/checkout.md +714 -714
- package/template/app/docs/components.md +270 -24
- 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/regions.md +269 -269
- package/template/app/docs/reviews.md +258 -238
- package/template/app/docs/search.md +227 -227
- package/template/app/docs/store.md +23 -1
- package/template/app/docs/subscriptions.md +148 -148
- package/template/app/docs/variables.md +331 -315
- package/template/app/package.json +1 -1
- package/template/app/postcss.config.cjs +11 -11
- package/template/app/src/app/checkout/checkout-empty.tsx +36 -0
- package/template/app/src/app/checkout/checkout-page-client.tsx +80 -73
- package/template/app/src/app/checkout/error.tsx +23 -0
- package/template/app/src/app/checkout/page.tsx +16 -7
- package/template/app/src/app/globals.css +26 -26
- package/template/app/src/app/layout.tsx +126 -126
- package/template/app/src/app/page.tsx +37 -37
- package/template/app/src/app/products/[handle]/page.tsx +89 -89
- package/template/app/src/app/search/page.tsx +33 -33
- package/template/app/src/lib/browser-client.ts +35 -35
- package/template/app/src/lib/catalog.ts +120 -120
- package/template/app/src/lib/server-client.ts +25 -25
- package/template/app/src/lib/store-client.ts +16 -16
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
import { Suspense } from "react"
|
|
2
|
-
import { StoreTemplate } from "@cartbase/storefront/store/store-template"
|
|
3
|
-
import { SkeletonProductGrid } from "@cartbase/storefront/store/skeleton-product-grid"
|
|
4
|
-
import type { SortOptions } from "@cartbase/storefront/lib/sort-products"
|
|
5
|
-
import { PRICING_CONTEXT, STORE_LOCALE } from "@/lib/config"
|
|
6
|
-
import { storeClient } from "@/lib/store-client"
|
|
7
|
-
|
|
8
|
-
type ListingQuery = { sortBy?: string; page?: string }
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Home = the all-products listing (runbook step 5: always pass a pricing
|
|
12
|
-
* context or prices come back undecorated). The sort and the page number
|
|
13
|
-
* are request data, so the listing renders behind a boundary and the shell
|
|
14
|
-
* around it stays prerendered.
|
|
15
|
-
*/
|
|
16
|
-
export default function HomePage({ searchParams }: { searchParams: Promise<ListingQuery> }) {
|
|
17
|
-
return (
|
|
18
|
-
<Suspense fallback={<SkeletonProductGrid />}>
|
|
19
|
-
<Listing searchParams={searchParams} />
|
|
20
|
-
</Suspense>
|
|
21
|
-
)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async function Listing({ searchParams }: { searchParams: Promise<ListingQuery> }) {
|
|
25
|
-
const { sortBy, page } = await searchParams
|
|
26
|
-
return (
|
|
27
|
-
<StoreTemplate
|
|
28
|
-
client={storeClient}
|
|
29
|
-
sortBy={sortBy as SortOptions | undefined}
|
|
30
|
-
page={page}
|
|
31
|
-
pricingContext={PRICING_CONTEXT}
|
|
32
|
-
// Server-rendered: the locale provider cannot reach it, so the pack
|
|
33
|
-
// comes as a prop, and the prop is required.
|
|
34
|
-
labels={STORE_LOCALE.store}
|
|
35
|
-
/>
|
|
36
|
-
)
|
|
37
|
-
}
|
|
1
|
+
import { Suspense } from "react"
|
|
2
|
+
import { StoreTemplate } from "@cartbase/storefront/store/store-template"
|
|
3
|
+
import { SkeletonProductGrid } from "@cartbase/storefront/store/skeleton-product-grid"
|
|
4
|
+
import type { SortOptions } from "@cartbase/storefront/lib/sort-products"
|
|
5
|
+
import { PRICING_CONTEXT, STORE_LOCALE } from "@/lib/config"
|
|
6
|
+
import { storeClient } from "@/lib/store-client"
|
|
7
|
+
|
|
8
|
+
type ListingQuery = { sortBy?: string; page?: string }
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Home = the all-products listing (runbook step 5: always pass a pricing
|
|
12
|
+
* context or prices come back undecorated). The sort and the page number
|
|
13
|
+
* are request data, so the listing renders behind a boundary and the shell
|
|
14
|
+
* around it stays prerendered.
|
|
15
|
+
*/
|
|
16
|
+
export default function HomePage({ searchParams }: { searchParams: Promise<ListingQuery> }) {
|
|
17
|
+
return (
|
|
18
|
+
<Suspense fallback={<SkeletonProductGrid />}>
|
|
19
|
+
<Listing searchParams={searchParams} />
|
|
20
|
+
</Suspense>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function Listing({ searchParams }: { searchParams: Promise<ListingQuery> }) {
|
|
25
|
+
const { sortBy, page } = await searchParams
|
|
26
|
+
return (
|
|
27
|
+
<StoreTemplate
|
|
28
|
+
client={storeClient}
|
|
29
|
+
sortBy={sortBy as SortOptions | undefined}
|
|
30
|
+
page={page}
|
|
31
|
+
pricingContext={PRICING_CONTEXT}
|
|
32
|
+
// Server-rendered: the locale provider cannot reach it, so the pack
|
|
33
|
+
// comes as a prop, and the prop is required.
|
|
34
|
+
labels={STORE_LOCALE.store}
|
|
35
|
+
/>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
@@ -1,89 +1,89 @@
|
|
|
1
|
-
import { notFound } from "next/navigation"
|
|
2
|
-
import { ProductTemplate } from "@cartbase/storefront/products/product-template"
|
|
3
|
-
import { getProduct, listProductHandles } from "@/lib/catalog"
|
|
4
|
-
import { PRICING_CONTEXT, STORE_LOCALE } from "@/lib/config"
|
|
5
|
-
import { storeClient } from "@/lib/store-client"
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Every product's page is prerendered at build, so it is a file on the CDN.
|
|
9
|
-
* A product added after the last deploy renders on its first visit and is
|
|
10
|
-
* kept from then on. Without this list the handle would be request data and
|
|
11
|
-
* every product page would render per visit (Cache Components).
|
|
12
|
-
*/
|
|
13
|
-
export async function generateStaticParams() {
|
|
14
|
-
const handles = await listProductHandles()
|
|
15
|
-
// Cache Components refuses an empty list; a store with no products yet
|
|
16
|
-
// still builds, and the placeholder answers 404.
|
|
17
|
-
return handles.length ? handles.map((handle) => ({ handle })) : [{ handle: "__none__" }]
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* PDP (runbook step 5): the product is a cached read by handle WITH the
|
|
22
|
-
* pricing context (lib/catalog.ts), and the template renders it. The buy
|
|
23
|
-
* box re-reads the product live behind its own boundary, so price and stock
|
|
24
|
-
* are this minute's while the rest of the page is prerendered.
|
|
25
|
-
*
|
|
26
|
-
* No `addToCart` is passed: the add goes through the cart drawer mounted in
|
|
27
|
-
* `providers.tsx`, so the drawer opens at the click with the product in it
|
|
28
|
-
* and no page render runs (the product page contract, rule 6). The drawer's
|
|
29
|
-
* `onCartChange` owns the cart cookie.
|
|
30
|
-
*/
|
|
31
|
-
export default async function ProductPage({
|
|
32
|
-
params,
|
|
33
|
-
searchParams,
|
|
34
|
-
}: {
|
|
35
|
-
params: Promise<{ handle: string }>
|
|
36
|
-
searchParams: Promise<{ variant?: string }>
|
|
37
|
-
}) {
|
|
38
|
-
const { handle } = await params
|
|
39
|
-
const product = await getProduct(handle)
|
|
40
|
-
if (!product) notFound()
|
|
41
|
-
|
|
42
|
-
return (
|
|
43
|
-
<ProductTemplate
|
|
44
|
-
client={storeClient}
|
|
45
|
-
product={product}
|
|
46
|
-
pricingContext={PRICING_CONTEXT}
|
|
47
|
-
// The product page contract: the address names the variant
|
|
48
|
-
// (`?variant=<digits>`, Shopify's parameter), and the FIRST paint of
|
|
49
|
-
// the buy box is that variant's price, code and stock. The query is
|
|
50
|
-
// request data, so it goes in as a promise and the live buy box reads
|
|
51
|
-
// it behind its boundary. Switching a variant rewrites the address in
|
|
52
|
-
// the browser without asking the server.
|
|
53
|
-
initialVariantId={searchParams.then((query) => query.variant)}
|
|
54
|
-
// NO `promises` here, deliberately. Until 0.13.0 the library carried
|
|
55
|
-
// delivery, exchange and return promises as label DEFAULTS, so every
|
|
56
|
-
// store scaffolded from this file told shoppers "your package will
|
|
57
|
-
// arrive in 3-5 business days" and "we'll refund your money" without
|
|
58
|
-
// anyone having decided that. Those are YOUR commitments, so you write
|
|
59
|
-
// them:
|
|
60
|
-
//
|
|
61
|
-
// promises={[
|
|
62
|
-
// { icon: "delivery", title: "Fast delivery",
|
|
63
|
-
// body: "Your order arrives in 2 working days." },
|
|
64
|
-
// ]}
|
|
65
|
-
//
|
|
66
|
-
// Pass nothing and the section does not exist, which is the right
|
|
67
|
-
// default for a store that has not decided yet. The physical-facts
|
|
68
|
-
// section appears on its own for products that HAVE facts, and is
|
|
69
|
-
// skipped for those that do not.
|
|
70
|
-
//
|
|
71
|
-
// The pack has to be handed over here, not just mounted at the root:
|
|
72
|
-
// the related-products strip is a server component and cannot read
|
|
73
|
-
// the locale provider, so its heading would stay English. Required.
|
|
74
|
-
labels={STORE_LOCALE.products}
|
|
75
|
-
/>
|
|
76
|
-
)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export async function generateMetadata({ params }: { params: Promise<{ handle: string }> }) {
|
|
80
|
-
const { handle } = await params
|
|
81
|
-
// The page's own read: one cache entry answers both.
|
|
82
|
-
const product = await getProduct(handle).catch(() => null)
|
|
83
|
-
if (!product) return {}
|
|
84
|
-
// SEO fields with title/description fallbacks (runbook step 5).
|
|
85
|
-
return {
|
|
86
|
-
title: product.seo_title ?? product.title,
|
|
87
|
-
description: product.seo_description ?? product.description ?? undefined,
|
|
88
|
-
}
|
|
89
|
-
}
|
|
1
|
+
import { notFound } from "next/navigation"
|
|
2
|
+
import { ProductTemplate } from "@cartbase/storefront/products/product-template"
|
|
3
|
+
import { getProduct, listProductHandles } from "@/lib/catalog"
|
|
4
|
+
import { PRICING_CONTEXT, STORE_LOCALE } from "@/lib/config"
|
|
5
|
+
import { storeClient } from "@/lib/store-client"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Every product's page is prerendered at build, so it is a file on the CDN.
|
|
9
|
+
* A product added after the last deploy renders on its first visit and is
|
|
10
|
+
* kept from then on. Without this list the handle would be request data and
|
|
11
|
+
* every product page would render per visit (Cache Components).
|
|
12
|
+
*/
|
|
13
|
+
export async function generateStaticParams() {
|
|
14
|
+
const handles = await listProductHandles()
|
|
15
|
+
// Cache Components refuses an empty list; a store with no products yet
|
|
16
|
+
// still builds, and the placeholder answers 404.
|
|
17
|
+
return handles.length ? handles.map((handle) => ({ handle })) : [{ handle: "__none__" }]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* PDP (runbook step 5): the product is a cached read by handle WITH the
|
|
22
|
+
* pricing context (lib/catalog.ts), and the template renders it. The buy
|
|
23
|
+
* box re-reads the product live behind its own boundary, so price and stock
|
|
24
|
+
* are this minute's while the rest of the page is prerendered.
|
|
25
|
+
*
|
|
26
|
+
* No `addToCart` is passed: the add goes through the cart drawer mounted in
|
|
27
|
+
* `providers.tsx`, so the drawer opens at the click with the product in it
|
|
28
|
+
* and no page render runs (the product page contract, rule 6). The drawer's
|
|
29
|
+
* `onCartChange` owns the cart cookie.
|
|
30
|
+
*/
|
|
31
|
+
export default async function ProductPage({
|
|
32
|
+
params,
|
|
33
|
+
searchParams,
|
|
34
|
+
}: {
|
|
35
|
+
params: Promise<{ handle: string }>
|
|
36
|
+
searchParams: Promise<{ variant?: string }>
|
|
37
|
+
}) {
|
|
38
|
+
const { handle } = await params
|
|
39
|
+
const product = await getProduct(handle)
|
|
40
|
+
if (!product) notFound()
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<ProductTemplate
|
|
44
|
+
client={storeClient}
|
|
45
|
+
product={product}
|
|
46
|
+
pricingContext={PRICING_CONTEXT}
|
|
47
|
+
// The product page contract: the address names the variant
|
|
48
|
+
// (`?variant=<digits>`, Shopify's parameter), and the FIRST paint of
|
|
49
|
+
// the buy box is that variant's price, code and stock. The query is
|
|
50
|
+
// request data, so it goes in as a promise and the live buy box reads
|
|
51
|
+
// it behind its boundary. Switching a variant rewrites the address in
|
|
52
|
+
// the browser without asking the server.
|
|
53
|
+
initialVariantId={searchParams.then((query) => query.variant)}
|
|
54
|
+
// NO `promises` here, deliberately. Until 0.13.0 the library carried
|
|
55
|
+
// delivery, exchange and return promises as label DEFAULTS, so every
|
|
56
|
+
// store scaffolded from this file told shoppers "your package will
|
|
57
|
+
// arrive in 3-5 business days" and "we'll refund your money" without
|
|
58
|
+
// anyone having decided that. Those are YOUR commitments, so you write
|
|
59
|
+
// them:
|
|
60
|
+
//
|
|
61
|
+
// promises={[
|
|
62
|
+
// { icon: "delivery", title: "Fast delivery",
|
|
63
|
+
// body: "Your order arrives in 2 working days." },
|
|
64
|
+
// ]}
|
|
65
|
+
//
|
|
66
|
+
// Pass nothing and the section does not exist, which is the right
|
|
67
|
+
// default for a store that has not decided yet. The physical-facts
|
|
68
|
+
// section appears on its own for products that HAVE facts, and is
|
|
69
|
+
// skipped for those that do not.
|
|
70
|
+
//
|
|
71
|
+
// The pack has to be handed over here, not just mounted at the root:
|
|
72
|
+
// the related-products strip is a server component and cannot read
|
|
73
|
+
// the locale provider, so its heading would stay English. Required.
|
|
74
|
+
labels={STORE_LOCALE.products}
|
|
75
|
+
/>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function generateMetadata({ params }: { params: Promise<{ handle: string }> }) {
|
|
80
|
+
const { handle } = await params
|
|
81
|
+
// The page's own read: one cache entry answers both.
|
|
82
|
+
const product = await getProduct(handle).catch(() => null)
|
|
83
|
+
if (!product) return {}
|
|
84
|
+
// SEO fields with title/description fallbacks (runbook step 5).
|
|
85
|
+
return {
|
|
86
|
+
title: product.seo_title ?? product.title,
|
|
87
|
+
description: product.seo_description ?? product.description ?? undefined,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
import { Suspense } from "react"
|
|
2
|
-
import { SearchTemplate } from "@cartbase/storefront/store/search-template"
|
|
3
|
-
import { SkeletonProductGrid } from "@cartbase/storefront/store/skeleton-product-grid"
|
|
4
|
-
import { PRICING_CONTEXT, STORE_LOCALE } from "@/lib/config"
|
|
5
|
-
import { storeClient } from "@/lib/store-client"
|
|
6
|
-
|
|
7
|
-
type Query = Record<string, string | string[] | undefined>
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Search page (runbook step 5) — fully URL-state driven search-template. The
|
|
11
|
-
* query is request data, so the results render behind a boundary.
|
|
12
|
-
*/
|
|
13
|
-
export default function SearchPage({ searchParams }: { searchParams: Promise<Query> }) {
|
|
14
|
-
return (
|
|
15
|
-
<Suspense fallback={<SkeletonProductGrid />}>
|
|
16
|
-
<Results searchParams={searchParams} />
|
|
17
|
-
</Suspense>
|
|
18
|
-
)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async function Results({ searchParams }: { searchParams: Promise<Query> }) {
|
|
22
|
-
const params = await searchParams
|
|
23
|
-
return (
|
|
24
|
-
<SearchTemplate
|
|
25
|
-
client={storeClient}
|
|
26
|
-
searchParams={params}
|
|
27
|
-
pricingContext={PRICING_CONTEXT}
|
|
28
|
-
// Server-rendered: the locale provider cannot reach it, so the pack
|
|
29
|
-
// comes as a prop, and the prop is required.
|
|
30
|
-
labels={STORE_LOCALE.store}
|
|
31
|
-
/>
|
|
32
|
-
)
|
|
33
|
-
}
|
|
1
|
+
import { Suspense } from "react"
|
|
2
|
+
import { SearchTemplate } from "@cartbase/storefront/store/search-template"
|
|
3
|
+
import { SkeletonProductGrid } from "@cartbase/storefront/store/skeleton-product-grid"
|
|
4
|
+
import { PRICING_CONTEXT, STORE_LOCALE } from "@/lib/config"
|
|
5
|
+
import { storeClient } from "@/lib/store-client"
|
|
6
|
+
|
|
7
|
+
type Query = Record<string, string | string[] | undefined>
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Search page (runbook step 5) — fully URL-state driven search-template. The
|
|
11
|
+
* query is request data, so the results render behind a boundary.
|
|
12
|
+
*/
|
|
13
|
+
export default function SearchPage({ searchParams }: { searchParams: Promise<Query> }) {
|
|
14
|
+
return (
|
|
15
|
+
<Suspense fallback={<SkeletonProductGrid />}>
|
|
16
|
+
<Results searchParams={searchParams} />
|
|
17
|
+
</Suspense>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function Results({ searchParams }: { searchParams: Promise<Query> }) {
|
|
22
|
+
const params = await searchParams
|
|
23
|
+
return (
|
|
24
|
+
<SearchTemplate
|
|
25
|
+
client={storeClient}
|
|
26
|
+
searchParams={params}
|
|
27
|
+
pricingContext={PRICING_CONTEXT}
|
|
28
|
+
// Server-rendered: the locale provider cannot reach it, so the pack
|
|
29
|
+
// comes as a prop, and the prop is required.
|
|
30
|
+
labels={STORE_LOCALE.store}
|
|
31
|
+
/>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
@@ -1,35 +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
|
-
})
|
|
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
|
+
})
|
|
@@ -1,120 +1,120 @@
|
|
|
1
|
-
import { cacheLife, cacheTag } from "next/cache"
|
|
2
|
-
import { getConsent, type ConsentConfig } from "@cartbase/storefront/api/consent"
|
|
3
|
-
import { getMenu, type Menu } from "@cartbase/storefront/api/menus"
|
|
4
|
-
import { listProducts, retrieveProduct, type StoreProduct } from "@cartbase/storefront/api/products"
|
|
5
|
-
import { getStore, type StoreIdentity } from "@cartbase/storefront/api/store"
|
|
6
|
-
import { getTrackingConfig } from "@cartbase/storefront/tracking/get-tracking-config"
|
|
7
|
-
import type { TrackingConfig } from "@cartbase/storefront/tracking/types"
|
|
8
|
-
import { BARTER_CLIENT_ID, PRICING_CONTEXT } from "./config"
|
|
9
|
-
import { storeClient } from "./store-client"
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* The store's cached reads (runbook step 3, Cache Components). Each is a
|
|
13
|
-
* `use cache: remote` function on the `catalog` life (next.config.ts): the
|
|
14
|
-
* platform is asked once per window, whichever instance renders, and every
|
|
15
|
-
* page built from these reads is a prerendered file on the CDN.
|
|
16
|
-
*
|
|
17
|
-
* Failure contract: inside the cache a failed read THROWS, so a platform
|
|
18
|
-
* blip is never stored as an empty answer for the whole window; the public
|
|
19
|
-
* function decides what an error reads as. An unknown handle (404) is an
|
|
20
|
-
* answer, null, and is kept.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
function isNotFound(error: unknown): boolean {
|
|
24
|
-
return (error as { status?: number } | null)?.status === 404
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const FALLBACK_STORE: StoreIdentity = {
|
|
28
|
-
id: BARTER_CLIENT_ID ?? "",
|
|
29
|
-
name: "Store",
|
|
30
|
-
slug: "",
|
|
31
|
-
brand: {
|
|
32
|
-
logo_url: null,
|
|
33
|
-
logo_square_url: null,
|
|
34
|
-
color_primary: null,
|
|
35
|
-
color_secondary: null,
|
|
36
|
-
slogan: null,
|
|
37
|
-
},
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async function readStore(): Promise<StoreIdentity> {
|
|
41
|
-
"use cache: remote"
|
|
42
|
-
cacheLife("catalog")
|
|
43
|
-
cacheTag("catalog")
|
|
44
|
-
const { store } = await getStore(storeClient)
|
|
45
|
-
return store
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* The store's own identity (store.md): name, slug, brand. A storefront never
|
|
50
|
-
* hardcodes its name, and a transient API error never takes the layout down.
|
|
51
|
-
*/
|
|
52
|
-
export async function getStoreIdentity(): Promise<StoreIdentity> {
|
|
53
|
-
return readStore().catch(() => FALLBACK_STORE)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** The store's consent configuration; the platform always answers a complete one. */
|
|
57
|
-
export async function getConsentSettings(): Promise<ConsentConfig> {
|
|
58
|
-
"use cache: remote"
|
|
59
|
-
cacheLife("catalog")
|
|
60
|
-
cacheTag("catalog")
|
|
61
|
-
const { consent } = await getConsent(storeClient)
|
|
62
|
-
return consent
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
async function readTracking(): Promise<TrackingConfig> {
|
|
66
|
-
"use cache: remote"
|
|
67
|
-
cacheLife("catalog")
|
|
68
|
-
cacheTag("catalog")
|
|
69
|
-
return getTrackingConfig(storeClient)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** The store's marketing tags; null when the platform does not answer, so no tag mounts. */
|
|
73
|
-
export async function getTracking(): Promise<TrackingConfig | null> {
|
|
74
|
-
return readTracking().catch(() => null)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async function readMenu(handle: string): Promise<Menu | null> {
|
|
78
|
-
"use cache: remote"
|
|
79
|
-
cacheLife("catalog")
|
|
80
|
-
cacheTag("catalog")
|
|
81
|
-
try {
|
|
82
|
-
const { menu } = await getMenu(storeClient, handle)
|
|
83
|
-
return menu
|
|
84
|
-
} catch (error) {
|
|
85
|
-
if (isNotFound(error)) return null
|
|
86
|
-
throw error
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** One menu by handle, or null: a store with no menus 404s every handle (menus.md). */
|
|
91
|
-
export async function getMenuSafe(handle: string): Promise<Menu | null> {
|
|
92
|
-
return readMenu(handle).catch(() => null)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** One product by handle, priced; null for a handle the store does not have. */
|
|
96
|
-
export async function getProduct(handle: string): Promise<StoreProduct | null> {
|
|
97
|
-
"use cache: remote"
|
|
98
|
-
cacheLife("catalog")
|
|
99
|
-
cacheTag("catalog")
|
|
100
|
-
try {
|
|
101
|
-
const { product } = await retrieveProduct(storeClient, handle, PRICING_CONTEXT)
|
|
102
|
-
return product
|
|
103
|
-
} catch (error) {
|
|
104
|
-
if (isNotFound(error)) return null
|
|
105
|
-
throw error
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/** The platform's largest page of products. */
|
|
110
|
-
const PAGE = 200
|
|
111
|
-
|
|
112
|
-
/** Every product handle, for the product pages the build prerenders. */
|
|
113
|
-
export async function listProductHandles(): Promise<string[]> {
|
|
114
|
-
const handles: string[] = []
|
|
115
|
-
for (let offset = 0; ; offset += PAGE) {
|
|
116
|
-
const { products, count } = await listProducts(storeClient, { limit: PAGE, offset })
|
|
117
|
-
handles.push(...products.map((product) => product.handle).filter(Boolean))
|
|
118
|
-
if (!products.length || offset + PAGE >= count) return handles
|
|
119
|
-
}
|
|
120
|
-
}
|
|
1
|
+
import { cacheLife, cacheTag } from "next/cache"
|
|
2
|
+
import { getConsent, type ConsentConfig } from "@cartbase/storefront/api/consent"
|
|
3
|
+
import { getMenu, type Menu } from "@cartbase/storefront/api/menus"
|
|
4
|
+
import { listProducts, retrieveProduct, type StoreProduct } from "@cartbase/storefront/api/products"
|
|
5
|
+
import { getStore, type StoreIdentity } from "@cartbase/storefront/api/store"
|
|
6
|
+
import { getTrackingConfig } from "@cartbase/storefront/tracking/get-tracking-config"
|
|
7
|
+
import type { TrackingConfig } from "@cartbase/storefront/tracking/types"
|
|
8
|
+
import { BARTER_CLIENT_ID, PRICING_CONTEXT } from "./config"
|
|
9
|
+
import { storeClient } from "./store-client"
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The store's cached reads (runbook step 3, Cache Components). Each is a
|
|
13
|
+
* `use cache: remote` function on the `catalog` life (next.config.ts): the
|
|
14
|
+
* platform is asked once per window, whichever instance renders, and every
|
|
15
|
+
* page built from these reads is a prerendered file on the CDN.
|
|
16
|
+
*
|
|
17
|
+
* Failure contract: inside the cache a failed read THROWS, so a platform
|
|
18
|
+
* blip is never stored as an empty answer for the whole window; the public
|
|
19
|
+
* function decides what an error reads as. An unknown handle (404) is an
|
|
20
|
+
* answer, null, and is kept.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
function isNotFound(error: unknown): boolean {
|
|
24
|
+
return (error as { status?: number } | null)?.status === 404
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const FALLBACK_STORE: StoreIdentity = {
|
|
28
|
+
id: BARTER_CLIENT_ID ?? "",
|
|
29
|
+
name: "Store",
|
|
30
|
+
slug: "",
|
|
31
|
+
brand: {
|
|
32
|
+
logo_url: null,
|
|
33
|
+
logo_square_url: null,
|
|
34
|
+
color_primary: null,
|
|
35
|
+
color_secondary: null,
|
|
36
|
+
slogan: null,
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function readStore(): Promise<StoreIdentity> {
|
|
41
|
+
"use cache: remote"
|
|
42
|
+
cacheLife("catalog")
|
|
43
|
+
cacheTag("catalog")
|
|
44
|
+
const { store } = await getStore(storeClient)
|
|
45
|
+
return store
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The store's own identity (store.md): name, slug, brand. A storefront never
|
|
50
|
+
* hardcodes its name, and a transient API error never takes the layout down.
|
|
51
|
+
*/
|
|
52
|
+
export async function getStoreIdentity(): Promise<StoreIdentity> {
|
|
53
|
+
return readStore().catch(() => FALLBACK_STORE)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The store's consent configuration; the platform always answers a complete one. */
|
|
57
|
+
export async function getConsentSettings(): Promise<ConsentConfig> {
|
|
58
|
+
"use cache: remote"
|
|
59
|
+
cacheLife("catalog")
|
|
60
|
+
cacheTag("catalog")
|
|
61
|
+
const { consent } = await getConsent(storeClient)
|
|
62
|
+
return consent
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function readTracking(): Promise<TrackingConfig> {
|
|
66
|
+
"use cache: remote"
|
|
67
|
+
cacheLife("catalog")
|
|
68
|
+
cacheTag("catalog")
|
|
69
|
+
return getTrackingConfig(storeClient)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The store's marketing tags; null when the platform does not answer, so no tag mounts. */
|
|
73
|
+
export async function getTracking(): Promise<TrackingConfig | null> {
|
|
74
|
+
return readTracking().catch(() => null)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function readMenu(handle: string): Promise<Menu | null> {
|
|
78
|
+
"use cache: remote"
|
|
79
|
+
cacheLife("catalog")
|
|
80
|
+
cacheTag("catalog")
|
|
81
|
+
try {
|
|
82
|
+
const { menu } = await getMenu(storeClient, handle)
|
|
83
|
+
return menu
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (isNotFound(error)) return null
|
|
86
|
+
throw error
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** One menu by handle, or null: a store with no menus 404s every handle (menus.md). */
|
|
91
|
+
export async function getMenuSafe(handle: string): Promise<Menu | null> {
|
|
92
|
+
return readMenu(handle).catch(() => null)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** One product by handle, priced; null for a handle the store does not have. */
|
|
96
|
+
export async function getProduct(handle: string): Promise<StoreProduct | null> {
|
|
97
|
+
"use cache: remote"
|
|
98
|
+
cacheLife("catalog")
|
|
99
|
+
cacheTag("catalog")
|
|
100
|
+
try {
|
|
101
|
+
const { product } = await retrieveProduct(storeClient, handle, PRICING_CONTEXT)
|
|
102
|
+
return product
|
|
103
|
+
} catch (error) {
|
|
104
|
+
if (isNotFound(error)) return null
|
|
105
|
+
throw error
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The platform's largest page of products. */
|
|
110
|
+
const PAGE = 200
|
|
111
|
+
|
|
112
|
+
/** Every product handle, for the product pages the build prerenders. */
|
|
113
|
+
export async function listProductHandles(): Promise<string[]> {
|
|
114
|
+
const handles: string[] = []
|
|
115
|
+
for (let offset = 0; ; offset += PAGE) {
|
|
116
|
+
const { products, count } = await listProducts(storeClient, { limit: PAGE, offset })
|
|
117
|
+
handles.push(...products.map((product) => product.handle).filter(Boolean))
|
|
118
|
+
if (!products.length || offset + PAGE >= count) return handles
|
|
119
|
+
}
|
|
120
|
+
}
|