@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/dev-server.js
CHANGED
|
@@ -26,6 +26,7 @@ class DevServer {
|
|
|
26
26
|
this.mode = options.mode || 'mock'; // 'mock' or 'real'
|
|
27
27
|
this.mockApiPort = options.mockApiPort || 3001;
|
|
28
28
|
this.openBrowser = options.open !== false;
|
|
29
|
+
this.debug = options.debug || process.env.O2VEND_DEBUG === 'true' || process.env.DEBUG === 'true';
|
|
29
30
|
|
|
30
31
|
this.app = express();
|
|
31
32
|
this.server = null;
|
|
@@ -423,7 +424,7 @@ class DevServer {
|
|
|
423
424
|
// Log context data for debugging
|
|
424
425
|
const widgetSections = Object.keys(context.widgets || {});
|
|
425
426
|
const widgetCounts = widgetSections.map(s => `${s}:${context.widgets[s]?.length || 0}`).join(', ');
|
|
426
|
-
console.log(`[HOME] Products: ${context.products?.length || 0}, Widgets: [${widgetCounts}], Menus: ${context.menus
|
|
427
|
+
console.log(`[HOME] Products: ${context.products?.length || 0}, Widgets: [${widgetCounts}], Menus: ${Object.keys(context.menus || {}).length}`);
|
|
427
428
|
|
|
428
429
|
// Ensure products are available
|
|
429
430
|
if (!context.products || context.products.length === 0) {
|
|
@@ -453,18 +454,15 @@ class DevServer {
|
|
|
453
454
|
if (this.mode === 'real') {
|
|
454
455
|
const opts = { headers: req.headers.cookie ? { Cookie: req.headers.cookie } : {} };
|
|
455
456
|
context.widgets = await webstoreApi.fetchWidgetsBySections('home', webstoreApi.DEFAULT_SECTIONS, opts) || { hero: [], content: [], footer: [] };
|
|
456
|
-
console.log(`[HOME] Reloaded widgets
|
|
457
|
+
console.log(`[HOME] Reloaded widgets (real)`);
|
|
457
458
|
} else if (this.widgetService) {
|
|
458
|
-
|
|
459
|
-
context.widgets = widgets || { hero: [], content: [], footer: [] };
|
|
460
|
-
console.log(`[HOME] Reloaded widgets: ${Object.keys(context.widgets).join(', ')}`);
|
|
459
|
+
context.widgets = await this.widgetService.fetchPageWidgets('home');
|
|
461
460
|
} else {
|
|
462
|
-
|
|
463
|
-
context.widgets = { hero: [], content: [], footer: [] };
|
|
461
|
+
context.widgets = { header: [], hero: [], content: [], footer: [] };
|
|
464
462
|
}
|
|
465
463
|
} catch (error) {
|
|
466
464
|
console.error('[HOME] Failed to reload widgets:', error.message);
|
|
467
|
-
context.widgets = { hero: [], content: [], footer: [] };
|
|
465
|
+
context.widgets = { header: [], hero: [], content: [], footer: [] };
|
|
468
466
|
}
|
|
469
467
|
}
|
|
470
468
|
|
|
@@ -541,7 +539,7 @@ class DevServer {
|
|
|
541
539
|
price: sample.price || sample.sellingPrice,
|
|
542
540
|
stock: sample.stock,
|
|
543
541
|
inStock: sample.inStock,
|
|
544
|
-
hasImage: !!(sample.imageUrl || sample.
|
|
542
|
+
hasImage: !!(sample.imageUrl || sample.thumbnailImage)
|
|
545
543
|
});
|
|
546
544
|
}
|
|
547
545
|
|
|
@@ -550,7 +548,7 @@ class DevServer {
|
|
|
550
548
|
console.log(` - Products count: ${context.products?.length || 0}`);
|
|
551
549
|
console.log(` - Widgets sections: ${Object.keys(context.widgets || {}).join(', ') || 'none'}`);
|
|
552
550
|
console.log(` - Widgets counts: ${Object.keys(context.widgets || {}).map(s => `${s}:${context.widgets[s]?.length || 0}`).join(', ') || 'none'}`);
|
|
553
|
-
console.log(` - Menus count: ${context.menus
|
|
551
|
+
console.log(` - Menus count: ${Object.keys(context.menus || {}).length}`);
|
|
554
552
|
console.log(` - Categories count: ${context.categories?.length || 0}`);
|
|
555
553
|
console.log(` - Brands count: ${context.brands?.length || 0}`);
|
|
556
554
|
|
|
@@ -597,7 +595,7 @@ class DevServer {
|
|
|
597
595
|
if (context.products.length > 0) {
|
|
598
596
|
const sampleProduct = context.products[0];
|
|
599
597
|
const hasStock = 'stock' in sampleProduct || 'quantity' in sampleProduct;
|
|
600
|
-
const hasImage = sampleProduct.
|
|
598
|
+
const hasImage = sampleProduct.thumbnailImage || sampleProduct.imageUrl || (sampleProduct.images && sampleProduct.images.length > 0);
|
|
601
599
|
const hasUrl = sampleProduct.url || sampleProduct.link;
|
|
602
600
|
// Use nullish coalescing to show 0 values correctly
|
|
603
601
|
const stockValue = sampleProduct.stock ?? sampleProduct.quantity ?? 'N/A';
|
|
@@ -612,46 +610,25 @@ class DevServer {
|
|
|
612
610
|
}
|
|
613
611
|
}
|
|
614
612
|
|
|
615
|
-
// Ensure widgets are available (they should be from buildContext, but double-check)
|
|
616
613
|
if (!context.widgets || Object.keys(context.widgets).length === 0) {
|
|
617
|
-
console.warn('[PRODUCTS PAGE]
|
|
614
|
+
console.warn('[PRODUCTS PAGE] No widgets in context, attempting reload...');
|
|
618
615
|
try {
|
|
619
616
|
if (this.mode === 'real') {
|
|
620
617
|
const opts = { headers: req.headers.cookie ? { Cookie: req.headers.cookie } : {} };
|
|
621
|
-
context.widgets = await webstoreApi.fetchWidgetsBySections('products', webstoreApi.DEFAULT_SECTIONS, opts) || {
|
|
622
|
-
console.log(`[PRODUCTS PAGE] ✅ Reloaded widgets: ${Object.keys(context.widgets).join(', ')} (real)`);
|
|
618
|
+
context.widgets = await webstoreApi.fetchWidgetsBySections('products', webstoreApi.DEFAULT_SECTIONS, opts) || { header: [], hero: [], content: [], footer: [] };
|
|
623
619
|
} else if (this.widgetService) {
|
|
624
|
-
|
|
625
|
-
context.widgets = widgets || { hero: [], content: [], footer: [], header: [] };
|
|
626
|
-
console.log(`[PRODUCTS PAGE] ✅ Reloaded widgets: ${Object.keys(context.widgets).join(', ')}`);
|
|
620
|
+
context.widgets = await this.widgetService.fetchPageWidgets('products');
|
|
627
621
|
} else {
|
|
628
|
-
context.widgets = {
|
|
622
|
+
context.widgets = { header: [], hero: [], content: [], footer: [] };
|
|
629
623
|
}
|
|
630
624
|
} catch (error) {
|
|
631
|
-
console.error('[PRODUCTS PAGE]
|
|
632
|
-
context.widgets = {
|
|
625
|
+
console.error('[PRODUCTS PAGE] Failed to reload widgets:', error.message);
|
|
626
|
+
context.widgets = { header: [], hero: [], content: [], footer: [] };
|
|
633
627
|
}
|
|
634
628
|
}
|
|
635
629
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
console.warn('[PRODUCTS PAGE] ⚠️ No menus in context, attempting to reload...');
|
|
639
|
-
try {
|
|
640
|
-
if (this.mode === 'real') {
|
|
641
|
-
context.menus = await webstoreApi.fetchMenus({ headers: req.headers.cookie ? { Cookie: req.headers.cookie } : {} });
|
|
642
|
-
context.menus = Array.isArray(context.menus) ? context.menus : [];
|
|
643
|
-
console.log(`[PRODUCTS PAGE] ✅ Reloaded ${context.menus.length} menus (real)`);
|
|
644
|
-
} else {
|
|
645
|
-
const axios = require('axios');
|
|
646
|
-
const menusResponse = await axios.get(`http://localhost:${this.mockApiPort}/webstoreapi/menus`);
|
|
647
|
-
const menusData = menusResponse.data;
|
|
648
|
-
context.menus = Array.isArray(menusData) ? menusData : (menusData.data?.menus || menusData.menus || []);
|
|
649
|
-
console.log(`[PRODUCTS PAGE] ✅ Reloaded ${context.menus.length} menus`);
|
|
650
|
-
}
|
|
651
|
-
} catch (error) {
|
|
652
|
-
console.error('[PRODUCTS PAGE] ❌ Failed to reload menus:', error.message);
|
|
653
|
-
context.menus = [];
|
|
654
|
-
}
|
|
630
|
+
if (!context.menus || Object.keys(context.menus).length === 0) {
|
|
631
|
+
console.warn('[PRODUCTS PAGE] No menus in context');
|
|
655
632
|
}
|
|
656
633
|
|
|
657
634
|
// Ensure collection.products is set for products template
|
|
@@ -690,9 +667,9 @@ class DevServer {
|
|
|
690
667
|
stockQuantity: sample.stockQuantity !== undefined ? sample.stockQuantity : 'MISSING',
|
|
691
668
|
inStock: sample.inStock !== undefined ? sample.inStock : 'MISSING',
|
|
692
669
|
available: sample.available !== undefined ? sample.available : 'MISSING',
|
|
693
|
-
hasImage: !!(sample.imageUrl || sample.
|
|
694
|
-
imageUrl: sample.imageUrl || sample.
|
|
695
|
-
|
|
670
|
+
hasImage: !!(sample.imageUrl || sample.thumbnailImage),
|
|
671
|
+
imageUrl: sample.imageUrl || sample.thumbnailImage?.url || 'MISSING',
|
|
672
|
+
hasThumbnailImage: !!sample.thumbnailImage,
|
|
696
673
|
hasImagesArray: !!(sample.images && sample.images.length > 0)
|
|
697
674
|
});
|
|
698
675
|
} else {
|
|
@@ -714,10 +691,10 @@ class DevServer {
|
|
|
714
691
|
title: firstProduct.title || firstProduct.name,
|
|
715
692
|
url: firstProduct.url || firstProduct.link,
|
|
716
693
|
price: firstProduct.price || firstProduct.sellingPrice,
|
|
717
|
-
|
|
694
|
+
stockQuantity: firstProduct.stockQuantity,
|
|
718
695
|
inStock: firstProduct.inStock,
|
|
719
|
-
hasImage: !!(firstProduct.imageUrl || firstProduct.
|
|
720
|
-
imageUrl: firstProduct.imageUrl || firstProduct.
|
|
696
|
+
hasImage: !!(firstProduct.imageUrl || firstProduct.thumbnailImage),
|
|
697
|
+
imageUrl: firstProduct.imageUrl || firstProduct.thumbnailImage
|
|
721
698
|
});
|
|
722
699
|
}
|
|
723
700
|
if (context.collection) {
|
|
@@ -869,31 +846,8 @@ class DevServer {
|
|
|
869
846
|
}
|
|
870
847
|
});
|
|
871
848
|
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
try {
|
|
875
|
-
const context = await this.buildContext(req, 'collections');
|
|
876
|
-
// Try categories.liquid first (as per webstore), then collections.liquid, then collection.liquid
|
|
877
|
-
let html;
|
|
878
|
-
try {
|
|
879
|
-
html = await renderWithLayout(this.liquid, 'templates/categories', context, this.themePath);
|
|
880
|
-
} catch (error) {
|
|
881
|
-
if (error.message && error.message.includes('Template not found')) {
|
|
882
|
-
try {
|
|
883
|
-
html = await renderWithLayout(this.liquid, 'templates/collections', context, this.themePath);
|
|
884
|
-
} catch (error2) {
|
|
885
|
-
html = await renderWithLayout(this.liquid, 'templates/collection', context, this.themePath);
|
|
886
|
-
}
|
|
887
|
-
} else {
|
|
888
|
-
throw error;
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
892
|
-
res.setHeader('Cache-Control', 'no-cache');
|
|
893
|
-
res.send(html);
|
|
894
|
-
} catch (error) {
|
|
895
|
-
next(error);
|
|
896
|
-
}
|
|
849
|
+
this.app.get('/collections', (req, res) => {
|
|
850
|
+
res.redirect(301, '/categories');
|
|
897
851
|
});
|
|
898
852
|
|
|
899
853
|
// Collection detail page - Try categories first, then collection
|
|
@@ -1209,6 +1163,55 @@ class DevServer {
|
|
|
1209
1163
|
}
|
|
1210
1164
|
});
|
|
1211
1165
|
|
|
1166
|
+
this.app.get('/dev/context', async (req, res, next) => {
|
|
1167
|
+
try {
|
|
1168
|
+
const pageType = req.query.page || 'home';
|
|
1169
|
+
const context = await this.buildContext(req, pageType);
|
|
1170
|
+
const debugOutput = {
|
|
1171
|
+
mode: this.mode,
|
|
1172
|
+
pageType,
|
|
1173
|
+
shopKeys: Object.keys(context.shop || {}),
|
|
1174
|
+
shop: context.shop,
|
|
1175
|
+
store: context.store,
|
|
1176
|
+
menusKeys: Object.keys(context.menus || {}),
|
|
1177
|
+
mainMenu: context.mainMenu ? { name: context.mainMenu.name, itemCount: context.mainMenu.items?.length } : null,
|
|
1178
|
+
widgetSections: Object.fromEntries(
|
|
1179
|
+
Object.entries(context.widgets || {}).map(([k, v]) =>
|
|
1180
|
+
[k, Array.isArray(v) ? v.map(w => ({ type: w.type, id: w.id, template_path: w.template_path })) : v]
|
|
1181
|
+
)
|
|
1182
|
+
),
|
|
1183
|
+
productsCount: (context.products || []).length,
|
|
1184
|
+
categoriesCount: (context.categories || []).length,
|
|
1185
|
+
brandsCount: (context.brands || []).length,
|
|
1186
|
+
cart: context.cart,
|
|
1187
|
+
customer: context.customer,
|
|
1188
|
+
request: context.request,
|
|
1189
|
+
deliveryZone: context.deliveryZone,
|
|
1190
|
+
analytics: context.analytics,
|
|
1191
|
+
settingsKeys: Object.keys(context.settings || {}),
|
|
1192
|
+
sampleProduct: (context.products || [])[0] ? {
|
|
1193
|
+
id: context.products[0].id,
|
|
1194
|
+
slug: context.products[0].slug,
|
|
1195
|
+
url: context.products[0].url,
|
|
1196
|
+
stockQuantity: context.products[0].stockQuantity,
|
|
1197
|
+
inStock: context.products[0].inStock,
|
|
1198
|
+
available: context.products[0].available,
|
|
1199
|
+
prices: context.products[0].prices,
|
|
1200
|
+
thumbnailImage: context.products[0].thumbnailImage
|
|
1201
|
+
} : null,
|
|
1202
|
+
sampleCategory: (context.categories || [])[0] ? {
|
|
1203
|
+
id: context.categories[0].id,
|
|
1204
|
+
slug: context.categories[0].slug,
|
|
1205
|
+
handle: context.categories[0].handle,
|
|
1206
|
+
thumbnailImage: context.categories[0].thumbnailImage
|
|
1207
|
+
} : null
|
|
1208
|
+
};
|
|
1209
|
+
res.json(debugOutput);
|
|
1210
|
+
} catch (error) {
|
|
1211
|
+
next(error);
|
|
1212
|
+
}
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1212
1215
|
// Development Dashboard - List all theme components
|
|
1213
1216
|
this.app.get('/dev', async (req, res, next) => {
|
|
1214
1217
|
try {
|
|
@@ -1447,8 +1450,8 @@ class DevServer {
|
|
|
1447
1450
|
price: product.price || product.sellingPrice,
|
|
1448
1451
|
stock: product.stock,
|
|
1449
1452
|
inStock: product.inStock,
|
|
1450
|
-
hasImage: !!(product.imageUrl || product.
|
|
1451
|
-
imageUrl: product.imageUrl || product.
|
|
1453
|
+
hasImage: !!(product.imageUrl || product.thumbnailImage),
|
|
1454
|
+
imageUrl: product.imageUrl || product.thumbnailImage?.url
|
|
1452
1455
|
});
|
|
1453
1456
|
});
|
|
1454
1457
|
}
|
|
@@ -1477,8 +1480,8 @@ class DevServer {
|
|
|
1477
1480
|
price: sample.price || sample.sellingPrice,
|
|
1478
1481
|
stock: sample.stock,
|
|
1479
1482
|
inStock: sample.inStock,
|
|
1480
|
-
hasImage: !!(sample.imageUrl || sample.
|
|
1481
|
-
imageUrl: sample.imageUrl || sample.
|
|
1483
|
+
hasImage: !!(sample.imageUrl || sample.thumbnailImage),
|
|
1484
|
+
imageUrl: sample.imageUrl || sample.thumbnailImage?.url,
|
|
1482
1485
|
allKeys: Object.keys(sample).slice(0, 20)
|
|
1483
1486
|
});
|
|
1484
1487
|
} else {
|
|
@@ -1503,9 +1506,9 @@ class DevServer {
|
|
|
1503
1506
|
title: firstProduct.title || firstProduct.name,
|
|
1504
1507
|
url: firstProduct.url || firstProduct.link,
|
|
1505
1508
|
price: firstProduct.price || firstProduct.sellingPrice,
|
|
1506
|
-
|
|
1509
|
+
stockQuantity: firstProduct.stockQuantity,
|
|
1507
1510
|
inStock: firstProduct.inStock,
|
|
1508
|
-
hasImage: !!(firstProduct.imageUrl || firstProduct.
|
|
1511
|
+
hasImage: !!(firstProduct.imageUrl || firstProduct.thumbnailImage)
|
|
1509
1512
|
});
|
|
1510
1513
|
}
|
|
1511
1514
|
|
|
@@ -1601,10 +1604,31 @@ class DevServer {
|
|
|
1601
1604
|
}
|
|
1602
1605
|
}
|
|
1603
1606
|
|
|
1604
|
-
// Not
|
|
1605
|
-
|
|
1607
|
+
// Not a category, brand, or product -- try as a CMS page
|
|
1608
|
+
{
|
|
1609
|
+
const pageContext = await this.buildContext(req, 'page', { pageHandle: handle });
|
|
1610
|
+
pageContext.page = { title: handle.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()), handle: handle };
|
|
1611
|
+
let html;
|
|
1612
|
+
const pageTemplates = ['templates/page', 'templates/pages'];
|
|
1613
|
+
for (const template of pageTemplates) {
|
|
1614
|
+
try {
|
|
1615
|
+
html = await renderWithLayout(this.liquid, template, pageContext, this.themePath);
|
|
1616
|
+
break;
|
|
1617
|
+
} catch (error) {
|
|
1618
|
+
if (!error.message?.includes('Template not found')) throw error;
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
if (html) {
|
|
1622
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1623
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
1624
|
+
return res.send(html);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
// Not found - 404
|
|
1629
|
+
res.status(404).send(`<!DOCTYPE html><html><head><title>Page Not Found</title></head><body><h1>Page Not Found</h1><p>The page "${handle}" was not found.</p><p><a href="/">Back to Home</a></p></body></html>`);
|
|
1606
1630
|
} catch (error) {
|
|
1607
|
-
|
|
1631
|
+
console.error(`[CATCH-ALL] Error handling /${req.params?.handle}:`, error.message);
|
|
1608
1632
|
next(error);
|
|
1609
1633
|
}
|
|
1610
1634
|
});
|
|
@@ -1688,13 +1712,10 @@ class DevServer {
|
|
|
1688
1712
|
* - If template expects 'quantity' instead of 'stock', consider adding both fields
|
|
1689
1713
|
*
|
|
1690
1714
|
* 2. WIDGETS LOADING:
|
|
1691
|
-
* - Widgets are loaded via widgetService.
|
|
1715
|
+
* - Widgets are loaded via widgetService.fetchPageWidgets(pageId)
|
|
1692
1716
|
* - Widgets are organized by section (header, hero, content, footer)
|
|
1693
|
-
* - Widgets
|
|
1694
|
-
* -
|
|
1695
|
-
* a) widgetService is initialized (should be created in setupServices)
|
|
1696
|
-
* b) Mock API is returning widgets correctly
|
|
1697
|
-
* c) Widget service is properly organizing widgets by section
|
|
1717
|
+
* - Widgets are filtered by pageId to match production behavior
|
|
1718
|
+
* - pageId is derived from pageType: home->home, product->product, collection->category, etc.
|
|
1698
1719
|
*
|
|
1699
1720
|
* 3. MENUS LOADING:
|
|
1700
1721
|
* - Menus are loaded from /webstoreapi/menus endpoint
|
|
@@ -1709,8 +1730,10 @@ class DevServer {
|
|
|
1709
1730
|
* @returns {Promise<Object>} Template context
|
|
1710
1731
|
*/
|
|
1711
1732
|
async buildContext(req, pageType, extra = {}) {
|
|
1733
|
+
const hostname = `${this.host}:${this.port}`;
|
|
1712
1734
|
const context = {
|
|
1713
1735
|
shop: {},
|
|
1736
|
+
store: {},
|
|
1714
1737
|
tenant: {},
|
|
1715
1738
|
page: {
|
|
1716
1739
|
type: pageType,
|
|
@@ -1720,29 +1743,40 @@ class DevServer {
|
|
|
1720
1743
|
collections: [],
|
|
1721
1744
|
categories: [],
|
|
1722
1745
|
brands: [],
|
|
1723
|
-
cart: {
|
|
1746
|
+
cart: {
|
|
1724
1747
|
id: 'mock-cart-1',
|
|
1725
|
-
items: [],
|
|
1726
|
-
total: 0,
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
shipping: 0
|
|
1732
|
-
},
|
|
1733
|
-
// Mock authenticated customer for dev mode (allows add-to-cart without login)
|
|
1734
|
-
customer: {
|
|
1735
|
-
id: 'mock-customer-1',
|
|
1736
|
-
isAuthenticated: true,
|
|
1737
|
-
firstName: 'Test',
|
|
1738
|
-
lastName: 'User',
|
|
1739
|
-
email: 'test@example.com',
|
|
1740
|
-
phone: '+1234567890',
|
|
1741
|
-
name: 'Test User'
|
|
1748
|
+
items: [],
|
|
1749
|
+
total: 0,
|
|
1750
|
+
subTotal: 0,
|
|
1751
|
+
taxAmount: 0,
|
|
1752
|
+
shippingAmount: 0,
|
|
1753
|
+
itemCount: 0
|
|
1742
1754
|
},
|
|
1755
|
+
customer: null,
|
|
1743
1756
|
widgets: {},
|
|
1744
1757
|
settings: {},
|
|
1745
|
-
menus:
|
|
1758
|
+
menus: {},
|
|
1759
|
+
mainMenu: null,
|
|
1760
|
+
request: {
|
|
1761
|
+
path: req.path,
|
|
1762
|
+
url: req.url,
|
|
1763
|
+
host: hostname
|
|
1764
|
+
},
|
|
1765
|
+
deliveryZone: {
|
|
1766
|
+
enabled: false,
|
|
1767
|
+
mode: 0,
|
|
1768
|
+
current: null,
|
|
1769
|
+
zipcodeSearchEnabled: false,
|
|
1770
|
+
defaultZipcode: null,
|
|
1771
|
+
showModal: false
|
|
1772
|
+
},
|
|
1773
|
+
analytics: {
|
|
1774
|
+
enabled: false,
|
|
1775
|
+
baseUrl: null,
|
|
1776
|
+
siteId: null,
|
|
1777
|
+
visitorId: null
|
|
1778
|
+
},
|
|
1779
|
+
content_for_header: '',
|
|
1746
1780
|
...extra
|
|
1747
1781
|
};
|
|
1748
1782
|
|
|
@@ -1770,63 +1804,54 @@ class DevServer {
|
|
|
1770
1804
|
// Use mock API (API client points to mock API via proxy)
|
|
1771
1805
|
try {
|
|
1772
1806
|
const storeInfo = await this.apiClient.getStoreInfo(true);
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1807
|
+
const si = storeInfo || {};
|
|
1808
|
+
const storeSettings = si.settings || {};
|
|
1809
|
+
context.shop = {
|
|
1810
|
+
name: si.name || 'My Store',
|
|
1811
|
+
domain: si.domain || hostname,
|
|
1812
|
+
currency: storeSettings.currency || 'USD',
|
|
1813
|
+
currencySymbol: storeSettings.currencySymbol || '$',
|
|
1814
|
+
timezone: storeSettings.timezone || si.timezone || 'America/New_York',
|
|
1815
|
+
language: storeSettings.language || si.language || 'en',
|
|
1816
|
+
logo: si.logoUrl || storeSettings.logo || '/assets/logo.png',
|
|
1817
|
+
favicon: si.favouriteIconUrl || storeSettings.favicon || '/favicon.ico',
|
|
1818
|
+
id: si.id || 1,
|
|
1819
|
+
identifier: si.identifier || 'store',
|
|
1820
|
+
companyName: si.companyName || si.name || 'My Store',
|
|
1821
|
+
companyAddress: si.companyAddress || '',
|
|
1822
|
+
companyPhoneNumber: si.companyPhoneNumber || si.phone || '',
|
|
1823
|
+
companyEmail: si.companyEmail || si.email || '',
|
|
1824
|
+
settings: storeSettings
|
|
1825
|
+
};
|
|
1826
|
+
context.store = {
|
|
1827
|
+
id: context.shop.id,
|
|
1828
|
+
identifier: context.shop.identifier,
|
|
1829
|
+
name: context.shop.name,
|
|
1830
|
+
settings: storeSettings,
|
|
1831
|
+
companyName: context.shop.companyName,
|
|
1832
|
+
companyAddress: context.shop.companyAddress,
|
|
1833
|
+
companyPhoneNumber: context.shop.companyPhoneNumber,
|
|
1834
|
+
companyEmail: context.shop.companyEmail
|
|
1835
|
+
};
|
|
1836
|
+
context.tenant = {
|
|
1837
|
+
id: si.id || 1,
|
|
1838
|
+
name: context.shop.name,
|
|
1839
|
+
settings: storeSettings
|
|
1778
1840
|
};
|
|
1779
1841
|
|
|
1780
|
-
//
|
|
1781
|
-
|
|
1782
|
-
|
|
1842
|
+
// Load widgets filtered by page type (matches production behavior)
|
|
1843
|
+
const pageIdMap = { home: 'home', product: 'product', collection: 'category', category: 'category', products: 'products', categories: 'categories', brand: 'brand', checkout: 'checkout', page: 'home' };
|
|
1844
|
+
const widgetPageId = pageIdMap[pageType] || 'home';
|
|
1783
1845
|
if (this.widgetService) {
|
|
1784
1846
|
try {
|
|
1785
|
-
console.log(`[CONTEXT] Loading widgets for page type: ${pageType}`);
|
|
1786
|
-
|
|
1787
|
-
context.widgets = widgets || {
|
|
1788
|
-
hero: [],
|
|
1789
|
-
products: [],
|
|
1790
|
-
footer: [],
|
|
1791
|
-
content: [],
|
|
1792
|
-
header: []
|
|
1793
|
-
};
|
|
1794
|
-
// Log widget counts by section
|
|
1795
|
-
const widgetCounts = Object.keys(context.widgets).map(section =>
|
|
1796
|
-
`${section}: ${context.widgets[section]?.length || 0}`
|
|
1797
|
-
).join(', ');
|
|
1798
|
-
console.log(`[CONTEXT] ✅ Widgets loaded: ${widgetCounts}`);
|
|
1799
|
-
|
|
1800
|
-
// DEBUG: Log detailed widget information
|
|
1801
|
-
Object.keys(context.widgets).forEach(section => {
|
|
1802
|
-
if (context.widgets[section] && context.widgets[section].length > 0) {
|
|
1803
|
-
console.log(`[CONTEXT DEBUG] Section '${section}' has ${context.widgets[section].length} widget(s):`);
|
|
1804
|
-
context.widgets[section].slice(0, 3).forEach((w, idx) => {
|
|
1805
|
-
console.log(` - Widget ${idx + 1}: ${w.type || 'Unknown'} (ID: ${w.id || 'no-id'})`);
|
|
1806
|
-
});
|
|
1807
|
-
}
|
|
1808
|
-
});
|
|
1847
|
+
console.log(`[CONTEXT] Loading widgets for page type: ${pageType} (pageId: ${widgetPageId})`);
|
|
1848
|
+
context.widgets = await this.widgetService.fetchPageWidgets(widgetPageId);
|
|
1809
1849
|
} catch (error) {
|
|
1810
|
-
console.error('[CONTEXT]
|
|
1811
|
-
|
|
1812
|
-
context.widgets = {
|
|
1813
|
-
hero: [],
|
|
1814
|
-
products: [],
|
|
1815
|
-
footer: [],
|
|
1816
|
-
content: [],
|
|
1817
|
-
header: []
|
|
1818
|
-
};
|
|
1850
|
+
console.error('[CONTEXT] Failed to load widgets:', error.message);
|
|
1851
|
+
context.widgets = { header: [], hero: [], content: [], footer: [] };
|
|
1819
1852
|
}
|
|
1820
1853
|
} else {
|
|
1821
|
-
|
|
1822
|
-
console.warn('[CONTEXT] This may happen if widgetService was not initialized properly');
|
|
1823
|
-
context.widgets = {
|
|
1824
|
-
hero: [],
|
|
1825
|
-
products: [],
|
|
1826
|
-
footer: [],
|
|
1827
|
-
content: [],
|
|
1828
|
-
header: []
|
|
1829
|
-
};
|
|
1854
|
+
context.widgets = { header: [], hero: [], content: [], footer: [] };
|
|
1830
1855
|
}
|
|
1831
1856
|
|
|
1832
1857
|
// Always load products, categories, brands, menus, and cart for navigation and widgets
|
|
@@ -1853,32 +1878,17 @@ class DevServer {
|
|
|
1853
1878
|
stockQuantity: product.stockQuantity !== undefined ? product.stockQuantity : 'MISSING',
|
|
1854
1879
|
inStock: product.inStock !== undefined ? product.inStock : 'MISSING',
|
|
1855
1880
|
available: product.available !== undefined ? product.available : 'MISSING',
|
|
1856
|
-
hasImage: !!(product.imageUrl || product.
|
|
1857
|
-
imageUrl: product.imageUrl || product.
|
|
1881
|
+
hasImage: !!(product.imageUrl || product.thumbnailImage),
|
|
1882
|
+
imageUrl: product.imageUrl || product.thumbnailImage?.url || 'MISSING'
|
|
1858
1883
|
});
|
|
1859
1884
|
});
|
|
1860
1885
|
}
|
|
1861
1886
|
|
|
1862
1887
|
console.log(`[CONTEXT] Loaded ${context.products.length} products`);
|
|
1863
1888
|
|
|
1864
|
-
// DEBUG: Verify products have quantity/stock field
|
|
1865
1889
|
if (context.products.length > 0) {
|
|
1866
|
-
const
|
|
1867
|
-
|
|
1868
|
-
// Use nullish coalescing to show 0 values correctly (0 || 'N/A' would show N/A incorrectly)
|
|
1869
|
-
const stockValue = firstProduct.stock ?? firstProduct.quantity ?? 'N/A';
|
|
1870
|
-
console.log(`[CONTEXT DEBUG] First product sample - Has stock field: ${hasStock}, Stock value: ${stockValue}`);
|
|
1871
|
-
console.log(`[CONTEXT DEBUG] First product keys: ${Object.keys(firstProduct).join(', ')}`);
|
|
1872
|
-
console.log(`[CONTEXT DEBUG] First product stock details: stock=${firstProduct.stock}, quantity=${firstProduct.quantity}, inStock=${firstProduct.inStock}`);
|
|
1873
|
-
|
|
1874
|
-
// Check if variants have stock
|
|
1875
|
-
if (firstProduct.variants && firstProduct.variants.length > 0) {
|
|
1876
|
-
const firstVariant = firstProduct.variants[0];
|
|
1877
|
-
const variantHasStock = 'stock' in firstVariant || 'quantity' in firstVariant;
|
|
1878
|
-
const variantStockValue = firstVariant.stock ?? firstVariant.quantity ?? 'N/A';
|
|
1879
|
-
console.log(`[CONTEXT DEBUG] First variant sample - Has stock field: ${variantHasStock}, Stock value: ${variantStockValue}`);
|
|
1880
|
-
console.log(`[CONTEXT DEBUG] First variant stock details: stock=${firstVariant.stock}, quantity=${firstVariant.quantity}, inStock=${firstVariant.inStock}, available=${firstVariant.available}`);
|
|
1881
|
-
}
|
|
1890
|
+
const fp = context.products[0];
|
|
1891
|
+
console.log(`[CONTEXT] First product: id=${fp.id}, slug=${fp.slug}, stockQuantity=${fp.stockQuantity}, inStock=${fp.inStock}`);
|
|
1882
1892
|
}
|
|
1883
1893
|
|
|
1884
1894
|
// Load categories for navigation
|
|
@@ -1886,13 +1896,12 @@ class DevServer {
|
|
|
1886
1896
|
const categoriesResponse = await this.apiClient.getCategories({ limit: 50, offset: 0 });
|
|
1887
1897
|
context.categories = categoriesResponse.categories || categoriesResponse.data?.categories || categoriesResponse || [];
|
|
1888
1898
|
|
|
1889
|
-
// Ensure all categories have proper URL fields for navigation
|
|
1890
1899
|
context.categories = context.categories.map(category => {
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
category.
|
|
1900
|
+
if (!category.slug) {
|
|
1901
|
+
category.slug = category.handle || category.name?.toLowerCase().replace(/\s+/g, '-') || String(category.id);
|
|
1902
|
+
}
|
|
1903
|
+
if (!category.handle) {
|
|
1904
|
+
category.handle = category.slug;
|
|
1896
1905
|
}
|
|
1897
1906
|
return category;
|
|
1898
1907
|
});
|
|
@@ -1915,39 +1924,41 @@ class DevServer {
|
|
|
1915
1924
|
context.brands = [];
|
|
1916
1925
|
}
|
|
1917
1926
|
|
|
1918
|
-
// Load menus (via webstoreapi) - use axios from apiClient
|
|
1919
|
-
// NOTE: Menus are loaded for navigation in header/footer sections
|
|
1920
|
-
// They should be available in context for all page types
|
|
1921
1927
|
try {
|
|
1922
1928
|
const axios = require('axios');
|
|
1923
|
-
console.log(`[CONTEXT] Loading menus from http://localhost:${this.mockApiPort}/webstoreapi/menus`);
|
|
1924
1929
|
const menusResponse = await axios.get(`http://localhost:${this.mockApiPort}/webstoreapi/menus`);
|
|
1925
|
-
const
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1930
|
+
const rawMenus = menusResponse.data;
|
|
1931
|
+
const allMenus = Array.isArray(rawMenus) ? rawMenus : (rawMenus.data?.menus || rawMenus.menus || []);
|
|
1932
|
+
|
|
1933
|
+
const menusData = {};
|
|
1934
|
+
allMenus.forEach(menu => {
|
|
1935
|
+
if (menu && menu.name) {
|
|
1936
|
+
const normalizedName = menu.name.toLowerCase()
|
|
1937
|
+
.replace(/\s+menu$/i, '')
|
|
1938
|
+
.replace(/\s+/g, '_')
|
|
1939
|
+
.replace(/[^a-z0-9_]/g, '');
|
|
1940
|
+
menusData[normalizedName] = menu;
|
|
1941
|
+
}
|
|
1942
|
+
});
|
|
1943
|
+
menusData.all = allMenus;
|
|
1944
|
+
context.menus = menusData;
|
|
1945
|
+
|
|
1946
|
+
const mainMenuData = allMenus.find(menu => menu && menu.type === 'Main Menu');
|
|
1947
|
+
if (mainMenuData) {
|
|
1948
|
+
if (mainMenuData.items && Array.isArray(mainMenuData.items)) {
|
|
1949
|
+
mainMenuData.items.sort((a, b) => (a?.displayOrder || 0) - (b?.displayOrder || 0));
|
|
1950
|
+
}
|
|
1951
|
+
context.mainMenu = mainMenuData;
|
|
1952
|
+
} else if (allMenus.length > 0) {
|
|
1953
|
+
context.mainMenu = allMenus[0];
|
|
1937
1954
|
}
|
|
1938
|
-
} catch (error) {
|
|
1939
|
-
console.error('[CONTEXT] ❌ Failed to load menus:', error.message);
|
|
1940
|
-
console.error('[CONTEXT] Error details:', error.response?.status, error.response?.data);
|
|
1941
|
-
context.menus = [];
|
|
1942
|
-
}
|
|
1943
1955
|
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
mainMenu =
|
|
1956
|
+
console.log(`[CONTEXT] Loaded ${allMenus.length} menus`);
|
|
1957
|
+
} catch (error) {
|
|
1958
|
+
console.error('[CONTEXT] Failed to load menus:', error.message);
|
|
1959
|
+
context.menus = {};
|
|
1960
|
+
context.mainMenu = null;
|
|
1949
1961
|
}
|
|
1950
|
-
context.mainMenu = mainMenu;
|
|
1951
1962
|
|
|
1952
1963
|
// Load cart data
|
|
1953
1964
|
try {
|
|
@@ -2030,18 +2041,27 @@ class DevServer {
|
|
|
2030
2041
|
}
|
|
2031
2042
|
} catch (error) {
|
|
2032
2043
|
console.warn('[CONTEXT] Failed to load from mock API:', error.message);
|
|
2033
|
-
// Fallback to default values
|
|
2034
2044
|
context.shop = {
|
|
2035
2045
|
name: 'My Store',
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2046
|
+
domain: hostname,
|
|
2047
|
+
currency: 'USD',
|
|
2048
|
+
timezone: 'America/New_York',
|
|
2049
|
+
language: 'en',
|
|
2050
|
+
logo: '/assets/logo.png',
|
|
2051
|
+
favicon: '/favicon.ico',
|
|
2052
|
+
id: 1,
|
|
2053
|
+
identifier: 'store',
|
|
2054
|
+
companyName: 'My Store',
|
|
2055
|
+
companyAddress: '',
|
|
2056
|
+
companyPhoneNumber: '',
|
|
2057
|
+
companyEmail: '',
|
|
2058
|
+
settings: {}
|
|
2039
2059
|
};
|
|
2040
2060
|
context.widgets = {
|
|
2041
2061
|
hero: [],
|
|
2042
|
-
|
|
2062
|
+
content: [],
|
|
2043
2063
|
footer: [],
|
|
2044
|
-
|
|
2064
|
+
header: []
|
|
2045
2065
|
};
|
|
2046
2066
|
}
|
|
2047
2067
|
} else if (this.mode === 'real') {
|
|
@@ -2063,21 +2083,35 @@ class DevServer {
|
|
|
2063
2083
|
context.shop = {
|
|
2064
2084
|
name: storeName,
|
|
2065
2085
|
domain: baseHost,
|
|
2066
|
-
description: 'O2VEND Store',
|
|
2067
|
-
email: storeSettings.companyEmail || 'store@example.com',
|
|
2068
|
-
logo,
|
|
2069
|
-
favicon,
|
|
2070
2086
|
currency: storeSettings.currency || themeSettings.currency || 'USD',
|
|
2087
|
+
currencySymbol: storeSettings.currencySymbol || '$',
|
|
2088
|
+
timezone: storeSettings.timezone || 'America/New_York',
|
|
2071
2089
|
language: storeSettings.language || themeSettings.language || 'en',
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2090
|
+
logo,
|
|
2091
|
+
favicon,
|
|
2092
|
+
id: storeInfo?.id || process.env.O2VEND_TENANT_ID || 1,
|
|
2093
|
+
identifier: storeInfo?.identifier || 'store',
|
|
2094
|
+
companyName: storeSettings.companyName || storeName,
|
|
2095
|
+
companyAddress: storeSettings.companyAddress || '',
|
|
2096
|
+
companyPhoneNumber: storeSettings.companyPhoneNumber || '',
|
|
2097
|
+
companyEmail: storeSettings.companyEmail || '',
|
|
2098
|
+
settings: storeSettings
|
|
2099
|
+
};
|
|
2100
|
+
context.store = {
|
|
2101
|
+
id: context.shop.id,
|
|
2102
|
+
identifier: context.shop.identifier,
|
|
2103
|
+
name: context.shop.name,
|
|
2104
|
+
settings: storeSettings,
|
|
2105
|
+
companyName: context.shop.companyName,
|
|
2106
|
+
companyAddress: context.shop.companyAddress,
|
|
2107
|
+
companyPhoneNumber: context.shop.companyPhoneNumber,
|
|
2108
|
+
companyEmail: context.shop.companyEmail
|
|
2109
|
+
};
|
|
2110
|
+
context.tenant = {
|
|
2111
|
+
id: process.env.O2VEND_TENANT_ID || 1,
|
|
2112
|
+
name: storeName,
|
|
2113
|
+
settings: storeSettings
|
|
2079
2114
|
};
|
|
2080
|
-
context.tenant = { id: process.env.O2VEND_TENANT_ID || 'real' };
|
|
2081
2115
|
|
|
2082
2116
|
if (process.env.O2VEND_LAYOUT_FULL === '1' || process.env.O2VEND_LAYOUT_FULL === 'true') {
|
|
2083
2117
|
context.settings = context.settings || {};
|
|
@@ -2111,12 +2145,10 @@ class DevServer {
|
|
|
2111
2145
|
const catRes = await webstoreApi.fetchCategories({ limit: 50, offset: 0 }, opts);
|
|
2112
2146
|
const raw = catRes?.categories || catRes?.data?.categories || catRes || [];
|
|
2113
2147
|
context.categories = raw.map((c) => {
|
|
2114
|
-
const h = (c.handle || (c.name || '').toLowerCase().replace(/\s+/g, '-') || c.id || '').toString().
|
|
2148
|
+
const h = (c.handle || (c.name || '').toLowerCase().replace(/\s+/g, '-') || c.id || '').toString().trim() || null;
|
|
2115
2149
|
if (h) {
|
|
2116
2150
|
c.handle = c.handle || h;
|
|
2117
2151
|
c.slug = c.slug || h;
|
|
2118
|
-
c.url = `/${h}`;
|
|
2119
|
-
c.link = `/${h}`;
|
|
2120
2152
|
}
|
|
2121
2153
|
return c;
|
|
2122
2154
|
});
|
|
@@ -2134,23 +2166,36 @@ class DevServer {
|
|
|
2134
2166
|
}
|
|
2135
2167
|
|
|
2136
2168
|
try {
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2169
|
+
const allMenus = await webstoreApi.fetchMenus(opts);
|
|
2170
|
+
const menusList = Array.isArray(allMenus) ? allMenus : [];
|
|
2171
|
+
const menusData = {};
|
|
2172
|
+
menusList.forEach(menu => {
|
|
2173
|
+
if (menu && menu.name) {
|
|
2174
|
+
const normalizedName = menu.name.toLowerCase()
|
|
2175
|
+
.replace(/\s+menu$/i, '')
|
|
2176
|
+
.replace(/\s+/g, '_')
|
|
2177
|
+
.replace(/[^a-z0-9_]/g, '');
|
|
2178
|
+
menusData[normalizedName] = menu;
|
|
2179
|
+
}
|
|
2180
|
+
});
|
|
2181
|
+
menusData.all = menusList;
|
|
2182
|
+
context.menus = menusData;
|
|
2183
|
+
|
|
2184
|
+
const mainMenuData = menusList.find(m => m && m.type === 'Main Menu');
|
|
2185
|
+
const candidate = mainMenuData || menusList[0];
|
|
2186
|
+
if (candidate && candidate.id) {
|
|
2187
|
+
try {
|
|
2188
|
+
const fullMenu = await webstoreApi.fetchMenuById(candidate.id, opts);
|
|
2189
|
+
if (fullMenu && fullMenu.items && Array.isArray(fullMenu.items)) {
|
|
2190
|
+
fullMenu.items.sort((a, b) => (a?.displayOrder || 0) - (b?.displayOrder || 0));
|
|
2148
2191
|
}
|
|
2192
|
+
context.mainMenu = fullMenu || candidate;
|
|
2193
|
+
} catch (e) {
|
|
2194
|
+
context.mainMenu = candidate;
|
|
2149
2195
|
}
|
|
2150
2196
|
}
|
|
2151
|
-
context.mainMenu = mainMenu;
|
|
2152
2197
|
} catch (e) {
|
|
2153
|
-
context.menus =
|
|
2198
|
+
context.menus = {};
|
|
2154
2199
|
context.mainMenu = null;
|
|
2155
2200
|
}
|
|
2156
2201
|
|
|
@@ -2335,199 +2380,81 @@ class DevServer {
|
|
|
2335
2380
|
* @returns {Object} Enriched product (creates a copy to avoid mutation)
|
|
2336
2381
|
*/
|
|
2337
2382
|
enrichProductData(product) {
|
|
2338
|
-
// Create a copy to avoid mutating the original
|
|
2339
2383
|
const enriched = { ...product };
|
|
2340
|
-
|
|
2341
|
-
// CRITICAL: Ensure productId exists - it's the most reliable identifier
|
|
2384
|
+
|
|
2342
2385
|
if (!enriched.productId && enriched.id) {
|
|
2343
|
-
// Try to extract numeric ID from string ID (e.g., "product-1" -> 1)
|
|
2344
2386
|
const numericMatch = String(enriched.id).match(/\d+/);
|
|
2345
|
-
|
|
2346
|
-
enriched.productId = parseInt(numericMatch[0], 10);
|
|
2347
|
-
} else {
|
|
2348
|
-
// Use the ID as-is if it's already numeric
|
|
2349
|
-
enriched.productId = enriched.id;
|
|
2350
|
-
}
|
|
2351
|
-
} else if (!enriched.productId && !enriched.id) {
|
|
2352
|
-
console.warn(`[ENRICH] ⚠️ Product missing both id and productId:`, {
|
|
2353
|
-
keys: Object.keys(enriched).slice(0, 15),
|
|
2354
|
-
title: enriched.title || enriched.name
|
|
2355
|
-
});
|
|
2387
|
+
enriched.productId = numericMatch ? parseInt(numericMatch[0], 10) : enriched.id;
|
|
2356
2388
|
}
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
if (!product.id && !product.handle && !product.slug && !product.productId) {
|
|
2360
|
-
console.warn(`[ENRICH] ⚠️ Product missing all identifiers (id/handle/slug/productId):`, {
|
|
2361
|
-
keys: Object.keys(product).slice(0, 15),
|
|
2362
|
-
title: product.title || product.name
|
|
2363
|
-
});
|
|
2364
|
-
}
|
|
2365
|
-
|
|
2366
|
-
// Ensure URL field exists - CRITICAL for product navigation
|
|
2367
|
-
// Always use /products/{handle} so links target dev server (never external URLs)
|
|
2368
|
-
const handle = enriched.handle || enriched.slug || enriched.id;
|
|
2389
|
+
|
|
2390
|
+
const handle = enriched.handle || enriched.slug || String(enriched.id || enriched.productId || '');
|
|
2369
2391
|
const effectiveHandle = (handle && String(handle).trim() && String(handle) !== '#')
|
|
2370
|
-
? String(handle).trim().toLowerCase()
|
|
2371
|
-
|
|
2392
|
+
? String(handle).trim().toLowerCase() : null;
|
|
2393
|
+
|
|
2372
2394
|
if (effectiveHandle) {
|
|
2373
|
-
enriched.url =
|
|
2374
|
-
enriched.link =
|
|
2375
|
-
if (!enriched.handle
|
|
2376
|
-
if (!enriched.slug
|
|
2377
|
-
} else {
|
|
2378
|
-
enriched.url = '#';
|
|
2379
|
-
enriched.link = '#';
|
|
2380
|
-
console.warn(`[ENRICH] ⚠️ Product has no id/handle/slug/productId, URL set to '#'`, {
|
|
2381
|
-
productKeys: Object.keys(enriched).slice(0, 10)
|
|
2382
|
-
});
|
|
2395
|
+
enriched.url = effectiveHandle;
|
|
2396
|
+
enriched.link = effectiveHandle;
|
|
2397
|
+
if (!enriched.handle) enriched.handle = effectiveHandle;
|
|
2398
|
+
if (!enriched.slug) enriched.slug = effectiveHandle;
|
|
2383
2399
|
}
|
|
2384
2400
|
|
|
2385
|
-
// Ensure handle/slug exist if missing (for template fallback logic)
|
|
2386
|
-
// Also normalize them (remove '#' or empty strings)
|
|
2387
|
-
if (!enriched.handle || enriched.handle === '#' || enriched.handle === '') {
|
|
2388
|
-
if (enriched.slug && enriched.slug !== '#' && enriched.slug !== '') {
|
|
2389
|
-
enriched.handle = enriched.slug;
|
|
2390
|
-
} else if (enriched.id && enriched.id !== '#') {
|
|
2391
|
-
enriched.handle = String(enriched.id).toLowerCase();
|
|
2392
|
-
} else if (enriched.productId) {
|
|
2393
|
-
enriched.handle = `product-${enriched.productId}`.toLowerCase();
|
|
2394
|
-
} else if (!enriched.handle) {
|
|
2395
|
-
// Generate from URL if it was set above
|
|
2396
|
-
if (enriched.url && enriched.url !== '#') {
|
|
2397
|
-
enriched.handle = enriched.url.replace(/^\/products\//, '').toLowerCase();
|
|
2398
|
-
}
|
|
2399
|
-
}
|
|
2400
|
-
}
|
|
2401
|
-
|
|
2402
|
-
if (!enriched.slug || enriched.slug === '#' || enriched.slug === '') {
|
|
2403
|
-
if (enriched.handle && enriched.handle !== '#' && enriched.handle !== '') {
|
|
2404
|
-
enriched.slug = enriched.handle;
|
|
2405
|
-
} else if (enriched.id && enriched.id !== '#') {
|
|
2406
|
-
enriched.slug = String(enriched.id).toLowerCase();
|
|
2407
|
-
} else if (enriched.productId) {
|
|
2408
|
-
enriched.slug = `product-${enriched.productId}`.toLowerCase();
|
|
2409
|
-
} else if (!enriched.slug) {
|
|
2410
|
-
// Generate from URL if it was set above
|
|
2411
|
-
if (enriched.url && enriched.url !== '#') {
|
|
2412
|
-
enriched.slug = enriched.url.replace(/^\/products\//, '').toLowerCase();
|
|
2413
|
-
}
|
|
2414
|
-
}
|
|
2415
|
-
}
|
|
2416
|
-
|
|
2417
|
-
// Ensure name/title exists - CRITICAL for product display
|
|
2418
2401
|
if (!enriched.name && !enriched.title) {
|
|
2419
2402
|
enriched.name = `Product ${enriched.id || 'Unknown'}`;
|
|
2420
2403
|
enriched.title = enriched.name;
|
|
2421
2404
|
} else if (!enriched.name) {
|
|
2422
|
-
enriched.name = enriched.title
|
|
2405
|
+
enriched.name = enriched.title;
|
|
2423
2406
|
} else if (!enriched.title) {
|
|
2424
2407
|
enriched.title = enriched.name;
|
|
2425
2408
|
}
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
if (enriched.
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
enriched.variants
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
// Ensure price exists - CRITICAL for product display
|
|
2435
|
-
if (!enriched.price && !enriched.sellingPrice) {
|
|
2436
|
-
// Try to get from variants/variations first
|
|
2437
|
-
const variantsOrVariations = enriched.variants || enriched.variations;
|
|
2438
|
-
if (variantsOrVariations && variantsOrVariations.length > 0) {
|
|
2439
|
-
enriched.price = variantsOrVariations[0].price || variantsOrVariations[0].sellingPrice || 0;
|
|
2440
|
-
enriched.sellingPrice = enriched.price;
|
|
2441
|
-
} else {
|
|
2442
|
-
enriched.price = 0;
|
|
2443
|
-
enriched.sellingPrice = 0;
|
|
2444
|
-
}
|
|
2445
|
-
} else if (!enriched.price) {
|
|
2446
|
-
enriched.price = enriched.sellingPrice;
|
|
2447
|
-
} else if (!enriched.sellingPrice) {
|
|
2448
|
-
enriched.sellingPrice = enriched.price;
|
|
2409
|
+
|
|
2410
|
+
if (enriched.variants && !enriched.variations) enriched.variations = enriched.variants;
|
|
2411
|
+
else if (enriched.variations && !enriched.variants) enriched.variants = enriched.variations;
|
|
2412
|
+
|
|
2413
|
+
if (enriched.price == null && enriched.sellingPrice == null) {
|
|
2414
|
+
const v = enriched.variants || enriched.variations;
|
|
2415
|
+
enriched.price = (v && v.length > 0) ? (v[0].price || 0) : 0;
|
|
2449
2416
|
}
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
// Make sure price is NOT 0 unless it's actually 0 (check if undefined/null)
|
|
2453
|
-
const finalPrice = (enriched.price !== undefined && enriched.price !== null)
|
|
2454
|
-
? enriched.price
|
|
2455
|
-
: ((enriched.sellingPrice !== undefined && enriched.sellingPrice !== null)
|
|
2456
|
-
? enriched.sellingPrice
|
|
2457
|
-
: 0);
|
|
2458
|
-
|
|
2417
|
+
if (enriched.price == null) enriched.price = enriched.sellingPrice || 0;
|
|
2418
|
+
|
|
2459
2419
|
if (!enriched.prices) {
|
|
2460
|
-
const mrpValue = enriched.compareAtPrice || enriched.comparePrice || enriched.mrp || 0;
|
|
2461
2420
|
enriched.prices = {
|
|
2462
|
-
price:
|
|
2463
|
-
mrp:
|
|
2464
|
-
currency: enriched.currency || 'USD'
|
|
2421
|
+
price: enriched.price,
|
|
2422
|
+
mrp: enriched.compareAtPrice || enriched.comparePrice || 0
|
|
2465
2423
|
};
|
|
2466
|
-
} else {
|
|
2467
|
-
// Update prices object if it exists but is missing fields
|
|
2468
|
-
if (enriched.prices.price === undefined || enriched.prices.price === null) {
|
|
2469
|
-
enriched.prices.price = finalPrice;
|
|
2470
|
-
}
|
|
2471
|
-
if (enriched.prices.mrp === undefined || enriched.prices.mrp === null) {
|
|
2472
|
-
enriched.prices.mrp = enriched.compareAtPrice || enriched.comparePrice || enriched.mrp || 0;
|
|
2473
|
-
}
|
|
2474
|
-
if (!enriched.prices.currency) {
|
|
2475
|
-
enriched.prices.currency = enriched.currency || 'USD';
|
|
2476
|
-
}
|
|
2477
|
-
}
|
|
2478
|
-
|
|
2479
|
-
// Ensure price/sellingPrice fields also exist (not just prices object)
|
|
2480
|
-
if (enriched.price === undefined || enriched.price === null) {
|
|
2481
|
-
enriched.price = finalPrice;
|
|
2482
2424
|
}
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
}
|
|
2486
|
-
|
|
2487
|
-
// Ensure image exists - CRITICAL for product display
|
|
2488
|
-
if (!enriched.imageUrl && !enriched.thumbnailImage1 && (!enriched.images || enriched.images.length === 0)) {
|
|
2489
|
-
// Fallback to picsum placeholder
|
|
2425
|
+
|
|
2426
|
+
if (!enriched.imageUrl && !enriched.thumbnailImage && (!enriched.images || enriched.images.length === 0)) {
|
|
2490
2427
|
const imageId = enriched.id ? String(enriched.id).replace(/\D/g, '') || '1' : '1';
|
|
2491
2428
|
enriched.imageUrl = `https://picsum.photos/seed/${imageId}/800/800`;
|
|
2492
|
-
enriched.
|
|
2493
|
-
url: enriched.imageUrl,
|
|
2494
|
-
altText: enriched.title || enriched.name || 'Product image'
|
|
2495
|
-
};
|
|
2429
|
+
enriched.thumbnailImage = enriched.imageUrl;
|
|
2496
2430
|
}
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
enriched.available = stockValue > 0;
|
|
2521
|
-
|
|
2522
|
-
// Also ensure stockQuantity is set if only stock is set (and vice versa)
|
|
2523
|
-
if (enriched.stock !== undefined && enriched.stockQuantity === undefined) {
|
|
2524
|
-
enriched.stockQuantity = enriched.stock;
|
|
2525
|
-
} else if (enriched.stockQuantity !== undefined && enriched.stock === undefined) {
|
|
2526
|
-
enriched.stock = enriched.stockQuantity;
|
|
2431
|
+
|
|
2432
|
+
this.enrichProductStockStatus(enriched);
|
|
2433
|
+
|
|
2434
|
+
return enriched;
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
/**
|
|
2438
|
+
* Compute inStock and available flags matching production enrichProductStockStatus().
|
|
2439
|
+
*/
|
|
2440
|
+
enrichProductStockStatus(product) {
|
|
2441
|
+
const stockQuantity = product.stockQuantity || 0;
|
|
2442
|
+
const stockTrackingIsEnabled = product.stockTrackingIsEnabled || false;
|
|
2443
|
+
const isAllowToOrder = product.isAllowToOrder || false;
|
|
2444
|
+
|
|
2445
|
+
product.inStock = !stockTrackingIsEnabled || stockQuantity > 0;
|
|
2446
|
+
product.available = !stockTrackingIsEnabled || stockQuantity > 0 || isAllowToOrder;
|
|
2447
|
+
|
|
2448
|
+
if (product.stockQuantity == null) {
|
|
2449
|
+
const variants = product.variants || product.variations;
|
|
2450
|
+
if (variants && variants.length > 0) {
|
|
2451
|
+
product.stockQuantity = variants.reduce((sum, v) => sum + (v.stockQuantity || 0), 0);
|
|
2452
|
+
product.inStock = !stockTrackingIsEnabled || product.stockQuantity > 0;
|
|
2453
|
+
product.available = !stockTrackingIsEnabled || product.stockQuantity > 0 || isAllowToOrder;
|
|
2527
2454
|
}
|
|
2528
2455
|
}
|
|
2529
|
-
|
|
2530
|
-
return
|
|
2456
|
+
|
|
2457
|
+
return product;
|
|
2531
2458
|
}
|
|
2532
2459
|
|
|
2533
2460
|
/**
|
|
@@ -2546,35 +2473,39 @@ class DevServer {
|
|
|
2546
2473
|
if (n.Name != null && n.name == null) n.name = n.Name;
|
|
2547
2474
|
if (n.Title != null && n.title == null) n.title = n.Title;
|
|
2548
2475
|
if (n.Attributes != null && n.attributes == null) n.attributes = n.Attributes;
|
|
2549
|
-
|
|
2476
|
+
|
|
2477
|
+
const t1 = n.ThumbnailImage1 || n.thumbnailImage || n.thumbnailImage;
|
|
2550
2478
|
if (t1 != null) {
|
|
2551
2479
|
const u = typeof t1 === 'string' ? t1 : (t1.url || t1.Url || t1.imageUrl || t1.ImageUrl);
|
|
2552
2480
|
if (u) {
|
|
2553
|
-
|
|
2554
|
-
n.
|
|
2555
|
-
|
|
2556
|
-
} else if (!n.thumbnailImage1) n.thumbnailImage1 = t1;
|
|
2481
|
+
n.thumbnailImage = u;
|
|
2482
|
+
if (!n.imageUrl) n.imageUrl = u;
|
|
2483
|
+
}
|
|
2557
2484
|
}
|
|
2485
|
+
|
|
2558
2486
|
let imgs = n.images || n.Images;
|
|
2559
2487
|
if (Array.isArray(imgs) && imgs.length > 0) {
|
|
2560
2488
|
n.images = imgs.map((img) => {
|
|
2561
2489
|
if (typeof img === 'string') return { url: img, altText: n.name || n.title };
|
|
2562
2490
|
const u = img.url || img.Url || img.imageUrl || img.ImageUrl;
|
|
2563
2491
|
const a = img.altText || img.AltText || n.name || n.title;
|
|
2564
|
-
return
|
|
2492
|
+
return { url: u || '', altText: a };
|
|
2565
2493
|
});
|
|
2566
|
-
} else if (imgs) {
|
|
2567
|
-
n.images = Array.isArray(imgs) ? imgs : [imgs];
|
|
2568
2494
|
} else {
|
|
2569
2495
|
const single = n.imageUrl || n.ImageUrl;
|
|
2570
2496
|
if (single) n.images = [{ url: single, altText: n.name || n.title }];
|
|
2571
2497
|
}
|
|
2498
|
+
|
|
2572
2499
|
const handle = (n.handle || n.slug || n.id || '').toString().trim();
|
|
2573
|
-
const h = handle && handle !== '#' ? handle.toLowerCase() : (n.productId ?
|
|
2500
|
+
const h = handle && handle !== '#' ? handle.toLowerCase() : (n.productId ? String(n.productId) : null);
|
|
2574
2501
|
if (h) {
|
|
2575
|
-
n.url =
|
|
2576
|
-
n.link =
|
|
2502
|
+
n.url = h;
|
|
2503
|
+
n.link = h;
|
|
2504
|
+
if (!n.handle) n.handle = h;
|
|
2505
|
+
if (!n.slug) n.slug = h;
|
|
2577
2506
|
}
|
|
2507
|
+
|
|
2508
|
+
this.enrichProductStockStatus(n);
|
|
2578
2509
|
return n;
|
|
2579
2510
|
}
|
|
2580
2511
|
|
|
@@ -2680,7 +2611,7 @@ class DevServer {
|
|
|
2680
2611
|
const products = context.collection?.products || [];
|
|
2681
2612
|
const productCards = products.map(p => `
|
|
2682
2613
|
<div style="background: #fff; border-radius: 8px; padding: 1rem; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
|
2683
|
-
<a href="
|
|
2614
|
+
<a href="/${p.slug || p.handle || p.id}">
|
|
2684
2615
|
<img src="${p.images?.[0] || p.imageUrl || 'https://picsum.photos/seed/' + p.id + '/300/300'}"
|
|
2685
2616
|
alt="${p.title || p.name}" style="width: 100%; height: 200px; object-fit: cover; border-radius: 4px;">
|
|
2686
2617
|
<h4 style="margin: 0.5rem 0;">${p.title || p.name}</h4>
|