@lightspeed/crane 2.0.5 → 3.1.0
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/CHANGELOG.md +50 -0
- package/UPGRADE.md +96 -0
- package/dist/cli.mjs +44 -25
- package/package.json +6 -3
- package/template/blank/sections/blank-section/BlankSection.vue +9 -0
- package/template/blank/sections/blank-section/assets/blank_section_showcase_1_preview.jpg +0 -0
- package/template/blank/sections/blank-section/client.ts +6 -0
- package/template/blank/sections/blank-section/server.ts +6 -0
- package/template/blank/sections/blank-section/settings/content.ts +2 -0
- package/template/blank/sections/blank-section/settings/design.ts +2 -0
- package/template/blank/sections/blank-section/settings/layout.ts +2 -0
- package/template/blank/sections/blank-section/settings/translations.ts +8 -0
- package/template/blank/sections/blank-section/showcases/1.ts +15 -0
- package/template/blank/sections/blank-section/showcases/translations.ts +10 -0
- package/template/blank/sections/blank-section/type.ts +5 -0
- package/template/collections/assets/collection_cover_image.png +0 -0
- package/template/collections/example-collection/configuration.ts +21 -0
- package/template/crane.config.json +1 -1
- package/template/footers/example-footer/ExampleFooter.vue +4 -4
- package/template/footers/example-footer/component/LegalLinks.vue +1 -1
- package/template/footers/example-footer/component/MadeWith.vue +1 -1
- package/template/footers/example-footer/component/ReportAbuse.vue +1 -1
- package/template/footers/example-footer/settings/design.ts +5 -4
- package/template/footers/example-footer/settings/translations.ts +4 -3
- package/template/footers/example-footer/showcases/1.ts +4 -2
- package/template/footers/example-footer/showcases/translations.ts +4 -2
- package/template/headers/example-header/ExampleHeader.vue +1 -1
- package/template/headers/example-header/component/Account.vue +1 -1
- package/template/headers/example-header/component/CategoriesDropdown.vue +1 -1
- package/template/headers/example-header/component/Logo.vue +7 -7
- package/template/headers/example-header/component/NavigationMenu.vue +1 -1
- package/template/headers/example-header/settings/content.ts +8 -1
- package/template/headers/example-header/settings/design.ts +7 -1
- package/template/headers/example-header/settings/layout.ts +1 -1
- package/template/headers/example-header/settings/translations.ts +4 -2
- package/template/headers/example-header/showcases/1.ts +15 -11
- package/template/headers/example-header/showcases/2.ts +12 -8
- package/template/headers/example-header/showcases/translations.ts +4 -2
- package/template/layouts/catalog/example-catalog/components/Icon.vue +14 -14
- package/template/layouts/catalog/example-catalog/slots/custom-bottom-bar/CustomBottomBar.vue +1 -1
- package/template/package.json +1 -0
- package/template/page-templates/example-template/configuration.ts +8 -10
- package/template/page-templates/example-template/pages/catalog.ts +3 -6
- package/template/page-templates/example-template/pages/category.ts +3 -6
- package/template/page-templates/example-template/pages/home.ts +42 -57
- package/template/page-templates/example-template/pages/product.ts +3 -6
- package/template/preview/sections/preview.html +10 -6
- package/template/preview/shared/api-routes.ts +236 -103
- package/template/preview/shared/logger.ts +9 -0
- package/template/preview/shared/preview.ts +117 -76
- package/template/preview/shared/utils.ts +68 -43
- package/template/preview/ssr-server.ts +1 -1
- package/template/reference/sections/about-us/AboutUs.vue +20 -22
- package/template/reference/sections/about-us/component/Image.vue +18 -18
- package/template/reference/sections/about-us/component/Stats.vue +40 -40
- package/template/reference/sections/about-us/component/Title.vue +1 -1
- package/template/reference/sections/about-us/settings/content.ts +15 -19
- package/template/reference/sections/about-us/settings/design.ts +14 -18
- package/template/reference/sections/about-us/settings/layout.ts +7 -5
- package/template/reference/sections/about-us/settings/translations.ts +4 -2
- package/template/reference/sections/about-us/showcases/1.ts +48 -62
- package/template/reference/sections/about-us/showcases/2.ts +44 -56
- package/template/reference/sections/about-us/showcases/translations.ts +4 -2
- package/template/reference/sections/featured-products/FeaturedProducts.vue +12 -6
- package/template/reference/sections/featured-products/component/ProductItem.vue +18 -1
- package/template/reference/sections/featured-products/component/ProductPlaceholder.vue +42 -0
- package/template/reference/sections/featured-products/component/Title.vue +1 -1
- package/template/reference/sections/featured-products/settings/content.ts +8 -10
- package/template/reference/sections/featured-products/settings/design.ts +7 -7
- package/template/reference/sections/featured-products/settings/translations.ts +4 -2
- package/template/reference/sections/featured-products/showcases/1.ts +8 -12
- package/template/reference/sections/featured-products/showcases/translations.ts +4 -2
- package/template/reference/sections/intro-slider/IntroSlider.vue +6 -6
- package/template/reference/sections/intro-slider/component/Slider.vue +42 -43
- package/template/reference/sections/intro-slider/component/Title.vue +7 -7
- package/template/reference/sections/intro-slider/settings/content.ts +33 -36
- package/template/reference/sections/intro-slider/settings/design.ts +17 -22
- package/template/reference/sections/intro-slider/settings/layout.ts +6 -4
- package/template/reference/sections/intro-slider/settings/translations.ts +4 -2
- package/template/reference/sections/intro-slider/showcases/1.ts +52 -75
- package/template/reference/sections/intro-slider/showcases/2.ts +50 -72
- package/template/reference/sections/intro-slider/showcases/translations.ts +4 -2
- package/template/reference/sections/tag-lines/TagLines.vue +41 -47
- package/template/reference/sections/tag-lines/component/HighlightedText.vue +1 -1
- package/template/reference/sections/tag-lines/component/SectionImage.vue +18 -18
- package/template/reference/sections/tag-lines/component/Title.vue +1 -1
- package/template/reference/sections/tag-lines/settings/content.ts +47 -47
- package/template/reference/sections/tag-lines/settings/design.ts +15 -19
- package/template/reference/sections/tag-lines/settings/layout.ts +6 -4
- package/template/reference/sections/tag-lines/settings/translations.ts +4 -2
- package/template/reference/sections/tag-lines/showcases/1.ts +40 -50
- package/template/reference/sections/tag-lines/showcases/2.ts +40 -50
- package/template/reference/sections/tag-lines/showcases/translations.ts +4 -2
- package/template/reference/sections/trending-categories/TrendingCategories.vue +1 -1
- package/template/reference/sections/trending-categories/component/CategoryItem.vue +18 -1
- package/template/reference/sections/trending-categories/component/Title.vue +1 -1
- package/template/reference/sections/trending-categories/settings/content.ts +8 -10
- package/template/reference/sections/trending-categories/settings/design.ts +7 -7
- package/template/reference/sections/trending-categories/settings/translations.ts +4 -2
- package/template/reference/sections/trending-categories/showcases/1.ts +14 -15
- package/template/reference/sections/trending-categories/showcases/translations.ts +4 -2
- package/template/reference/shared/components/Button.vue +6 -6
- package/template/reference/shared/components/SectionWrapper.vue +5 -5
- package/template/reference/shared/components/Tagline.vue +12 -11
- package/template/reference/templates/reference-template-apparel/configuration.ts +8 -8
- package/template/reference/templates/reference-template-apparel/pages/catalog.ts +3 -6
- package/template/reference/templates/reference-template-apparel/pages/category.ts +3 -6
- package/template/reference/templates/reference-template-apparel/pages/home.ts +14 -18
- package/template/reference/templates/reference-template-apparel/pages/product.ts +3 -6
- package/template/reference/templates/reference-template-bike/configuration.ts +9 -9
- package/template/reference/templates/reference-template-bike/pages/catalog.ts +3 -6
- package/template/reference/templates/reference-template-bike/pages/category.ts +3 -6
- package/template/reference/templates/reference-template-bike/pages/home.ts +14 -18
- package/template/reference/templates/reference-template-bike/pages/product.ts +3 -6
- package/template/sections/example-section/ExampleSection.vue +3 -5
- package/template/sections/example-section/component/button/Button.vue +1 -1
- package/template/sections/example-section/component/image/Image.vue +43 -43
- package/template/sections/example-section/component/image/ImagesGrid.vue +21 -32
- package/template/sections/example-section/component/selectbox/Selectbox.vue +1 -1
- package/template/sections/example-section/component/title/Title.vue +1 -1
- package/template/sections/example-section/component/toggle/Toggle.vue +4 -4
- package/template/sections/example-section/settings/content.ts +25 -34
- package/template/sections/example-section/settings/design.ts +23 -19
- package/template/sections/example-section/settings/layout.ts +18 -14
- package/template/sections/example-section/settings/translations.ts +17 -2
- package/template/sections/example-section/showcases/1.ts +55 -79
- package/template/sections/example-section/showcases/2.ts +49 -62
- package/template/sections/example-section/showcases/3.ts +53 -76
- package/template/sections/example-section/showcases/translations.ts +13 -2
- package/template/shared/components/LanguageSelector.vue +1 -1
- package/template/shared/components/SectionWrapper.vue +5 -5
|
@@ -2,9 +2,18 @@
|
|
|
2
2
|
import { getExternalContentMock } from './mock';
|
|
3
3
|
import { loadModule, renderServerModule } from './utils';
|
|
4
4
|
|
|
5
|
+
export type BlockType = 'sections' | 'headers' | 'footers';
|
|
6
|
+
export const BLOCK_TYPES: BlockType[] = ['sections', 'headers', 'footers'];
|
|
7
|
+
|
|
8
|
+
const BLOCK_TYPE_LABELS: Record<BlockType, string> = {
|
|
9
|
+
sections: 'Section',
|
|
10
|
+
headers: 'Header',
|
|
11
|
+
footers: 'Footer',
|
|
12
|
+
};
|
|
13
|
+
|
|
5
14
|
let distFolderPath: string | null = null;
|
|
6
15
|
let currentApp: any = null;
|
|
7
|
-
let
|
|
16
|
+
let currentBlockKey: string | null = null;
|
|
8
17
|
|
|
9
18
|
export function setDistFolderPath(path: string): void {
|
|
10
19
|
distFolderPath = path;
|
|
@@ -17,8 +26,8 @@ export function setDistFolderPath(path: string): void {
|
|
|
17
26
|
/**
|
|
18
27
|
* Loads all required modules for a showcase.
|
|
19
28
|
*/
|
|
20
|
-
async function loadShowcaseModules(
|
|
21
|
-
const basePath = `${distFolder}
|
|
29
|
+
async function loadShowcaseModules(blockType: BlockType, blockName: string, showcaseId: string, distFolder: string): Promise<[any, any, any, any, any]> {
|
|
30
|
+
const basePath = `${distFolder}/${blockType}/${blockName}`;
|
|
22
31
|
return Promise.all([
|
|
23
32
|
loadModule(`${basePath}/js/settings/content.mjs`),
|
|
24
33
|
loadModule(`${basePath}/js/settings/translations.mjs`),
|
|
@@ -230,12 +239,12 @@ export function mergeDesign(
|
|
|
230
239
|
return result;
|
|
231
240
|
}
|
|
232
241
|
|
|
233
|
-
export function designTransformer(design: Record<string, any>, showCaseDesign: Record<string, any
|
|
242
|
+
export function designTransformer(design: Record<string, any>, showCaseDesign: Record<string, any>, isApiRender: boolean = false): Record<string, any> {
|
|
234
243
|
const parsedDesign: Record<string, any> = {};
|
|
235
244
|
const parsedShowcaseDesign: Record<string, any> = {};
|
|
236
245
|
Object.entries(design || {}).forEach(([key, comp]) => {
|
|
237
|
-
// Skip DIVIDER elements - they're UI-only and don't have runtime data
|
|
238
|
-
if (comp?.type === 'DIVIDER') {
|
|
246
|
+
// Skip DIVIDER and INFO elements - they're UI-only and don't have runtime data
|
|
247
|
+
if (comp?.type === 'DIVIDER' || comp?.type === 'INFO') {
|
|
239
248
|
return;
|
|
240
249
|
}
|
|
241
250
|
// Skip BACKGROUND - it's handled separately by createBackgroundDesign
|
|
@@ -247,8 +256,8 @@ export function designTransformer(design: Record<string, any>, showCaseDesign: R
|
|
|
247
256
|
});
|
|
248
257
|
|
|
249
258
|
Object.entries(showCaseDesign || {}).forEach(([key, comp]) => {
|
|
250
|
-
// Skip DIVIDER elements - they're UI-only and don't have runtime data
|
|
251
|
-
if (comp?.type === 'DIVIDER') {
|
|
259
|
+
// Skip DIVIDER and INFO elements - they're UI-only and don't have runtime data
|
|
260
|
+
if (comp?.type === 'DIVIDER' || comp?.type === 'INFO') {
|
|
252
261
|
return;
|
|
253
262
|
}
|
|
254
263
|
// Skip BACKGROUND - it's handled separately by createBackgroundDesign
|
|
@@ -267,15 +276,17 @@ export function designTransformer(design: Record<string, any>, showCaseDesign: R
|
|
|
267
276
|
console.error('[Preview] Failed to update hex colors:', error);
|
|
268
277
|
}
|
|
269
278
|
|
|
270
|
-
|
|
279
|
+
if ( !isApiRender ) {
|
|
280
|
+
replaceGlobalFont(overridenDesign, 'Roboto');
|
|
281
|
+
}
|
|
271
282
|
return overridenDesign;
|
|
272
283
|
}
|
|
273
284
|
|
|
274
|
-
function processImage(component: any,
|
|
285
|
+
function processImage(component: any, blockType: BlockType, blockName: string, key: string, isApiRender: boolean = false): Record<string, any> {
|
|
275
286
|
try {
|
|
276
287
|
const assetLocation = isApiRender ?
|
|
277
|
-
`http://${distFolderPath}
|
|
278
|
-
`${distFolderPath}
|
|
288
|
+
`http://${distFolderPath}/${blockType}/${blockName}/assets/` :
|
|
289
|
+
`${distFolderPath}/${blockType}/${blockName}/assets/`;
|
|
279
290
|
|
|
280
291
|
const set = component.defaults?.imageData?.set || component.imageData?.set;
|
|
281
292
|
const newSet = {
|
|
@@ -297,7 +308,7 @@ function processImage(component: any, sectionName: string, key: string, isApiRen
|
|
|
297
308
|
}
|
|
298
309
|
}
|
|
299
310
|
|
|
300
|
-
export function processComponent(key: string, component: any, translations: any,
|
|
311
|
+
export function processComponent(key: string, component: any, translations: any, blockType?: BlockType, blockName?: string,
|
|
301
312
|
isApiRender: boolean = false, extraParams?: Record<string, any>): any {
|
|
302
313
|
if (!component?.type) return {};
|
|
303
314
|
switch (component.type) {
|
|
@@ -313,16 +324,16 @@ export function processComponent(key: string, component: any, translations: any,
|
|
|
313
324
|
if (defaultSettings) {
|
|
314
325
|
const result: Record<string, any> = {};
|
|
315
326
|
Object.entries(defaultSettings).forEach(([k, c]) => {
|
|
316
|
-
Object.assign(result, processComponent(k, c, translations,
|
|
327
|
+
Object.assign(result, processComponent(k, c, translations, blockType, blockName, isApiRender));
|
|
317
328
|
});
|
|
318
329
|
return { [key]: result };
|
|
319
330
|
} else {
|
|
320
331
|
const cards = component.cards.map((card: any, index: number) => {
|
|
321
332
|
const cardContent: Record<string, any> = {};
|
|
322
333
|
Object.entries(card.settings).forEach(([k, c]) => {
|
|
323
|
-
Object.assign(cardContent, processComponent(k, c, translations,
|
|
334
|
+
Object.assign(cardContent, processComponent(k, c, translations, blockType, blockName, isApiRender));
|
|
324
335
|
});
|
|
325
|
-
return { settings: cardContent, id: index.toString(), title: extraParams?.deckCardLabel};
|
|
336
|
+
return { settings: cardContent, id: index.toString(), title: extraParams?.deckCardLabel };
|
|
326
337
|
});
|
|
327
338
|
return { [key]: { cards } };
|
|
328
339
|
}
|
|
@@ -340,14 +351,14 @@ export function processComponent(key: string, component: any, translations: any,
|
|
|
340
351
|
case 'TOGGLE': {
|
|
341
352
|
return {
|
|
342
353
|
[key]: {
|
|
343
|
-
enabled: component.defaults?.enabled ?? component.enabled
|
|
344
|
-
}
|
|
354
|
+
enabled: component.defaults?.enabled ?? component.enabled,
|
|
355
|
+
},
|
|
345
356
|
};
|
|
346
357
|
}
|
|
347
358
|
case 'SELECTBOX':
|
|
348
359
|
return { [key]: component.defaults?.value ?? component.value };
|
|
349
360
|
case 'IMAGE':
|
|
350
|
-
return processImage(component,
|
|
361
|
+
return processImage(component, blockType!, blockName!, key, isApiRender);
|
|
351
362
|
case 'INFO':
|
|
352
363
|
return {
|
|
353
364
|
[key]: {
|
|
@@ -359,11 +370,38 @@ export function processComponent(key: string, component: any, translations: any,
|
|
|
359
370
|
},
|
|
360
371
|
},
|
|
361
372
|
};
|
|
362
|
-
// Leaving PRODUCT_SELECTOR and CATEGORY_SELECTOR references for future implementation
|
|
363
373
|
case 'PRODUCT_SELECTOR':
|
|
364
|
-
return {
|
|
374
|
+
return {
|
|
375
|
+
[key]: {
|
|
376
|
+
maxProducts: component?.maxProducts ?? component?.defaults?.maxProducts,
|
|
377
|
+
products: {
|
|
378
|
+
selectionType: 'MANUAL',
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
};
|
|
365
382
|
case 'CATEGORY_SELECTOR':
|
|
366
|
-
return {
|
|
383
|
+
return {
|
|
384
|
+
[key]: {
|
|
385
|
+
maxCategories: component?.maxCategories ?? component?.defaults?.maxCategories,
|
|
386
|
+
categories: {
|
|
387
|
+
selectionType: 'ROOT',
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
case 'NAVIGATION_MENU':
|
|
392
|
+
return {
|
|
393
|
+
[key]: {
|
|
394
|
+
items: component.items ?? component?.defaults?.items ?? [],
|
|
395
|
+
},
|
|
396
|
+
};
|
|
397
|
+
case 'LOGO':
|
|
398
|
+
return {
|
|
399
|
+
[key]: {
|
|
400
|
+
text: translations[component?.text ?? component?.defaults?.text ?? ''],
|
|
401
|
+
type: component.logoType,
|
|
402
|
+
image: component.image,
|
|
403
|
+
},
|
|
404
|
+
};
|
|
367
405
|
default:
|
|
368
406
|
return {};
|
|
369
407
|
}
|
|
@@ -374,7 +412,8 @@ function getContentToRender(
|
|
|
374
412
|
showcase: any,
|
|
375
413
|
contentTranslations: any,
|
|
376
414
|
showcaseTranslations: any,
|
|
377
|
-
|
|
415
|
+
blockType: BlockType,
|
|
416
|
+
blockName: string,
|
|
378
417
|
isApiRender: boolean = false,
|
|
379
418
|
): any {
|
|
380
419
|
let deckCardLabel: string | undefined = undefined;
|
|
@@ -382,12 +421,11 @@ function getContentToRender(
|
|
|
382
421
|
if ((c as any).type === 'DECK') {
|
|
383
422
|
deckCardLabel = contentTranslations[(c as any).cards?.defaultCardContent?.label];
|
|
384
423
|
}
|
|
385
|
-
return { ...acc, ...processComponent(k, c, contentTranslations,
|
|
424
|
+
return { ...acc, ...processComponent(k, c, contentTranslations, blockType, blockName, isApiRender) };
|
|
386
425
|
}, {});
|
|
387
426
|
|
|
388
|
-
|
|
389
427
|
const parsedShowcase = Object.entries(showcase || {}).reduce((acc, [k, c]) => {
|
|
390
|
-
return { ...acc, ...processComponent(k, c, showcaseTranslations,
|
|
428
|
+
return { ...acc, ...processComponent(k, c, showcaseTranslations, blockType, blockName, isApiRender, { deckCardLabel }) };
|
|
391
429
|
}, {});
|
|
392
430
|
|
|
393
431
|
return overrideSettingsFromShowcase(parsedContent, parsedShowcase);
|
|
@@ -395,44 +433,34 @@ function getContentToRender(
|
|
|
395
433
|
|
|
396
434
|
export function dropdownOptions(showcaseModules: Record<string, any>): Array<{ value: string; label: string }> {
|
|
397
435
|
return Object.keys(showcaseModules).map((path) => {
|
|
398
|
-
const match = path.match(/\/sections\/([^/]+)\/js\/showcases\/(\d+)\.mjs$/);
|
|
436
|
+
const match = path.match(/\/(sections|headers|footers)\/([^/]+)\/js\/showcases\/(\d+)\.mjs$/);
|
|
399
437
|
if (!match) return null;
|
|
438
|
+
const blockType = match[1] as BlockType;
|
|
439
|
+
const blockName = match[2];
|
|
440
|
+
const showcaseId = match[3];
|
|
400
441
|
return {
|
|
401
|
-
value: `${
|
|
402
|
-
label: `${
|
|
442
|
+
value: `${blockType}:${blockName}:${showcaseId}`,
|
|
443
|
+
label: `${BLOCK_TYPE_LABELS[blockType]}: ${blockName} - Showcase ${showcaseId}`,
|
|
403
444
|
};
|
|
404
445
|
}).filter(Boolean) as Array<{ value: string; label: string }>;
|
|
405
446
|
}
|
|
406
447
|
|
|
407
|
-
export function
|
|
408
|
-
const cssHref = `${distFolderPath}/sections/${sectionName}/js/main/client/assets/client.css`;
|
|
409
|
-
|
|
410
|
-
// Check if CSS for this section is already loaded
|
|
411
|
-
const existingLink = document.querySelector(`link[href="${cssHref}"]`);
|
|
412
|
-
if (existingLink) {
|
|
413
|
-
return; // CSS already loaded
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// Remove CSS from other sections (cleanup)
|
|
417
|
-
const oldLinks = document.querySelectorAll('link[data-section-css]');
|
|
418
|
-
oldLinks.forEach(link => link.remove());
|
|
419
|
-
|
|
448
|
+
export function loadBlockCss(blockType: BlockType, blockName: string): void {
|
|
420
449
|
const link = document.createElement('link');
|
|
421
450
|
link.rel = 'stylesheet';
|
|
422
|
-
link.href = `${distFolderPath}
|
|
423
|
-
link.setAttribute('data-section-css', sectionName);
|
|
451
|
+
link.href = `${distFolderPath}/${blockType}/${blockName}/js/main/client/assets/client.css`;
|
|
424
452
|
document.head.appendChild(link);
|
|
425
453
|
}
|
|
426
454
|
|
|
427
455
|
/**
|
|
428
456
|
* Renders a showcase in the browser (client-side).
|
|
429
457
|
*/
|
|
430
|
-
export async function renderShowcase(
|
|
458
|
+
export async function renderShowcase(blockType: BlockType, blockName: string, showcaseId: string): Promise<void> {
|
|
431
459
|
if (!distFolderPath) throw new Error('distFolderPath not set');
|
|
432
460
|
|
|
433
|
-
const basePath = `${distFolderPath}
|
|
461
|
+
const basePath = `${distFolderPath}/${blockType}/${blockName}`;
|
|
434
462
|
const [content, contentTranslations, showcaseTranslations, showcase, design]
|
|
435
|
-
= await loadShowcaseModules(
|
|
463
|
+
= await loadShowcaseModules(blockType, blockName, showcaseId, distFolderPath);
|
|
436
464
|
|
|
437
465
|
const client = await loadModule(`${basePath}/js/main/client/client.js`) as any;
|
|
438
466
|
|
|
@@ -448,11 +476,12 @@ export async function renderShowcase(sectionName: string, showcaseId: string): P
|
|
|
448
476
|
(showcase as any).default.content || {},
|
|
449
477
|
(contentTranslations as any).default.en,
|
|
450
478
|
(showcaseTranslations as any).default.en,
|
|
451
|
-
|
|
479
|
+
blockType,
|
|
480
|
+
blockName,
|
|
452
481
|
);
|
|
453
482
|
|
|
454
483
|
// Load CSS
|
|
455
|
-
|
|
484
|
+
loadBlockCss(blockType, blockName);
|
|
456
485
|
|
|
457
486
|
// Prepare state
|
|
458
487
|
const state = {
|
|
@@ -466,63 +495,63 @@ export async function renderShowcase(sectionName: string, showcaseId: string): P
|
|
|
466
495
|
},
|
|
467
496
|
};
|
|
468
497
|
|
|
469
|
-
// Check if we're switching to a different
|
|
470
|
-
const
|
|
498
|
+
// Check if we're switching to a different block
|
|
499
|
+
const blockKey = `${blockType}:${blockName}`;
|
|
500
|
+
const isSameBlock = currentBlockKey === blockKey;
|
|
471
501
|
|
|
472
502
|
// Mount or update app
|
|
473
|
-
if (currentApp &&
|
|
474
|
-
// Same section, just update with new showcase data
|
|
503
|
+
if (currentApp && isSameBlock) {
|
|
475
504
|
currentApp.update(state);
|
|
476
505
|
} else {
|
|
477
|
-
// Different section or first load - unmount old app and mount new one
|
|
478
506
|
if (currentApp) {
|
|
479
507
|
currentApp.unmount();
|
|
480
|
-
// Clear the app container
|
|
481
508
|
const appContainer = document.getElementById('app');
|
|
482
509
|
if (appContainer) {
|
|
483
510
|
appContainer.innerHTML = '';
|
|
484
511
|
}
|
|
485
512
|
}
|
|
486
|
-
|
|
487
513
|
const { mount, update, unmount } = (client as any).default.init();
|
|
488
514
|
currentApp = { update, unmount };
|
|
489
|
-
|
|
515
|
+
currentBlockKey = blockKey;
|
|
490
516
|
mount('#app', state);
|
|
491
517
|
}
|
|
492
518
|
}
|
|
493
519
|
|
|
494
|
-
async function getLayout(showcase: any,
|
|
520
|
+
async function getLayout(showcase: any, blockType: BlockType, blockName: string, distFolder: string) {
|
|
495
521
|
// First, try to get layoutId from showcase
|
|
496
522
|
if (showcase.default?.layoutId) {
|
|
497
523
|
return showcase.default.layoutId;
|
|
498
524
|
}
|
|
499
525
|
|
|
500
|
-
// If not
|
|
501
|
-
|
|
502
|
-
|
|
526
|
+
// If not in showcase, try reading from section's layout.mjs (optional)
|
|
527
|
+
try {
|
|
528
|
+
const basePath = `${distFolder}/${blockType}/${blockName}`;
|
|
529
|
+
const layoutModule = await loadModule(`${basePath}/js/settings/layout.mjs`) as any;
|
|
503
530
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
531
|
+
// layout.mjs exports an array of layout configurations
|
|
532
|
+
// Return the first element's layoutId
|
|
533
|
+
if (Array.isArray(layoutModule.default) && layoutModule.default.length > 0) {
|
|
534
|
+
return layoutModule.default[0].layoutId;
|
|
535
|
+
}
|
|
536
|
+
} catch {
|
|
537
|
+
// Layout file is optional
|
|
508
538
|
}
|
|
509
|
-
throw new Error('Layout module is not an array or is empty');
|
|
510
539
|
}
|
|
511
540
|
|
|
512
541
|
/**
|
|
513
542
|
* Prepares showcase data (content and design) without rendering.
|
|
514
543
|
* Used by API routes to return tile data.
|
|
515
544
|
*/
|
|
516
|
-
export async function getShowcaseData(
|
|
545
|
+
export async function getShowcaseData(blockType: BlockType, blockName: string, showcaseId: string, distFolder: string) {
|
|
517
546
|
const [content, contentTranslations, showcaseTranslations, showcase, design]
|
|
518
|
-
= await loadShowcaseModules(
|
|
547
|
+
= await loadShowcaseModules(blockType, blockName, showcaseId, distFolder);
|
|
519
548
|
|
|
520
549
|
// Prepare design
|
|
521
550
|
const showcaseBackground = (showcase as any).default?.design?.background;
|
|
522
551
|
const backgroundDesign = createBackgroundDesign(showcaseBackground, true); // API render
|
|
523
|
-
const overriddenDesign = designTransformer((design as any).default, (showcase as any).default.design || {});
|
|
552
|
+
const overriddenDesign = designTransformer((design as any).default, (showcase as any).default.design || {}, true);
|
|
524
553
|
overriddenDesign.background = backgroundDesign;
|
|
525
|
-
overriddenDesign.layout = await getLayout(showcase,
|
|
554
|
+
overriddenDesign.layout = await getLayout(showcase, blockType, blockName, distFolder);
|
|
526
555
|
|
|
527
556
|
// Prepare content
|
|
528
557
|
const overriddenContent = getContentToRender(
|
|
@@ -530,13 +559,19 @@ export async function getShowcaseData(sectionName: string, showcaseId: string, d
|
|
|
530
559
|
(showcase as any).default.content || {},
|
|
531
560
|
(contentTranslations as any).default.en,
|
|
532
561
|
(showcaseTranslations as any).default.en,
|
|
533
|
-
|
|
534
|
-
|
|
562
|
+
blockType,
|
|
563
|
+
blockName,
|
|
564
|
+
true,
|
|
535
565
|
);
|
|
536
566
|
|
|
567
|
+
// Resolve tileName from showcase blockName translation
|
|
568
|
+
const blockNameKey = (showcase as any).default?.blockName;
|
|
569
|
+
const tileName = blockNameKey ? (showcaseTranslations as any).default.en[blockNameKey] : undefined;
|
|
570
|
+
|
|
537
571
|
return {
|
|
538
572
|
content: overriddenContent,
|
|
539
573
|
design: overriddenDesign,
|
|
574
|
+
tileName,
|
|
540
575
|
};
|
|
541
576
|
}
|
|
542
577
|
|
|
@@ -544,20 +579,26 @@ export async function getShowcaseData(sectionName: string, showcaseId: string, d
|
|
|
544
579
|
* Gets the showcase state by rendering server-side.
|
|
545
580
|
* Uses renderServerModule to get HTML directly from the SSR server.
|
|
546
581
|
*/
|
|
547
|
-
export async function getShowcaseState(
|
|
582
|
+
export async function getShowcaseState(
|
|
583
|
+
blockType: BlockType,
|
|
584
|
+
blockName: string,
|
|
585
|
+
showcaseId: string,
|
|
586
|
+
distFolder: string,
|
|
587
|
+
context: Record<string, any>,
|
|
588
|
+
externalContent: Record<string, any>) {
|
|
548
589
|
const { content: overriddenContent, design: overriddenDesign }
|
|
549
|
-
= await getShowcaseData(
|
|
590
|
+
= await getShowcaseData(blockType, blockName, showcaseId, distFolder);
|
|
550
591
|
|
|
551
592
|
const data = {
|
|
552
593
|
content: overriddenContent,
|
|
553
594
|
design: overriddenDesign,
|
|
554
595
|
defaults: {},
|
|
555
596
|
background: {},
|
|
556
|
-
externalContent:
|
|
597
|
+
externalContent: externalContent,
|
|
557
598
|
};
|
|
558
599
|
|
|
559
600
|
// Render server-side using the SSR server
|
|
560
|
-
const basePath = `${distFolder}
|
|
601
|
+
const basePath = `${distFolder}/${blockType}/${blockName}`;
|
|
561
602
|
const serverModulePath = `${basePath}/js/main/server/server.js`;
|
|
562
603
|
const html = await renderServerModule(serverModulePath, context, data);
|
|
563
604
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
|
|
5
|
-
import { getShowcaseState } from './preview';
|
|
5
|
+
import { getShowcaseState, BlockType, BLOCK_TYPES } from './preview';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Renders a server.js module by directly calling the SSR server.
|
|
@@ -96,6 +96,7 @@ export async function fetchTiles(authToken?: string, tilesUrl?: string): Promise
|
|
|
96
96
|
* @param showcaseId - The showcase ID to match
|
|
97
97
|
* @param content - The new content to replace
|
|
98
98
|
* @param design - The new design to replace
|
|
99
|
+
* @param tileName - The new tile name to replace
|
|
99
100
|
* @returns The updated tiles response object
|
|
100
101
|
*/
|
|
101
102
|
export function updateTilesSection(
|
|
@@ -104,24 +105,26 @@ export function updateTilesSection(
|
|
|
104
105
|
showcaseId: string,
|
|
105
106
|
content: any,
|
|
106
107
|
design: any,
|
|
108
|
+
tileName?: string,
|
|
107
109
|
): any {
|
|
108
110
|
const targetPattern = `_${sectionName}_${showcaseId}`;
|
|
109
111
|
|
|
110
|
-
const
|
|
112
|
+
const updatedTiles = tilesResponse.tiles.map((item: any) => {
|
|
111
113
|
const itemId = item.sourceId;
|
|
112
|
-
if (itemId && typeof itemId === 'string') {
|
|
113
|
-
|
|
114
|
+
if (itemId && typeof itemId === 'string' && itemId.includes(targetPattern)) {
|
|
115
|
+
item.content = content;
|
|
116
|
+
// If new design has no layout, use CUSTOM_LAYOUT
|
|
117
|
+
const layout = design.layout || 'CUSTOM_LAYOUT';
|
|
118
|
+
item.design = design;
|
|
119
|
+
item.design.layout = layout;
|
|
120
|
+
if (tileName) {
|
|
121
|
+
item.tileName = tileName;
|
|
122
|
+
}
|
|
114
123
|
}
|
|
115
|
-
return
|
|
124
|
+
return item;
|
|
116
125
|
});
|
|
117
126
|
|
|
118
|
-
|
|
119
|
-
// Replace only content and design, preserve all other properties including tils
|
|
120
|
-
targetItem.content = content;
|
|
121
|
-
targetItem.design = design;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return tilesResponse;
|
|
127
|
+
return { ...tilesResponse, tiles: updatedTiles };
|
|
125
128
|
}
|
|
126
129
|
|
|
127
130
|
/**
|
|
@@ -154,17 +157,18 @@ export async function getSectionIds(sectionName: string, showcaseId: string, aut
|
|
|
154
157
|
}
|
|
155
158
|
|
|
156
159
|
/**
|
|
157
|
-
* Gets all showcase IDs for a given
|
|
158
|
-
* @param
|
|
160
|
+
* Gets all showcase IDs for a given block
|
|
161
|
+
* @param blockType - The block type ('sections', 'headers', 'footers')
|
|
162
|
+
* @param blockName - The block name (e.g., 'example-section')
|
|
159
163
|
* @param distPath - The dist path
|
|
160
164
|
* @returns Array of showcase IDs
|
|
161
165
|
*/
|
|
162
|
-
export function getShowcaseIds(
|
|
166
|
+
export function getShowcaseIds(blockType: BlockType, blockName: string, distPath: string): string[] {
|
|
163
167
|
try {
|
|
164
|
-
const showcasesPath = path.join(distPath,
|
|
168
|
+
const showcasesPath = path.join(distPath, blockType, blockName, 'js', 'showcases');
|
|
165
169
|
|
|
166
170
|
if (!fs.existsSync(showcasesPath)) {
|
|
167
|
-
console.error(`Showcases directory not found for
|
|
171
|
+
console.error(`Showcases directory not found for ${blockType}: ${blockName}`);
|
|
168
172
|
return [];
|
|
169
173
|
}
|
|
170
174
|
|
|
@@ -174,23 +178,38 @@ export function getShowcaseIds(sectionName: string, distPath: string): string[]
|
|
|
174
178
|
|
|
175
179
|
return showcaseIds;
|
|
176
180
|
} catch (error) {
|
|
177
|
-
console.error(`Error getting showcase IDs for
|
|
181
|
+
console.error(`Error getting showcase IDs for ${blockType} ${blockName}:`, error);
|
|
178
182
|
return [];
|
|
179
183
|
}
|
|
180
184
|
}
|
|
181
185
|
|
|
182
186
|
/**
|
|
183
|
-
*
|
|
184
|
-
|
|
187
|
+
* Determine the block type by checking which folder contains the block
|
|
188
|
+
*/
|
|
189
|
+
export function getBlockType(blockName: string, distPath: string): BlockType | null {
|
|
190
|
+
for (const blockType of BLOCK_TYPES) {
|
|
191
|
+
const blockPath = path.join(distPath, blockType, blockName);
|
|
192
|
+
if (fs.existsSync(blockPath)) {
|
|
193
|
+
return blockType;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Updates all matching block showcases in custom content
|
|
201
|
+
* @param blockType - The block type ('sections', 'headers', 'footers')
|
|
202
|
+
* @param blockName - The block name
|
|
185
203
|
* @param showcaseId - The showcase ID
|
|
186
204
|
* @param distPath - The dist path
|
|
187
205
|
* @param customContentSections - The sections array from custom content
|
|
188
206
|
* @param authToken - Bearer token
|
|
189
207
|
* @param tilesUrl - The tiles URL
|
|
190
|
-
* @returns True if at least one
|
|
208
|
+
* @returns True if at least one block was updated successfully, false otherwise
|
|
191
209
|
*/
|
|
192
|
-
async function
|
|
193
|
-
|
|
210
|
+
async function updateBlockShowcase(
|
|
211
|
+
blockType: BlockType,
|
|
212
|
+
blockName: string,
|
|
194
213
|
showcaseId: string,
|
|
195
214
|
distPath: string,
|
|
196
215
|
customContentSections: Array<Record<string, unknown>>,
|
|
@@ -198,61 +217,62 @@ async function updateSectionShowcase(
|
|
|
198
217
|
tilesUrl?: string,
|
|
199
218
|
): Promise<boolean> {
|
|
200
219
|
try {
|
|
201
|
-
// Get all section IDs from tiles
|
|
202
|
-
const sectionIds = await getSectionIds(
|
|
220
|
+
// Get all section IDs from tiles matching this block/showcase
|
|
221
|
+
const sectionIds = await getSectionIds(blockName, showcaseId, authToken, tilesUrl);
|
|
203
222
|
|
|
204
223
|
if (!sectionIds || sectionIds.length === 0) {
|
|
205
224
|
return false;
|
|
206
|
-
}
|
|
207
|
-
// Process each section ID
|
|
225
|
+
}// Process each section ID
|
|
208
226
|
for (const sectionId of sectionIds) {
|
|
209
|
-
// Find the
|
|
210
|
-
const
|
|
227
|
+
// Find the block by block ID in custom content
|
|
228
|
+
const targetBlock = customContentSections.find((section) => {
|
|
211
229
|
const state = section.state as Record<string, unknown>;
|
|
212
230
|
const data = state.data as Record<string, unknown>;
|
|
213
231
|
return data.tileId === sectionId;
|
|
214
232
|
});
|
|
215
233
|
|
|
216
|
-
if (!
|
|
217
|
-
console.error(`
|
|
234
|
+
if (!targetBlock) {
|
|
235
|
+
console.error(`Block not found in custom content with ID: ${sectionId}`);
|
|
218
236
|
continue;
|
|
219
237
|
}
|
|
220
238
|
|
|
221
239
|
// Fetch content, design, and html using getShowcaseState
|
|
222
|
-
const state =
|
|
240
|
+
const state = targetBlock.state as Record<string, unknown>;
|
|
223
241
|
// eslint-disable-next-line no-await-in-loop
|
|
224
242
|
const { content, design, html } = await getShowcaseState(
|
|
225
|
-
|
|
243
|
+
blockType,
|
|
244
|
+
blockName,
|
|
226
245
|
showcaseId,
|
|
227
246
|
distPath,
|
|
228
247
|
state.context as Record<string, unknown>,
|
|
248
|
+
(state.data as Record<string, unknown>).externalContent as Record<string, unknown>,
|
|
229
249
|
);
|
|
230
250
|
|
|
231
251
|
// Update content, design, and html
|
|
232
252
|
const data = state.data as Record<string, unknown>;
|
|
233
253
|
data.content = JSON.stringify(content);
|
|
234
254
|
data.design = JSON.stringify(design);
|
|
235
|
-
|
|
255
|
+
targetBlock.html = html;
|
|
236
256
|
}
|
|
237
257
|
|
|
238
258
|
return true;
|
|
239
259
|
} catch (error) {
|
|
240
|
-
console.error(`Error updating ${
|
|
260
|
+
console.error(`Error updating ${blockType} ${blockName} showcase ${showcaseId}:`, error);
|
|
241
261
|
return false;
|
|
242
262
|
}
|
|
243
263
|
}
|
|
244
264
|
|
|
245
265
|
/**
|
|
246
266
|
* Fetches custom content and updates all specified sections with their showcases
|
|
247
|
-
* @param
|
|
267
|
+
* @param blockNames - Array of block names to update (e.g., ['about-us', 'example-header', 'example-footer'])
|
|
248
268
|
* @param distPath - The dist path for getContentAndDesign
|
|
249
269
|
* @param authToken - Bearer token from request header (e.g., "Bearer xxx")
|
|
250
270
|
* @param customContentUrl - The custom content URL
|
|
251
|
-
* @param tilesUrl - The tiles URL to fetch
|
|
271
|
+
* @param tilesUrl - The tiles URL to fetch block IDs
|
|
252
272
|
* @returns The updated custom content response or null if failed
|
|
253
273
|
*/
|
|
254
274
|
export async function updateCustomContent(
|
|
255
|
-
|
|
275
|
+
blockNames: string[],
|
|
256
276
|
distPath: string,
|
|
257
277
|
authToken?: string,
|
|
258
278
|
customContentUrl?: string,
|
|
@@ -290,13 +310,18 @@ export async function updateCustomContent(
|
|
|
290
310
|
}
|
|
291
311
|
|
|
292
312
|
const customContentSections = customContent.sections as Array<Record<string, unknown>>;
|
|
313
|
+
// Process all blocks and showcases in parallel
|
|
314
|
+
const updatePromises = blockNames.flatMap((sectionName) => {
|
|
315
|
+
const blockType = getBlockType(sectionName, distPath);
|
|
316
|
+
if (!blockType) {
|
|
317
|
+
console.warn(`Block not found: ${sectionName}`);
|
|
318
|
+
return [];
|
|
319
|
+
}
|
|
293
320
|
|
|
294
|
-
|
|
295
|
-
const updatePromises = sectionNames.flatMap((sectionName) => {
|
|
296
|
-
const showcaseIds = getShowcaseIds(sectionName, distPath);
|
|
297
|
-
|
|
321
|
+
const showcaseIds = getShowcaseIds(blockType, sectionName, distPath);
|
|
298
322
|
return showcaseIds.map(showcaseId =>
|
|
299
|
-
|
|
323
|
+
updateBlockShowcase(
|
|
324
|
+
blockType,
|
|
300
325
|
sectionName,
|
|
301
326
|
showcaseId,
|
|
302
327
|
distPath,
|
|
@@ -391,7 +391,7 @@ const server = http.createServer(async (req: http.IncomingMessage, res: http.Ser
|
|
|
391
391
|
const { render } = serverModule.init();
|
|
392
392
|
|
|
393
393
|
// Call render with context and data
|
|
394
|
-
const result = await render(context, data || { defaults: {}, background: {}
|
|
394
|
+
const result = await render(context, data || { defaults: {}, background: {} });
|
|
395
395
|
|
|
396
396
|
res.statusCode = 200;
|
|
397
397
|
res.setHeader('Content-Type', 'application/json');
|