@akinon/pz-theme 2.0.0-beta.21

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 (62) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/package.json +27 -0
  3. package/readme.md +23 -0
  4. package/src/blocks/accordion-block.tsx +136 -0
  5. package/src/blocks/block-renderer-registry.tsx +77 -0
  6. package/src/blocks/button-block.tsx +593 -0
  7. package/src/blocks/counter-block.tsx +348 -0
  8. package/src/blocks/divider-block.tsx +20 -0
  9. package/src/blocks/embed-block.tsx +208 -0
  10. package/src/blocks/group-block.tsx +116 -0
  11. package/src/blocks/hotspot-block.tsx +147 -0
  12. package/src/blocks/icon-block.tsx +230 -0
  13. package/src/blocks/image-block.tsx +142 -0
  14. package/src/blocks/image-gallery-block.tsx +269 -0
  15. package/src/blocks/input-block.tsx +123 -0
  16. package/src/blocks/link-block.tsx +216 -0
  17. package/src/blocks/lottie-block.tsx +325 -0
  18. package/src/blocks/map-block.tsx +89 -0
  19. package/src/blocks/slider-block.tsx +595 -0
  20. package/src/blocks/tab-block.tsx +10 -0
  21. package/src/blocks/text-block.tsx +52 -0
  22. package/src/blocks/video-block.tsx +122 -0
  23. package/src/components/action-toolbar.tsx +305 -0
  24. package/src/components/designer-overlay.tsx +74 -0
  25. package/src/components/with-designer-features.tsx +142 -0
  26. package/src/dynamic-font-loader.tsx +79 -0
  27. package/src/hooks/use-designer-features.tsx +100 -0
  28. package/src/hooks/use-visibility-context.ts +27 -0
  29. package/src/index.ts +21 -0
  30. package/src/placeholder-registry.ts +31 -0
  31. package/src/sections/before-after-section.tsx +245 -0
  32. package/src/sections/contact-form-section.tsx +564 -0
  33. package/src/sections/countdown-campaign-banner-section.tsx +433 -0
  34. package/src/sections/coupon-banner-section.tsx +710 -0
  35. package/src/sections/divider-section.tsx +62 -0
  36. package/src/sections/featured-product-spotlight-section.tsx +507 -0
  37. package/src/sections/find-in-store-section.tsx +1995 -0
  38. package/src/sections/hover-showcase-section.tsx +326 -0
  39. package/src/sections/image-hotspot-section.tsx +142 -0
  40. package/src/sections/installment-options-section.tsx +1065 -0
  41. package/src/sections/notification-banner-section.tsx +173 -0
  42. package/src/sections/order-tracking-lookup-section.tsx +1379 -0
  43. package/src/sections/posts-slider-section.tsx +472 -0
  44. package/src/sections/pre-order-launch-banner-section.tsx +687 -0
  45. package/src/sections/section-renderer-registry.tsx +89 -0
  46. package/src/sections/section-wrapper.tsx +135 -0
  47. package/src/sections/shipping-threshold-progress-section.tsx +586 -0
  48. package/src/sections/stats-counter-section.tsx +486 -0
  49. package/src/sections/tabs-section.tsx +578 -0
  50. package/src/theme-block.tsx +102 -0
  51. package/src/theme-page-context.tsx +27 -0
  52. package/src/theme-placeholder-client.tsx +218 -0
  53. package/src/theme-placeholder-wrapper.tsx +786 -0
  54. package/src/theme-placeholder.tsx +305 -0
  55. package/src/theme-section.tsx +1241 -0
  56. package/src/theme-settings-context.tsx +13 -0
  57. package/src/utils/index.ts +791 -0
  58. package/src/utils/iterator-utils.test.ts +224 -0
  59. package/src/utils/iterator-utils.ts +617 -0
  60. package/src/utils/page-context-discovery.ts +119 -0
  61. package/src/utils/publish-window.ts +86 -0
  62. package/src/utils/visibility-rules.ts +188 -0
