@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.
- package/CHANGELOG.md +51 -0
- package/UPGRADE.md +19 -0
- package/dist/app.d.mts +1 -1028
- package/dist/app.d.ts +1 -1028
- package/dist/app.mjs +1 -1
- package/dist/cli.mjs +20 -7
- package/package.json +4 -3
- package/template/footers/example-footer/ExampleFooter.vue +1 -1
- package/template/footers/example-footer/client.ts +2 -1
- 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/entity/color.ts +2 -2
- package/template/footers/example-footer/server.ts +2 -1
- package/template/headers/example-header/client.ts +2 -1
- package/template/headers/example-header/component/Account.vue +1 -1
- package/template/headers/example-header/component/Cart.vue +1 -1
- package/template/headers/example-header/component/CategoriesDropdown.vue +1 -1
- package/template/headers/example-header/component/Logo.vue +1 -1
- package/template/headers/example-header/component/NavigationMenu.vue +1 -1
- package/template/headers/example-header/component/SearchForm.vue +1 -1
- package/template/headers/example-header/server.ts +2 -1
- package/template/index.d.ts +1 -1
- package/template/layouts/catalog/example-catalog/Main.vue +1 -1
- package/template/layouts/catalog/example-catalog/slots/custom-bottom-bar/client.ts +2 -1
- package/template/layouts/catalog/example-catalog/slots/custom-bottom-bar/server.ts +2 -1
- package/template/layouts/category/example-category/Main.vue +1 -1
- package/template/layouts/category/example-category/settings/content.ts +1 -1
- package/template/layouts/category/example-category/settings/design.ts +1 -1
- package/template/layouts/product/example-product/Main.vue +1 -1
- package/template/layouts/product/example-product/settings/content.ts +1 -1
- package/template/layouts/product/example-product/settings/design.ts +1 -1
- package/template/package.json +6 -3
- package/template/page-templates/example-template/pages/catalog.ts +1 -1
- package/template/page-templates/example-template/pages/category.ts +1 -1
- package/template/page-templates/example-template/pages/product.ts +1 -1
- package/template/preview/sections/preview.html +1 -1
- package/template/preview/shared/api-routes.ts +515 -41
- package/template/preview/shared/mock.ts +43 -41
- package/template/preview/shared/preview.ts +220 -123
- package/template/preview/shared/utils.ts +209 -62
- package/template/preview/ssr-server.ts +430 -0
- package/template/preview/vite.config.js +76 -75
- package/template/reference/sections/about-us/AboutUs.vue +1 -1
- package/template/reference/sections/about-us/client.ts +1 -1
- package/template/reference/sections/about-us/component/Image.vue +1 -1
- package/template/reference/sections/about-us/component/Stats.vue +2 -2
- package/template/reference/sections/about-us/component/Title.vue +1 -1
- package/template/reference/sections/about-us/server.ts +1 -1
- package/template/reference/sections/about-us/util/visibility-provider.ts +1 -1
- package/template/reference/sections/featured-products/FeaturedProducts.vue +65 -0
- package/template/reference/sections/featured-products/assets/arrow.svg +3 -0
- package/template/reference/sections/featured-products/assets/custom_section_showcase_1_preview.png +0 -0
- package/template/reference/sections/featured-products/client.ts +6 -0
- package/template/reference/sections/featured-products/component/ProductItem.vue +71 -0
- package/template/reference/sections/featured-products/component/Title.vue +31 -0
- package/template/reference/sections/featured-products/entity/color.ts +4 -0
- package/template/reference/sections/featured-products/server.ts +6 -0
- package/template/reference/sections/featured-products/settings/content.ts +14 -0
- package/template/reference/sections/featured-products/settings/design.ts +33 -0
- package/template/reference/sections/featured-products/settings/translations.ts +24 -0
- package/template/reference/sections/featured-products/showcases/1.ts +28 -0
- package/template/reference/sections/featured-products/showcases/translations.ts +16 -0
- package/template/reference/sections/featured-products/type.ts +5 -0
- package/template/reference/sections/intro-slider/IntroSlider.vue +1 -1
- package/template/reference/sections/intro-slider/client.ts +2 -1
- package/template/reference/sections/intro-slider/component/Slider.vue +8 -2
- package/template/reference/sections/intro-slider/component/Title.vue +1 -1
- package/template/reference/sections/intro-slider/entity/color.ts +2 -2
- package/template/reference/sections/intro-slider/server.ts +2 -1
- package/template/reference/sections/tag-lines/TagLines.vue +1 -1
- package/template/reference/sections/tag-lines/client.ts +2 -1
- package/template/reference/sections/tag-lines/component/SectionImage.vue +1 -1
- package/template/reference/sections/tag-lines/component/Title.vue +1 -1
- package/template/reference/sections/tag-lines/composables/highlighted-text-image-list.ts +4 -3
- package/template/reference/sections/tag-lines/server.ts +2 -1
- package/template/reference/sections/trending-categories/TrendingCategories.vue +70 -0
- package/template/reference/sections/trending-categories/assets/arrow.svg +3 -0
- package/template/reference/sections/trending-categories/assets/custom_section_showcase_1_preview.png +0 -0
- package/template/reference/sections/trending-categories/client.ts +6 -0
- package/template/reference/sections/trending-categories/component/CategoryItem.vue +62 -0
- package/template/reference/sections/trending-categories/component/Title.vue +32 -0
- package/template/reference/sections/trending-categories/entity/color.ts +4 -0
- package/template/reference/sections/trending-categories/server.ts +6 -0
- package/template/reference/sections/trending-categories/settings/content.ts +14 -0
- package/template/reference/sections/trending-categories/settings/design.ts +33 -0
- package/template/reference/sections/trending-categories/settings/translations.ts +24 -0
- package/template/reference/sections/trending-categories/showcases/1.ts +36 -0
- package/template/reference/sections/trending-categories/showcases/translations.ts +22 -0
- package/template/reference/sections/trending-categories/type.ts +5 -0
- package/template/reference/shared/components/Button.vue +1 -1
- package/template/reference/shared/utils/styles.ts +1 -0
- package/template/reference/templates/reference-template-apparel/pages/catalog.ts +1 -1
- package/template/reference/templates/reference-template-apparel/pages/category.ts +1 -1
- package/template/reference/templates/reference-template-apparel/pages/home.ts +10 -0
- package/template/reference/templates/reference-template-apparel/pages/product.ts +1 -1
- package/template/reference/templates/reference-template-bike/pages/catalog.ts +1 -1
- package/template/reference/templates/reference-template-bike/pages/category.ts +1 -1
- package/template/reference/templates/reference-template-bike/pages/home.ts +10 -0
- package/template/reference/templates/reference-template-bike/pages/product.ts +1 -1
- package/template/sections/example-section/ExampleSection.vue +8 -1
- package/template/sections/example-section/client.ts +2 -1
- package/template/sections/example-section/component/button/Button.vue +1 -1
- package/template/sections/example-section/component/image/Image.vue +1 -1
- package/template/sections/example-section/component/image/ImagesGrid.vue +1 -1
- 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 +1 -1
- package/template/sections/example-section/entity/color.ts +2 -2
- package/template/sections/example-section/server.ts +2 -1
- package/template/sections/example-section/settings/translations.ts +1 -1
- package/template/sections/example-section/showcases/1.ts +2 -22
- package/template/sections/example-section/showcases/2.ts +2 -22
- package/template/sections/example-section/showcases/3.ts +2 -22
- package/template/sections/example-section/showcases/translations.ts +11 -149
- package/template/shared/components/LanguageSelector.vue +1 -1
- package/template/shared/translation.ts +16 -0
- package/template/shared/utils.ts +3 -1
- package/template/tsconfig.json +1 -0
- 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 {
|
|
4
|
+
import { URL } from 'url';
|
|
5
|
+
|
|
6
|
+
import { getShowcaseData } from './preview';
|
|
3
7
|
import { fetchTiles, updateTilesSection, updateCustomContent } from './utils';
|
|
4
|
-
|
|
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
|
-
|
|
100
|
+
async function handleGetTile(
|
|
21
101
|
_req: IncomingMessage,
|
|
22
102
|
res: ServerResponse,
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
//
|
|
30
|
-
|
|
110
|
+
// Fetch tiles from remote once
|
|
111
|
+
let responseData = await fetchTiles(authToken, tileUrl);
|
|
31
112
|
|
|
32
|
-
// Fetch
|
|
33
|
-
const
|
|
34
|
-
|
|
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
|
|
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: '
|
|
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
|
|
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
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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 =
|
|
363
|
+
const responseData = updatedCustomContent || {
|
|
78
364
|
error: 'Failed to update custom content',
|
|
79
|
-
|
|
80
|
-
showcaseId
|
|
365
|
+
sectionNames,
|
|
81
366
|
};
|
|
82
367
|
|
|
83
|
-
res.statusCode =
|
|
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
|
|
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
|
-
|
|
97
|
-
|
|
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
|
-
|
|
110
|
-
showcaseId: string
|
|
555
|
+
authToken?: string,
|
|
111
556
|
): void {
|
|
112
557
|
const url = req.url || '';
|
|
113
558
|
|
|
114
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
607
|
+
url,
|
|
133
608
|
}));
|
|
134
609
|
}
|
|
135
|
-
|