@lightspeed/crane 1.4.2 → 2.0.1

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 (120) hide show
  1. package/CHANGELOG.md +51 -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 +4 -3
  8. package/template/footers/example-footer/ExampleFooter.vue +1 -1
  9. package/template/footers/example-footer/client.ts +2 -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 +2 -1
  15. package/template/headers/example-header/client.ts +2 -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 +2 -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 +2 -1
  26. package/template/layouts/catalog/example-catalog/slots/custom-bottom-bar/server.ts +2 -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 +515 -41
  39. package/template/preview/shared/mock.ts +43 -41
  40. package/template/preview/shared/preview.ts +220 -123
  41. package/template/preview/shared/utils.ts +209 -62
  42. package/template/preview/ssr-server.ts +430 -0
  43. package/template/preview/vite.config.js +76 -75
  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 +6 -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 +6 -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 +2 -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 +2 -1
  71. package/template/reference/sections/tag-lines/TagLines.vue +1 -1
  72. package/template/reference/sections/tag-lines/client.ts +2 -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 +4 -3
  76. package/template/reference/sections/tag-lines/server.ts +2 -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 +6 -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 +6 -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/shared/utils/styles.ts +1 -0
  93. package/template/reference/templates/reference-template-apparel/pages/catalog.ts +1 -1
  94. package/template/reference/templates/reference-template-apparel/pages/category.ts +1 -1
  95. package/template/reference/templates/reference-template-apparel/pages/home.ts +10 -0
  96. package/template/reference/templates/reference-template-apparel/pages/product.ts +1 -1
  97. package/template/reference/templates/reference-template-bike/pages/catalog.ts +1 -1
  98. package/template/reference/templates/reference-template-bike/pages/category.ts +1 -1
  99. package/template/reference/templates/reference-template-bike/pages/home.ts +10 -0
  100. package/template/reference/templates/reference-template-bike/pages/product.ts +1 -1
  101. package/template/sections/example-section/ExampleSection.vue +8 -1
  102. package/template/sections/example-section/client.ts +2 -1
  103. package/template/sections/example-section/component/button/Button.vue +1 -1
  104. package/template/sections/example-section/component/image/Image.vue +1 -1
  105. package/template/sections/example-section/component/image/ImagesGrid.vue +1 -1
  106. package/template/sections/example-section/component/selectbox/Selectbox.vue +1 -1
  107. package/template/sections/example-section/component/title/Title.vue +1 -1
  108. package/template/sections/example-section/component/toggle/Toggle.vue +1 -1
  109. package/template/sections/example-section/entity/color.ts +2 -2
  110. package/template/sections/example-section/server.ts +2 -1
  111. package/template/sections/example-section/settings/translations.ts +1 -1
  112. package/template/sections/example-section/showcases/1.ts +2 -22
  113. package/template/sections/example-section/showcases/2.ts +2 -22
  114. package/template/sections/example-section/showcases/3.ts +2 -22
  115. package/template/sections/example-section/showcases/translations.ts +11 -149
  116. package/template/shared/components/LanguageSelector.vue +1 -1
  117. package/template/shared/translation.ts +16 -0
  118. package/template/shared/utils.ts +3 -1
  119. package/template/tsconfig.json +1 -0
  120. package/types.d.ts +6 -457
@@ -1,7 +1,46 @@
1
+ import * as fs from 'fs';
2
+ import type { IncomingMessage, ServerResponse } from 'http';
1
3
  import * as path from 'path';
2
- import { getContentAndDesign } from './preview';
4
+ import { URL } from 'url';
5
+
6
+ import { getShowcaseData } from './preview';
3
7
  import { fetchTiles, updateTilesSection, updateCustomContent } from './utils';
