@o2vend/theme-cli 1.0.35 → 1.0.36
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/lib/lib/dev-server.js +352 -421
- package/lib/lib/liquid-engine.js +88 -65
- package/lib/lib/liquid-filters.js +10 -29
- package/lib/lib/mock-api-server.js +58 -144
- package/lib/lib/mock-data.js +126 -78
- package/lib/lib/widget-service.js +49 -0
- package/package.json +1 -1
package/lib/lib/liquid-engine.js
CHANGED
|
@@ -101,63 +101,50 @@ function createLiquidEngine(themePath, options = {}) {
|
|
|
101
101
|
* @param {string} themePath - Theme path
|
|
102
102
|
*/
|
|
103
103
|
function registerCustomTags(liquid, themePath) {
|
|
104
|
-
// Register section tag
|
|
105
104
|
liquid.registerTag('section', {
|
|
106
105
|
parse: function(tagToken, remainTokens) {
|
|
107
106
|
this.sectionPath = tagToken.args.trim().replace(/^['"]|['"]$/g, '');
|
|
108
107
|
},
|
|
109
108
|
render: async function(scope, hash) {
|
|
110
109
|
let sectionPath = this.sectionPath;
|
|
111
|
-
if (!sectionPath)
|
|
112
|
-
|
|
113
|
-
return '';
|
|
114
|
-
}
|
|
115
|
-
|
|
110
|
+
if (!sectionPath) return '';
|
|
111
|
+
|
|
116
112
|
try {
|
|
117
|
-
// Normalize section path - remove 'sections/' prefix if present
|
|
118
|
-
// Handle both 'hero' and 'sections/hero' formats
|
|
119
113
|
sectionPath = sectionPath.replace(/^sections\//, '').replace(/^sections\\/, '');
|
|
120
|
-
|
|
114
|
+
|
|
121
115
|
const sectionFile = path.join(themePath, 'sections', `${sectionPath}.liquid`);
|
|
122
|
-
if (fs.existsSync(sectionFile)) {
|
|
123
|
-
let sectionContent = fs.readFileSync(sectionFile, 'utf8');
|
|
124
|
-
|
|
125
|
-
// Pre-process section to convert render_widget filter to tag
|
|
126
|
-
const originalContent = sectionContent;
|
|
127
|
-
try {
|
|
128
|
-
sectionContent = preprocessTemplate(sectionContent);
|
|
129
|
-
} catch (error) {
|
|
130
|
-
console.error(chalk.red(`[SECTION] ❌ Error preprocessing ${sectionPath}: ${error.message}`));
|
|
131
|
-
sectionContent = originalContent;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const scopeContexts = Array.isArray(scope?.contexts) ? scope.contexts : [];
|
|
135
|
-
const primaryScope = scopeContexts.length > 0
|
|
136
|
-
? scopeContexts[0]
|
|
137
|
-
: (scope?.environments || scope?.context || {});
|
|
138
|
-
|
|
139
|
-
// Merge with current rendering context to ensure all data is available
|
|
140
|
-
const fullContext = currentRenderingContext
|
|
141
|
-
? { ...currentRenderingContext, ...primaryScope }
|
|
142
|
-
: primaryScope;
|
|
143
|
-
|
|
144
|
-
const context = {
|
|
145
|
-
...fullContext,
|
|
146
|
-
section: fullContext.section || {}
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
// Render section
|
|
150
|
-
try {
|
|
151
|
-
const rendered = await liquid.parseAndRender(sectionContent, context);
|
|
152
|
-
return rendered;
|
|
153
|
-
} catch (renderError) {
|
|
154
|
-
console.error(chalk.red(`[SECTION] ❌ Error rendering ${sectionPath}: ${renderError.message}`));
|
|
155
|
-
throw renderError;
|
|
156
|
-
}
|
|
157
|
-
} else {
|
|
116
|
+
if (!fs.existsSync(sectionFile)) {
|
|
158
117
|
console.warn(`[SECTION] Section file not found: ${sectionFile}`);
|
|
159
118
|
return '';
|
|
160
119
|
}
|
|
120
|
+
|
|
121
|
+
let sectionContent = fs.readFileSync(sectionFile, 'utf8');
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
sectionContent = preprocessTemplate(sectionContent);
|
|
125
|
+
} catch (e) {
|
|
126
|
+
// Keep original content if preprocessing fails
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const scopeContexts = Array.isArray(scope?.contexts) ? scope.contexts : [];
|
|
130
|
+
const primaryScope = scopeContexts.length > 0
|
|
131
|
+
? scopeContexts[0]
|
|
132
|
+
: (scope?.environments || scope?.context || {});
|
|
133
|
+
|
|
134
|
+
const fullContext = currentRenderingContext
|
|
135
|
+
? { ...currentRenderingContext, ...primaryScope }
|
|
136
|
+
: primaryScope;
|
|
137
|
+
|
|
138
|
+
const context = {
|
|
139
|
+
...fullContext,
|
|
140
|
+
section: {
|
|
141
|
+
id: sectionPath.replace(/\//g, '-'),
|
|
142
|
+
name: sectionPath.split('/').pop(),
|
|
143
|
+
settings: fullContext.section?.settings || {}
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
return await liquid.parseAndRender(sectionContent, context);
|
|
161
148
|
} catch (error) {
|
|
162
149
|
console.error(`[SECTION] Error rendering section ${sectionPath}:`, error.message);
|
|
163
150
|
return '';
|
|
@@ -202,16 +189,30 @@ function registerCustomTags(liquid, themePath) {
|
|
|
202
189
|
}
|
|
203
190
|
});
|
|
204
191
|
|
|
205
|
-
// Register endschema tag (closing tag for schema)
|
|
206
192
|
liquid.registerTag('endschema', {
|
|
193
|
+
parse: function(tagToken, remainTokens) {},
|
|
194
|
+
render: async function(scope, hash) { return ''; }
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
liquid.registerTag('cache', {
|
|
207
198
|
parse: function(tagToken, remainTokens) {
|
|
208
|
-
|
|
199
|
+
this.tpls = [];
|
|
200
|
+
const stream = liquid.parser.parseStream(remainTokens);
|
|
201
|
+
stream.on('tag:endcache', () => stream.stop());
|
|
202
|
+
stream.on('template', (tpl) => this.tpls.push(tpl));
|
|
203
|
+
stream.on('end', () => { throw new Error('tag cache not closed'); });
|
|
204
|
+
stream.start();
|
|
209
205
|
},
|
|
210
206
|
render: async function(scope, hash) {
|
|
211
|
-
return
|
|
207
|
+
return liquid.renderer.renderTemplates(this.tpls, scope);
|
|
212
208
|
}
|
|
213
209
|
});
|
|
214
210
|
|
|
211
|
+
liquid.registerTag('endcache', {
|
|
212
|
+
parse: function() {},
|
|
213
|
+
render: async function() { return ''; }
|
|
214
|
+
});
|
|
215
|
+
|
|
215
216
|
// Register render_widget tag as an alternative to the filter
|
|
216
217
|
// This allows templates to use: {% render_widget widget %}
|
|
217
218
|
liquid.registerTag('render_widget', {
|
|
@@ -283,7 +284,6 @@ function registerCustomTags(liquid, themePath) {
|
|
|
283
284
|
// CRITICAL: Include tag must parse hash arguments like 'product: product' to match production behavior
|
|
284
285
|
liquid.registerTag('include', {
|
|
285
286
|
parse: function(tagToken, remainTokens) {
|
|
286
|
-
// Parse arguments - handle 'snippets/product-card', product: product format
|
|
287
287
|
const args = tagToken.args.trim();
|
|
288
288
|
|
|
289
289
|
// Split by comma to separate file path from hash parameters
|
|
@@ -296,12 +296,26 @@ function registerCustomTags(liquid, themePath) {
|
|
|
296
296
|
this.hashArgs = '';
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
+
// Handle "| default:" filter in file argument
|
|
300
|
+
const pipeIndex = this.fileArg.indexOf('|');
|
|
301
|
+
if (pipeIndex > 0) {
|
|
302
|
+
const filterPart = this.fileArg.substring(pipeIndex + 1).trim();
|
|
303
|
+
this.fileArg = this.fileArg.substring(0, pipeIndex).trim();
|
|
304
|
+
const defaultMatch = filterPart.match(/^default:\s*['"]([^'"]+)['"]/);
|
|
305
|
+
this.defaultFile = defaultMatch ? defaultMatch[1] : null;
|
|
306
|
+
} else {
|
|
307
|
+
this.defaultFile = null;
|
|
308
|
+
}
|
|
309
|
+
|
|
299
310
|
// Extract file path - remove quotes if present
|
|
300
311
|
const fileMatch = this.fileArg.match(/^['"]([^'"]+)['"]/);
|
|
301
312
|
this.file = fileMatch ? fileMatch[1] : this.fileArg.replace(/^['"]|['"]$/g, '');
|
|
302
313
|
},
|
|
303
314
|
render: async function(scope, hash) {
|
|
304
315
|
let filePath = this.file;
|
|
316
|
+
if (!filePath && this.defaultFile) {
|
|
317
|
+
filePath = this.defaultFile;
|
|
318
|
+
}
|
|
305
319
|
if (!filePath) {
|
|
306
320
|
console.warn('[INCLUDE] No file path provided');
|
|
307
321
|
return '';
|
|
@@ -392,8 +406,6 @@ function registerCustomTags(liquid, themePath) {
|
|
|
392
406
|
// Override the render tag (similar to include but with isolated scope)
|
|
393
407
|
liquid.registerTag('render', {
|
|
394
408
|
parse: function(tagToken, remainTokens) {
|
|
395
|
-
// Parse the tag arguments
|
|
396
|
-
// Format: {% render 'path' %} or {% render variable.path, param: value %}
|
|
397
409
|
const args = tagToken.args.trim();
|
|
398
410
|
|
|
399
411
|
// Split by comma to separate file path from hash parameters
|
|
@@ -405,21 +417,28 @@ function registerCustomTags(liquid, themePath) {
|
|
|
405
417
|
this.fileArg = args;
|
|
406
418
|
this.hashArgs = '';
|
|
407
419
|
}
|
|
420
|
+
|
|
421
|
+
// Handle "| default:" filter on the file argument
|
|
422
|
+
// e.g., footer_menu_widget.template_path | default: 'widgets/footer-menu'
|
|
423
|
+
this.defaultPath = null;
|
|
424
|
+
const pipeIndex = this.fileArg.indexOf('|');
|
|
425
|
+
if (pipeIndex > 0) {
|
|
426
|
+
const filterPart = this.fileArg.substring(pipeIndex + 1).trim();
|
|
427
|
+
this.fileArg = this.fileArg.substring(0, pipeIndex).trim();
|
|
428
|
+
const defaultMatch = filterPart.match(/^default:\s*['"]([^'"]+)['"]/);
|
|
429
|
+
if (defaultMatch) {
|
|
430
|
+
this.defaultPath = defaultMatch[1];
|
|
431
|
+
}
|
|
432
|
+
}
|
|
408
433
|
},
|
|
409
434
|
render: async function(scope, hash) {
|
|
410
|
-
// RECURSION GUARD: Check render depth to prevent infinite loops
|
|
411
435
|
currentRenderDepth++;
|
|
412
436
|
if (currentRenderDepth > MAX_RENDER_DEPTH) {
|
|
413
|
-
console.error(chalk.red(`[RENDER TAG]
|
|
414
|
-
console.error(chalk.red(`[RENDER TAG] Current rendering stack: ${Array.from(renderingStack).join(' -> ')}`));
|
|
437
|
+
console.error(chalk.red(`[RENDER TAG] Maximum render depth (${MAX_RENDER_DEPTH}) exceeded`));
|
|
415
438
|
currentRenderDepth--;
|
|
416
439
|
return `<div class="widget-error">Error: Maximum render depth exceeded</div>`;
|
|
417
440
|
}
|
|
418
441
|
|
|
419
|
-
// CRITICAL: Use scope.get() to properly resolve variables from the LiquidJS scope chain
|
|
420
|
-
// This correctly handles loop variables like 'widget' in {% for widget in widgets.hero %}
|
|
421
|
-
|
|
422
|
-
// Build context starting with currentRenderingContext
|
|
423
442
|
let fullContext = {};
|
|
424
443
|
if (currentRenderingContext) {
|
|
425
444
|
fullContext = { ...currentRenderingContext };
|
|
@@ -428,24 +447,28 @@ function registerCustomTags(liquid, themePath) {
|
|
|
428
447
|
// Resolve file path - could be a string literal or a variable
|
|
429
448
|
let filePath = this.fileArg;
|
|
430
449
|
|
|
431
|
-
// If it's wrapped in quotes, it's a string literal
|
|
432
450
|
if ((filePath.startsWith('"') && filePath.endsWith('"')) ||
|
|
433
451
|
(filePath.startsWith("'") && filePath.endsWith("'"))) {
|
|
434
452
|
filePath = filePath.slice(1, -1);
|
|
435
453
|
} else {
|
|
436
|
-
//
|
|
454
|
+
// Variable resolution with | default: fallback support
|
|
437
455
|
try {
|
|
438
|
-
// For paths like 'widget.template_path', we need to resolve via scope.get()
|
|
439
456
|
const resolvedValue = await scope.get(filePath.split('.'));
|
|
440
457
|
if (resolvedValue && typeof resolvedValue === 'string') {
|
|
441
458
|
filePath = resolvedValue;
|
|
459
|
+
} else if (this.defaultPath) {
|
|
460
|
+
filePath = this.defaultPath;
|
|
442
461
|
} else {
|
|
443
462
|
currentRenderDepth--;
|
|
444
463
|
return '';
|
|
445
464
|
}
|
|
446
465
|
} catch (error) {
|
|
447
|
-
|
|
448
|
-
|
|
466
|
+
if (this.defaultPath) {
|
|
467
|
+
filePath = this.defaultPath;
|
|
468
|
+
} else {
|
|
469
|
+
currentRenderDepth--;
|
|
470
|
+
return '';
|
|
471
|
+
}
|
|
449
472
|
}
|
|
450
473
|
}
|
|
451
474
|
|
|
@@ -741,8 +764,8 @@ async function renderWithLayout(liquid, templatePath, context, themePath) {
|
|
|
741
764
|
price: sampleProduct.price || sampleProduct.sellingPrice,
|
|
742
765
|
stock: sampleProduct.stock,
|
|
743
766
|
inStock: sampleProduct.inStock,
|
|
744
|
-
hasImage: !!(sampleProduct.imageUrl || sampleProduct.
|
|
745
|
-
imageUrl: sampleProduct.imageUrl || sampleProduct.
|
|
767
|
+
hasImage: !!(sampleProduct.imageUrl || sampleProduct.thumbnailImage),
|
|
768
|
+
imageUrl: sampleProduct.imageUrl || sampleProduct.thumbnailImage,
|
|
746
769
|
categoryId: sampleProduct.categoryId
|
|
747
770
|
});
|
|
748
771
|
} else {
|
|
@@ -528,25 +528,22 @@ class LiquidHelperService {
|
|
|
528
528
|
return `<img src="${input}" alt="${alt}" ${attributes}>`;
|
|
529
529
|
}
|
|
530
530
|
|
|
531
|
-
// Product filters
|
|
532
531
|
productUrlFilter(product) {
|
|
533
532
|
if (!product) return '#';
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
if (!identifier) return '#';
|
|
537
|
-
// Ensure it's a string and remove any leading slashes
|
|
538
|
-
const handle = String(identifier).replace(/^\//, '');
|
|
539
|
-
return `/products/${handle}`;
|
|
533
|
+
const slug = product.slug || product.handle || product.id;
|
|
534
|
+
return slug ? `/${String(slug).replace(/^\//, '')}` : '#';
|
|
540
535
|
}
|
|
541
536
|
|
|
542
537
|
collectionUrlFilter(collection) {
|
|
543
538
|
if (!collection) return '#';
|
|
544
|
-
|
|
539
|
+
const slug = collection.slug || collection.handle || collection.id;
|
|
540
|
+
return slug ? `/${String(slug).replace(/^\//, '')}` : '#';
|
|
545
541
|
}
|
|
546
542
|
|
|
547
543
|
pageUrlFilter(page) {
|
|
548
544
|
if (!page) return '#';
|
|
549
|
-
|
|
545
|
+
const slug = page.slug || page.handle || page.id;
|
|
546
|
+
return slug ? `/${String(slug).replace(/^\//, '')}` : '#';
|
|
550
547
|
}
|
|
551
548
|
|
|
552
549
|
// Utility filters
|
|
@@ -591,26 +588,10 @@ class LiquidHelperService {
|
|
|
591
588
|
*/
|
|
592
589
|
assetUrlFilter(input) {
|
|
593
590
|
if (!input || typeof input !== 'string') return input;
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
// Remove /themes/default/ or /themes/theme-name/ prefixes if present
|
|
599
|
-
// Handle both "/themes/default/assets/logo.png" and "themes/default/assets/logo.png"
|
|
600
|
-
url = url.replace(/^\/?themes\/[^\/]+\//, '/');
|
|
601
|
-
|
|
602
|
-
// Extract just the filename if path contains assets
|
|
603
|
-
// Handle paths like "themes/default/assets/logo.png" -> "/assets/logo.png"
|
|
604
|
-
// or "/themes/default/assets/logo.png" -> "/assets/logo.png"
|
|
605
|
-
const assetsMatch = url.match(/assets\/(.+)$/);
|
|
606
|
-
if (assetsMatch) {
|
|
607
|
-
url = '/assets/' + assetsMatch[1];
|
|
608
|
-
} else if (!url.startsWith('/assets/') && !url.startsWith('http://') && !url.startsWith('https://') && !url.startsWith('data:')) {
|
|
609
|
-
// Ensure /assets/ prefix if not already present (skip if already absolute URL or data URI)
|
|
610
|
-
url = '/assets/' + url.replace(/^\/+/, '');
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
return url;
|
|
591
|
+
|
|
592
|
+
if (input.startsWith('/assets/')) return input;
|
|
593
|
+
|
|
594
|
+
return `/assets/${input}`;
|
|
614
595
|
}
|
|
615
596
|
|
|
616
597
|
/**
|
|
@@ -66,22 +66,10 @@ class MockApiServer {
|
|
|
66
66
|
// Paginate
|
|
67
67
|
const paginated = products.slice(offset, offset + limit);
|
|
68
68
|
|
|
69
|
-
// DEBUG: Verify products have stock/quantity before sending
|
|
70
69
|
if (paginated.length > 0) {
|
|
71
|
-
const
|
|
72
|
-
const hasStock = 'stock' in sampleProduct;
|
|
73
|
-
// Use nullish coalescing to show 0 values correctly
|
|
74
|
-
const stockValue = sampleProduct.stock ?? 'N/A';
|
|
70
|
+
const sp = paginated[0];
|
|
75
71
|
console.log(`[MOCK API] Returning ${paginated.length} products (total: ${products.length})`);
|
|
76
|
-
console.log(`[MOCK API
|
|
77
|
-
console.log(`[MOCK API DEBUG] Sample product stock details: stock=${sampleProduct.stock}, inStock=${sampleProduct.inStock}`);
|
|
78
|
-
if (sampleProduct.variants && sampleProduct.variants.length > 0) {
|
|
79
|
-
const sampleVariant = sampleProduct.variants[0];
|
|
80
|
-
const variantHasStock = 'stock' in sampleVariant;
|
|
81
|
-
const variantStockValue = sampleVariant.stock ?? 'N/A';
|
|
82
|
-
console.log(`[MOCK API DEBUG] Sample variant - Has stock field: ${variantHasStock}, Stock value: ${variantStockValue}`);
|
|
83
|
-
console.log(`[MOCK API DEBUG] Sample variant stock details: stock=${sampleVariant.stock}, inStock=${sampleVariant.inStock}, available=${sampleVariant.available}`);
|
|
84
|
-
}
|
|
72
|
+
console.log(`[MOCK API] Sample: ${sp.title}, stockQuantity=${sp.stockQuantity}, inStock=${sp.inStock}`);
|
|
85
73
|
}
|
|
86
74
|
|
|
87
75
|
res.json({
|
|
@@ -94,7 +82,10 @@ class MockApiServer {
|
|
|
94
82
|
|
|
95
83
|
// Product by ID
|
|
96
84
|
this.app.get('/shopfront/api/v2/products/:id', (req, res) => {
|
|
97
|
-
const
|
|
85
|
+
const id = req.params.id;
|
|
86
|
+
const product = this.mockData.products.find(p =>
|
|
87
|
+
String(p.id) === id || String(p.productId) === id || p.slug === id || p.handle === id
|
|
88
|
+
);
|
|
98
89
|
if (product) {
|
|
99
90
|
res.json(product);
|
|
100
91
|
} else {
|
|
@@ -138,7 +129,10 @@ class MockApiServer {
|
|
|
138
129
|
|
|
139
130
|
// Category by ID
|
|
140
131
|
this.app.get('/shopfront/api/v2/categories/:id', (req, res) => {
|
|
141
|
-
const
|
|
132
|
+
const id = req.params.id;
|
|
133
|
+
const category = this.mockData.categories.find(c =>
|
|
134
|
+
String(c.id) === id || String(c.categoryId) === id || c.slug === id || c.handle === id
|
|
135
|
+
);
|
|
142
136
|
if (category) {
|
|
143
137
|
res.json(category);
|
|
144
138
|
} else {
|
|
@@ -165,17 +159,16 @@ class MockApiServer {
|
|
|
165
159
|
|
|
166
160
|
let widgets = [...this.mockData.widgets];
|
|
167
161
|
|
|
168
|
-
|
|
162
|
+
if (pageId) {
|
|
163
|
+
widgets = widgets.filter(w => w.pageId === 'all' || w.pageId === pageId);
|
|
164
|
+
}
|
|
169
165
|
if (section) {
|
|
170
166
|
widgets = widgets.filter(w => w.section === section || w.sectionName === section);
|
|
171
167
|
}
|
|
172
168
|
|
|
173
|
-
// Filter by status (default to 'active' if not specified)
|
|
174
169
|
const filterStatus = status || 'active';
|
|
175
170
|
widgets = widgets.filter(w => w.status === filterStatus);
|
|
176
|
-
|
|
177
|
-
// Sort by position
|
|
178
|
-
widgets.sort((a, b) => (a.position || 0) - (b.position || 0));
|
|
171
|
+
widgets.sort((a, b) => (a.Position || a.position || 0) - (b.Position || b.position || 0));
|
|
179
172
|
|
|
180
173
|
res.json({
|
|
181
174
|
widgets: widgets,
|
|
@@ -189,17 +182,16 @@ class MockApiServer {
|
|
|
189
182
|
|
|
190
183
|
let widgets = [...this.mockData.widgets];
|
|
191
184
|
|
|
192
|
-
|
|
185
|
+
if (pageId) {
|
|
186
|
+
widgets = widgets.filter(w => w.pageId === 'all' || w.pageId === pageId);
|
|
187
|
+
}
|
|
193
188
|
if (section) {
|
|
194
189
|
widgets = widgets.filter(w => w.section === section || w.sectionName === section);
|
|
195
190
|
}
|
|
196
191
|
|
|
197
|
-
// Filter by status (default to 'active' if not specified)
|
|
198
192
|
const filterStatus = status || 'active';
|
|
199
193
|
widgets = widgets.filter(w => w.status === filterStatus);
|
|
200
|
-
|
|
201
|
-
// Sort by position
|
|
202
|
-
widgets.sort((a, b) => (a.position || 0) - (b.position || 0));
|
|
194
|
+
widgets.sort((a, b) => (a.Position || a.position || 0) - (b.Position || b.position || 0));
|
|
203
195
|
|
|
204
196
|
res.json({
|
|
205
197
|
widgets: widgets,
|
|
@@ -207,6 +199,21 @@ class MockApiServer {
|
|
|
207
199
|
});
|
|
208
200
|
});
|
|
209
201
|
|
|
202
|
+
// Page-section widgets endpoint (matches production: POST /pages/{pageId}/sections/{section}/widgets)
|
|
203
|
+
this.app.post('/pages/:pageId/sections/:section/widgets', (req, res) => {
|
|
204
|
+
const { pageId, section } = req.params;
|
|
205
|
+
|
|
206
|
+
let widgets = this.mockData.widgets.filter(w => {
|
|
207
|
+
const matchesPage = w.pageId === 'all' || w.pageId === pageId;
|
|
208
|
+
const matchesSection = w.section === section || w.sectionName === section;
|
|
209
|
+
const isActive = w.status === 'active';
|
|
210
|
+
return matchesPage && matchesSection && isActive;
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
widgets.sort((a, b) => (a.Position || a.position || 0) - (b.Position || b.position || 0));
|
|
214
|
+
res.json(widgets);
|
|
215
|
+
});
|
|
216
|
+
|
|
210
217
|
// Cart endpoints
|
|
211
218
|
this.app.get('/shopfront/api/v2/cart/:cartId', (req, res) => {
|
|
212
219
|
res.json(this.mockData.cart);
|
|
@@ -239,86 +246,35 @@ class MockApiServer {
|
|
|
239
246
|
{
|
|
240
247
|
id: 'main-menu',
|
|
241
248
|
name: 'Main Menu',
|
|
242
|
-
type: 'Main Menu',
|
|
249
|
+
type: 'Main Menu',
|
|
243
250
|
items: [
|
|
244
|
-
{
|
|
245
|
-
|
|
246
|
-
name: '
|
|
247
|
-
title: 'Home',
|
|
248
|
-
link: '/',
|
|
249
|
-
url: '/',
|
|
250
|
-
displayOrder: 1,
|
|
251
|
-
childItems: []
|
|
252
|
-
},
|
|
253
|
-
{
|
|
254
|
-
id: '2',
|
|
255
|
-
name: 'Products',
|
|
256
|
-
title: 'Products',
|
|
257
|
-
link: '/products',
|
|
258
|
-
url: '/products',
|
|
259
|
-
displayOrder: 2,
|
|
260
|
-
childItems: [
|
|
261
|
-
{ id: '2-1', name: 'Electronics', title: 'Electronics', link: '/products?category=electronics', url: '/products?category=electronics', displayOrder: 1 },
|
|
262
|
-
{ id: '2-2', name: 'Clothing', title: 'Clothing', link: '/products?category=clothing', url: '/products?category=clothing', displayOrder: 2 },
|
|
263
|
-
{ id: '2-3', name: 'Accessories', title: 'Accessories', link: '/products?category=accessories', url: '/products?category=accessories', displayOrder: 3 },
|
|
264
|
-
{ id: '2-4', name: 'Home & Garden', title: 'Home & Garden', link: '/products?category=home-garden', url: '/products?category=home-garden', displayOrder: 4 }
|
|
265
|
-
]
|
|
266
|
-
},
|
|
267
|
-
{
|
|
268
|
-
id: '3',
|
|
269
|
-
name: 'Collections',
|
|
270
|
-
title: 'Collections',
|
|
271
|
-
link: '/collections',
|
|
272
|
-
url: '/collections',
|
|
273
|
-
displayOrder: 3,
|
|
251
|
+
{ id: '1', name: 'Home', title: 'Home', link: '/', displayOrder: 1, childItems: [] },
|
|
252
|
+
{
|
|
253
|
+
id: '2', name: 'Products', title: 'Products', link: '/categories', displayOrder: 2,
|
|
274
254
|
childItems: [
|
|
275
|
-
{ id: '
|
|
276
|
-
{ id: '
|
|
277
|
-
{ id: '
|
|
255
|
+
{ id: '2-1', name: 'Electronics', title: 'Electronics', link: '/electronics', displayOrder: 1 },
|
|
256
|
+
{ id: '2-2', name: 'Clothing', title: 'Clothing', link: '/clothing', displayOrder: 2 },
|
|
257
|
+
{ id: '2-3', name: 'Accessories', title: 'Accessories', link: '/accessories', displayOrder: 3 },
|
|
258
|
+
{ id: '2-4', name: 'Home & Garden', title: 'Home & Garden', link: '/home-and-garden', displayOrder: 4 }
|
|
278
259
|
]
|
|
279
260
|
},
|
|
280
|
-
{
|
|
281
|
-
|
|
282
|
-
name: 'About',
|
|
283
|
-
title: 'About',
|
|
284
|
-
link: '/page/about',
|
|
285
|
-
url: '/page/about',
|
|
286
|
-
displayOrder: 4,
|
|
287
|
-
childItems: []
|
|
288
|
-
},
|
|
289
|
-
{
|
|
290
|
-
id: '5',
|
|
291
|
-
name: 'Contact',
|
|
292
|
-
title: 'Contact',
|
|
293
|
-
link: '/page/contact',
|
|
294
|
-
url: '/page/contact',
|
|
295
|
-
displayOrder: 5,
|
|
296
|
-
childItems: []
|
|
297
|
-
}
|
|
261
|
+
{ id: '3', name: 'About', title: 'About', link: '/about', displayOrder: 3, childItems: [] },
|
|
262
|
+
{ id: '4', name: 'Contact', title: 'Contact', link: '/contact', displayOrder: 4, childItems: [] }
|
|
298
263
|
]
|
|
299
264
|
},
|
|
300
265
|
{
|
|
301
266
|
id: 'footer-menu',
|
|
302
267
|
name: 'Footer Menu',
|
|
303
|
-
type: 'Footer Menu',
|
|
268
|
+
type: 'Footer Menu',
|
|
304
269
|
items: [
|
|
305
|
-
{ id: '6', name: 'Privacy Policy', title: 'Privacy Policy', link: '/
|
|
306
|
-
{ id: '7', name: 'Terms of Service', title: 'Terms of Service', link: '/
|
|
307
|
-
{ id: '8', name: 'Shipping', title: 'Shipping', link: '/
|
|
308
|
-
{ id: '9', name: 'Returns', title: 'Returns', link: '/
|
|
270
|
+
{ id: '6', name: 'Privacy Policy', title: 'Privacy Policy', link: '/privacy', displayOrder: 1 },
|
|
271
|
+
{ id: '7', name: 'Terms of Service', title: 'Terms of Service', link: '/terms', displayOrder: 2 },
|
|
272
|
+
{ id: '8', name: 'Shipping', title: 'Shipping', link: '/shipping', displayOrder: 3 },
|
|
273
|
+
{ id: '9', name: 'Returns', title: 'Returns', link: '/returns', displayOrder: 4 }
|
|
309
274
|
]
|
|
310
275
|
}
|
|
311
276
|
];
|
|
312
|
-
|
|
313
|
-
// Support both array format (for frontend) and object format (for backward compatibility)
|
|
314
|
-
if (req.query.format === 'object') {
|
|
315
|
-
res.json({
|
|
316
|
-
success: true,
|
|
317
|
-
data: { menus }
|
|
318
|
-
});
|
|
319
|
-
} else {
|
|
320
|
-
res.json(menus);
|
|
321
|
-
}
|
|
277
|
+
res.json(menus);
|
|
322
278
|
});
|
|
323
279
|
|
|
324
280
|
this.app.get('/webstoreapi/menus/:menuId', (req, res) => {
|
|
@@ -329,60 +285,18 @@ class MockApiServer {
|
|
|
329
285
|
name: 'Main Menu',
|
|
330
286
|
type: 'Main Menu',
|
|
331
287
|
items: [
|
|
332
|
-
{
|
|
333
|
-
|
|
334
|
-
name: '
|
|
335
|
-
title: 'Home',
|
|
336
|
-
link: '/',
|
|
337
|
-
url: '/',
|
|
338
|
-
displayOrder: 1,
|
|
339
|
-
childItems: []
|
|
340
|
-
},
|
|
341
|
-
{
|
|
342
|
-
id: '2',
|
|
343
|
-
name: 'Products',
|
|
344
|
-
title: 'Products',
|
|
345
|
-
link: '/products',
|
|
346
|
-
url: '/products',
|
|
347
|
-
displayOrder: 2,
|
|
288
|
+
{ id: '1', name: 'Home', title: 'Home', link: '/', displayOrder: 1, childItems: [] },
|
|
289
|
+
{
|
|
290
|
+
id: '2', name: 'Products', title: 'Products', link: '/categories', displayOrder: 2,
|
|
348
291
|
childItems: [
|
|
349
|
-
{ id: '2-1', name: 'Electronics', title: 'Electronics', link: '/
|
|
350
|
-
{ id: '2-2', name: 'Clothing', title: 'Clothing', link: '/
|
|
351
|
-
{ id: '2-3', name: 'Accessories', title: 'Accessories', link: '/
|
|
352
|
-
{ id: '2-4', name: 'Home & Garden', title: 'Home & Garden', link: '/
|
|
292
|
+
{ id: '2-1', name: 'Electronics', title: 'Electronics', link: '/electronics', displayOrder: 1 },
|
|
293
|
+
{ id: '2-2', name: 'Clothing', title: 'Clothing', link: '/clothing', displayOrder: 2 },
|
|
294
|
+
{ id: '2-3', name: 'Accessories', title: 'Accessories', link: '/accessories', displayOrder: 3 },
|
|
295
|
+
{ id: '2-4', name: 'Home & Garden', title: 'Home & Garden', link: '/home-and-garden', displayOrder: 4 }
|
|
353
296
|
]
|
|
354
297
|
},
|
|
355
|
-
{
|
|
356
|
-
|
|
357
|
-
name: 'Collections',
|
|
358
|
-
title: 'Collections',
|
|
359
|
-
link: '/collections',
|
|
360
|
-
url: '/collections',
|
|
361
|
-
displayOrder: 3,
|
|
362
|
-
childItems: [
|
|
363
|
-
{ id: '3-1', name: 'New Arrivals', title: 'New Arrivals', link: '/collections/new-arrivals', url: '/collections/new-arrivals', displayOrder: 1 },
|
|
364
|
-
{ id: '3-2', name: 'Best Sellers', title: 'Best Sellers', link: '/collections/best-sellers', url: '/collections/best-sellers', displayOrder: 2 },
|
|
365
|
-
{ id: '3-3', name: 'On Sale', title: 'On Sale', link: '/collections/sale', url: '/collections/sale', displayOrder: 3 }
|
|
366
|
-
]
|
|
367
|
-
},
|
|
368
|
-
{
|
|
369
|
-
id: '4',
|
|
370
|
-
name: 'About',
|
|
371
|
-
title: 'About',
|
|
372
|
-
link: '/page/about',
|
|
373
|
-
url: '/page/about',
|
|
374
|
-
displayOrder: 4,
|
|
375
|
-
childItems: []
|
|
376
|
-
},
|
|
377
|
-
{
|
|
378
|
-
id: '5',
|
|
379
|
-
name: 'Contact',
|
|
380
|
-
title: 'Contact',
|
|
381
|
-
link: '/page/contact',
|
|
382
|
-
url: '/page/contact',
|
|
383
|
-
displayOrder: 5,
|
|
384
|
-
childItems: []
|
|
385
|
-
}
|
|
298
|
+
{ id: '3', name: 'About', title: 'About', link: '/about', displayOrder: 3, childItems: [] },
|
|
299
|
+
{ id: '4', name: 'Contact', title: 'Contact', link: '/contact', displayOrder: 4, childItems: [] }
|
|
386
300
|
]
|
|
387
301
|
},
|
|
388
302
|
'footer-menu': {
|