@lightspeed/crane 1.4.1 → 2.0.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.
Files changed (116) hide show
  1. package/CHANGELOG.md +51 -4
  2. package/UPGRADE.md +19 -0
  3. package/dist/app.d.mts +1 -1028
  4. package/dist/app.d.ts +1 -1028
  5. package/dist/app.mjs +1 -1
  6. package/dist/cli.mjs +20 -7
  7. package/package.json +6 -3
  8. package/template/footers/example-footer/ExampleFooter.vue +1 -1
  9. package/template/footers/example-footer/client.ts +1 -1
  10. package/template/footers/example-footer/component/LegalLinks.vue +1 -1
  11. package/template/footers/example-footer/component/MadeWith.vue +1 -1
  12. package/template/footers/example-footer/component/ReportAbuse.vue +1 -1
  13. package/template/footers/example-footer/entity/color.ts +2 -2
  14. package/template/footers/example-footer/server.ts +1 -1
  15. package/template/headers/example-header/client.ts +1 -1
  16. package/template/headers/example-header/component/Account.vue +1 -1
  17. package/template/headers/example-header/component/Cart.vue +1 -1
  18. package/template/headers/example-header/component/CategoriesDropdown.vue +1 -1
  19. package/template/headers/example-header/component/Logo.vue +1 -1
  20. package/template/headers/example-header/component/NavigationMenu.vue +1 -1
  21. package/template/headers/example-header/component/SearchForm.vue +1 -1
  22. package/template/headers/example-header/server.ts +1 -1
  23. package/template/index.d.ts +1 -1
  24. package/template/layouts/catalog/example-catalog/Main.vue +1 -1
  25. package/template/layouts/catalog/example-catalog/slots/custom-bottom-bar/client.ts +1 -1
  26. package/template/layouts/catalog/example-catalog/slots/custom-bottom-bar/server.ts +1 -1
  27. package/template/layouts/category/example-category/Main.vue +1 -1
  28. package/template/layouts/category/example-category/settings/content.ts +1 -1
  29. package/template/layouts/category/example-category/settings/design.ts +1 -1
  30. package/template/layouts/product/example-product/Main.vue +1 -1
  31. package/template/layouts/product/example-product/settings/content.ts +1 -1
  32. package/template/layouts/product/example-product/settings/design.ts +1 -1
  33. package/template/package.json +7 -3
  34. package/template/page-templates/example-template/pages/catalog.ts +1 -1
  35. package/template/page-templates/example-template/pages/category.ts +1 -1
  36. package/template/page-templates/example-template/pages/product.ts +1 -1
  37. package/template/preview/sections/preview.html +1 -1
  38. package/template/preview/shared/api-routes.ts +443 -0
  39. package/template/preview/shared/mock.ts +43 -41
  40. package/template/preview/shared/preview.ts +213 -101
  41. package/template/preview/shared/utils.ts +315 -2
  42. package/template/preview/ssr-server.ts +429 -0
  43. package/template/preview/vite.config.js +76 -34
  44. package/template/reference/sections/about-us/AboutUs.vue +1 -1
  45. package/template/reference/sections/about-us/client.ts +1 -1
  46. package/template/reference/sections/about-us/component/Image.vue +1 -1
  47. package/template/reference/sections/about-us/component/Stats.vue +2 -2
  48. package/template/reference/sections/about-us/component/Title.vue +1 -1
  49. package/template/reference/sections/about-us/server.ts +1 -1
  50. package/template/reference/sections/about-us/util/visibility-provider.ts +1 -1
  51. package/template/reference/sections/featured-products/FeaturedProducts.vue +65 -0
  52. package/template/reference/sections/featured-products/assets/arrow.svg +3 -0
  53. package/template/reference/sections/featured-products/assets/custom_section_showcase_1_preview.png +0 -0
  54. package/template/reference/sections/featured-products/client.ts +5 -0
  55. package/template/reference/sections/featured-products/component/ProductItem.vue +71 -0
  56. package/template/reference/sections/featured-products/component/Title.vue +31 -0
  57. package/template/reference/sections/featured-products/entity/color.ts +4 -0
  58. package/template/reference/sections/featured-products/server.ts +5 -0
  59. package/template/reference/sections/featured-products/settings/content.ts +14 -0
  60. package/template/reference/sections/featured-products/settings/design.ts +33 -0
  61. package/template/reference/sections/featured-products/settings/translations.ts +24 -0
  62. package/template/reference/sections/featured-products/showcases/1.ts +28 -0
  63. package/template/reference/sections/featured-products/showcases/translations.ts +16 -0
  64. package/template/reference/sections/featured-products/type.ts +5 -0
  65. package/template/reference/sections/intro-slider/IntroSlider.vue +1 -1
  66. package/template/reference/sections/intro-slider/client.ts +1 -1
  67. package/template/reference/sections/intro-slider/component/Slider.vue +8 -2
  68. package/template/reference/sections/intro-slider/component/Title.vue +1 -1
  69. package/template/reference/sections/intro-slider/entity/color.ts +2 -2
  70. package/template/reference/sections/intro-slider/server.ts +1 -1
  71. package/template/reference/sections/tag-lines/TagLines.vue +1 -1
  72. package/template/reference/sections/tag-lines/client.ts +1 -1
  73. package/template/reference/sections/tag-lines/component/SectionImage.vue +1 -1
  74. package/template/reference/sections/tag-lines/component/Title.vue +1 -1
  75. package/template/reference/sections/tag-lines/composables/highlighted-text-image-list.ts +2 -2
  76. package/template/reference/sections/tag-lines/server.ts +1 -1
  77. package/template/reference/sections/trending-categories/TrendingCategories.vue +70 -0
  78. package/template/reference/sections/trending-categories/assets/arrow.svg +3 -0
  79. package/template/reference/sections/trending-categories/assets/custom_section_showcase_1_preview.png +0 -0
  80. package/template/reference/sections/trending-categories/client.ts +5 -0
  81. package/template/reference/sections/trending-categories/component/CategoryItem.vue +62 -0
  82. package/template/reference/sections/trending-categories/component/Title.vue +32 -0
  83. package/template/reference/sections/trending-categories/entity/color.ts +4 -0
  84. package/template/reference/sections/trending-categories/server.ts +5 -0
  85. package/template/reference/sections/trending-categories/settings/content.ts +14 -0
  86. package/template/reference/sections/trending-categories/settings/design.ts +33 -0
  87. package/template/reference/sections/trending-categories/settings/translations.ts +24 -0
  88. package/template/reference/sections/trending-categories/showcases/1.ts +36 -0
  89. package/template/reference/sections/trending-categories/showcases/translations.ts +22 -0
  90. package/template/reference/sections/trending-categories/type.ts +5 -0
  91. package/template/reference/shared/components/Button.vue +1 -1
  92. package/template/reference/templates/reference-template-apparel/pages/catalog.ts +1 -1
  93. package/template/reference/templates/reference-template-apparel/pages/category.ts +1 -1
  94. package/template/reference/templates/reference-template-apparel/pages/home.ts +10 -0
  95. package/template/reference/templates/reference-template-apparel/pages/product.ts +1 -1
  96. package/template/reference/templates/reference-template-bike/pages/catalog.ts +1 -1
  97. package/template/reference/templates/reference-template-bike/pages/category.ts +1 -1
  98. package/template/reference/templates/reference-template-bike/pages/home.ts +10 -0
  99. package/template/reference/templates/reference-template-bike/pages/product.ts +1 -1
  100. package/template/sections/example-section/ExampleSection.vue +8 -1
  101. package/template/sections/example-section/client.ts +1 -1
  102. package/template/sections/example-section/component/button/Button.vue +1 -1
  103. package/template/sections/example-section/component/image/Image.vue +1 -1
  104. package/template/sections/example-section/component/image/ImagesGrid.vue +1 -1
  105. package/template/sections/example-section/component/selectbox/Selectbox.vue +1 -1
  106. package/template/sections/example-section/component/title/Title.vue +1 -1
  107. package/template/sections/example-section/component/toggle/Toggle.vue +1 -1
  108. package/template/sections/example-section/entity/color.ts +2 -2
  109. package/template/sections/example-section/server.ts +1 -1
  110. package/template/sections/example-section/settings/translations.ts +1 -1
  111. package/template/sections/example-section/showcases/translations.ts +13 -13
  112. package/template/shared/components/LanguageSelector.vue +1 -1
  113. package/template/shared/translation.ts +16 -0
  114. package/template/shared/utils.ts +3 -1
  115. package/template/tsconfig.json +1 -0
  116. package/types.d.ts +6 -457