4
- import type { IncomingMessage, ServerResponse } from 'http';
8
+
9
+
10
+ /**
11
+ * Extract query parameter from URL
12
+ */
13
+ function getQueryParam(url: string, paramName: string): string | null {
14
+ try {
15
+ const urlObj = new URL(url, 'http://localhost');
16
+ return urlObj.searchParams.get(paramName);
17
+ } catch {
18
+ return null;
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Read request body as JSON
24
+ */
25
+ function readRequestBody(req: IncomingMessage): Promise<Record<string, unknown>> {
26
+ return new Promise((resolve, reject) => {
27
+ let body = '';
28
+ req.on('data', (chunk) => {
29
+ body += chunk.toString();
30
+ });
31
+ req.on('end', () => {
32
+ try {
33
+ const parsed = body ? JSON.parse(body) : null;
34
+ resolve(parsed);
35
+ } catch (error) {
36
+ reject(error);
37
+ }
38
+ });
39
+ req.on('error', (error) => {
40
+ reject(error);
41
+ });
42
+ });
43
+ }
5
44
 
6
45
  /**
7
46
  * Set no-cache headers to prevent browser caching
@@ -13,88 +52,493 @@ function setNoCacheHeaders(res: ServerResponse): void {
13
52
  res.setHeader('Surrogate-Control', 'no-store');
14
53
  }
15
54
 
55
+ /**
56
+ * Get all available showcases for a section from the dist folder
57
+ */
58
+ function getAvailableShowcases(sectionName: string, distPath: string): string[] {
59
+ try {
60
+ const showcasesPath = path.join(distPath, 'sections', sectionName, 'js', 'showcases');
61
+
62
+ if (!fs.existsSync(showcasesPath)) {
63
+ console.warn(`[API Routes] Showcases directory not found for section "${sectionName}": ${showcasesPath}`);
64
+ return [];
65
+ }
66
+
67
+ const showcases = fs.readdirSync(showcasesPath, { withFileTypes: true })
68
+ .filter(dirent => dirent.isFile() && dirent.name.endsWith('.mjs'))
69
+ .map(dirent => dirent.name.replace('.mjs', ''))
70
+ .sort((a, b) => {
71
+ // Sort numerically if both are numbers, otherwise alphabetically
72
+ const aNum = parseInt(a, 10);
73
+ const bNum = parseInt(b, 10);
74
+ if (!isNaN(aNum) && !isNaN(bNum)) {
75
+ return aNum - bNum;
76
+ }
77
+ return a.localeCompare(b);
78
+ });
79
+
80
+ return showcases;
81
+ } catch (error) {
82
+ console.error(`[API Routes] Error reading showcases for section "${sectionName}":`, error);
83
+ return [];
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Get all showcase IDs for a given section
89
+ */
90
+ function getShowcaseIds(sectionName: string, distPath: string): string[] {
91
+ return getAvailableShowcases(sectionName, distPath);
92
+ }
93
+
16
94
  /**
17
95
  * Handle GET /api/v1/tile
18
- * Returns tile data with content and design
96
+ * Returns tile data with content and design for all showcases in multiple sections
97
+ * Fetches tiles from remote once and updates them for each section/showcase
98
+ * Accepts section names as parameter from Chrome extension
19
99
  */
20
- export async function handleGetTile(
100
+ async function handleGetTile(
21
101
  _req: IncomingMessage,
22
102
  res: ServerResponse,
23
- sectionName: string,
24
- showcaseId: string
103
+ sectionNames: string[],
104
+ authToken?: string,
105
+ tileUrl?: string,
25
106
  ): Promise<void> {
26
107
  try {
27
108
  const distPath = path.resolve(process.cwd(), 'dist');
28
109
 
29
- // Use getContentAndDesign from preview.ts to fetch content and design
30
- const { content, design } = await getContentAndDesign(sectionName, showcaseId, distPath);
110
+ // Fetch tiles from remote once
111
+ let responseData = await fetchTiles(authToken, tileUrl);
31
112
 
32
- // Fetch and update tiles
33
- const tilesResponse = await fetchTiles();
34
- const responseData = updateTilesSection(tilesResponse, sectionName, showcaseId, content, design);
113
+ // Fetch all showcase data in parallel
114
+ const showcaseDataPromises = sectionNames.flatMap((sectionName) => {
115
+ const showcaseIds = getShowcaseIds(sectionName, distPath);
116
+ return showcaseIds.map(async (showcaseId) => {
117
+ const { content, design } = await getShowcaseData(
118
+ sectionName,
119
+ showcaseId,
120
+ distPath,
121
+ );
122
+ return { sectionName, showcaseId, content, design };
123
+ });
124
+ });
125
+
126
+ const allShowcaseData = await Promise.all(showcaseDataPromises);
127
+
128
+ for (const { sectionName, showcaseId, content, design } of allShowcaseData) {
129
+ responseData = updateTilesSection(responseData, sectionName, showcaseId, content, design);
130
+ }
35
131
 
36
132
  res.statusCode = 200;
37
133
  res.setHeader('Content-Type', 'application/json');
38
134
  setNoCacheHeaders(res);
39
135
  res.end(JSON.stringify(responseData, null, 2));
136
+ } catch (error) {
137
+ console.error('[API Routes] ❌ Error processing tile data:', error);
138
+ res.statusCode = 500;
139
+ res.setHeader('Content-Type', 'application/json');
140
+ setNoCacheHeaders(res);
141
+ res.end(JSON.stringify({
142
+ error: 'Failed to process tile data',
143
+ message: error instanceof Error ? error.message : String(error),
144
+ }));
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Handle PUT /api/v1/tile
150
+ * Updates tiles with content and design from remote
151
+ * Accepts an array of tiles in the request body
152
+ * For each tile, fetches remote data, updates content and design, and sends to original_url
153
+ */
154
+ async function handleUpdateTile(
155
+ req: IncomingMessage,
156
+ res: ServerResponse,
157
+ authToken?: string,
158
+ originalUrl?: string,
159
+ ): Promise<void> {
160
+ const requestBody = await readRequestBody(req);
161
+ try {
162
+ // Read request body containing tiles array
163
+
164
+ if (!requestBody || !Array.isArray(requestBody.tiles)) {
165
+ res.statusCode = 400;
166
+ res.setHeader('Content-Type', 'application/json');
167
+ setNoCacheHeaders(res);
168
+ res.end(JSON.stringify({
169
+ error: 'Invalid request body',
170
+ message: 'Expected { tiles: [...] } in request body',
171
+ }));
172
+ return;
173
+ }
40
174
 
175
+ const tiles = requestBody.tiles;
176
+
177
+ // Fetch tiles from remote - add published=false query parameter
178
+ const tilesUrl = originalUrl ? `${originalUrl}?published=false` : '';
179
+ const remoteTiles = await fetchTiles(authToken, tilesUrl);
180
+
181
+ if (!remoteTiles || !remoteTiles.tiles) {
182
+ console.error('Can\'t fetch remote tiles or they dont exist from url', tilesUrl);
183
+ res.setHeader('Content-Type', 'application/json');
184
+ setNoCacheHeaders(res);
185
+ res.end(JSON.stringify(requestBody, null, 2));
186
+ return;
187
+ }
188
+
189
+ // Iterate over tiles from request body and update with remote data
190
+ const updatedTiles = tiles.map((tile: Record<string, unknown>) => {
191
+ // Find matching tile in remote data by id
192
+ const remoteTile = remoteTiles.tiles.find((rt: Record<string, unknown>) => rt.id === tile.id);
193
+
194
+ if (remoteTile) {
195
+ // Update content and design from remote
196
+ return {
197
+ ...tile,
198
+ content: remoteTile.content,
199
+ design: remoteTile.design,
200
+ };
201
+ }
202
+
203
+ // If no match found, return tile as-is
204
+ return tile;
205
+ });
206
+
207
+ // Prepare updated request body
208
+ const updatedRequestBody = {
209
+ ...requestBody,
210
+ tiles: updatedTiles,
211
+ };
212
+
213
+ // Make PUT request to original_url with updated tiles
214
+ if (!originalUrl) {
215
+ console.error('Tile update url was not provided');
216
+ res.statusCode = 400;
217
+ res.setHeader('Content-Type', 'application/json');
218
+ setNoCacheHeaders(res);
219
+ res.end(JSON.stringify(requestBody, null, 2));
220
+ return;
221
+ }
222
+
223
+ const headers: Record<string, string> = {
224
+ 'Content-Type': 'application/json',
225
+ };
226
+
227
+ if (authToken) {
228
+ headers['Authorization'] = authToken;
229
+ }
230
+
231
+ const response = await fetch(originalUrl, {
232
+ method: 'PUT',
233
+ headers,
234
+ body: JSON.stringify(updatedRequestBody),
235
+ });
236
+
237
+ if (!response.ok) {
238
+ console.error('[API Routes] Failed to update tiles at original_url:', response.status, response.statusText);
239
+ res.statusCode = response.status;
240
+ res.setHeader('Content-Type', 'application/json');
241
+ setNoCacheHeaders(res);
242
+ res.end(JSON.stringify(requestBody, null, 2));
243
+ return;
244
+ }
245
+
246
+ const responseData = await response.json();
247
+
248
+ res.statusCode = 200;
249
+ res.setHeader('Content-Type', 'application/json');
250
+ setNoCacheHeaders(res);
251
+ res.end(JSON.stringify(responseData, null, 2));
41
252
  } catch (error) {
42
- console.error('[API Routes] ❌ Error loading tile data:', error);
253
+ console.error('[API Routes] ❌ Error updating tiles:', error);
43
254
  res.statusCode = 500;
44
255
  res.setHeader('Content-Type', 'application/json');
45
256
  setNoCacheHeaders(res);
257
+ res.end(JSON.stringify(requestBody, null, 2));
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Handle /api/v1/tile (GET and PUT)
263
+ * Routes to appropriate handler based on request method
264
+ */
265
+ export async function handleTile(
266
+ req: IncomingMessage,
267
+ res: ServerResponse,
268
+ sectionNames: string[],
269
+ authToken?: string,
270
+ originalUrl?: string,
271
+ ): Promise<void> {
272
+ const method = req.method?.toUpperCase();
273
+
274
+ if (method === 'GET') {
275
+ await handleGetTile(req, res, sectionNames, authToken, originalUrl);
276
+ } else if (method === 'PUT') {
277
+ await handleUpdateTile(req, res, authToken, originalUrl);
278
+ } else {
279
+ res.statusCode = 405;
280
+ res.setHeader('Content-Type', 'application/json');
281
+ setNoCacheHeaders(res);
46
282
  res.end(JSON.stringify({
47
- error: 'Failed to load tile data',
283
+ error: 'Method not allowed',
284
+ allowedMethods: ['GET', 'PUT'],
285
+ }));
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Handle GET /api/v1/sections
291
+ * Returns list of available sections with their showcases
292
+ */
293
+ export function handleGetSections(
294
+ _req: IncomingMessage,
295
+ res: ServerResponse,
296
+ ): void {
297
+ try {
298
+ const distPath = path.resolve(process.cwd(), 'dist');
299
+ const sectionsPath = path.join(distPath, 'sections');
300
+
301
+ // Check if sections directory exists
302
+ if (!fs.existsSync(sectionsPath)) {
303
+ res.statusCode = 200;
304
+ res.setHeader('Content-Type', 'application/json');
305
+ setNoCacheHeaders(res);
306
+ res.end(JSON.stringify({ sections: [] }));
307
+ return;
308
+ }
309
+
310
+ // Read all directories in the sections folder
311
+ const sectionNames = fs.readdirSync(sectionsPath, { withFileTypes: true })
312
+ .filter(dirent => dirent.isDirectory())
313
+ .map(dirent => dirent.name);
314
+
315
+ // Build sections array with showcase data
316
+ const sections = sectionNames.map(sectionName => ({
317
+ name: sectionName,
318
+ showcases: getAvailableShowcases(sectionName, distPath),
319
+ }));
320
+
321
+ res.statusCode = 200;
322
+ res.setHeader('Content-Type', 'application/json');
323
+ setNoCacheHeaders(res);
324
+ res.end(JSON.stringify({ sections }, null, 2));
325
+ } catch (error) {
326
+ console.error('[API Routes] ❌ Error listing sections:', error);
327
+ res.statusCode = 500;
328
+ res.setHeader('Content-Type', 'application/json');
329
+ setNoCacheHeaders(res);
330
+ res.end(JSON.stringify({
331
+ error: 'Failed to list sections',
48
332
  message: error instanceof Error ? error.message : String(error),
49
- sectionName,
50
- showcaseId
51
333
  }));
52
334
  }
53
335
  }
54
336
 
55
337
  /**
56
338
  * Handle POST /api/v1/custom_content
57
- * Updates custom content for a section
339
+ * Updates custom content for multiple sections
340
+ * Accepts section names as parameter from Chrome extension
58
341
  */
59
342
  export async function handleUpdateCustomContent(
60
343
  _req: IncomingMessage,
61
344
  res: ServerResponse,
62
- sectionName: string,
63
- showcaseId: string
345
+ sectionNames: string[],
346
+ authToken?: string,
347
+ customContentUrl?: string,
64
348
  ): Promise<void> {
65
349
  try {
66
350
  const distPath = path.resolve(process.cwd(), 'dist');
67
- let finalResponseData = null;
68
-
69
- // Use updateCustomContent function
70
- const updatedCustomContent = await updateCustomContent(sectionName, showcaseId, distPath);
351
+ const tilesUrl = customContentUrl ? customContentUrl.replace('/custom_content', '/tile?published=false') : '';
71
352
 
72
- if (updatedCustomContent) {
73
- finalResponseData = updatedCustomContent;
74
- }
353
+ // Update custom content for all sections
354
+ const updatedCustomContent = await updateCustomContent(
355
+ sectionNames,
356
+ distPath,
357
+ authToken,
358
+ customContentUrl,
359
+ tilesUrl,
360
+ );
75
361
 
76
362
  // Build the response data
77
- const responseData = finalResponseData || {
363
+ const responseData = updatedCustomContent || {
78
364
  error: 'Failed to update custom content',
79
- sectionName,
80
- showcaseId
365
+ sectionNames,
81
366
  };
82
367
 
83
- res.statusCode = finalResponseData ? 200 : 500;
368
+ res.statusCode = updatedCustomContent ? 200 : 500;
84
369
  res.setHeader('Content-Type', 'application/json');
85
370
  setNoCacheHeaders(res);
86
371
  res.end(JSON.stringify(responseData, null, 2));
87
-
88
372
  } catch (error) {
89
- console.error('[API Routes] ❌ Error in update-custom-content endpoint:', error);
373
+ console.error('[API Routes] ❌ Error processing custom content update:', error);
90
374
  res.statusCode = 500;
91
375
  res.setHeader('Content-Type', 'application/json');
92
376
  setNoCacheHeaders(res);
93
377
  res.end(JSON.stringify({
94
378
  error: 'Failed to process request',
95
379
  message: error instanceof Error ? error.message : String(error),
96
- sectionName,
97
- showcaseId
380
+ }));
381
+ }
382
+ }
383
+
384
+ /**
385
+ * Serve a client file (JS or CSS)
386
+ */
387
+ async function serveClientFile(
388
+ res: ServerResponse,
389
+ sectionName: string,
390
+ filePath: string,
391
+ contentType: string,
392
+ ): Promise<void> {
393
+ try {
394
+ const distPath = path.resolve(process.cwd(), 'dist');
395
+ const fullPath = path.join(distPath, 'sections', sectionName, 'js', 'main', 'client', filePath);
396
+
397
+ // Security: prevent directory traversal
398
+ const resolvedPath = path.resolve(fullPath);
399
+ const basePath = path.resolve(distPath, 'sections', sectionName);
400
+ if (!resolvedPath.startsWith(basePath)) {
401
+ res.statusCode = 403;
402
+ res.setHeader('Content-Type', 'application/json');
403
+ setNoCacheHeaders(res);
404
+ res.end(JSON.stringify({ error: 'Access denied' }));
405
+ return;
406
+ }
407
+
408
+ if (!fs.existsSync(resolvedPath)) {
409
+ res.statusCode = 404;
410
+ res.setHeader('Content-Type', 'application/json');
411
+ setNoCacheHeaders(res);
412
+ res.end(JSON.stringify({ error: 'File not found', file: filePath, section: sectionName }));
413
+ return;
414
+ }
415
+
416
+ const fileContent = fs.readFileSync(resolvedPath, 'utf-8');
417
+ res.statusCode = 200;
418
+ res.setHeader('Content-Type', contentType);
419
+ setNoCacheHeaders(res);
420
+ res.end(fileContent);
421
+ } catch (error) {
422
+ console.error('[API Routes] ❌ Error serving client file:', error);
423
+ res.statusCode = 500;
424
+ res.setHeader('Content-Type', 'application/json');
425
+ setNoCacheHeaders(res);
426
+ res.end(JSON.stringify({
427
+ error: 'Failed to serve file',
428
+ message: error instanceof Error ? error.message : String(error),
429
+ }));
430
+ }
431
+ }
432
+
433
+ /**
434
+ * Handle GET /api/v1/client-js
435
+ * Returns client.js for a specific section
436
+ */
437
+ async function handleGetClientJs(
438
+ _req: IncomingMessage,
439
+ res: ServerResponse,
440
+ sectionName: string,
441
+ ): Promise<void> {
442
+ await serveClientFile(res, sectionName, 'client.js', 'application/javascript');
443
+ }
444
+
445
+ /**
446
+ * Handle GET /api/v1/client-css
447
+ * Returns client.css for a specific section
448
+ */
449
+ async function handleGetClientCss(
450
+ _req: IncomingMessage,
451
+ res: ServerResponse,
452
+ sectionName: string,
453
+ ): Promise<void> {
454
+ await serveClientFile(res, sectionName, 'assets/client.css', 'text/css');
455
+ }
456
+
457
+ /**
458
+ * Handle GET /api/v1/assets
459
+ * Returns assets for a specific section
460
+ */
461
+ async function handleGetAssets(
462
+ req: IncomingMessage,
463
+ res: ServerResponse,
464
+ sectionName: string,
465
+ ): Promise<void> {
466
+ try {
467
+ const url = req.url || '';
468
+ const filePath = getQueryParam(url, 'file');
469
+
470
+ if (!filePath) {
471
+ res.statusCode = 400;
472
+ res.setHeader('Content-Type', 'application/json');
473
+ setNoCacheHeaders(res);
474
+ res.end(JSON.stringify({ error: 'Missing file parameter' }));
475
+ return;
476
+ }
477
+
478
+ // Security: prevent directory traversal
479
+ if (filePath.includes('..') || filePath.startsWith('/')) {
480
+ res.statusCode = 403;
481
+ res.setHeader('Content-Type', 'application/json');
482
+ setNoCacheHeaders(res);
483
+ res.end(JSON.stringify({ error: 'Access denied' }));
484
+ return;
485
+ }
486
+
487
+ const distPath = path.resolve(process.cwd(), 'dist');
488
+ const fullPath = path.join(distPath, 'sections', sectionName, 'assets', filePath);
489
+ const resolvedPath = path.resolve(fullPath);
490
+ const basePath = path.resolve(distPath, 'sections', sectionName, 'assets');
491
+
492
+ if (!resolvedPath.startsWith(basePath)) {
493
+ res.statusCode = 403;
494
+ res.setHeader('Content-Type', 'application/json');
495
+ setNoCacheHeaders(res);
496
+ res.end(JSON.stringify({ error: 'Access denied' }));
497
+ return;
498
+ }
499
+
500
+ if (!fs.existsSync(resolvedPath)) {
501
+ res.statusCode = 404;
502
+ res.setHeader('Content-Type', 'application/json');
503
+ setNoCacheHeaders(res);
504
+ res.end(JSON.stringify({ error: 'Asset not found', file: filePath, section: sectionName }));
505
+ return;
506
+ }
507
+
508
+ // Detect content type based on file extension
509
+ const ext = path.extname(filePath).toLowerCase();
510
+ const contentTypeMap: { [key: string]: string } = {
511
+ '.png': 'image/png',
512
+ '.jpg': 'image/jpeg',
513
+ '.jpeg': 'image/jpeg',
514
+ '.gif': 'image/gif',
515
+ '.webp': 'image/webp',
516
+ '.svg': 'image/svg+xml',
517
+ '.ico': 'image/x-icon',
518
+ '.woff': 'font/woff',
519
+ '.woff2': 'font/woff2',
520
+ '.ttf': 'font/ttf',
521
+ '.eot': 'application/vnd.ms-fontobject',
522
+ '.json': 'application/json',
523
+ '.css': 'text/css',
524
+ '.js': 'application/javascript',
525
+ };
526
+
527
+ const contentType = contentTypeMap[ext] || 'application/octet-stream';
528
+ const fileContent = fs.readFileSync(resolvedPath);
529
+
530
+ res.statusCode = 200;
531
+ res.setHeader('Content-Type', contentType);
532
+ setNoCacheHeaders(res);
533
+ res.end(fileContent);
534
+ } catch (error) {
535
+ console.error('[API Routes] ❌ Error serving asset:', error);
536
+ res.statusCode = 500;
537
+ res.setHeader('Content-Type', 'application/json');
538
+ setNoCacheHeaders(res);
539
+ res.end(JSON.stringify({
540
+ error: 'Failed to serve asset',
541
+ message: error instanceof Error ? error.message : String(error),
98
542
  }));
99
543
  }
100
544
  }
@@ -102,24 +546,55 @@ export async function handleUpdateCustomContent(
102
546
  /**
103
547
  * Main API router
104
548
  * Routes requests to appropriate handlers
549
+ * Extracts section name and original_url from query parameters if available
550
+ * Resolves showcase ID from dist folder based on available showcases
105
551
  */
106
552
  export function handleApiRequest(
107
553
  req: IncomingMessage,
108
554
  res: ServerResponse,
109
- sectionName: string,
110
- showcaseId: string
555
+ authToken?: string,
111
556
  ): void {
112
557
  const url = req.url || '';
113
558
 
114
- // Route: GET /api/v1/tile
559
+ if (url.startsWith('/api/v1/sections')) {
560
+ handleGetSections(req, res);
561
+ return;
562
+ }
563
+
564
+ // Extract section name from query parameter if provided
565
+ const sectionName = getQueryParam(url, 'section') || '';
566
+
567
+ // Extract original_url from query parameter if provided
568
+ const originalUrl = getQueryParam(url, 'original_url') || '';
569
+
115
570
  if (url.startsWith('/api/v1/tile')) {
116
- handleGetTile(req, res, sectionName, showcaseId);
571
+ // Extract section names from query parameter (comma-separated)
572
+ const sectionNamesParam = getQueryParam(url, 'sections');
573
+ const sectionNames = sectionNamesParam ? sectionNamesParam.split(',').map(s => s.trim()) : [];
574
+ handleTile(req, res, sectionNames, authToken, originalUrl);
117
575
  return;
118
576
  }
119
577
 
120
- // Route: POST /api/v1/custom_content
121
578
  if (url.startsWith('/api/v1/custom_content')) {
122
- handleUpdateCustomContent(req, res, sectionName, showcaseId);
579
+ // Extract section names from query parameter (comma-separated)
580
+ const sectionNamesParam = getQueryParam(url, 'sections');
581
+ const sectionNames = sectionNamesParam ? sectionNamesParam.split(',').map(s => s.trim()) : [];
582
+ handleUpdateCustomContent(req, res, sectionNames, authToken, originalUrl);
583
+ return;
584
+ }
585
+
586
+ if (url.startsWith('/api/v1/client-js')) {
587
+ handleGetClientJs(req, res, sectionName);
588
+ return;
589
+ }
590
+
591
+ if (url.startsWith('/api/v1/client-css')) {
592
+ handleGetClientCss(req, res, sectionName);
593
+ return;
594
+ }
595
+
596
+ if (url.startsWith('/api/v1/assets')) {
597
+ handleGetAssets(req, res, sectionName);
123
598
  return;
124
599
  }
125
600
 
@@ -129,7 +604,6 @@ export function handleApiRequest(
129
604
  setNoCacheHeaders(res);
130
605
  res.end(JSON.stringify({
131
606
  error: 'API route not found',
132
- url: url
607
+ url,
133
608
  }));
134
609
  }
135
-