@ecomiq/storefront 2.0.22 → 2.1.3
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/package.json +4 -2
- package/src/cart-session.ts +13 -15
- package/src/client.ts +1 -1
- package/src/cloud-runtime-client.ts +165 -5
- package/src/codegen.mjs +0 -1
- package/src/components/checkout/checkout-session-view.tsx +4 -4
- package/src/components/checkout/format.ts +2 -2
- package/src/components/checkout/use-checkout-customer-prefill.ts +4 -4
- package/src/components/checkout/use-checkout-session.ts +37 -38
- package/src/config.mjs +1 -0
- package/src/hooks/recently-viewed.ts +2 -2
- package/src/index.ts +2 -4
- package/src/pages/checkout.$checkoutId.tsx +1 -1
- package/src/pages/pages.$pageSlug.tsx +68 -31
- package/src/pages/thank-you.tsx +1 -1
- package/src/providers/cart.tsx +17 -17
- package/src/server-config-store.ts +1 -0
- package/src/server.ts +16 -7
- package/src/storefront-definition/data.ts +188 -0
- package/src/storefront-definition/renderer.tsx +610 -34
- package/src/storefront-layout.ts +33 -11
- package/src/storefront-runtime.ts +83 -187
- package/src/styles.css +9 -1
package/src/server.ts
CHANGED
|
@@ -58,8 +58,8 @@ import {
|
|
|
58
58
|
type RemoveCartItemParams,
|
|
59
59
|
type UpdateCartItemQuantityParams,
|
|
60
60
|
type UploadPaymentReceiptParams,
|
|
61
|
+
CartTokenSchema,
|
|
61
62
|
EntityIdSchema,
|
|
62
|
-
OptionalEntityIdSchema,
|
|
63
63
|
} from '@ecomiq/core'
|
|
64
64
|
import { z } from 'zod'
|
|
65
65
|
import { getStorefrontClient } from './client'
|
|
@@ -71,7 +71,10 @@ import {
|
|
|
71
71
|
type AddProductToCartInput,
|
|
72
72
|
} from './cart-session'
|
|
73
73
|
import { submitProductReview } from './review-submission'
|
|
74
|
-
import {
|
|
74
|
+
import {
|
|
75
|
+
loadActiveStorefrontHome,
|
|
76
|
+
loadActiveStorefrontPage,
|
|
77
|
+
} from './storefront-runtime'
|
|
75
78
|
|
|
76
79
|
const getSettingsFn = createServerFn({ method: 'GET' }).handler(() =>
|
|
77
80
|
getStorefrontClient().getSettings(),
|
|
@@ -81,6 +84,10 @@ const getActiveStorefrontHomeFn = createServerFn({ method: 'GET' }).handler(
|
|
|
81
84
|
() => loadActiveStorefrontHome(),
|
|
82
85
|
)
|
|
83
86
|
|
|
87
|
+
const getActiveStorefrontPageFn = createServerFn({ method: 'GET' })
|
|
88
|
+
.validator(z.object({ pathname: z.string().min(1).max(2_048) }))
|
|
89
|
+
.handler(({ data }) => loadActiveStorefrontPage(data.pathname))
|
|
90
|
+
|
|
84
91
|
const getStoreIdFn = createServerFn({ method: 'GET' }).handler(() =>
|
|
85
92
|
getStorefrontClient().getStoreId(),
|
|
86
93
|
)
|
|
@@ -140,9 +147,9 @@ const removeCartItemFn = createServerFn({ method: 'POST' })
|
|
|
140
147
|
.handler(({ data }) => submitCartItemRemoval(getStorefrontClient(), data))
|
|
141
148
|
|
|
142
149
|
const AddProductToCartServerSchema = z.object({
|
|
143
|
-
|
|
150
|
+
cartToken: CartTokenSchema.optional(),
|
|
144
151
|
currencyCode: z.string().length(3),
|
|
145
|
-
variantId:
|
|
152
|
+
variantId: EntityIdSchema,
|
|
146
153
|
quantity: z.number().int().positive(),
|
|
147
154
|
})
|
|
148
155
|
|
|
@@ -179,7 +186,7 @@ const checkoutCartFn = createServerFn({ method: 'POST' })
|
|
|
179
186
|
.handler(({ data }) => getStorefrontClient().checkoutCart(data))
|
|
180
187
|
|
|
181
188
|
const CartPaymentReceiptServerSchema = z.object({
|
|
182
|
-
|
|
189
|
+
cartToken: CartTokenSchema,
|
|
183
190
|
fileBase64: z.string().min(1),
|
|
184
191
|
fileName: z.string().min(1),
|
|
185
192
|
mimeType: z.string().min(1).optional(),
|
|
@@ -191,7 +198,7 @@ const uploadPaymentReceiptFn = createServerFn({ method: 'POST' })
|
|
|
191
198
|
const binary = Uint8Array.from(atob(data.fileBase64), (c) => c.charCodeAt(0))
|
|
192
199
|
const file = new Blob([binary], { type: data.mimeType ?? 'application/octet-stream' })
|
|
193
200
|
return getStorefrontClient().uploadPaymentReceipt({
|
|
194
|
-
|
|
201
|
+
cartToken: data.cartToken,
|
|
195
202
|
file,
|
|
196
203
|
fileName: data.fileName,
|
|
197
204
|
})
|
|
@@ -228,6 +235,8 @@ const getDistrictsFn = createServerFn({ method: 'GET' })
|
|
|
228
235
|
export const serverApi = {
|
|
229
236
|
getSettings: () => getSettingsFn(),
|
|
230
237
|
getActiveStorefrontHome: () => getActiveStorefrontHomeFn(),
|
|
238
|
+
getActiveStorefrontPage: (pathname: string) =>
|
|
239
|
+
getActiveStorefrontPageFn({ data: { pathname } }),
|
|
231
240
|
getStoreId: () => getStoreIdFn(),
|
|
232
241
|
getCollections: () => getCollectionsFn(),
|
|
233
242
|
getCollectionBySlug: (slug: string) => getCollectionBySlugFn({ data: { slug } }),
|
|
@@ -271,7 +280,7 @@ export const serverApi = {
|
|
|
271
280
|
for (const byte of bytes) binary += String.fromCharCode(byte)
|
|
272
281
|
return uploadPaymentReceiptFn({
|
|
273
282
|
data: {
|
|
274
|
-
|
|
283
|
+
cartToken: params.cartToken,
|
|
275
284
|
fileBase64: btoa(binary),
|
|
276
285
|
fileName:
|
|
277
286
|
params.fileName ??
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Collection,
|
|
3
|
+
Collections,
|
|
4
|
+
ProductsParamsInput,
|
|
5
|
+
ProductsResponse,
|
|
6
|
+
ProductSummary,
|
|
7
|
+
} from '@ecomiq/core'
|
|
8
|
+
import {
|
|
9
|
+
type CollectionGridStorefrontSection,
|
|
10
|
+
type ProductGridStorefrontSection,
|
|
11
|
+
type StorefrontDefinition,
|
|
12
|
+
} from '@ecomiq/storefront-definition'
|
|
13
|
+
import { storefrontPageFor } from './renderer'
|
|
14
|
+
import type { DefinitionData } from './renderer'
|
|
15
|
+
|
|
16
|
+
const MAX_UNIQUE_PRODUCT_SOURCES = 12
|
|
17
|
+
const MANUAL_PRODUCT_PAGE_SIZE = 100
|
|
18
|
+
const MAX_MANUAL_PRODUCT_SCAN_PAGES = 5
|
|
19
|
+
|
|
20
|
+
export interface StorefrontRuntimeCatalogClient {
|
|
21
|
+
getProducts(params?: ProductsParamsInput): Promise<ProductsResponse>
|
|
22
|
+
getCollections(): Promise<Collections>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ResolvedStorefrontDefinitionData
|
|
26
|
+
extends DefinitionData {
|
|
27
|
+
productsBySectionId: Record<string, ProductSummary[]>
|
|
28
|
+
collectionsBySectionId: Record<string, Collection[]>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function emptyRendererData(): ResolvedStorefrontDefinitionData {
|
|
32
|
+
return {
|
|
33
|
+
productsBySectionId: {},
|
|
34
|
+
collectionsBySectionId: {},
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function productSourceKey(
|
|
39
|
+
section: ProductGridStorefrontSection,
|
|
40
|
+
): string | null {
|
|
41
|
+
if (section.source.kind === 'manual') return null
|
|
42
|
+
if (section.source.kind === 'collection') {
|
|
43
|
+
return `collection:${section.source.slug}`
|
|
44
|
+
}
|
|
45
|
+
return 'all'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function loadProductSource(
|
|
49
|
+
section: ProductGridStorefrontSection,
|
|
50
|
+
client: StorefrontRuntimeCatalogClient,
|
|
51
|
+
): Promise<ProductSummary[]> {
|
|
52
|
+
const params: ProductsParamsInput = {
|
|
53
|
+
page: 1,
|
|
54
|
+
pageSize: section.limit,
|
|
55
|
+
sort: 'newest',
|
|
56
|
+
}
|
|
57
|
+
if (section.source.kind === 'collection') {
|
|
58
|
+
params.collection = section.source.slug
|
|
59
|
+
}
|
|
60
|
+
const response = await client.getProducts(params)
|
|
61
|
+
return response.products.slice(0, section.limit)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function scanManualProducts(
|
|
65
|
+
productIds: ReadonlySet<string>,
|
|
66
|
+
client: StorefrontRuntimeCatalogClient,
|
|
67
|
+
): Promise<Map<string, ProductSummary>> {
|
|
68
|
+
const found = new Map<string, ProductSummary>()
|
|
69
|
+
|
|
70
|
+
for (let page = 1; page <= MAX_MANUAL_PRODUCT_SCAN_PAGES; page += 1) {
|
|
71
|
+
const response = await client.getProducts({
|
|
72
|
+
page,
|
|
73
|
+
pageSize: MANUAL_PRODUCT_PAGE_SIZE,
|
|
74
|
+
sort: 'newest',
|
|
75
|
+
})
|
|
76
|
+
for (const product of response.products) {
|
|
77
|
+
if (productIds.has(product.id)) found.set(product.id, product)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (
|
|
81
|
+
found.size === productIds.size ||
|
|
82
|
+
page >= response.totalPages ||
|
|
83
|
+
response.products.length === 0
|
|
84
|
+
) {
|
|
85
|
+
break
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return found
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function resolveStorefrontDefinitionData(
|
|
93
|
+
definition: StorefrontDefinition,
|
|
94
|
+
client: StorefrontRuntimeCatalogClient,
|
|
95
|
+
pageId?: string,
|
|
96
|
+
): Promise<ResolvedStorefrontDefinitionData> {
|
|
97
|
+
const data = emptyRendererData()
|
|
98
|
+
const page = storefrontPageFor(definition, pageId)
|
|
99
|
+
const sections = page?.sections ?? []
|
|
100
|
+
const productSections = sections.filter(
|
|
101
|
+
(section): section is ProductGridStorefrontSection =>
|
|
102
|
+
section.enabled && section.type === 'product-grid',
|
|
103
|
+
)
|
|
104
|
+
const collectionSections = sections.filter(
|
|
105
|
+
(section): section is CollectionGridStorefrontSection =>
|
|
106
|
+
section.enabled && section.type === 'collection-grid',
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
const sourceSections = new Map<string, ProductGridStorefrontSection>()
|
|
110
|
+
for (const section of productSections) {
|
|
111
|
+
const sourceKey = productSourceKey(section)
|
|
112
|
+
if (!sourceKey) continue
|
|
113
|
+
const current = sourceSections.get(sourceKey)
|
|
114
|
+
if (current) {
|
|
115
|
+
if (section.limit > current.limit) sourceSections.set(sourceKey, section)
|
|
116
|
+
continue
|
|
117
|
+
}
|
|
118
|
+
if (sourceSections.size >= MAX_UNIQUE_PRODUCT_SOURCES) continue
|
|
119
|
+
sourceSections.set(sourceKey, section)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const sourceRequests = new Map<string, Promise<ProductSummary[]>>()
|
|
123
|
+
for (const [sourceKey, section] of sourceSections) {
|
|
124
|
+
sourceRequests.set(
|
|
125
|
+
sourceKey,
|
|
126
|
+
loadProductSource(section, client).catch(() => []),
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const manualIds = new Set(
|
|
131
|
+
productSections.flatMap((section) =>
|
|
132
|
+
section.source.kind === 'manual' ? section.source.productIds : [],
|
|
133
|
+
),
|
|
134
|
+
)
|
|
135
|
+
const manualProducts =
|
|
136
|
+
manualIds.size > 0
|
|
137
|
+
? scanManualProducts(manualIds, client).catch(
|
|
138
|
+
() => new Map<string, ProductSummary>(),
|
|
139
|
+
)
|
|
140
|
+
: Promise.resolve(new Map<string, ProductSummary>())
|
|
141
|
+
|
|
142
|
+
await Promise.all(
|
|
143
|
+
productSections.map(async (section) => {
|
|
144
|
+
if (section.source.kind === 'manual') {
|
|
145
|
+
const productsById = await manualProducts
|
|
146
|
+
data.productsBySectionId[section.id] = section.source.productIds
|
|
147
|
+
.map((productId) => productsById.get(productId))
|
|
148
|
+
.filter(
|
|
149
|
+
(product): product is ProductSummary => product !== undefined,
|
|
150
|
+
)
|
|
151
|
+
.slice(0, section.limit)
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const sourceKey = productSourceKey(section)
|
|
156
|
+
const products = sourceKey
|
|
157
|
+
? await (sourceRequests.get(sourceKey) ?? Promise.resolve([]))
|
|
158
|
+
: []
|
|
159
|
+
data.productsBySectionId[section.id] = products.slice(0, section.limit)
|
|
160
|
+
}),
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
if (collectionSections.length > 0) {
|
|
164
|
+
const collections = await client.getCollections().catch(() => [])
|
|
165
|
+
const collectionsBySlug = new Map(
|
|
166
|
+
collections.map((collection) => [collection.slug, collection]),
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
for (const section of collectionSections) {
|
|
170
|
+
if (section.source.kind === 'manual') {
|
|
171
|
+
data.collectionsBySectionId[section.id] = section.source.slugs
|
|
172
|
+
.map((slug) => collectionsBySlug.get(slug))
|
|
173
|
+
.filter(
|
|
174
|
+
(collection): collection is Collection =>
|
|
175
|
+
collection !== undefined,
|
|
176
|
+
)
|
|
177
|
+
.slice(0, section.limit)
|
|
178
|
+
continue
|
|
179
|
+
}
|
|
180
|
+
data.collectionsBySectionId[section.id] = collections.slice(
|
|
181
|
+
0,
|
|
182
|
+
section.limit,
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return data
|
|
188
|
+
}
|