@@ -0,0 +1,443 @@
1
+ import * as path from 'path';
2
+ import * as fs from 'fs';
3
+ import { URL } from 'url';
4
+ import { getShowcaseData } from './preview';
5
+ import { fetchTiles, updateTilesSection, updateCustomContent } from './utils';
6
+ import type { IncomingMessage, ServerResponse } from 'http';
7
+
8
+ /**
9
+ * Extract query parameter from URL
10
+ */
11
+ function getQueryParam(url: string, paramName: string): string | null {
12
+ try {
13
+ const urlObj = new URL(url, 'http://localhost');
14
+ return urlObj.searchParams.get(paramName);
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Set no-cache headers to prevent browser caching
22
+ */
23
+ function setNoCacheHeaders(res: ServerResponse): void {
24
+ res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
25
+ res.setHeader('Pragma', 'no-cache');
26
+ res.setHeader('Expires', '0');
27
+ res.setHeader('Surrogate-Control', 'no-store');
28
+ }
29
+
30
+ /**
31
+ * Get all available showcases for a section from the dist folder
32
+ */
33
+ function getAvailableShowcases(sectionName: string, distPath: string): string[] {
34
+ try {
35
+ const showcasesPath = path.join(distPath, 'sections', sectionName, 'js', 'showcases');
36
+
37
+ if (!fs.existsSync(showcasesPath)) {
38
+ console.warn(`[API Routes] Showcases directory not found for section "${sectionName}": ${showcasesPath}`);
39
+ return [];
40
+ }
41
+
42
+ const showcases = fs.readdirSync(showcasesPath, { withFileTypes: true })
43
+ .filter(dirent => dirent.isFile() && dirent.name.endsWith('.mjs'))
44
+ .map(dirent => dirent.name.replace('.mjs', ''))
45
+ .sort((a, b) => {
46
+ // Sort numerically if both are numbers, otherwise alphabetically
47
+ const aNum = parseInt(a, 10);
48
+ const bNum = parseInt(b, 10);
49
+ if (!isNaN(aNum) && !isNaN(bNum)) {
50
+ return aNum - bNum;
51
+ }
52
+ return a.localeCompare(b);
53
+ });
54
+
55
+ return showcases;
56
+ } catch (error) {
57
+ console.error(`[API Routes] Error reading showcases for section "${sectionName}":`, error);
58
+ return [];
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Get all showcase IDs for a given section
64
+ */
65
+ function getShowcaseIds(sectionName: string, distPath: string): string[] {
66
+ return getAvailableShowcases(sectionName, distPath);
67
+ }
68
+
69
+ /**
70
+ * Handle GET /api/v1/tile
71
+ * Returns tile data with content and design for all showcases in multiple sections
72
+ * Fetches tiles from remote once and updates them for each section/showcase
73
+ * Accepts section names as parameter from Chrome extension
74
+ */
75
+ export async function handleGetTile(
76
+ _req: IncomingMessage,
77
+ res: ServerResponse,
78
+ sectionNames: string[],
79
+ authToken?: string,
80
+ tileUrl?: string,
81
+ ): Promise<void> {
82
+ try {
83
+ const distPath = path.resolve(process.cwd(), 'dist');
84
+
85
+ // Fetch tiles from remote once
86
+ let responseData = await fetchTiles(authToken, tileUrl);
87
+
88
+ // Fetch all showcase data in parallel
89
+ const showcaseDataPromises = sectionNames.flatMap((sectionName) => {
90
+ const showcaseIds = getShowcaseIds(sectionName, distPath);
91
+ return showcaseIds.map(async (showcaseId) => {
92
+ const { content, design } = await getShowcaseData(
93
+ sectionName,
94
+ showcaseId,
95
+ distPath,
96
+ );
97
+ return { sectionName, showcaseId, content, design };
98
+ });
99
+ });
100
+
101
+ const allShowcaseData = await Promise.all(showcaseDataPromises);
102
+
103
+ for (const { sectionName, showcaseId, content, design } of allShowcaseData) {
104
+ responseData = updateTilesSection(responseData, sectionName, showcaseId, content, design);
105
+ }
106
+
107
+ res.statusCode = 200;
108
+ res.setHeader('Content-Type', 'application/json');
109
+ setNoCacheHeaders(res);
110
+ res.end(JSON.stringify(responseData, null, 2));
111
+ } catch (error) {
112
+ console.error('[API Routes] ❌ Error processing tile data:', error);
113
+ res.statusCode = 500;
114
+ res.setHeader('Content-Type', 'application/json');
115
+ setNoCacheHeaders(res);
116
+ res.end(JSON.stringify({
117
+ error: 'Failed to process tile data',
118
+ message: error instanceof Error ? error.message : String(error),
119
+ }));
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Handle GET /api/v1/sections
125
+ * Returns list of available sections with their showcases
126
+ */
127
+ export function handleGetSections(
128
+ _req: IncomingMessage,
129
+ res: ServerResponse,
130
+ ): void {
131
+ try {
132
+ const distPath = path.resolve(process.cwd(), 'dist');
133
+ const sectionsPath = path.join(distPath, 'sections');
134
+
135
+ // Check if sections directory exists
136
+ if (!fs.existsSync(sectionsPath)) {
137
+ res.statusCode = 200;
138
+ res.setHeader('Content-Type', 'application/json');
139
+ setNoCacheHeaders(res);
140
+ res.end(JSON.stringify({ sections: [] }));
141
+ return;
142
+ }
143
+
144
+ // Read all directories in the sections folder
145
+ const sectionNames = fs.readdirSync(sectionsPath, { withFileTypes: true })
146
+ .filter(dirent => dirent.isDirectory())
147
+ .map(dirent => dirent.name);
148
+
149
+ // Build sections array with showcase data
150
+ const sections = sectionNames.map(sectionName => ({
151
+ name: sectionName,
152
+ showcases: getAvailableShowcases(sectionName, distPath),
153
+ }));
154
+
155
+ res.statusCode = 200;
156
+ res.setHeader('Content-Type', 'application/json');
157
+ setNoCacheHeaders(res);
158
+ res.end(JSON.stringify({ sections }, null, 2));
159
+ } catch (error) {
160
+ console.error('[API Routes] ❌ Error listing sections:', error);
161
+ res.statusCode = 500;
162
+ res.setHeader('Content-Type', 'application/json');
163
+ setNoCacheHeaders(res);
164
+ res.end(JSON.stringify({
165
+ error: 'Failed to list sections',
166
+ message: error instanceof Error ? error.message : String(error),
167
+ }));
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Handle POST /api/v1/custom_content
173
+ * Updates custom content for multiple sections
174
+ * Accepts section names as parameter from Chrome extension
175
+ */
176
+ export async function handleUpdateCustomContent(
177
+ _req: IncomingMessage,
178
+ res: ServerResponse,
179
+ sectionNames: string[],
180
+ authToken?: string,
181
+ customContentUrl?: string,
182
+ ): Promise<void> {
183
+ try {
184
+ const distPath = path.resolve(process.cwd(), 'dist');
185
+ const tilesUrl = customContentUrl ? customContentUrl.replace('/custom_content', '/tile?published=false') : '';
186
+
187
+ // Update custom content for all sections
188
+ const updatedCustomContent = await updateCustomContent(
189
+ sectionNames,
190
+ distPath,
191
+ authToken,
192
+ customContentUrl,
193
+ tilesUrl,
194
+ );
195
+
196
+ // Build the response data
197
+ const responseData = updatedCustomContent || {
198
+ error: 'Failed to update custom content',
199
+ sectionNames,
200
+ };
201
+
202
+ res.statusCode = updatedCustomContent ? 200 : 500;
203
+ res.setHeader('Content-Type', 'application/json');
204
+ setNoCacheHeaders(res);
205
+ res.end(JSON.stringify(responseData, null, 2));
206
+ } catch (error) {
207
+ console.error('[API Routes] ❌ Error processing custom content update:', error);
208
+ res.statusCode = 500;
209
+ res.setHeader('Content-Type', 'application/json');
210
+ setNoCacheHeaders(res);
211
+ res.end(JSON.stringify({
212
+ error: 'Failed to process request',
213
+ message: error instanceof Error ? error.message : String(error),
214
+ }));
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Serve a client file (JS or CSS)
220
+ */
221
+ async function serveClientFile(
222
+ res: ServerResponse,
223
+ sectionName: string,
224
+ filePath: string,
225
+ contentType: string,
226
+ ): Promise<void> {
227
+ try {
228
+ const distPath = path.resolve(process.cwd(), 'dist');
229
+ const fullPath = path.join(distPath, 'sections', sectionName, 'js', 'main', 'client', filePath);
230
+
231
+ // Security: prevent directory traversal
232
+ const resolvedPath = path.resolve(fullPath);
233
+ const basePath = path.resolve(distPath, 'sections', sectionName);
234
+ if (!resolvedPath.startsWith(basePath)) {
235
+ res.statusCode = 403;
236
+ res.setHeader('Content-Type', 'application/json');
237
+ setNoCacheHeaders(res);
238
+ res.end(JSON.stringify({ error: 'Access denied' }));
239
+ return;
240
+ }
241
+
242
+ if (!fs.existsSync(resolvedPath)) {
243
+ res.statusCode = 404;
244
+ res.setHeader('Content-Type', 'application/json');
245
+ setNoCacheHeaders(res);
246
+ res.end(JSON.stringify({ error: 'File not found', file: filePath, section: sectionName }));
247
+ return;
248
+ }
249
+
250
+ const fileContent = fs.readFileSync(resolvedPath, 'utf-8');
251
+ res.statusCode = 200;
252
+ res.setHeader('Content-Type', contentType);
253
+ setNoCacheHeaders(res);
254
+ res.end(fileContent);
255
+ } catch (error) {
256
+ console.error('[API Routes] ❌ Error serving client file:', error);
257
+ res.statusCode = 500;
258
+ res.setHeader('Content-Type', 'application/json');
259
+ setNoCacheHeaders(res);
260
+ res.end(JSON.stringify({
261
+ error: 'Failed to serve file',
262
+ message: error instanceof Error ? error.message : String(error),
263
+ }));
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Handle GET /api/v1/client-js
269
+ * Returns client.js for a specific section
270
+ */
271
+ async function handleGetClientJs(
272
+ _req: IncomingMessage,
273
+ res: ServerResponse,
274
+ sectionName: string,
275
+ ): Promise<void> {
276
+ await serveClientFile(res, sectionName, 'client.js', 'application/javascript');
277
+ }
278
+
279
+ /**
280
+ * Handle GET /api/v1/client-css
281
+ * Returns client.css for a specific section
282
+ */
283
+ async function handleGetClientCss(
284
+ _req: IncomingMessage,
285
+ res: ServerResponse,
286
+ sectionName: string,
287
+ ): Promise<void> {
288
+ await serveClientFile(res, sectionName, 'assets/client.css', 'text/css');
289
+ }
290
+
291
+ /**
292
+ * Handle GET /api/v1/assets
293
+ * Returns assets for a specific section
294
+ */
295
+ async function handleGetAssets(
296
+ req: IncomingMessage,
297
+ res: ServerResponse,
298
+ sectionName: string,
299
+ ): Promise<void> {
300
+ try {
301
+ const url = req.url || '';
302
+ const filePath = getQueryParam(url, 'file');
303
+
304
+ if (!filePath) {
305
+ res.statusCode = 400;
306
+ res.setHeader('Content-Type', 'application/json');
307
+ setNoCacheHeaders(res);
308
+ res.end(JSON.stringify({ error: 'Missing file parameter' }));
309
+ return;
310
+ }
311
+
312
+ // Security: prevent directory traversal
313
+ if (filePath.includes('..') || filePath.startsWith('/')) {
314
+ res.statusCode = 403;
315
+ res.setHeader('Content-Type', 'application/json');
316
+ setNoCacheHeaders(res);
317
+ res.end(JSON.stringify({ error: 'Access denied' }));
318
+ return;
319
+ }
320
+
321
+ const distPath = path.resolve(process.cwd(), 'dist');
322
+ const fullPath = path.join(distPath, 'sections', sectionName, 'assets', filePath);
323
+ const resolvedPath = path.resolve(fullPath);
324
+ const basePath = path.resolve(distPath, 'sections', sectionName, 'assets');
325
+
326
+ if (!resolvedPath.startsWith(basePath)) {
327
+ res.statusCode = 403;
328
+ res.setHeader('Content-Type', 'application/json');
329
+ setNoCacheHeaders(res);
330
+ res.end(JSON.stringify({ error: 'Access denied' }));
331
+ return;
332
+ }
333
+
334
+ if (!fs.existsSync(resolvedPath)) {
335
+ res.statusCode = 404;
336
+ res.setHeader('Content-Type', 'application/json');
337
+ setNoCacheHeaders(res);
338
+ res.end(JSON.stringify({ error: 'Asset not found', file: filePath, section: sectionName }));
339
+ return;
340
+ }
341
+
342
+ // Detect content type based on file extension
343
+ const ext = path.extname(filePath).toLowerCase();
344
+ const contentTypeMap: { [key: string]: string } = {
345
+ '.png': 'image/png',
346
+ '.jpg': 'image/jpeg',
347
+ '.jpeg': 'image/jpeg',
348
+ '.gif': 'image/gif',
349
+ '.webp': 'image/webp',
350
+ '.svg': 'image/svg+xml',
351
+ '.ico': 'image/x-icon',
352
+ '.woff': 'font/woff',
353
+ '.woff2': 'font/woff2',
354
+ '.ttf': 'font/ttf',
355
+ '.eot': 'application/vnd.ms-fontobject',
356
+ '.json': 'application/json',
357
+ '.css': 'text/css',
358
+ '.js': 'application/javascript',
359
+ };
360
+
361
+ const contentType = contentTypeMap[ext] || 'application/octet-stream';
362
+ const fileContent = fs.readFileSync(resolvedPath);
363
+
364
+ res.statusCode = 200;
365
+ res.setHeader('Content-Type', contentType);
366
+ setNoCacheHeaders(res);
367
+ res.end(fileContent);
368
+ } catch (error) {
369
+ console.error('[API Routes] ❌ Error serving asset:', error);
370
+ res.statusCode = 500;
371
+ res.setHeader('Content-Type', 'application/json');
372
+ setNoCacheHeaders(res);
373
+ res.end(JSON.stringify({
374
+ error: 'Failed to serve asset',
375
+ message: error instanceof Error ? error.message : String(error),
376
+ }));
377
+ }
378
+ }
379
+
380
+ /**
381
+ * Main API router
382
+ * Routes requests to appropriate handlers
383
+ * Extracts section name and original_url from query parameters if available
384
+ * Resolves showcase ID from dist folder based on available showcases
385
+ */
386
+ export function handleApiRequest(
387
+ req: IncomingMessage,
388
+ res: ServerResponse,
389
+ authToken?: string,
390
+ ): void {
391
+ const url = req.url || '';
392
+
393
+ if (url.startsWith('/api/v1/sections')) {
394
+ handleGetSections(req, res);
395
+ return;
396
+ }
397
+
398
+ // Extract section name from query parameter if provided
399
+ const sectionName = getQueryParam(url, 'section') || '';
400
+
401
+ // Extract original_url from query parameter if provided
402
+ const originalUrl = getQueryParam(url, 'original_url') || '';
403
+
404
+ if (url.startsWith('/api/v1/tile')) {
405
+ // Extract section names from query parameter (comma-separated)
406
+ const sectionNamesParam = getQueryParam(url, 'sections');
407
+ const sectionNames = sectionNamesParam ? sectionNamesParam.split(',').map(s => s.trim()) : [];
408
+ handleGetTile(req, res, sectionNames, authToken, originalUrl);
409
+ return;
410
+ }
411
+
412
+ if (url.startsWith('/api/v1/custom_content')) {
413
+ // Extract section names from query parameter (comma-separated)
414
+ const sectionNamesParam = getQueryParam(url, 'sections');
415
+ const sectionNames = sectionNamesParam ? sectionNamesParam.split(',').map(s => s.trim()) : [];
416
+ handleUpdateCustomContent(req, res, sectionNames, authToken, originalUrl);
417
+ return;
418
+ }
419
+
420
+ if (url.startsWith('/api/v1/client-js')) {
421
+ handleGetClientJs(req, res, sectionName);
422
+ return;
423
+ }
424
+
425
+ if (url.startsWith('/api/v1/client-css')) {
426
+ handleGetClientCss(req, res, sectionName);
427
+ return;
428
+ }
429
+
430
+ if (url.startsWith('/api/v1/assets')) {
431
+ handleGetAssets(req, res, sectionName);
432
+ return;
433
+ }
434
+
435
+ // Unknown API route
436
+ res.statusCode = 404;
437
+ res.setHeader('Content-Type', 'application/json');
438
+ setNoCacheHeaders(res);
439
+ res.end(JSON.stringify({
440
+ error: 'API route not found',
441
+ url,
442
+ }));
443
+ }
@@ -1,5 +1,4 @@
1
-
2
- export type ExternalContentMock = ExternalContentData
1
+ export type ExternalContentMock = ExternalContentData;
3
2
 
4
3
  /**
5
4
  * Default mock configuration
@@ -11,11 +10,11 @@ export const externalContentMock: ExternalContentMock = {
11
10
  account: {
12
11
  title: 'My Account',
13
12
  url: '/account',
14
- target: '_self'
13
+ target: '_self',
15
14
  },
16
15
  cart: {
17
16
  url: '/cart',
18
- count: 3
17
+ count: 3,
19
18
  },
20
19
  languages: [
21
20
  {
@@ -23,42 +22,42 @@ export const externalContentMock: ExternalContentMock = {
23
22
  description: 'English',
24
23
  main: true,
25
24
  selected: true,
26
- url: '/en'
25
+ url: '/en',
27
26
  },
28
27
  {
29
28
  code: 'es',
30
29
  description: 'Español',
31
30
  main: false,
32
31
  selected: false,
33
- url: '/es'
34
- }
32
+ url: '/es',
33
+ },
35
34
  ],
36
35
  legalPages: [
37
36
  {
38
37
  title: 'Privacy Policy',
39
- url: '/privacy'
38
+ url: '/privacy',
40
39
  },
41
40
  {
42
41
  title: 'Terms of Service',
43
- url: '/terms'
42
+ url: '/terms',
44
43
  },
45
44
  {
46
45
  title: 'Refund Policy',
47
- url: '/refunds'
48
- }
46
+ url: '/refunds',
47
+ },
49
48
  ],
50
49
  reportAbuse: {
51
50
  title: 'Report Abuse',
52
51
  url: '/report-abuse',
53
- target: '_blank'
52
+ target: '_blank',
54
53
  },
55
54
  madeWith: {
56
55
  url: 'https://www.lightspeedhq.com',
57
56
  target: '_blank',
58
57
  icon: '/assets/lightspeed-icon.png',
59
58
  poweredBy: 'Powered by',
60
- company: 'Lightspeed'
61
- }
59
+ company: 'Lightspeed',
60
+ },
62
61
  },
63
62
 
64
63
  category: {
@@ -70,15 +69,16 @@ export const externalContentMock: ExternalContentMock = {
70
69
  imageUrl: '/assets/electronics-category.jpg',
71
70
  thumbnailImageUrl: '/assets/electronics-thumb.jpg',
72
71
  alt: 'Electronics category',
72
+ productsCount: 0,
73
73
  imageBorderInfo: {
74
74
  homogeneity: true,
75
- color: {
76
- r: 255,
77
- g: 255,
78
- b: 255,
79
- a: 1
80
- }
81
- }
75
+ dominatingColor: {
76
+ red: 255,
77
+ green: 255,
78
+ blue: 255,
79
+ alpha: 1,
80
+ },
81
+ },
82
82
  },
83
83
  {
84
84
  id: 2,
@@ -86,7 +86,8 @@ export const externalContentMock: ExternalContentMock = {
86
86
  url: '/categories/clothing',
87
87
  imageUrl: '/assets/clothing-category.jpg',
88
88
  thumbnailImageUrl: '/assets/clothing-thumb.jpg',
89
- alt: 'Clothing category'
89
+ alt: 'Clothing category',
90
+ productsCount: 0,
90
91
  },
91
92
  {
92
93
  id: 3,
@@ -94,8 +95,9 @@ export const externalContentMock: ExternalContentMock = {
94
95
  url: '/categories/home-garden',
95
96
  imageUrl: '/assets/home-garden-category.jpg',
96
97
  thumbnailImageUrl: '/assets/home-garden-thumb.jpg',
97
- alt: 'Home & Garden category'
98
- }
98
+ alt: 'Home & Garden category',
99
+ productsCount: 0,
100
+ },
99
101
  ],
100
102
  categoryTree: [
101
103
  {
@@ -103,7 +105,7 @@ export const externalContentMock: ExternalContentMock = {
103
105
  name: 'Electronics',
104
106
  nameTranslated: {
105
107
  en: 'Electronics',
106
- es: 'Electrónicos'
108
+ es: 'Electrónicos',
107
109
  },
108
110
  urlPath: '/electronics',
109
111
  enabled: true,
@@ -113,31 +115,31 @@ export const externalContentMock: ExternalContentMock = {
113
115
  name: 'Smartphones',
114
116
  nameTranslated: {
115
117
  en: 'Smartphones',
116
- es: 'Teléfonos inteligentes'
118
+ es: 'Teléfonos inteligentes',
117
119
  },
118
120
  urlPath: '/electronics/smartphones',
119
121
  enabled: true,
120
- children: []
122
+ children: [],
121
123
  },
122
124
  {
123
125
  id: 12,
124
126
  name: 'Laptops',
125
127
  nameTranslated: {
126
128
  en: 'Laptops',
127
- es: 'Portátiles'
129
+ es: 'Portátiles',
128
130
  },
129
131
  urlPath: '/electronics/laptops',
130
132
  enabled: true,
131
- children: []
132
- }
133
- ]
133
+ children: [],
134
+ },
135
+ ],
134
136
  },
135
137
  {
136
138
  id: 2,
137
139
  name: 'Clothing',
138
140
  nameTranslated: {
139
141
  en: 'Clothing',
140
- es: 'Ropa'
142
+ es: 'Ropa',
141
143
  },
142
144
  urlPath: '/clothing',
143
145
  enabled: true,
@@ -147,27 +149,27 @@ export const externalContentMock: ExternalContentMock = {
147
149
  name: 'Men\'s Clothing',
148
150
  nameTranslated: {
149
151
  en: 'Men\'s Clothing',
150
- es: 'Ropa para hombres'
152
+ es: 'Ropa para hombres',
151
153
  },
152
154
  urlPath: '/clothing/mens',
153
155
  enabled: true,
154
- children: []
156
+ children: [],
155
157
  },
156
158
  {
157
159
  id: 22,
158
160
  name: 'Women\'s Clothing',
159
161
  nameTranslated: {
160
162
  en: 'Women\'s Clothing',
161
- es: 'Ropa para mujeres'
163
+ es: 'Ropa para mujeres',
162
164
  },
163
165
  urlPath: '/clothing/womens',
164
166
  enabled: true,
165
- children: []
166
- }
167
- ]
168
- }
169
- ]
170
- }
167
+ children: [],
168
+ },
169
+ ],
170
+ },
171
+ ],
172
+ },
171
173
  };
172
174
 
173
175
  export function getExternalContentMock(): ExternalContentMock {