@ecomiq/storefront 2.0.21 → 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.
@@ -1,18 +1,21 @@
1
1
  import { useRouterState } from '@tanstack/react-router'
2
2
  import {
3
3
  StorefrontDefinitionSchema,
4
+ resolveStorefrontRoute,
4
5
  type StorefrontDefinition,
6
+ type StorefrontPage,
5
7
  } from '@ecomiq/storefront-definition'
6
8
 
7
- export function definitionForRoute(
9
+ export type StorefrontDefinitionRoute = {
10
+ definition: StorefrontDefinition
11
+ page: StorefrontPage
12
+ }
13
+
14
+ export function definitionRouteFor(
8
15
  pathname: string,
9
16
  loaderData: unknown,
10
- ): StorefrontDefinition | null {
11
- if (
12
- pathname !== '/' ||
13
- loaderData === null ||
14
- typeof loaderData !== 'object'
15
- ) {
17
+ ): StorefrontDefinitionRoute | null {
18
+ if (loaderData === null || typeof loaderData !== 'object') {
16
19
  return null
17
20
  }
18
21
  if (!('kind' in loaderData) || loaderData.kind !== 'definition') {
@@ -22,16 +25,35 @@ export function definitionForRoute(
22
25
  return null
23
26
  }
24
27
 
25
- const definition = StorefrontDefinitionSchema.safeParse(loaderData.definition)
26
- return definition.success ? definition.data : null
28
+ const parsed = StorefrontDefinitionSchema.safeParse(loaderData.definition)
29
+ if (!parsed.success) return null
30
+
31
+ const resolution = resolveStorefrontRoute(pathname, parsed.data.pages)
32
+ if (resolution.kind !== 'page') return null
33
+ const page = parsed.data.pages.find(
34
+ (candidate) => candidate.id === resolution.pageId,
35
+ )
36
+ if (!page) return null
37
+ return { definition: parsed.data, page }
27
38
  }
28
39
 
29
- export function useDefinition(): StorefrontDefinition | null {
40
+ export function definitionForRoute(
41
+ pathname: string,
42
+ loaderData: unknown,
43
+ ): StorefrontDefinition | null {
44
+ return definitionRouteFor(pathname, loaderData)?.definition ?? null
45
+ }
46
+
47
+ export function useDefinitionRoute(): StorefrontDefinitionRoute | null {
30
48
  return useRouterState({
31
49
  select: (state) =>
32
- definitionForRoute(
50
+ definitionRouteFor(
33
51
  state.location.pathname,
34
52
  state.matches.at(-1)?.loaderData,
35
53
  ),
36
54
  })
37
55
  }
56
+
57
+ export function useDefinition(): StorefrontDefinition | null {
58
+ return useDefinitionRoute()?.definition ?? null
59
+ }
@@ -1,10 +1,3 @@
1
- import type {
2
- Collection,
3
- Collections,
4
- ProductsParamsInput,
5
- ProductsResponse,
6
- ProductSummary,
7
- } from '@ecomiq/core'
8
1
  import {
9
2
  CloudRuntimeStoreSlugSchema,
10
3
  getCloudRuntimeClient,
@@ -15,21 +8,24 @@ import {
15
8
  getServerConfig,
16
9
  type ServerEcomiqConfig,
17
10
  } from './server-config-store'
18
- import type {
19
- CollectionGridStorefrontSection,
20
- ProductGridStorefrontSection,
21
- StorefrontDefinition,
11
+ import {
12
+ resolveStorefrontRoute,
13
+ type StorefrontDefinition,
14
+ type StorefrontPage,
22
15
  } from '@ecomiq/storefront-definition'
23
- import type { DefinitionData } from './storefront-definition/renderer'
24
-
25
- const MAX_UNIQUE_PRODUCT_SOURCES = 12
26
- const MANUAL_PRODUCT_PAGE_SIZE = 100
27
- const MAX_MANUAL_PRODUCT_SCAN_PAGES = 5
28
-
29
- export interface StorefrontRuntimeCatalogClient {
30
- getProducts(params?: ProductsParamsInput): Promise<ProductsResponse>
31
- getCollections(): Promise<Collections>
32
- }
16
+ import { storefrontPageFor } from './storefront-definition/renderer'
17
+ import {
18
+ emptyRendererData,
19
+ resolveStorefrontDefinitionData,
20
+ type ResolvedStorefrontDefinitionData,
21
+ type StorefrontRuntimeCatalogClient,
22
+ } from './storefront-definition/data'
23
+
24
+ export {
25
+ resolveStorefrontDefinitionData,
26
+ type ResolvedStorefrontDefinitionData,
27
+ type StorefrontRuntimeCatalogClient,
28
+ } from './storefront-definition/data'
33
29
 
34
30
  export interface StorefrontRuntimeCloudClient {
35
31
  resolveActiveStorefront(
@@ -37,12 +33,6 @@ export interface StorefrontRuntimeCloudClient {
37
33
  ): Promise<ActiveStorefrontRuntime | null>
38
34
  }
39
35
 
40
- export interface ResolvedStorefrontDefinitionData
41
- extends DefinitionData {
42
- productsBySectionId: Record<string, ProductSummary[]>
43
- collectionsBySectionId: Record<string, Collection[]>
44
- }
45
-
46
36
  export type ActiveStorefrontHome =
47
37
  | {
48
38
  kind: 'legacy'
@@ -50,165 +40,25 @@ export type ActiveStorefrontHome =
50
40
  | {
51
41
  kind: 'definition'
52
42
  definition: StorefrontDefinition
43
+ page: StorefrontPage
53
44
  publication: ActiveStorefrontRuntime['publication']
54
45
  data: ResolvedStorefrontDefinitionData
55
46
  }
56
47
 
57
- function emptyRendererData(): ResolvedStorefrontDefinitionData {
58
- return {
59
- productsBySectionId: {},
60
- collectionsBySectionId: {},
61
- }
62
- }
63
-
64
- function productSourceKey(
65
- section: ProductGridStorefrontSection,
66
- ): string | null {
67
- if (section.source.kind === 'manual') return null
68
- if (section.source.kind === 'collection') {
69
- return `collection:${section.source.slug}`
70
- }
71
- return 'all'
72
- }
73
-
74
- async function loadProductSource(
75
- section: ProductGridStorefrontSection,
76
- client: StorefrontRuntimeCatalogClient,
77
- ): Promise<ProductSummary[]> {
78
- const params: ProductsParamsInput = {
79
- page: 1,
80
- pageSize: section.limit,
81
- sort: 'newest',
82
- }
83
- if (section.source.kind === 'collection') {
84
- params.collection = section.source.slug
85
- }
86
- const response = await client.getProducts(params)
87
- return response.products.slice(0, section.limit)
88
- }
89
-
90
- async function scanManualProducts(
91
- productIds: ReadonlySet<string>,
92
- client: StorefrontRuntimeCatalogClient,
93
- ): Promise<Map<string, ProductSummary>> {
94
- const found = new Map<string, ProductSummary>()
95
-
96
- for (let page = 1; page <= MAX_MANUAL_PRODUCT_SCAN_PAGES; page += 1) {
97
- const response = await client.getProducts({
98
- page,
99
- pageSize: MANUAL_PRODUCT_PAGE_SIZE,
100
- sort: 'newest',
101
- })
102
- for (const product of response.products) {
103
- if (productIds.has(product.id)) found.set(product.id, product)
104
- }
105
-
106
- if (
107
- found.size === productIds.size ||
108
- page >= response.totalPages ||
109
- response.products.length === 0
110
- ) {
111
- break
48
+ export type ActiveStorefrontPage =
49
+ | {
50
+ kind: 'unpublished'
112
51
  }
113
- }
114
-
115
- return found
116
- }
117
-
118
- export async function resolveStorefrontDefinitionData(
119
- definition: StorefrontDefinition,
120
- client: StorefrontRuntimeCatalogClient,
121
- ): Promise<ResolvedStorefrontDefinitionData> {
122
- const data = emptyRendererData()
123
- const productSections = definition.sections.filter(
124
- (section): section is ProductGridStorefrontSection =>
125
- section.enabled && section.type === 'product-grid',
126
- )
127
- const collectionSections = definition.sections.filter(
128
- (section): section is CollectionGridStorefrontSection =>
129
- section.enabled && section.type === 'collection-grid',
130
- )
131
-
132
- const sourceSections = new Map<string, ProductGridStorefrontSection>()
133
- for (const section of productSections) {
134
- const sourceKey = productSourceKey(section)
135
- if (!sourceKey) continue
136
- const current = sourceSections.get(sourceKey)
137
- if (current) {
138
- if (section.limit > current.limit) sourceSections.set(sourceKey, section)
139
- continue
52
+ | {
53
+ kind: 'not-found'
140
54
  }
141
- if (sourceSections.size >= MAX_UNIQUE_PRODUCT_SOURCES) continue
142
- sourceSections.set(sourceKey, section)
143
- }
144
-
145
- const sourceRequests = new Map<string, Promise<ProductSummary[]>>()
146
- for (const [sourceKey, section] of sourceSections) {
147
- sourceRequests.set(
148
- sourceKey,
149
- loadProductSource(section, client).catch(() => []),
150
- )
151
- }
152
-
153
- const manualIds = new Set(
154
- productSections.flatMap((section) =>
155
- section.source.kind === 'manual' ? section.source.productIds : [],
156
- ),
157
- )
158
- const manualProducts =
159
- manualIds.size > 0
160
- ? scanManualProducts(manualIds, client).catch(
161
- () => new Map<string, ProductSummary>(),
162
- )
163
- : Promise.resolve(new Map<string, ProductSummary>())
164
-
165
- await Promise.all(
166
- productSections.map(async (section) => {
167
- if (section.source.kind === 'manual') {
168
- const productsById = await manualProducts
169
- data.productsBySectionId[section.id] = section.source.productIds
170
- .map((productId) => productsById.get(productId))
171
- .filter(
172
- (product): product is ProductSummary => product !== undefined,
173
- )
174
- .slice(0, section.limit)
175
- return
176
- }
177
-
178
- const sourceKey = productSourceKey(section)
179
- const products = sourceKey
180
- ? await (sourceRequests.get(sourceKey) ?? Promise.resolve([]))
181
- : []
182
- data.productsBySectionId[section.id] = products.slice(0, section.limit)
183
- }),
184
- )
185
-
186
- if (collectionSections.length > 0) {
187
- const collections = await client.getCollections().catch(() => [])
188
- const collectionsBySlug = new Map(
189
- collections.map((collection) => [collection.slug, collection]),
190
- )
191
-
192
- for (const section of collectionSections) {
193
- if (section.source.kind === 'manual') {
194
- data.collectionsBySectionId[section.id] = section.source.slugs
195
- .map((slug) => collectionsBySlug.get(slug))
196
- .filter(
197
- (collection): collection is Collection =>
198
- collection !== undefined,
199
- )
200
- .slice(0, section.limit)
201
- continue
202
- }
203
- data.collectionsBySectionId[section.id] = collections.slice(
204
- 0,
205
- section.limit,
206
- )
55
+ | {
56
+ kind: 'definition'
57
+ definition: StorefrontDefinition
58
+ page: StorefrontPage
59
+ publication: ActiveStorefrontRuntime['publication']
60
+ data: ResolvedStorefrontDefinitionData
207
61
  }
208
- }
209
-
210
- return data
211
- }
212
62
 
213
63
  export function deriveCloudStoreSlug(domain: string): string | null {
214
64
  const normalized = domain.trim().toLowerCase()
@@ -234,43 +84,89 @@ export interface LoadActiveStorefrontHomeDependencies {
234
84
  catalogClient?: StorefrontRuntimeCatalogClient
235
85
  }
236
86
 
237
- export async function loadActiveStorefrontHome(
238
- dependencies: LoadActiveStorefrontHomeDependencies = {},
239
- ): Promise<ActiveStorefrontHome> {
87
+ async function resolveActiveRuntime(
88
+ dependencies: LoadActiveStorefrontHomeDependencies,
89
+ ): Promise<ActiveStorefrontRuntime | null> {
240
90
  const config = dependencies.config ?? getServerConfig()
241
91
  const cloudConfig = config.cloud
242
- if (!cloudConfig?.baseUrl) return { kind: 'legacy' }
92
+ if (!cloudConfig?.baseUrl) return null
243
93
 
244
94
  const storeSlug =
245
95
  cloudConfig.storeSlug ?? deriveCloudStoreSlug(config.domain)
246
- if (!storeSlug) return { kind: 'legacy' }
96
+ if (!storeSlug) return null
247
97
 
248
98
  const cloudClient =
249
99
  dependencies.cloudClient ??
250
100
  getCloudRuntimeClient({
251
101
  baseUrl: cloudConfig.baseUrl,
102
+ artifactBaseUrl: cloudConfig.artifactBaseUrl,
252
103
  timeoutMs: cloudConfig.timeoutMs,
253
104
  cacheTtlMs: cloudConfig.cacheTtlMs,
254
105
  })
255
106
 
256
- let runtime: ActiveStorefrontRuntime | null
257
107
  try {
258
- runtime = await cloudClient.resolveActiveStorefront(storeSlug)
108
+ return await cloudClient.resolveActiveStorefront(storeSlug)
259
109
  } catch {
260
- return { kind: 'legacy' }
110
+ return null
261
111
  }
112
+ }
113
+
114
+ export async function loadActiveStorefrontHome(
115
+ dependencies: LoadActiveStorefrontHomeDependencies = {},
116
+ ): Promise<ActiveStorefrontHome> {
117
+ const runtime = await resolveActiveRuntime(dependencies)
262
118
  if (!runtime) return { kind: 'legacy' }
263
119
 
120
+ const page = storefrontPageFor(runtime.definition)
121
+ if (!page) return { kind: 'legacy' }
122
+
123
+ const catalogClient =
124
+ dependencies.catalogClient ?? getStorefrontClient()
125
+ const data = await resolveStorefrontDefinitionData(
126
+ runtime.definition,
127
+ catalogClient,
128
+ page.id,
129
+ ).catch(emptyRendererData)
130
+
131
+ return {
132
+ kind: 'definition',
133
+ definition: runtime.definition,
134
+ page,
135
+ publication: runtime.publication,
136
+ data,
137
+ }
138
+ }
139
+
140
+ export async function loadActiveStorefrontPage(
141
+ pathname: string,
142
+ dependencies: LoadActiveStorefrontHomeDependencies = {},
143
+ ): Promise<ActiveStorefrontPage> {
144
+ const runtime = await resolveActiveRuntime(dependencies)
145
+ if (!runtime) return { kind: 'unpublished' }
146
+
147
+ const resolution = resolveStorefrontRoute(
148
+ pathname,
149
+ runtime.definition.pages,
150
+ )
151
+ if (resolution.kind !== 'page') return { kind: 'not-found' }
152
+
153
+ const page = runtime.definition.pages.find(
154
+ (candidate) => candidate.id === resolution.pageId,
155
+ )
156
+ if (!page) return { kind: 'not-found' }
157
+
264
158
  const catalogClient =
265
159
  dependencies.catalogClient ?? getStorefrontClient()
266
160
  const data = await resolveStorefrontDefinitionData(
267
161
  runtime.definition,
268
162
  catalogClient,
163
+ page.id,
269
164
  ).catch(emptyRendererData)
270
165
 
271
166
  return {
272
167
  kind: 'definition',
273
168
  definition: runtime.definition,
169
+ page,
274
170
  publication: runtime.publication,
275
171
  data,
276
172
  }
package/src/styles.css CHANGED
@@ -78,6 +78,7 @@
78
78
 
79
79
  .storefront-definition {
80
80
  font-family: var(--storefront-font-body);
81
+ --storefront-content-width: 72rem;
81
82
  }
82
83
 
83
84
  .storefront-definition :where(h1, h2, h3, h4, h5, h6) {
@@ -85,5 +86,12 @@
85
86
  }
86
87
 
87
88
  .storefront-definition-section {
88
- padding-block: var(--storefront-section-padding);
89
+ padding-block: var(
90
+ --storefront-section-padding-top,
91
+ var(--storefront-section-padding)
92
+ )
93
+ var(
94
+ --storefront-section-padding-bottom,
95
+ var(--storefront-section-padding)
96
+ );
89
97
  }