@lightspeed/crane 1.4.2 → 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 +33 -0
  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 +3 -2
  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 +6 -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 +347 -39
  39. package/template/preview/shared/mock.ts +43 -41
  40. package/template/preview/shared/preview.ts +205 -126
  41. package/template/preview/shared/utils.ts +208 -62
  42. package/template/preview/ssr-server.ts +429 -0
  43. package/template/preview/vite.config.js +64 -65
  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
@@ -1,8 +1,22 @@
1
1
  import * as path from 'path';
2
- import { getContentAndDesign } from './preview';
2
+ import * as fs from 'fs';
3
+ import { URL } from 'url';
4
+ import { getShowcaseData } from './preview';
3
5
  import { fetchTiles, updateTilesSection, updateCustomContent } from './utils';
4
6
  import type { IncomingMessage, ServerResponse } from 'http';
5
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
+
6
20
  /**
7
21
  * Set no-cache headers to prevent browser caching
8
22
  */
@@ -13,88 +27,352 @@ function setNoCacheHeaders(res: ServerResponse): void {
13
27
  res.setHeader('Surrogate-Control', 'no-store');
14
28
  }
15
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
+
16
69
  /**
17
70
  * Handle GET /api/v1/tile
18
- * Returns tile data with content and design
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
19
74
  */
20
75
  export async function handleGetTile(
21
76
  _req: IncomingMessage,
22
77
  res: ServerResponse,
23
- sectionName: string,
24
- showcaseId: string
78
+ sectionNames: string[],
79
+ authToken?: string,
80
+ tileUrl?: string,
25
81
  ): Promise<void> {
26
82
  try {
27
83
  const distPath = path.resolve(process.cwd(), 'dist');
28
84
 
29
- // Use getContentAndDesign from preview.ts to fetch content and design
30
- const { content, design } = await getContentAndDesign(sectionName, showcaseId, distPath);
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
+ });
31
100
 
32
- // Fetch and update tiles
33
- const tilesResponse = await fetchTiles();
34
- const responseData = updateTilesSection(tilesResponse, sectionName, showcaseId, content, design);
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
+ }
35
106
 
36
107
  res.statusCode = 200;
37
108
  res.setHeader('Content-Type', 'application/json');
38
109
  setNoCacheHeaders(res);
39
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);
40
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));
41
159
  } catch (error) {
42
- console.error('[API Routes] ❌ Error loading tile data:', error);
160
+ console.error('[API Routes] ❌ Error listing sections:', error);
43
161
  res.statusCode = 500;
44
162
  res.setHeader('Content-Type', 'application/json');
45
163
  setNoCacheHeaders(res);
46
164
  res.end(JSON.stringify({
47
- error: 'Failed to load tile data',
165
+ error: 'Failed to list sections',
48
166
  message: error instanceof Error ? error.message : String(error),
49
- sectionName,
50
- showcaseId
51
167
  }));
52
168
  }
53
169
  }
54
170
 
55
171
  /**
56
172
  * Handle POST /api/v1/custom_content
57
- * Updates custom content for a section
173
+ * Updates custom content for multiple sections
174
+ * Accepts section names as parameter from Chrome extension
58
175
  */
59
176
  export async function handleUpdateCustomContent(
60
177
  _req: IncomingMessage,
61
178
  res: ServerResponse,
62
- sectionName: string,
63
- showcaseId: string
179
+ sectionNames: string[],
180
+ authToken?: string,
181
+ customContentUrl?: string,
64
182
  ): Promise<void> {
65
183
  try {
66
184
  const distPath = path.resolve(process.cwd(), 'dist');
67
- let finalResponseData = null;
68
-
69
- // Use updateCustomContent function
70
- const updatedCustomContent = await updateCustomContent(sectionName, showcaseId, distPath);
185
+ const tilesUrl = customContentUrl ? customContentUrl.replace('/custom_content', '/tile?published=false') : '';
71
186
 
72
- if (updatedCustomContent) {
73
- finalResponseData = updatedCustomContent;
74
- }
187
+ // Update custom content for all sections
188
+ const updatedCustomContent = await updateCustomContent(
189
+ sectionNames,
190
+ distPath,
191
+ authToken,
192
+ customContentUrl,
193
+ tilesUrl,
194
+ );
75
195
 
76
196
  // Build the response data
77
- const responseData = finalResponseData || {
197
+ const responseData = updatedCustomContent || {
78
198
  error: 'Failed to update custom content',
79
- sectionName,
80
- showcaseId
199
+ sectionNames,
81
200
  };
82
201
 
83
- res.statusCode = finalResponseData ? 200 : 500;
202
+ res.statusCode = updatedCustomContent ? 200 : 500;
84
203
  res.setHeader('Content-Type', 'application/json');
85
204
  setNoCacheHeaders(res);
86
205
  res.end(JSON.stringify(responseData, null, 2));
87
-
88
206
  } catch (error) {
89
- console.error('[API Routes] ❌ Error in update-custom-content endpoint:', error);
207
+ console.error('[API Routes] ❌ Error processing custom content update:', error);
90
208
  res.statusCode = 500;
91
209
  res.setHeader('Content-Type', 'application/json');
92
210
  setNoCacheHeaders(res);
93
211
  res.end(JSON.stringify({
94
212
  error: 'Failed to process request',
95
213
  message: error instanceof Error ? error.message : String(error),
96
- sectionName,
97
- showcaseId
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),
98
376
  }));
99
377
  }
100
378
  }
@@ -102,24 +380,55 @@ export async function handleUpdateCustomContent(
102
380
  /**
103
381
  * Main API router
104
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
105
385
  */
106
386
  export function handleApiRequest(
107
387
  req: IncomingMessage,
108
388
  res: ServerResponse,
109
- sectionName: string,
110
- showcaseId: string
389
+ authToken?: string,
111
390
  ): void {
112
391
  const url = req.url || '';
113
392
 
114
- // Route: GET /api/v1/tile
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
+
115
404
  if (url.startsWith('/api/v1/tile')) {
116
- handleGetTile(req, res, sectionName, showcaseId);
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);
117
409
  return;
118
410
  }
119
411
 
120
- // Route: POST /api/v1/custom_content
121
412
  if (url.startsWith('/api/v1/custom_content')) {
122
- handleUpdateCustomContent(req, res, sectionName, showcaseId);
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);
123
432
  return;
124
433
  }
125
434
 
@@ -129,7 +438,6 @@ export function handleApiRequest(
129
438
  setNoCacheHeaders(res);
130
439
  res.end(JSON.stringify({
131
440
  error: 'API route not found',
132
- url: url
441
+ url,
133
442
  }));
134
443
  }
135
-
@@ -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 {