@@ -0,0 +1,305 @@
1
+ import 'server-only';
2
+
3
+ import {
4
+ getCollectionWidgetData,
5
+ getWidgetData,
6
+ getWidgetSchemaData
7
+ } from '@akinon/next/data/server';
8
+ import ThemePlaceholderWrapper from './theme-placeholder-wrapper';
9
+ import { Section } from './theme-section';
10
+ import { generateThemeCSS } from './utils';
11
+
12
+ type ThemePlaceholderData = {
13
+ theme_editor_placeholder: Array<{
14
+ section_slug: string;
15
+ order: number;
16
+ }>;
17
+ };
18
+
19
+ interface ThemePlaceholderProps {
20
+ slug: string;
21
+ pageContext?: Record<string, unknown> | null;
22
+ }
23
+
24
+ export default async function ThemePlaceholder({
25
+ slug,
26
+ pageContext = null
27
+ }: ThemePlaceholderProps) {
28
+ try {
29
+ let dataSources: any[] = [];
30
+ let themeSettings: Record<string, unknown> | null = null;
31
+ try {
32
+ const themeConfigData = await getWidgetData<any>({
33
+ slug: 'theme-config'
34
+ });
35
+
36
+ if (themeConfigData?.attributes?.theme_dataSources) {
37
+ try {
38
+ const raw = themeConfigData.attributes.theme_dataSources;
39
+ // Handle both plain string/array and { value: "..." } attribute format
40
+ const resolved =
41
+ typeof raw === 'string'
42
+ ? raw
43
+ : typeof raw === 'object' && raw !== null && !Array.isArray(raw)
44
+ ? (raw as { value?: any }).value ?? raw
45
+ : raw;
46
+
47
+ if (typeof resolved === 'string') {
48
+ dataSources = JSON.parse(resolved);
49
+ } else if (Array.isArray(resolved)) {
50
+ dataSources = resolved;
51
+ }
52
+ } catch (e) {
53
+ console.error('Failed to parse theme_dataSources:', e);
54
+ }
55
+ }
56
+
57
+ if (themeConfigData?.attributes?.theme_settings) {
58
+ try {
59
+ const rawSettings = themeConfigData.attributes.theme_settings;
60
+ const resolvedValue =
61
+ typeof rawSettings === 'string'
62
+ ? rawSettings
63
+ : typeof rawSettings === 'object' && rawSettings !== null
64
+ ? (rawSettings as { value?: string }).value ?? rawSettings
65
+ : null;
66
+
67
+ if (resolvedValue) {
68
+ if (typeof resolvedValue === 'string') {
69
+ themeSettings = JSON.parse(resolvedValue);
70
+ } else if (typeof resolvedValue === 'object') {
71
+ themeSettings = resolvedValue as Record<string, unknown>;
72
+ }
73
+ }
74
+ } catch (e) {
75
+ console.error('Failed to parse theme_settings:', e);
76
+ }
77
+ }
78
+ } catch (configError) {
79
+ // theme-config is optional for placeholder rendering
80
+ }
81
+
82
+ let data: { slug?: string; attributes?: { theme_editor_placeholder?: unknown; [key: string]: unknown } } | null = null;
83
+ try {
84
+ data = await getWidgetData<ThemePlaceholderData>({
85
+ slug
86
+ }) as typeof data;
87
+ } catch (placeholderError) {
88
+ console.warn(
89
+ `Theme placeholder widget "${slug}" was not found. Rendering empty placeholder.`,
90
+ placeholderError
91
+ );
92
+ }
93
+
94
+ const sections: Section[] = [];
95
+
96
+ if (data?.attributes?.theme_editor_placeholder) {
97
+ let placeholderData: any = data.attributes.theme_editor_placeholder;
98
+
99
+ if (
100
+ typeof placeholderData === 'object' &&
101
+ placeholderData !== null &&
102
+ 'value' in placeholderData
103
+ ) {
104
+ placeholderData = (
105
+ placeholderData as {
106
+ value: { section_slug: string; order: number }[];
107
+ }
108
+ ).value;
109
+ }
110
+
111
+ if (typeof placeholderData === 'string') {
112
+ try {
113
+ placeholderData = JSON.parse(placeholderData);
114
+ } catch {
115
+ placeholderData = [];
116
+ }
117
+ }
118
+
119
+ if (!Array.isArray(placeholderData)) {
120
+ placeholderData = [];
121
+ }
122
+
123
+ const sectionPromises = placeholderData.map(async (item: any) => {
124
+ try {
125
+ const sectionSlug = item.value?.section_slug || item.section_slug;
126
+ const sectionOrder = item.value?.order ?? item.order ?? 0;
127
+
128
+ if (!sectionSlug) {
129
+ return null;
130
+ }
131
+
132
+ const [regularSectionData, sectionSchema] = await Promise.all([
133
+ getWidgetData<any>({
134
+ slug: sectionSlug
135
+ }),
136
+ getWidgetSchemaData<any>({
137
+ widgetSlug: sectionSlug
138
+ })
139
+ ]);
140
+
141
+ let sectionData = regularSectionData;
142
+ if (!sectionData) {
143
+ try {
144
+ sectionData = await getCollectionWidgetData<any>({
145
+ slug: sectionSlug
146
+ });
147
+ } catch {
148
+ // Not a collection widget either; sectionData stays null
149
+ }
150
+ }
151
+
152
+ const sectionSchemaEntry = sectionSchema?.schema?.[sectionSlug];
153
+ const sectionMetadata = sectionSchemaEntry?.metadata || {};
154
+
155
+ const getBlockValue = (blockId: string): unknown => {
156
+ const attrData = sectionData?.attributes?.[blockId];
157
+ if (attrData === undefined || attrData === null) return undefined;
158
+
159
+ if (typeof attrData === 'string') {
160
+ try {
161
+ return JSON.parse(attrData);
162
+ } catch {
163
+ return attrData;
164
+ }
165
+ } else if (typeof attrData === 'object' && 'value' in attrData) {
166
+ return attrData.value;
167
+ }
168
+ return attrData;
169
+ };
170
+
171
+ const reconstructBlock = (blockSchema: any): any => {
172
+ return {
173
+ id: blockSchema.id,
174
+ type: blockSchema.type || 'text',
175
+ label: blockSchema.label || blockSchema.id,
176
+ order: blockSchema.order || 0,
177
+ isIterator: blockSchema.isIterator,
178
+ iteratorDataPath: blockSchema.iteratorDataPath,
179
+ value: getBlockValue(blockSchema.id),
180
+ styles: blockSchema.styles || {},
181
+ properties: blockSchema.properties || {},
182
+ hidden: blockSchema.hidden || false,
183
+ blocks: (blockSchema.blocks || []).map((b: any) =>
184
+ reconstructBlock(b)
185
+ )
186
+ };
187
+ };
188
+
189
+ const blocks: any[] = [];
190
+ if (sectionMetadata.blocks && Array.isArray(sectionMetadata.blocks)) {
191
+ sectionMetadata.blocks.forEach((blockSchema: any) => {
192
+ blocks.push(reconstructBlock(blockSchema));
193
+ });
194
+ }
195
+
196
+ blocks.sort((a, b) => a.order - b.order);
197
+
198
+ // dataSourceId may be stored in schema metadata OR in widget data attributes
199
+ // (theme editor saves it as a widget attribute when user selects a data source)
200
+ const dataSourceId =
201
+ sectionMetadata.dataSourceId ||
202
+ (getBlockValue('dataSourceId') as string | undefined) ||
203
+ (getBlockValue('selectedDataSourceId') as string | undefined);
204
+
205
+ const dataSource = dataSourceId
206
+ ? dataSources.find((ds) => ds.id === dataSourceId)
207
+ : undefined;
208
+
209
+ let dataSourceWithData = dataSource;
210
+ if (dataSource && dataSource.details) {
211
+ // Only fetch collection data if we have a valid widget slug
212
+ // (not the section's own slug which contains config, not products)
213
+ const collectionSlug = dataSource.details.collection?.slug;
214
+ if (collectionSlug) {
215
+ try {
216
+ let collectionData = await getWidgetData<{
217
+ [key: string]: unknown;
218
+ }>({
219
+ slug: collectionSlug
220
+ });
221
+
222
+ if (!collectionData) {
223
+ collectionData = await getCollectionWidgetData<{
224
+ [key: string]: unknown;
225
+ }>({
226
+ slug: collectionSlug
227
+ });
228
+ }
229
+
230
+ if (collectionData) {
231
+ dataSourceWithData = {
232
+ ...dataSource,
233
+ details: {
234
+ ...dataSource.details,
235
+ collection: {
236
+ ...dataSource.details.collection,
237
+ data: collectionData
238
+ }
239
+ }
240
+ };
241
+ }
242
+ } catch (error) {
243
+ console.error(
244
+ `Error fetching widget data for section ${sectionSlug}:`,
245
+ error
246
+ );
247
+ }
248
+ }
249
+ // If no collection slug, the saved products from editor
250
+ // (details.collection.products) will be used as fallback
251
+ }
252
+
253
+ const section: Section = {
254
+ id: sectionSlug,
255
+ type: sectionMetadata.type || 'default',
256
+ name: sectionMetadata.name || sectionSlug,
257
+ label: sectionMetadata.label || sectionSlug,
258
+ properties: sectionMetadata.properties || {},
259
+ styles: sectionMetadata.styles || {},
260
+ blocks,
261
+ order: sectionOrder,
262
+ hidden: sectionMetadata.hidden || false,
263
+ dataSourceId,
264
+ dataSource: dataSourceWithData
265
+ };
266
+
267
+ return section;
268
+ } catch (error) {
269
+ const errorSlug =
270
+ item.value?.section_slug || item.section_slug || 'unknown';
271
+ console.error(
272
+ `Error parsing section metadata for ${errorSlug}:`,
273
+ error
274
+ );
275
+ return null;
276
+ }
277
+ });
278
+
279
+ const resolvedSections = await Promise.all(sectionPromises);
280
+ sections.push(
281
+ ...resolvedSections.filter((s): s is Section => s !== null)
282
+ );
283
+ }
284
+
285
+ const themeCSS = generateThemeCSS(sections);
286
+
287
+ return (
288
+ <>
289
+ {themeCSS && <style dangerouslySetInnerHTML={{ __html: themeCSS }} />}
290
+ <ThemePlaceholderWrapper
291
+ slug={slug}
292
+ initialSections={sections}
293
+ initialPlaceholderId={data?.slug?.toString() || slug}
294
+ isDesignMode={false}
295
+ dataSources={dataSources}
296
+ initialThemeSettings={themeSettings}
297
+ initialPageContext={pageContext}
298
+ />
299
+ </>
300
+ );
301
+ } catch (error) {
302
+ console.error(`Error fetching theme placeholder data for ${slug}:`, error);
303
+ return null;
304
+ }
305
+ }