@o2vend/theme-cli 1.0.32 → 1.0.34
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 +912 -69
- package/lib/lib/liquid-engine.js +83 -11
- package/lib/lib/mock-data.js +45 -7
- package/package.json +1 -1
package/lib/lib/dev-server.js
CHANGED
|
@@ -106,23 +106,31 @@ class DevServer {
|
|
|
106
106
|
|
|
107
107
|
// Process CSS files through Liquid to support theme variables
|
|
108
108
|
// This handles CSS files that contain Liquid syntax like {{ settings.color_primary }}
|
|
109
|
-
|
|
109
|
+
// Supports both root assets and subdirectories: /assets/file.css and /assets/css/file.css
|
|
110
|
+
this.app.get('/assets/*.css', async (req, res, next) => {
|
|
110
111
|
try {
|
|
111
|
-
//
|
|
112
|
-
const
|
|
113
|
-
const cssPath = path.join(this.themePath, 'assets',
|
|
112
|
+
// Extract the relative path from /assets/ (remove leading /assets/)
|
|
113
|
+
const relativePath = req.path.replace(/^\/assets\//, '');
|
|
114
|
+
const cssPath = path.join(this.themePath, 'assets', relativePath);
|
|
115
|
+
|
|
116
|
+
// Normalize path to prevent directory traversal
|
|
117
|
+
const normalizedPath = path.normalize(cssPath);
|
|
118
|
+
const assetsDir = path.normalize(path.join(this.themePath, 'assets'));
|
|
119
|
+
if (!normalizedPath.startsWith(assetsDir)) {
|
|
120
|
+
return res.status(403).send('Invalid path');
|
|
121
|
+
}
|
|
114
122
|
|
|
115
|
-
if (!fs.existsSync(
|
|
123
|
+
if (!fs.existsSync(normalizedPath)) {
|
|
116
124
|
return next();
|
|
117
125
|
}
|
|
118
126
|
|
|
119
|
-
const cssContent = fs.readFileSync(
|
|
127
|
+
const cssContent = fs.readFileSync(normalizedPath, 'utf8');
|
|
120
128
|
|
|
121
129
|
// Check if CSS contains Liquid variable syntax ({{ }})
|
|
122
130
|
// Only process if it contains actual Liquid variables, not just comments
|
|
123
131
|
if (cssContent.includes('{{') && cssContent.includes('settings.')) {
|
|
124
132
|
try {
|
|
125
|
-
// Load settings from theme config
|
|
133
|
+
// Load settings from theme config (already extracts 'current' section)
|
|
126
134
|
const settings = this.loadThemeSettings();
|
|
127
135
|
|
|
128
136
|
// Process through Liquid
|
|
@@ -132,14 +140,14 @@ class DevServer {
|
|
|
132
140
|
res.setHeader('Cache-Control', 'no-cache');
|
|
133
141
|
return res.send(processedCss);
|
|
134
142
|
} catch (liquidError) {
|
|
135
|
-
console.error(`[CSS] Liquid processing error for ${
|
|
143
|
+
console.error(`[CSS] Liquid processing error for ${relativePath}:`, liquidError.message);
|
|
136
144
|
// Fall through to serve static file
|
|
137
145
|
}
|
|
138
146
|
}
|
|
139
147
|
|
|
140
148
|
// Serve static file if no Liquid processing needed or if processing failed
|
|
141
149
|
res.setHeader('Content-Type', 'text/css; charset=utf-8');
|
|
142
|
-
res.sendFile(
|
|
150
|
+
res.sendFile(normalizedPath);
|
|
143
151
|
} catch (error) {
|
|
144
152
|
console.error(`[CSS] Error processing ${req.path}:`, error.message);
|
|
145
153
|
next();
|
|
@@ -444,6 +452,7 @@ class DevServer {
|
|
|
444
452
|
|
|
445
453
|
const html = await renderWithLayout(this.liquid, 'templates/index', context, this.themePath);
|
|
446
454
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
455
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
447
456
|
res.send(html);
|
|
448
457
|
} catch (error) {
|
|
449
458
|
next(error);
|
|
@@ -453,7 +462,73 @@ class DevServer {
|
|
|
453
462
|
// Products listing page
|
|
454
463
|
this.app.get('/products', async (req, res, next) => {
|
|
455
464
|
try {
|
|
456
|
-
|
|
465
|
+
// Check for category filter in query parameters (handle both 'category' and 'Category')
|
|
466
|
+
const categoryFilter = req.query.category || req.query.Category;
|
|
467
|
+
|
|
468
|
+
let context = await this.buildContext(req, 'products');
|
|
469
|
+
|
|
470
|
+
// If category filter is present, filter products by category
|
|
471
|
+
if (categoryFilter) {
|
|
472
|
+
console.log(`[PRODUCTS PAGE] Filtering by category: ${categoryFilter}`);
|
|
473
|
+
|
|
474
|
+
// Find category by name or handle (case-insensitive)
|
|
475
|
+
const category = context.categories?.find(c =>
|
|
476
|
+
(c.name && c.name.toLowerCase() === categoryFilter.toLowerCase()) ||
|
|
477
|
+
(c.handle && c.handle.toLowerCase() === categoryFilter.toLowerCase())
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
if (category) {
|
|
481
|
+
console.log(`[PRODUCTS PAGE] Found category: ${category.name} (ID: ${category.id})`);
|
|
482
|
+
|
|
483
|
+
// Filter products by categoryId and enrich them
|
|
484
|
+
const filteredProducts = context.products
|
|
485
|
+
.filter(p =>
|
|
486
|
+
p.categoryId === category.id ||
|
|
487
|
+
String(p.categoryId).toLowerCase() === String(category.id).toLowerCase()
|
|
488
|
+
)
|
|
489
|
+
.map(p => this.enrichProductData(p)); // Enrich each product
|
|
490
|
+
|
|
491
|
+
// CRITICAL: Set both context.products and context.collection.products to the SAME enriched array
|
|
492
|
+
// This ensures they always reference the same objects (like widgets do)
|
|
493
|
+
context.products = filteredProducts;
|
|
494
|
+
context.collection = context.collection || {};
|
|
495
|
+
context.collection.products = context.products; // Same reference as context.products
|
|
496
|
+
context.collection.title = category.name || 'Products';
|
|
497
|
+
context.collection.handle = category.handle || categoryFilter;
|
|
498
|
+
context.collection.description = category.description;
|
|
499
|
+
context.collection.totalProducts = filteredProducts.length;
|
|
500
|
+
|
|
501
|
+
console.log(`[PRODUCTS PAGE] Filtered products: ${filteredProducts.length} products in category`);
|
|
502
|
+
console.log(`[PRODUCTS PAGE] ✅ collection.products === context.products: ${context.collection.products === context.products}`);
|
|
503
|
+
|
|
504
|
+
console.log(`[PRODUCTS PAGE] ✅ Category filtered: ${filteredProducts.length} enriched products in collection`);
|
|
505
|
+
} else {
|
|
506
|
+
console.warn(`[PRODUCTS PAGE] Category not found: ${categoryFilter}, showing all products`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Ensure all products have proper data: URL, name/title, price, images
|
|
511
|
+
// Only enrich if not already enriched (check if first product has url field)
|
|
512
|
+
if (context.products && context.products.length > 0 && !context.products[0].url && !context.products[0].link) {
|
|
513
|
+
console.log(`[PRODUCTS PAGE] Products need enrichment - enriching now...`);
|
|
514
|
+
context.products = this.enrichProductsData(context.products);
|
|
515
|
+
} else if (context.products && context.products.length > 0) {
|
|
516
|
+
// Products already enriched
|
|
517
|
+
console.log(`[PRODUCTS PAGE] Products already enriched, first product has URL: ${context.products[0].url || context.products[0].link}`);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Debug: Log sample product data to verify enrichment
|
|
521
|
+
if (context.products.length > 0) {
|
|
522
|
+
const sample = context.products[0];
|
|
523
|
+
console.log(`[PRODUCTS PAGE] Sample product data:`, {
|
|
524
|
+
title: sample.title || sample.name,
|
|
525
|
+
url: sample.url || sample.link,
|
|
526
|
+
price: sample.price || sample.sellingPrice,
|
|
527
|
+
stock: sample.stock,
|
|
528
|
+
inStock: sample.inStock,
|
|
529
|
+
hasImage: !!(sample.imageUrl || sample.thumbnailImage1)
|
|
530
|
+
});
|
|
531
|
+
}
|
|
457
532
|
|
|
458
533
|
// DEBUG: Log context data for /products page
|
|
459
534
|
console.log(`[PRODUCTS PAGE] Context summary:`);
|
|
@@ -468,9 +543,34 @@ class DevServer {
|
|
|
468
543
|
if (!context.products || context.products.length === 0) {
|
|
469
544
|
console.warn('[PRODUCTS PAGE] ⚠️ No products in context, attempting to reload...');
|
|
470
545
|
try {
|
|
471
|
-
const
|
|
546
|
+
const queryParams = {};
|
|
547
|
+
if (categoryFilter) {
|
|
548
|
+
// Find category and filter by categoryId
|
|
549
|
+
const category = context.categories?.find(c =>
|
|
550
|
+
(c.name && c.name.toLowerCase() === categoryFilter.toLowerCase()) ||
|
|
551
|
+
(c.handle && c.handle.toLowerCase() === categoryFilter.toLowerCase())
|
|
552
|
+
);
|
|
553
|
+
if (category) {
|
|
554
|
+
queryParams.categoryId = category.id;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
const productsResponse = await this.apiClient.getProducts({ limit: 50, offset: 0, ...queryParams });
|
|
472
558
|
context.products = productsResponse.products || productsResponse.data?.products || [];
|
|
559
|
+
|
|
560
|
+
// Ensure URLs and all product data are set
|
|
561
|
+
context.products = this.enrichProductsData(context.products);
|
|
562
|
+
|
|
473
563
|
console.log(`[PRODUCTS PAGE] ✅ Reloaded ${context.products.length} products`);
|
|
564
|
+
if (context.products.length > 0) {
|
|
565
|
+
const sample = context.products[0];
|
|
566
|
+
console.log(`[PRODUCTS PAGE] Sample reloaded product:`, {
|
|
567
|
+
title: sample.title || sample.name,
|
|
568
|
+
url: sample.url || sample.link,
|
|
569
|
+
price: sample.price || sample.sellingPrice,
|
|
570
|
+
stock: sample.stock,
|
|
571
|
+
inStock: sample.inStock
|
|
572
|
+
});
|
|
573
|
+
}
|
|
474
574
|
} catch (error) {
|
|
475
575
|
console.error('[PRODUCTS PAGE] ❌ Failed to load products:', error.message);
|
|
476
576
|
}
|
|
@@ -479,10 +579,14 @@ class DevServer {
|
|
|
479
579
|
if (context.products.length > 0) {
|
|
480
580
|
const sampleProduct = context.products[0];
|
|
481
581
|
const hasStock = 'stock' in sampleProduct || 'quantity' in sampleProduct;
|
|
582
|
+
const hasImage = sampleProduct.thumbnailImage1 || sampleProduct.imageUrl || (sampleProduct.images && sampleProduct.images.length > 0);
|
|
583
|
+
const hasUrl = sampleProduct.url || sampleProduct.link;
|
|
482
584
|
// Use nullish coalescing to show 0 values correctly
|
|
483
585
|
const stockValue = sampleProduct.stock ?? sampleProduct.quantity ?? 'N/A';
|
|
484
|
-
console.log(`[PRODUCTS PAGE DEBUG] Sample product - Title: ${sampleProduct.title || sampleProduct.name}
|
|
485
|
-
console.log(`[PRODUCTS PAGE DEBUG]
|
|
586
|
+
console.log(`[PRODUCTS PAGE DEBUG] Sample product - Title: ${sampleProduct.title || sampleProduct.name}`);
|
|
587
|
+
console.log(`[PRODUCTS PAGE DEBUG] - Has stock: ${hasStock}, Stock: ${stockValue}`);
|
|
588
|
+
console.log(`[PRODUCTS PAGE DEBUG] - Has image: ${hasImage}, Has URL: ${hasUrl}`);
|
|
589
|
+
console.log(`[PRODUCTS PAGE DEBUG] - URL: ${sampleProduct.url || sampleProduct.link || 'missing'}`);
|
|
486
590
|
|
|
487
591
|
// Check a few more products to see stock distribution
|
|
488
592
|
const stockCounts = context.products.slice(0, 5).map(p => p.stock ?? 0);
|
|
@@ -524,16 +628,78 @@ class DevServer {
|
|
|
524
628
|
}
|
|
525
629
|
|
|
526
630
|
// Ensure collection.products is set for products template
|
|
631
|
+
// CRITICAL: Always sync collection.products with context.products AFTER enrichment
|
|
632
|
+
// This ensures collection.products always has the same enriched objects as context.products
|
|
527
633
|
if (context.products && context.products.length > 0) {
|
|
634
|
+
// Always reassign collection.products to match context.products (they should be the same)
|
|
528
635
|
context.collection = context.collection || {};
|
|
529
|
-
context.collection.products = context.products;
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
context.collection.
|
|
636
|
+
context.collection.products = context.products; // Use the same enriched array
|
|
637
|
+
|
|
638
|
+
// Update collection metadata if not set
|
|
639
|
+
if (!context.collection.title) {
|
|
640
|
+
context.collection.title = categoryFilter ? `${categoryFilter} Products` : 'All Products';
|
|
641
|
+
}
|
|
642
|
+
if (!context.collection.handle) {
|
|
643
|
+
context.collection.handle = categoryFilter || 'all';
|
|
644
|
+
}
|
|
645
|
+
if (!context.collection.totalProducts) {
|
|
646
|
+
context.collection.totalProducts = context.products.length;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Debug: Verify collection.products has same reference as context.products
|
|
650
|
+
const sameReference = context.collection.products === context.products;
|
|
651
|
+
console.log(`[PRODUCTS PAGE] collection.products sync: same reference=${sameReference}, length=${context.collection.products?.length || 0}`);
|
|
652
|
+
|
|
653
|
+
// Debug: Log sample product from collection to verify enrichment
|
|
654
|
+
if (context.collection.products && context.collection.products.length > 0) {
|
|
655
|
+
const sample = context.collection.products[0];
|
|
656
|
+
console.log(`[PRODUCTS PAGE] ✅ Sample collection product before render:`, {
|
|
657
|
+
id: sample.id,
|
|
658
|
+
title: sample.title || sample.name,
|
|
659
|
+
url: sample.url || sample.link || 'MISSING',
|
|
660
|
+
price: sample.price || sample.sellingPrice || 'MISSING',
|
|
661
|
+
prices: sample.prices ? { price: sample.prices.price, mrp: sample.prices.mrp } : 'MISSING',
|
|
662
|
+
stock: sample.stock !== undefined ? sample.stock : 'MISSING',
|
|
663
|
+
stockQuantity: sample.stockQuantity !== undefined ? sample.stockQuantity : 'MISSING',
|
|
664
|
+
inStock: sample.inStock !== undefined ? sample.inStock : 'MISSING',
|
|
665
|
+
available: sample.available !== undefined ? sample.available : 'MISSING',
|
|
666
|
+
hasImage: !!(sample.imageUrl || sample.thumbnailImage1),
|
|
667
|
+
imageUrl: sample.imageUrl || sample.thumbnailImage1?.url || 'MISSING',
|
|
668
|
+
hasThumbnailImage1: !!sample.thumbnailImage1,
|
|
669
|
+
hasImagesArray: !!(sample.images && sample.images.length > 0)
|
|
670
|
+
});
|
|
671
|
+
} else {
|
|
672
|
+
console.warn(`[PRODUCTS PAGE] ⚠️ collection.products is empty or undefined!`);
|
|
673
|
+
console.warn(`[PRODUCTS PAGE] context.products.length: ${context.products?.length || 0}`);
|
|
674
|
+
console.warn(`[PRODUCTS PAGE] context.collection:`, context.collection ? Object.keys(context.collection) : 'undefined');
|
|
675
|
+
}
|
|
676
|
+
} else {
|
|
677
|
+
console.warn(`[PRODUCTS PAGE] ⚠️ No products in context!`);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Debug: Log final context before rendering
|
|
681
|
+
console.log(`[PRODUCTS PAGE] Final context before rendering:`);
|
|
682
|
+
console.log(` - Products count: ${context.products?.length || 0}`);
|
|
683
|
+
if (context.products && context.products.length > 0) {
|
|
684
|
+
const firstProduct = context.products[0];
|
|
685
|
+
console.log(` - First product:`, {
|
|
686
|
+
id: firstProduct.id,
|
|
687
|
+
title: firstProduct.title || firstProduct.name,
|
|
688
|
+
url: firstProduct.url || firstProduct.link,
|
|
689
|
+
price: firstProduct.price || firstProduct.sellingPrice,
|
|
690
|
+
stock: firstProduct.stock,
|
|
691
|
+
inStock: firstProduct.inStock,
|
|
692
|
+
hasImage: !!(firstProduct.imageUrl || firstProduct.thumbnailImage1),
|
|
693
|
+
imageUrl: firstProduct.imageUrl || firstProduct.thumbnailImage1?.url
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
if (context.collection) {
|
|
697
|
+
console.log(` - Collection: ${context.collection.title || context.collection.name}, Products: ${context.collection.products?.length || 0}`);
|
|
533
698
|
}
|
|
534
699
|
|
|
535
700
|
const html = await renderWithLayout(this.liquid, 'templates/products', context, this.themePath);
|
|
536
701
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
702
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
537
703
|
res.send(html);
|
|
538
704
|
} catch (error) {
|
|
539
705
|
// If products template doesn't exist, try collections template as fallback
|
|
@@ -542,6 +708,7 @@ class DevServer {
|
|
|
542
708
|
const context = await this.buildContext(req, 'collection');
|
|
543
709
|
const html = await renderWithLayout(this.liquid, 'templates/collection', context, this.themePath);
|
|
544
710
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
711
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
545
712
|
res.send(html);
|
|
546
713
|
} catch (fallbackError) {
|
|
547
714
|
next(error);
|
|
@@ -555,7 +722,46 @@ class DevServer {
|
|
|
555
722
|
// Product detail page
|
|
556
723
|
this.app.get('/products/:handle', async (req, res, next) => {
|
|
557
724
|
try {
|
|
558
|
-
const
|
|
725
|
+
const handle = req.params.handle;
|
|
726
|
+
console.log(`[PRODUCT PAGE] Looking up product with handle: ${handle}`);
|
|
727
|
+
|
|
728
|
+
const context = await this.buildContext(req, 'product', { productHandle: handle });
|
|
729
|
+
|
|
730
|
+
// Check if product was found
|
|
731
|
+
if (!context.product) {
|
|
732
|
+
console.warn(`[PRODUCT PAGE] Product not found with handle: ${handle}`);
|
|
733
|
+
// Try to find product by ID if handle looks like an ID
|
|
734
|
+
if (handle.match(/^\d+$/) || handle.startsWith('product-')) {
|
|
735
|
+
const productId = handle.replace(/^product-/, '');
|
|
736
|
+
console.log(`[PRODUCT PAGE] Trying to find product by ID: ${productId}`);
|
|
737
|
+
const allProducts = context.products || [];
|
|
738
|
+
const productById = allProducts.find(p =>
|
|
739
|
+
String(p.id) === productId ||
|
|
740
|
+
String(p.id) === handle ||
|
|
741
|
+
(p.handle && p.handle.includes(productId))
|
|
742
|
+
);
|
|
743
|
+
if (productById) {
|
|
744
|
+
context.product = productById;
|
|
745
|
+
console.log(`[PRODUCT PAGE] Found product by ID: ${productById.title || productById.name}`);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// If still not found, return 404
|
|
750
|
+
if (!context.product) {
|
|
751
|
+
return res.status(404).send(`
|
|
752
|
+
<!DOCTYPE html>
|
|
753
|
+
<html>
|
|
754
|
+
<head><title>Product Not Found</title></head>
|
|
755
|
+
<body>
|
|
756
|
+
<h1>Product Not Found</h1>
|
|
757
|
+
<p>Product with handle "${handle}" was not found.</p>
|
|
758
|
+
<p><a href="/products">Back to Products</a></p>
|
|
759
|
+
</body>
|
|
760
|
+
</html>
|
|
761
|
+
`);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
559
765
|
// Try multiple template names: product-detail, product, product-page
|
|
560
766
|
let html;
|
|
561
767
|
const templateOptions = ['templates/product-detail', 'templates/product', 'templates/product-page'];
|
|
@@ -573,8 +779,10 @@ class DevServer {
|
|
|
573
779
|
throw new Error(`Product template not found. Tried: ${templateOptions.join(', ')}`);
|
|
574
780
|
}
|
|
575
781
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
782
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
576
783
|
res.send(html);
|
|
577
784
|
} catch (error) {
|
|
785
|
+
console.error(`[PRODUCT PAGE] Error rendering product page:`, error);
|
|
578
786
|
next(error);
|
|
579
787
|
}
|
|
580
788
|
});
|
|
@@ -600,6 +808,7 @@ class DevServer {
|
|
|
600
808
|
throw new Error(`Product template not found. Tried: ${templateOptions.join(', ')}`);
|
|
601
809
|
}
|
|
602
810
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
811
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
603
812
|
res.send(html);
|
|
604
813
|
} catch (error) {
|
|
605
814
|
next(error);
|
|
@@ -626,6 +835,7 @@ class DevServer {
|
|
|
626
835
|
throw new Error(`Product template not found. Tried: ${templateOptions.join(', ')}`);
|
|
627
836
|
}
|
|
628
837
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
838
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
629
839
|
res.send(html);
|
|
630
840
|
} catch (error) {
|
|
631
841
|
next(error);
|
|
@@ -652,6 +862,7 @@ class DevServer {
|
|
|
652
862
|
}
|
|
653
863
|
}
|
|
654
864
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
865
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
655
866
|
res.send(html);
|
|
656
867
|
} catch (error) {
|
|
657
868
|
next(error);
|
|
@@ -670,10 +881,30 @@ class DevServer {
|
|
|
670
881
|
if (category) {
|
|
671
882
|
context.category = category;
|
|
672
883
|
context.collection = category;
|
|
884
|
+
|
|
885
|
+
// Filter products by this category and enrich them
|
|
886
|
+
const filteredProducts = (context.products || [])
|
|
887
|
+
.filter(p =>
|
|
888
|
+
p.categoryId === category.id ||
|
|
889
|
+
String(p.categoryId).toLowerCase() === String(category.id).toLowerCase()
|
|
890
|
+
)
|
|
891
|
+
.map(p => this.enrichProductData(p));
|
|
892
|
+
|
|
893
|
+
context.collection.products = filteredProducts;
|
|
894
|
+
context.collection.totalProducts = filteredProducts.length;
|
|
895
|
+
context.products = filteredProducts; // Also set in products array for consistency
|
|
896
|
+
|
|
897
|
+
console.log(`[COLLECTION PAGE] Category: ${category.name}, Products: ${filteredProducts.length}`);
|
|
898
|
+
} else {
|
|
899
|
+
// No category found, use all products with enrichment
|
|
900
|
+
context.products = this.enrichProductsData(context.products || []);
|
|
901
|
+
context.collection = context.collection || {};
|
|
902
|
+
context.collection.products = context.products;
|
|
673
903
|
}
|
|
674
904
|
|
|
905
|
+
// Prioritize products template to show product list, not categories list
|
|
675
906
|
let html;
|
|
676
|
-
const templateOptions = ['templates/
|
|
907
|
+
const templateOptions = ['templates/products', 'templates/collection', 'templates/category', 'templates/categories'];
|
|
677
908
|
for (const template of templateOptions) {
|
|
678
909
|
try {
|
|
679
910
|
html = await renderWithLayout(this.liquid, template, context, this.themePath);
|
|
@@ -688,6 +919,7 @@ class DevServer {
|
|
|
688
919
|
throw new Error(`Category/Collection template not found. Tried: ${templateOptions.join(', ')}`);
|
|
689
920
|
}
|
|
690
921
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
922
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
691
923
|
res.send(html);
|
|
692
924
|
} catch (error) {
|
|
693
925
|
next(error);
|
|
@@ -714,6 +946,7 @@ class DevServer {
|
|
|
714
946
|
throw new Error(`Categories template not found. Tried: ${templateOptions.join(', ')}`);
|
|
715
947
|
}
|
|
716
948
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
949
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
717
950
|
res.send(html);
|
|
718
951
|
} catch (error) {
|
|
719
952
|
next(error);
|
|
@@ -730,10 +963,30 @@ class DevServer {
|
|
|
730
963
|
if (category) {
|
|
731
964
|
context.category = category;
|
|
732
965
|
context.collection = category;
|
|
966
|
+
|
|
967
|
+
// Filter products by this category and enrich them
|
|
968
|
+
const filteredProducts = (context.products || [])
|
|
969
|
+
.filter(p =>
|
|
970
|
+
p.categoryId === category.id ||
|
|
971
|
+
String(p.categoryId).toLowerCase() === String(category.id).toLowerCase()
|
|
972
|
+
)
|
|
973
|
+
.map(p => this.enrichProductData(p));
|
|
974
|
+
|
|
975
|
+
context.collection.products = filteredProducts;
|
|
976
|
+
context.collection.totalProducts = filteredProducts.length;
|
|
977
|
+
context.products = filteredProducts; // Also set in products array for consistency
|
|
978
|
+
|
|
979
|
+
console.log(`[CATEGORY PAGE] Category: ${category.name}, Products: ${filteredProducts.length}`);
|
|
980
|
+
} else {
|
|
981
|
+
// No category found, use all products with enrichment
|
|
982
|
+
context.products = this.enrichProductsData(context.products || []);
|
|
983
|
+
context.collection = context.collection || {};
|
|
984
|
+
context.collection.products = context.products;
|
|
733
985
|
}
|
|
734
986
|
|
|
987
|
+
// Prioritize products template to show product list, not categories list
|
|
735
988
|
let html;
|
|
736
|
-
const templateOptions = ['templates/
|
|
989
|
+
const templateOptions = ['templates/products', 'templates/collection', 'templates/category', 'templates/categories'];
|
|
737
990
|
for (const template of templateOptions) {
|
|
738
991
|
try {
|
|
739
992
|
html = await renderWithLayout(this.liquid, template, context, this.themePath);
|
|
@@ -748,6 +1001,7 @@ class DevServer {
|
|
|
748
1001
|
throw new Error(`Category template not found. Tried: ${templateOptions.join(', ')}`);
|
|
749
1002
|
}
|
|
750
1003
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1004
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
751
1005
|
res.send(html);
|
|
752
1006
|
} catch (error) {
|
|
753
1007
|
next(error);
|
|
@@ -775,6 +1029,7 @@ class DevServer {
|
|
|
775
1029
|
html = this.createSimpleBrandsPage(context);
|
|
776
1030
|
}
|
|
777
1031
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1032
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
778
1033
|
res.send(html);
|
|
779
1034
|
} catch (error) {
|
|
780
1035
|
next(error);
|
|
@@ -813,6 +1068,7 @@ class DevServer {
|
|
|
813
1068
|
html = this.createSimpleBrandPage(context, brand);
|
|
814
1069
|
}
|
|
815
1070
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1071
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
816
1072
|
res.send(html);
|
|
817
1073
|
} catch (error) {
|
|
818
1074
|
next(error);
|
|
@@ -825,6 +1081,7 @@ class DevServer {
|
|
|
825
1081
|
const context = await this.buildContext(req, 'cart');
|
|
826
1082
|
const html = await renderWithLayout(this.liquid, 'templates/cart', context, this.themePath);
|
|
827
1083
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1084
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
828
1085
|
res.send(html);
|
|
829
1086
|
} catch (error) {
|
|
830
1087
|
next(error);
|
|
@@ -837,6 +1094,7 @@ class DevServer {
|
|
|
837
1094
|
const context = await this.buildContext(req, 'search', { query: req.query.q });
|
|
838
1095
|
const html = await renderWithLayout(this.liquid, 'templates/search', context, this.themePath);
|
|
839
1096
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1097
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
840
1098
|
res.send(html);
|
|
841
1099
|
} catch (error) {
|
|
842
1100
|
next(error);
|
|
@@ -890,12 +1148,14 @@ class DevServer {
|
|
|
890
1148
|
try {
|
|
891
1149
|
const html = await renderWithLayout(this.liquid, 'templates/page', context, this.themePath);
|
|
892
1150
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1151
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
893
1152
|
res.send(html);
|
|
894
1153
|
} catch (templateError) {
|
|
895
1154
|
// If no page template, render raw HTML content within layout
|
|
896
1155
|
console.log(`[PAGE] No page template found, rendering raw HTML for ${req.params.handle}`);
|
|
897
1156
|
const html = this.renderLandingPage(context);
|
|
898
1157
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1158
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
899
1159
|
res.send(html);
|
|
900
1160
|
}
|
|
901
1161
|
} catch (error) {
|
|
@@ -934,6 +1194,7 @@ class DevServer {
|
|
|
934
1194
|
|
|
935
1195
|
const html = await renderWithLayout(this.liquid, 'templates/page', context, this.themePath);
|
|
936
1196
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1197
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
937
1198
|
res.send(html);
|
|
938
1199
|
} catch (error) {
|
|
939
1200
|
next(error);
|
|
@@ -950,6 +1211,7 @@ class DevServer {
|
|
|
950
1211
|
|
|
951
1212
|
const html = this.generateDevDashboard({ templates, sections, widgets, snippets });
|
|
952
1213
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1214
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
953
1215
|
res.send(html);
|
|
954
1216
|
} catch (error) {
|
|
955
1217
|
next(error);
|
|
@@ -974,6 +1236,7 @@ class DevServer {
|
|
|
974
1236
|
backUrl: '/dev'
|
|
975
1237
|
});
|
|
976
1238
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1239
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
977
1240
|
res.send(html);
|
|
978
1241
|
} catch (error) {
|
|
979
1242
|
next(error);
|
|
@@ -1033,6 +1296,7 @@ class DevServer {
|
|
|
1033
1296
|
backUrl: '/dev'
|
|
1034
1297
|
});
|
|
1035
1298
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1299
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
1036
1300
|
res.send(html);
|
|
1037
1301
|
} catch (error) {
|
|
1038
1302
|
next(error);
|
|
@@ -1081,6 +1345,7 @@ class DevServer {
|
|
|
1081
1345
|
backUrl: '/dev'
|
|
1082
1346
|
});
|
|
1083
1347
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1348
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
1084
1349
|
res.send(html);
|
|
1085
1350
|
} catch (error) {
|
|
1086
1351
|
next(error);
|
|
@@ -1118,12 +1383,235 @@ class DevServer {
|
|
|
1118
1383
|
const context = await this.buildContext(req, pageType);
|
|
1119
1384
|
const html = await renderWithLayout(this.liquid, `templates/${templateName}`, context, this.themePath);
|
|
1120
1385
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1386
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
1121
1387
|
res.send(html);
|
|
1122
1388
|
} catch (error) {
|
|
1123
1389
|
next(error);
|
|
1124
1390
|
}
|
|
1125
1391
|
});
|
|
1126
1392
|
|
|
1393
|
+
// Catch-all route for category/collection handles at root level (e.g., /toys-and-games)
|
|
1394
|
+
// This handles links from widgets that use category handles directly
|
|
1395
|
+
this.app.get('/:handle', async (req, res, next) => {
|
|
1396
|
+
try {
|
|
1397
|
+
const handle = req.params.handle;
|
|
1398
|
+
|
|
1399
|
+
// Skip if it's a known route path or static file extension
|
|
1400
|
+
const knownPaths = ['assets', 'images', 'api', 'dev', 'favicon.ico', 'products', 'collections', 'categories', 'brands', 'cart', 'search', 'page', 'pages'];
|
|
1401
|
+
const staticExtensions = ['.css', '.js', '.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp', '.woff', '.woff2', '.ttf', '.eot', '.ico'];
|
|
1402
|
+
const hasStaticExtension = staticExtensions.some(ext => handle.toLowerCase().endsWith(ext));
|
|
1403
|
+
|
|
1404
|
+
if (knownPaths.includes(handle) || hasStaticExtension) {
|
|
1405
|
+
return next();
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// Build context to get categories, brands, and products
|
|
1409
|
+
const context = await this.buildContext(req, 'collection', { collectionHandle: handle });
|
|
1410
|
+
|
|
1411
|
+
// Try to find category by handle (case-insensitive)
|
|
1412
|
+
const category = context.categories?.find(c =>
|
|
1413
|
+
(c.handle && c.handle.toLowerCase() === handle.toLowerCase()) ||
|
|
1414
|
+
(c.name && c.name.toLowerCase().replace(/\s+/g, '-') === handle.toLowerCase())
|
|
1415
|
+
);
|
|
1416
|
+
|
|
1417
|
+
if (category) {
|
|
1418
|
+
// Found a category - render products list (NOT categories list)
|
|
1419
|
+
context.category = category;
|
|
1420
|
+
context.collection = category;
|
|
1421
|
+
|
|
1422
|
+
console.log(`[ROOT COLLECTION] Found category: ${category.name} (ID: ${category.id})`);
|
|
1423
|
+
console.log(`[ROOT COLLECTION] Total products in context: ${context.products?.length || 0}`);
|
|
1424
|
+
|
|
1425
|
+
// Filter products by this category and enrich them
|
|
1426
|
+
const filteredProducts = (context.products || [])
|
|
1427
|
+
.filter(p => {
|
|
1428
|
+
const matches = p.categoryId === category.id ||
|
|
1429
|
+
String(p.categoryId).toLowerCase() === String(category.id).toLowerCase();
|
|
1430
|
+
if (matches) {
|
|
1431
|
+
console.log(`[ROOT COLLECTION] Product matches: ${p.title || p.name || p.id}, categoryId: ${p.categoryId}`);
|
|
1432
|
+
}
|
|
1433
|
+
return matches;
|
|
1434
|
+
})
|
|
1435
|
+
.map(p => this.enrichProductData(p)); // Enrich each product
|
|
1436
|
+
|
|
1437
|
+
console.log(`[ROOT COLLECTION] Filtered products count: ${filteredProducts.length}`);
|
|
1438
|
+
|
|
1439
|
+
// Log sample enriched products
|
|
1440
|
+
if (filteredProducts.length > 0) {
|
|
1441
|
+
filteredProducts.slice(0, 3).forEach((product, index) => {
|
|
1442
|
+
console.log(`[ROOT COLLECTION] Product ${index} AFTER enrichment:`, {
|
|
1443
|
+
id: product.id,
|
|
1444
|
+
title: product.title || product.name,
|
|
1445
|
+
url: product.url || product.link,
|
|
1446
|
+
price: product.price || product.sellingPrice,
|
|
1447
|
+
stock: product.stock,
|
|
1448
|
+
inStock: product.inStock,
|
|
1449
|
+
hasImage: !!(product.imageUrl || product.thumbnailImage1),
|
|
1450
|
+
imageUrl: product.imageUrl || product.thumbnailImage1?.url
|
|
1451
|
+
});
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
// Set products in multiple places to ensure templates can access them
|
|
1456
|
+
context.collection.products = filteredProducts;
|
|
1457
|
+
context.collection.totalProducts = filteredProducts.length;
|
|
1458
|
+
context.products = filteredProducts; // Also set in products array for consistency
|
|
1459
|
+
|
|
1460
|
+
// Also set in collection object for template compatibility
|
|
1461
|
+
if (!context.collection.products) {
|
|
1462
|
+
context.collection.products = filteredProducts;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
console.log(`[ROOT COLLECTION] Final context setup:`);
|
|
1466
|
+
console.log(` - context.products.length: ${context.products?.length || 0}`);
|
|
1467
|
+
console.log(` - context.collection.products.length: ${context.collection.products?.length || 0}`);
|
|
1468
|
+
console.log(` - context.collection.totalProducts: ${context.collection.totalProducts || 0}`);
|
|
1469
|
+
|
|
1470
|
+
if (filteredProducts.length > 0) {
|
|
1471
|
+
const sample = filteredProducts[0];
|
|
1472
|
+
console.log(`[ROOT COLLECTION] Sample enriched product:`, {
|
|
1473
|
+
id: sample.id,
|
|
1474
|
+
title: sample.title || sample.name,
|
|
1475
|
+
url: sample.url || sample.link,
|
|
1476
|
+
price: sample.price || sample.sellingPrice,
|
|
1477
|
+
stock: sample.stock,
|
|
1478
|
+
inStock: sample.inStock,
|
|
1479
|
+
hasImage: !!(sample.imageUrl || sample.thumbnailImage1),
|
|
1480
|
+
imageUrl: sample.imageUrl || sample.thumbnailImage1?.url,
|
|
1481
|
+
allKeys: Object.keys(sample).slice(0, 20)
|
|
1482
|
+
});
|
|
1483
|
+
} else {
|
|
1484
|
+
console.warn(`[ROOT COLLECTION] ⚠️ No products found for category ${category.name}!`);
|
|
1485
|
+
console.warn(`[ROOT COLLECTION] Available products in context: ${context.products?.length || 0}`);
|
|
1486
|
+
if (context.products && context.products.length > 0) {
|
|
1487
|
+
console.warn(`[ROOT COLLECTION] Sample product categoryIds:`, context.products.slice(0, 3).map(p => ({
|
|
1488
|
+
id: p.id,
|
|
1489
|
+
title: p.title || p.name,
|
|
1490
|
+
categoryId: p.categoryId
|
|
1491
|
+
})));
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
// Debug: Log final context before rendering
|
|
1496
|
+
console.log(`[ROOT COLLECTION] Final context before rendering:`);
|
|
1497
|
+
console.log(` - Products count: ${context.products?.length || 0}`);
|
|
1498
|
+
if (context.products && context.products.length > 0) {
|
|
1499
|
+
const firstProduct = context.products[0];
|
|
1500
|
+
console.log(` - First product:`, {
|
|
1501
|
+
id: firstProduct.id,
|
|
1502
|
+
title: firstProduct.title || firstProduct.name,
|
|
1503
|
+
url: firstProduct.url || firstProduct.link,
|
|
1504
|
+
price: firstProduct.price || firstProduct.sellingPrice,
|
|
1505
|
+
stock: firstProduct.stock,
|
|
1506
|
+
inStock: firstProduct.inStock,
|
|
1507
|
+
hasImage: !!(firstProduct.imageUrl || firstProduct.thumbnailImage1)
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
// Try to render products template first (to show product list, not categories list)
|
|
1512
|
+
let html;
|
|
1513
|
+
const templateOptions = ['templates/products', 'templates/collection', 'templates/category', 'templates/categories'];
|
|
1514
|
+
for (const template of templateOptions) {
|
|
1515
|
+
try {
|
|
1516
|
+
html = await renderWithLayout(this.liquid, template, context, this.themePath);
|
|
1517
|
+
break;
|
|
1518
|
+
} catch (error) {
|
|
1519
|
+
if (!error.message?.includes('Template not found')) {
|
|
1520
|
+
throw error;
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
if (html) {
|
|
1526
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1527
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
1528
|
+
return res.send(html);
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
// Not a category - try to find brand by handle
|
|
1533
|
+
const brand = context.brands?.find(b =>
|
|
1534
|
+
(b.handle && b.handle.toLowerCase() === handle.toLowerCase()) ||
|
|
1535
|
+
(b.name && b.name.toLowerCase().replace(/\s+/g, '-') === handle.toLowerCase())
|
|
1536
|
+
);
|
|
1537
|
+
|
|
1538
|
+
if (brand) {
|
|
1539
|
+
// Found a brand - render products list
|
|
1540
|
+
context.brand = brand;
|
|
1541
|
+
|
|
1542
|
+
// Filter products by this brand and enrich them
|
|
1543
|
+
const filteredProducts = (context.products || [])
|
|
1544
|
+
.filter(p =>
|
|
1545
|
+
p.brandId === brand.id ||
|
|
1546
|
+
String(p.brandId).toLowerCase() === String(brand.id).toLowerCase() ||
|
|
1547
|
+
(p.brand && p.brand.toLowerCase() === brand.name.toLowerCase())
|
|
1548
|
+
)
|
|
1549
|
+
.map(p => this.enrichProductData(p));
|
|
1550
|
+
|
|
1551
|
+
context.brand.products = filteredProducts;
|
|
1552
|
+
context.products = filteredProducts;
|
|
1553
|
+
|
|
1554
|
+
console.log(`[ROOT BRAND] Brand: ${brand.name}, Products: ${filteredProducts.length}`);
|
|
1555
|
+
|
|
1556
|
+
// Try to render products template
|
|
1557
|
+
let html;
|
|
1558
|
+
const templateOptions = ['templates/products', 'templates/brand', 'templates/collection', 'templates/brands'];
|
|
1559
|
+
for (const template of templateOptions) {
|
|
1560
|
+
try {
|
|
1561
|
+
html = await renderWithLayout(this.liquid, template, context, this.themePath);
|
|
1562
|
+
break;
|
|
1563
|
+
} catch (error) {
|
|
1564
|
+
if (!error.message?.includes('Template not found')) {
|
|
1565
|
+
throw error;
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
if (html) {
|
|
1571
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1572
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
1573
|
+
return res.send(html);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
// Not a category - check if it's a product handle
|
|
1578
|
+
const product = context.products?.find(p =>
|
|
1579
|
+
(p.handle && p.handle.toLowerCase() === handle.toLowerCase()) ||
|
|
1580
|
+
(p.slug && p.slug.toLowerCase() === handle.toLowerCase())
|
|
1581
|
+
);
|
|
1582
|
+
|
|
1583
|
+
if (product) {
|
|
1584
|
+
// Found a product - render product detail page
|
|
1585
|
+
const productContext = await this.buildContext(req, 'product', { productHandle: product.handle || product.slug || handle });
|
|
1586
|
+
|
|
1587
|
+
let html;
|
|
1588
|
+
const templateOptions = ['templates/product-detail', 'templates/product', 'templates/product-page'];
|
|
1589
|
+
for (const template of templateOptions) {
|
|
1590
|
+
try {
|
|
1591
|
+
html = await renderWithLayout(this.liquid, template, productContext, this.themePath);
|
|
1592
|
+
break;
|
|
1593
|
+
} catch (error) {
|
|
1594
|
+
if (!error.message?.includes('Template not found')) {
|
|
1595
|
+
throw error;
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
if (html) {
|
|
1601
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1602
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
1603
|
+
return res.send(html);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
// Not found - pass to next middleware (will eventually 404)
|
|
1608
|
+
next();
|
|
1609
|
+
} catch (error) {
|
|
1610
|
+
// If there's an error, pass to next middleware
|
|
1611
|
+
next(error);
|
|
1612
|
+
}
|
|
1613
|
+
});
|
|
1614
|
+
|
|
1127
1615
|
// Favicon handler
|
|
1128
1616
|
this.app.get('/favicon.ico', (req, res) => {
|
|
1129
1617
|
res.status(204).end(); // No Content
|
|
@@ -1267,10 +1755,13 @@ class DevServer {
|
|
|
1267
1755
|
|
|
1268
1756
|
try {
|
|
1269
1757
|
// Load settings from settings_data.json
|
|
1758
|
+
// Extract 'current' section to match webstore behavior
|
|
1270
1759
|
const settingsPath = path.join(this.themePath, 'config', 'settings_data.json');
|
|
1271
1760
|
if (fs.existsSync(settingsPath)) {
|
|
1272
1761
|
const settingsData = fs.readFileSync(settingsPath, 'utf8');
|
|
1273
|
-
|
|
1762
|
+
const parsedData = JSON.parse(settingsData);
|
|
1763
|
+
// Extract 'current' section like loadThemeSettings() does
|
|
1764
|
+
context.settings = parsedData.current || parsedData.presets?.default || {};
|
|
1274
1765
|
}
|
|
1275
1766
|
} catch (error) {
|
|
1276
1767
|
console.warn('[CONTEXT] Failed to load settings:', error.message);
|
|
@@ -1350,6 +1841,31 @@ class DevServer {
|
|
|
1350
1841
|
// Load products
|
|
1351
1842
|
const productsResponse = await this.apiClient.getProducts({ limit: 50, offset: 0 });
|
|
1352
1843
|
context.products = productsResponse.products || productsResponse.data?.products || [];
|
|
1844
|
+
|
|
1845
|
+
// Enrich all products using the reusable enrichment function
|
|
1846
|
+
// This ensures consistency with products used in widgets and pages
|
|
1847
|
+
context.products = this.enrichProductsData(context.products);
|
|
1848
|
+
|
|
1849
|
+
// Debug first few products to verify enrichment
|
|
1850
|
+
if (context.products.length > 0) {
|
|
1851
|
+
context.products.slice(0, 3).forEach((product, index) => {
|
|
1852
|
+
console.log(`[CONTEXT] Product ${index + 1} after enrichment:`, {
|
|
1853
|
+
id: product.id,
|
|
1854
|
+
handle: product.handle || 'none',
|
|
1855
|
+
url: product.url || product.link || 'MISSING',
|
|
1856
|
+
title: product.title || product.name || 'MISSING',
|
|
1857
|
+
price: product.price || product.sellingPrice || 'MISSING',
|
|
1858
|
+
hasPrices: !!product.prices,
|
|
1859
|
+
stock: product.stock !== undefined ? product.stock : 'MISSING',
|
|
1860
|
+
stockQuantity: product.stockQuantity !== undefined ? product.stockQuantity : 'MISSING',
|
|
1861
|
+
inStock: product.inStock !== undefined ? product.inStock : 'MISSING',
|
|
1862
|
+
available: product.available !== undefined ? product.available : 'MISSING',
|
|
1863
|
+
hasImage: !!(product.imageUrl || product.thumbnailImage1),
|
|
1864
|
+
imageUrl: product.imageUrl || product.thumbnailImage1?.url || 'MISSING'
|
|
1865
|
+
});
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1353
1869
|
console.log(`[CONTEXT] Loaded ${context.products.length} products`);
|
|
1354
1870
|
|
|
1355
1871
|
// DEBUG: Verify products have quantity/stock field
|
|
@@ -1376,6 +1892,18 @@ class DevServer {
|
|
|
1376
1892
|
try {
|
|
1377
1893
|
const categoriesResponse = await this.apiClient.getCategories({ limit: 50, offset: 0 });
|
|
1378
1894
|
context.categories = categoriesResponse.categories || categoriesResponse.data?.categories || categoriesResponse || [];
|
|
1895
|
+
|
|
1896
|
+
// Ensure all categories have proper URL fields for navigation
|
|
1897
|
+
context.categories = context.categories.map(category => {
|
|
1898
|
+
// Set URL to category handle (e.g., /accessories)
|
|
1899
|
+
if (!category.url && !category.link) {
|
|
1900
|
+
const handle = category.handle || category.name?.toLowerCase().replace(/\s+/g, '-') || category.id;
|
|
1901
|
+
category.url = `/${handle}`;
|
|
1902
|
+
category.link = `/${handle}`;
|
|
1903
|
+
}
|
|
1904
|
+
return category;
|
|
1905
|
+
});
|
|
1906
|
+
|
|
1379
1907
|
context.collections = context.categories; // Also set collections for compatibility
|
|
1380
1908
|
console.log(`[CONTEXT] Loaded ${context.categories.length} categories`);
|
|
1381
1909
|
} catch (error) {
|
|
@@ -1446,40 +1974,57 @@ class DevServer {
|
|
|
1446
1974
|
if (pageType === 'product' && extra.productHandle) {
|
|
1447
1975
|
// Load single product data
|
|
1448
1976
|
try {
|
|
1977
|
+
const searchHandle = extra.productHandle.toLowerCase().trim();
|
|
1978
|
+
console.log(`[CONTEXT] Searching for product with handle: ${searchHandle}`);
|
|
1979
|
+
|
|
1449
1980
|
// First try to find in already loaded products
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
(p.
|
|
1453
|
-
(p.
|
|
1454
|
-
|
|
1981
|
+
// Check multiple fields: handle, slug, id, and also check if handle contains the ID
|
|
1982
|
+
let product = context.products?.find(p => {
|
|
1983
|
+
const pHandle = (p.handle || '').toLowerCase().trim();
|
|
1984
|
+
const pSlug = (p.slug || '').toLowerCase().trim();
|
|
1985
|
+
const pId = String(p.id || '').toLowerCase().trim();
|
|
1986
|
+
const pIdStr = String(p.id || '');
|
|
1987
|
+
|
|
1988
|
+
return pHandle === searchHandle ||
|
|
1989
|
+
pSlug === searchHandle ||
|
|
1990
|
+
pId === searchHandle ||
|
|
1991
|
+
(searchHandle.startsWith('product-') && pId === searchHandle.replace('product-', '')) ||
|
|
1992
|
+
(pHandle && searchHandle.includes(pIdStr)) ||
|
|
1993
|
+
(pSlug && searchHandle.includes(pIdStr));
|
|
1994
|
+
});
|
|
1455
1995
|
|
|
1456
1996
|
// If not found, try loading more products
|
|
1457
1997
|
if (!product) {
|
|
1998
|
+
console.log(`[CONTEXT] Product not found in initial list, loading more products...`);
|
|
1458
1999
|
const productsResponse = await this.apiClient.getProducts({ limit: 100 });
|
|
1459
2000
|
const allProducts = productsResponse.products || productsResponse.data?.products || [];
|
|
1460
|
-
product = allProducts.find(p =>
|
|
1461
|
-
(p.handle
|
|
1462
|
-
(p.slug
|
|
1463
|
-
|
|
1464
|
-
|
|
2001
|
+
product = allProducts.find(p => {
|
|
2002
|
+
const pHandle = (p.handle || '').toLowerCase().trim();
|
|
2003
|
+
const pSlug = (p.slug || '').toLowerCase().trim();
|
|
2004
|
+
const pId = String(p.id || '').toLowerCase().trim();
|
|
2005
|
+
const pIdStr = String(p.id || '');
|
|
2006
|
+
|
|
2007
|
+
return pHandle === searchHandle ||
|
|
2008
|
+
pSlug === searchHandle ||
|
|
2009
|
+
pId === searchHandle ||
|
|
2010
|
+
(searchHandle.startsWith('product-') && pId === searchHandle.replace('product-', '')) ||
|
|
2011
|
+
(pHandle && searchHandle.includes(pIdStr)) ||
|
|
2012
|
+
(pSlug && searchHandle.includes(pIdStr));
|
|
2013
|
+
});
|
|
1465
2014
|
}
|
|
1466
2015
|
|
|
1467
2016
|
if (product) {
|
|
1468
2017
|
context.product = product;
|
|
1469
|
-
console.log(`[CONTEXT] Found product: ${product.title || product.name} (handle: ${product.handle || product.slug})`);
|
|
2018
|
+
console.log(`[CONTEXT] ✅ Found product: ${product.title || product.name} (handle: ${product.handle || product.slug || product.id})`);
|
|
1470
2019
|
} else {
|
|
1471
|
-
console.warn(`[CONTEXT] Product not found with handle/slug/id: ${extra.productHandle}`);
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
context.product = context.products[0];
|
|
1475
|
-
console.log(`[CONTEXT] Using fallback product: ${context.product.title || context.product.name}`);
|
|
1476
|
-
}
|
|
2020
|
+
console.warn(`[CONTEXT] ⚠️ Product not found with handle/slug/id: ${extra.productHandle}`);
|
|
2021
|
+
console.warn(`[CONTEXT] Available product handles: ${(context.products || []).slice(0, 5).map(p => p.handle || p.slug || p.id).join(', ')}`);
|
|
2022
|
+
// Don't use fallback - let the route handler deal with 404
|
|
1477
2023
|
}
|
|
1478
2024
|
} catch (error) {
|
|
1479
|
-
console.
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
}
|
|
2025
|
+
console.error('[CONTEXT] ❌ Failed to load product:', error.message);
|
|
2026
|
+
console.error('[CONTEXT] Error stack:', error.stack);
|
|
2027
|
+
// Don't use fallback on error - let route handle it
|
|
1483
2028
|
}
|
|
1484
2029
|
}
|
|
1485
2030
|
} catch (error) {
|
|
@@ -1589,18 +2134,35 @@ class DevServer {
|
|
|
1589
2134
|
if (type.includes('product') || type === 'featuredproducts' || type === 'bestsellerproducts' || type === 'newproducts' || type === 'simpleproduct' || type === 'singleproduct') {
|
|
1590
2135
|
const widgetProducts = this.getProductsForWidget(widget, products);
|
|
1591
2136
|
|
|
2137
|
+
// Products are already enriched by getProductsForWidget, but ensure they're properly formatted
|
|
2138
|
+
const enrichedProducts = this.enrichProductsData(widgetProducts);
|
|
2139
|
+
|
|
1592
2140
|
// Set products at multiple levels for template compatibility
|
|
1593
|
-
widget.data.products =
|
|
1594
|
-
widget.data.Products =
|
|
1595
|
-
widget.products =
|
|
2141
|
+
widget.data.products = enrichedProducts;
|
|
2142
|
+
widget.data.Products = enrichedProducts;
|
|
2143
|
+
widget.products = enrichedProducts;
|
|
1596
2144
|
|
|
1597
2145
|
// Also set in content for some template variations
|
|
1598
2146
|
widget.data.content = widget.data.content || {};
|
|
1599
|
-
widget.data.content.products =
|
|
1600
|
-
widget.data.content.Products =
|
|
2147
|
+
widget.data.content.products = enrichedProducts;
|
|
2148
|
+
widget.data.content.Products = enrichedProducts;
|
|
1601
2149
|
|
|
1602
2150
|
enrichedCount++;
|
|
1603
|
-
console.log(`[ENRICH] ${widget.type} (${widget.id}): Added ${
|
|
2151
|
+
console.log(`[ENRICH] ${widget.type} (${widget.id}): Added ${enrichedProducts.length} enriched products`);
|
|
2152
|
+
|
|
2153
|
+
// Debug: Log sample product data
|
|
2154
|
+
if (enrichedProducts.length > 0) {
|
|
2155
|
+
const sample = enrichedProducts[0];
|
|
2156
|
+
console.log(`[ENRICH] Sample widget product:`, {
|
|
2157
|
+
id: sample.id,
|
|
2158
|
+
title: sample.title || sample.name,
|
|
2159
|
+
url: sample.url || sample.link,
|
|
2160
|
+
price: sample.price || sample.sellingPrice,
|
|
2161
|
+
stock: sample.stock,
|
|
2162
|
+
inStock: sample.inStock,
|
|
2163
|
+
hasImage: !!(sample.imageUrl || sample.thumbnailImage1)
|
|
2164
|
+
});
|
|
2165
|
+
}
|
|
1604
2166
|
}
|
|
1605
2167
|
|
|
1606
2168
|
// Enrich CategoryList and CategoryListCarousel widgets with categories
|
|
@@ -1637,13 +2199,18 @@ class DevServer {
|
|
|
1637
2199
|
|
|
1638
2200
|
// Enrich RecentlyViewed widgets with products (use first 6 products as mock recent views)
|
|
1639
2201
|
if (type === 'recentlyviewed' || type === 'recently-viewed') {
|
|
1640
|
-
const recentProducts = products.slice(0, 6);
|
|
2202
|
+
const recentProducts = this.enrichProductsData(products.slice(0, 6));
|
|
1641
2203
|
widget.data.products = recentProducts;
|
|
1642
2204
|
widget.data.Products = recentProducts;
|
|
1643
2205
|
widget.products = recentProducts;
|
|
1644
2206
|
|
|
2207
|
+
// Also set in content
|
|
2208
|
+
widget.data.content = widget.data.content || {};
|
|
2209
|
+
widget.data.content.products = recentProducts;
|
|
2210
|
+
widget.data.content.Products = recentProducts;
|
|
2211
|
+
|
|
1645
2212
|
enrichedCount++;
|
|
1646
|
-
console.log(`[ENRICH] ${widget.type} (${widget.id}): Added ${recentProducts.length} recently viewed products`);
|
|
2213
|
+
console.log(`[ENRICH] ${widget.type} (${widget.id}): Added ${recentProducts.length} enriched recently viewed products`);
|
|
1647
2214
|
}
|
|
1648
2215
|
});
|
|
1649
2216
|
});
|
|
@@ -1651,37 +2218,268 @@ class DevServer {
|
|
|
1651
2218
|
console.log(`[CONTEXT] ✅ Enriched ${enrichedCount} widgets with data`);
|
|
1652
2219
|
}
|
|
1653
2220
|
|
|
2221
|
+
/**
|
|
2222
|
+
* Enrich a single product with all required data (URL, name/title, price, images, stock)
|
|
2223
|
+
* This ensures consistency between widgets and pages
|
|
2224
|
+
* @param {Object} product - Product object to enrich
|
|
2225
|
+
* @returns {Object} Enriched product (creates a copy to avoid mutation)
|
|
2226
|
+
*/
|
|
2227
|
+
enrichProductData(product) {
|
|
2228
|
+
// Create a copy to avoid mutating the original
|
|
2229
|
+
const enriched = { ...product };
|
|
2230
|
+
|
|
2231
|
+
// CRITICAL: Ensure productId exists - it's the most reliable identifier
|
|
2232
|
+
if (!enriched.productId && enriched.id) {
|
|
2233
|
+
// Try to extract numeric ID from string ID (e.g., "product-1" -> 1)
|
|
2234
|
+
const numericMatch = String(enriched.id).match(/\d+/);
|
|
2235
|
+
if (numericMatch) {
|
|
2236
|
+
enriched.productId = parseInt(numericMatch[0], 10);
|
|
2237
|
+
} else {
|
|
2238
|
+
// Use the ID as-is if it's already numeric
|
|
2239
|
+
enriched.productId = enriched.id;
|
|
2240
|
+
}
|
|
2241
|
+
} else if (!enriched.productId && !enriched.id) {
|
|
2242
|
+
console.warn(`[ENRICH] ⚠️ Product missing both id and productId:`, {
|
|
2243
|
+
keys: Object.keys(enriched).slice(0, 15),
|
|
2244
|
+
title: enriched.title || enriched.name
|
|
2245
|
+
});
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
// DEBUG: Log incoming product to diagnose missing fields
|
|
2249
|
+
if (!product.id && !product.handle && !product.slug && !product.productId) {
|
|
2250
|
+
console.warn(`[ENRICH] ⚠️ Product missing all identifiers (id/handle/slug/productId):`, {
|
|
2251
|
+
keys: Object.keys(product).slice(0, 15),
|
|
2252
|
+
title: product.title || product.name
|
|
2253
|
+
});
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
// Ensure URL field exists - CRITICAL for product navigation
|
|
2257
|
+
// Treat '#' as missing URL and regenerate it
|
|
2258
|
+
const hasValidUrl = enriched.url && enriched.url !== '#' && enriched.url !== '';
|
|
2259
|
+
const hasValidLink = enriched.link && enriched.link !== '#' && enriched.link !== '';
|
|
2260
|
+
|
|
2261
|
+
if (!hasValidUrl && !hasValidLink) {
|
|
2262
|
+
const handle = enriched.handle || enriched.slug || enriched.id;
|
|
2263
|
+
if (handle && handle !== '#' && handle !== '') {
|
|
2264
|
+
enriched.url = `/products/${handle}`;
|
|
2265
|
+
enriched.link = `/products/${handle}`;
|
|
2266
|
+
} else {
|
|
2267
|
+
// Last resort: use numeric ID if available
|
|
2268
|
+
const numericId = enriched.productId || enriched.id;
|
|
2269
|
+
if (numericId) {
|
|
2270
|
+
const fallbackHandle = `product-${numericId}`.toLowerCase();
|
|
2271
|
+
enriched.url = `/products/${fallbackHandle}`;
|
|
2272
|
+
enriched.link = `/products/${fallbackHandle}`;
|
|
2273
|
+
enriched.handle = enriched.handle || fallbackHandle; // Also set handle if missing
|
|
2274
|
+
enriched.slug = enriched.slug || fallbackHandle; // Also set slug if missing
|
|
2275
|
+
} else {
|
|
2276
|
+
enriched.url = '#';
|
|
2277
|
+
enriched.link = '#';
|
|
2278
|
+
console.warn(`[ENRICH] ⚠️ Product has no id/handle/slug/productId, URL set to '#'`, {
|
|
2279
|
+
productKeys: Object.keys(enriched).slice(0, 10)
|
|
2280
|
+
});
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
} else if (!hasValidUrl && hasValidLink) {
|
|
2284
|
+
enriched.url = enriched.link;
|
|
2285
|
+
} else if (hasValidUrl && !hasValidLink) {
|
|
2286
|
+
enriched.link = enriched.url;
|
|
2287
|
+
} else if (enriched.url === '#' || enriched.link === '#') {
|
|
2288
|
+
// If one is valid but the other is '#', update the '#' one
|
|
2289
|
+
if (enriched.url === '#') enriched.url = enriched.link;
|
|
2290
|
+
if (enriched.link === '#') enriched.link = enriched.url;
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
// Ensure handle/slug exist if missing (for template fallback logic)
|
|
2294
|
+
// Also normalize them (remove '#' or empty strings)
|
|
2295
|
+
if (!enriched.handle || enriched.handle === '#' || enriched.handle === '') {
|
|
2296
|
+
if (enriched.slug && enriched.slug !== '#' && enriched.slug !== '') {
|
|
2297
|
+
enriched.handle = enriched.slug;
|
|
2298
|
+
} else if (enriched.id && enriched.id !== '#') {
|
|
2299
|
+
enriched.handle = String(enriched.id).toLowerCase();
|
|
2300
|
+
} else if (enriched.productId) {
|
|
2301
|
+
enriched.handle = `product-${enriched.productId}`.toLowerCase();
|
|
2302
|
+
} else if (!enriched.handle) {
|
|
2303
|
+
// Generate from URL if it was set above
|
|
2304
|
+
if (enriched.url && enriched.url !== '#') {
|
|
2305
|
+
enriched.handle = enriched.url.replace(/^\/products\//, '').toLowerCase();
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
if (!enriched.slug || enriched.slug === '#' || enriched.slug === '') {
|
|
2311
|
+
if (enriched.handle && enriched.handle !== '#' && enriched.handle !== '') {
|
|
2312
|
+
enriched.slug = enriched.handle;
|
|
2313
|
+
} else if (enriched.id && enriched.id !== '#') {
|
|
2314
|
+
enriched.slug = String(enriched.id).toLowerCase();
|
|
2315
|
+
} else if (enriched.productId) {
|
|
2316
|
+
enriched.slug = `product-${enriched.productId}`.toLowerCase();
|
|
2317
|
+
} else if (!enriched.slug) {
|
|
2318
|
+
// Generate from URL if it was set above
|
|
2319
|
+
if (enriched.url && enriched.url !== '#') {
|
|
2320
|
+
enriched.slug = enriched.url.replace(/^\/products\//, '').toLowerCase();
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
// Ensure name/title exists - CRITICAL for product display
|
|
2326
|
+
if (!enriched.name && !enriched.title) {
|
|
2327
|
+
enriched.name = `Product ${enriched.id || 'Unknown'}`;
|
|
2328
|
+
enriched.title = enriched.name;
|
|
2329
|
+
} else if (!enriched.name) {
|
|
2330
|
+
enriched.name = enriched.title || `Product ${enriched.id || 'Unknown'}`;
|
|
2331
|
+
} else if (!enriched.title) {
|
|
2332
|
+
enriched.title = enriched.name;
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
// Ensure both variations and variants exist (product-card uses variations)
|
|
2336
|
+
if (enriched.variants && !enriched.variations) {
|
|
2337
|
+
enriched.variations = enriched.variants;
|
|
2338
|
+
} else if (enriched.variations && !enriched.variants) {
|
|
2339
|
+
enriched.variants = enriched.variations;
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
// Ensure price exists - CRITICAL for product display
|
|
2343
|
+
if (!enriched.price && !enriched.sellingPrice) {
|
|
2344
|
+
// Try to get from variants/variations first
|
|
2345
|
+
const variantsOrVariations = enriched.variants || enriched.variations;
|
|
2346
|
+
if (variantsOrVariations && variantsOrVariations.length > 0) {
|
|
2347
|
+
enriched.price = variantsOrVariations[0].price || variantsOrVariations[0].sellingPrice || 0;
|
|
2348
|
+
enriched.sellingPrice = enriched.price;
|
|
2349
|
+
} else {
|
|
2350
|
+
enriched.price = 0;
|
|
2351
|
+
enriched.sellingPrice = 0;
|
|
2352
|
+
}
|
|
2353
|
+
} else if (!enriched.price) {
|
|
2354
|
+
enriched.price = enriched.sellingPrice;
|
|
2355
|
+
} else if (!enriched.sellingPrice) {
|
|
2356
|
+
enriched.sellingPrice = enriched.price;
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
// Ensure prices object exists for product-card compatibility - CRITICAL
|
|
2360
|
+
// Make sure price is NOT 0 unless it's actually 0 (check if undefined/null)
|
|
2361
|
+
const finalPrice = (enriched.price !== undefined && enriched.price !== null)
|
|
2362
|
+
? enriched.price
|
|
2363
|
+
: ((enriched.sellingPrice !== undefined && enriched.sellingPrice !== null)
|
|
2364
|
+
? enriched.sellingPrice
|
|
2365
|
+
: 0);
|
|
2366
|
+
|
|
2367
|
+
if (!enriched.prices) {
|
|
2368
|
+
const mrpValue = enriched.compareAtPrice || enriched.comparePrice || enriched.mrp || 0;
|
|
2369
|
+
enriched.prices = {
|
|
2370
|
+
price: finalPrice,
|
|
2371
|
+
mrp: mrpValue,
|
|
2372
|
+
currency: enriched.currency || 'USD'
|
|
2373
|
+
};
|
|
2374
|
+
} else {
|
|
2375
|
+
// Update prices object if it exists but is missing fields
|
|
2376
|
+
if (enriched.prices.price === undefined || enriched.prices.price === null) {
|
|
2377
|
+
enriched.prices.price = finalPrice;
|
|
2378
|
+
}
|
|
2379
|
+
if (enriched.prices.mrp === undefined || enriched.prices.mrp === null) {
|
|
2380
|
+
enriched.prices.mrp = enriched.compareAtPrice || enriched.comparePrice || enriched.mrp || 0;
|
|
2381
|
+
}
|
|
2382
|
+
if (!enriched.prices.currency) {
|
|
2383
|
+
enriched.prices.currency = enriched.currency || 'USD';
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
// Ensure price/sellingPrice fields also exist (not just prices object)
|
|
2388
|
+
if (enriched.price === undefined || enriched.price === null) {
|
|
2389
|
+
enriched.price = finalPrice;
|
|
2390
|
+
}
|
|
2391
|
+
if (enriched.sellingPrice === undefined || enriched.sellingPrice === null) {
|
|
2392
|
+
enriched.sellingPrice = finalPrice;
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
// Ensure image exists - CRITICAL for product display
|
|
2396
|
+
if (!enriched.imageUrl && !enriched.thumbnailImage1 && (!enriched.images || enriched.images.length === 0)) {
|
|
2397
|
+
// Fallback to picsum placeholder
|
|
2398
|
+
const imageId = enriched.id ? String(enriched.id).replace(/\D/g, '') || '1' : '1';
|
|
2399
|
+
enriched.imageUrl = `https://picsum.photos/seed/${imageId}/800/800`;
|
|
2400
|
+
enriched.thumbnailImage1 = {
|
|
2401
|
+
url: enriched.imageUrl,
|
|
2402
|
+
altText: enriched.title || enriched.name || 'Product image'
|
|
2403
|
+
};
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
// Ensure stock/availability data exists - CRITICAL to prevent incorrect "SOLD OUT" display
|
|
2407
|
+
if (enriched.stock === undefined && enriched.stockQuantity === undefined) {
|
|
2408
|
+
// Calculate from variants/variations if available
|
|
2409
|
+
const variantsOrVariations = enriched.variants || enriched.variations;
|
|
2410
|
+
if (variantsOrVariations && variantsOrVariations.length > 0) {
|
|
2411
|
+
const totalStock = variantsOrVariations.reduce((sum, v) => sum + (v.stock || v.quantity || 0), 0);
|
|
2412
|
+
enriched.stock = totalStock;
|
|
2413
|
+
enriched.stockQuantity = totalStock;
|
|
2414
|
+
enriched.inStock = totalStock > 0;
|
|
2415
|
+
enriched.available = totalStock > 0;
|
|
2416
|
+
} else {
|
|
2417
|
+
// Default to in stock if no stock info (prevents false "SOLD OUT" display)
|
|
2418
|
+
enriched.stock = 10;
|
|
2419
|
+
enriched.stockQuantity = 10;
|
|
2420
|
+
enriched.inStock = true;
|
|
2421
|
+
enriched.available = true;
|
|
2422
|
+
}
|
|
2423
|
+
} else {
|
|
2424
|
+
// Ensure inStock/available flags are set based on stock value
|
|
2425
|
+
// CRITICAL: Use nullish coalescing to handle 0 values correctly
|
|
2426
|
+
const stockValue = enriched.stock !== undefined ? enriched.stock : (enriched.stockQuantity !== undefined ? enriched.stockQuantity : 0);
|
|
2427
|
+
enriched.inStock = stockValue > 0;
|
|
2428
|
+
enriched.available = stockValue > 0;
|
|
2429
|
+
|
|
2430
|
+
// Also ensure stockQuantity is set if only stock is set (and vice versa)
|
|
2431
|
+
if (enriched.stock !== undefined && enriched.stockQuantity === undefined) {
|
|
2432
|
+
enriched.stockQuantity = enriched.stock;
|
|
2433
|
+
} else if (enriched.stockQuantity !== undefined && enriched.stock === undefined) {
|
|
2434
|
+
enriched.stock = enriched.stockQuantity;
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
return enriched;
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
/**
|
|
2442
|
+
* Enrich multiple products with all required data
|
|
2443
|
+
* @param {Array} products - Array of products to enrich
|
|
2444
|
+
* @returns {Array} Array of enriched products
|
|
2445
|
+
*/
|
|
2446
|
+
enrichProductsData(products) {
|
|
2447
|
+
if (!Array.isArray(products)) return [];
|
|
2448
|
+
return products.map(product => this.enrichProductData(product));
|
|
2449
|
+
}
|
|
2450
|
+
|
|
1654
2451
|
/**
|
|
1655
2452
|
* Get products for a specific widget based on its settings
|
|
1656
2453
|
*/
|
|
1657
2454
|
getProductsForWidget(widget, products) {
|
|
1658
2455
|
// Get the limit from widget settings, default to 12
|
|
1659
|
-
const limit = widget.settings?.limit || widget.data?.content?.Limit || 12;
|
|
2456
|
+
const limit = widget.settings?.limit || widget.data?.content?.Limit || widget.settings?.numberOfProducts || widget.settings?.NumberOfProducts || 12;
|
|
1660
2457
|
|
|
1661
2458
|
// Get products based on widget type
|
|
1662
2459
|
const type = (widget.type || '').toLowerCase();
|
|
1663
2460
|
|
|
2461
|
+
let widgetProducts = [];
|
|
2462
|
+
|
|
1664
2463
|
if (type.includes('featured') || type === 'featuredproducts') {
|
|
1665
2464
|
// Featured products - filter by featured tag or just take first N
|
|
1666
2465
|
const featured = products.filter(p => p.tags?.includes('featured') || p.featured);
|
|
1667
|
-
|
|
1668
|
-
}
|
|
1669
|
-
|
|
1670
|
-
if (type.includes('bestseller') || type === 'bestsellerproducts') {
|
|
2466
|
+
widgetProducts = featured.length > 0 ? featured.slice(0, limit) : products.slice(0, limit);
|
|
2467
|
+
} else if (type.includes('bestseller') || type === 'bestsellerproducts') {
|
|
1671
2468
|
// Best seller products - sort by sales or just take random N
|
|
1672
|
-
|
|
1673
|
-
}
|
|
1674
|
-
|
|
1675
|
-
if (type.includes('new') || type === 'newproducts') {
|
|
2469
|
+
widgetProducts = products.slice(0, limit);
|
|
2470
|
+
} else if (type.includes('new') || type === 'newproducts') {
|
|
1676
2471
|
// New products - sort by created date
|
|
1677
2472
|
const sorted = [...products].sort((a, b) =>
|
|
1678
2473
|
new Date(b.createdAt || 0) - new Date(a.createdAt || 0)
|
|
1679
2474
|
);
|
|
1680
|
-
|
|
2475
|
+
widgetProducts = sorted.slice(0, limit);
|
|
2476
|
+
} else {
|
|
2477
|
+
// Default: return first N products
|
|
2478
|
+
widgetProducts = products.slice(0, limit);
|
|
1681
2479
|
}
|
|
1682
2480
|
|
|
1683
|
-
//
|
|
1684
|
-
return
|
|
2481
|
+
// Enrich all products with required data before returning
|
|
2482
|
+
return this.enrichProductsData(widgetProducts);
|
|
1685
2483
|
}
|
|
1686
2484
|
|
|
1687
2485
|
/**
|
|
@@ -2586,14 +3384,36 @@ class DevServer {
|
|
|
2586
3384
|
*/
|
|
2587
3385
|
async stop() {
|
|
2588
3386
|
return new Promise((resolve) => {
|
|
3387
|
+
let resolved = false;
|
|
3388
|
+
const safeResolve = () => {
|
|
3389
|
+
if (!resolved) {
|
|
3390
|
+
resolved = true;
|
|
3391
|
+
console.log(chalk.yellow('\n👋 Server stopped'));
|
|
3392
|
+
resolve();
|
|
3393
|
+
}
|
|
3394
|
+
};
|
|
3395
|
+
|
|
3396
|
+
// Increase max listeners to prevent warning if multiple shutdown attempts
|
|
3397
|
+
if (this.server && this.server.setMaxListeners) {
|
|
3398
|
+
this.server.setMaxListeners(20);
|
|
3399
|
+
}
|
|
3400
|
+
|
|
2589
3401
|
// Stop file watcher
|
|
2590
3402
|
if (this.fileWatcher && typeof this.fileWatcher.close === 'function') {
|
|
2591
|
-
|
|
3403
|
+
try {
|
|
3404
|
+
this.fileWatcher.close();
|
|
3405
|
+
} catch (error) {
|
|
3406
|
+
// Ignore errors when closing watcher
|
|
3407
|
+
}
|
|
2592
3408
|
}
|
|
2593
3409
|
|
|
2594
3410
|
// Close Socket.IO
|
|
2595
3411
|
if (this.io && typeof this.io.close === 'function') {
|
|
2596
|
-
|
|
3412
|
+
try {
|
|
3413
|
+
this.io.close();
|
|
3414
|
+
} catch (error) {
|
|
3415
|
+
// Ignore errors when closing Socket.IO
|
|
3416
|
+
}
|
|
2597
3417
|
}
|
|
2598
3418
|
|
|
2599
3419
|
// Stop mock API
|
|
@@ -2601,14 +3421,37 @@ class DevServer {
|
|
|
2601
3421
|
this.mockApi.stop().catch(() => {});
|
|
2602
3422
|
}
|
|
2603
3423
|
|
|
2604
|
-
// Close HTTP server
|
|
3424
|
+
// Close HTTP server - check if it's actually listening/running
|
|
2605
3425
|
if (this.server) {
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
3426
|
+
// Check if server is listening before trying to close
|
|
3427
|
+
const isListening = this.server.listening || false;
|
|
3428
|
+
|
|
3429
|
+
if (!isListening) {
|
|
3430
|
+
// Server is already closed/not running
|
|
3431
|
+
safeResolve();
|
|
3432
|
+
return;
|
|
3433
|
+
}
|
|
3434
|
+
|
|
3435
|
+
// Remove all existing listeners to prevent memory leak warning
|
|
3436
|
+
this.server.removeAllListeners('close');
|
|
3437
|
+
|
|
3438
|
+
// Close the server - use callback instead of event listener to avoid duplicate calls
|
|
3439
|
+
this.server.close((err) => {
|
|
3440
|
+
if (err) {
|
|
3441
|
+
// Ignore ERR_SERVER_NOT_RUNNING errors as they're harmless
|
|
3442
|
+
if (err.code !== 'ERR_SERVER_NOT_RUNNING') {
|
|
3443
|
+
console.error(chalk.red('Error closing server:'), err.message);
|
|
3444
|
+
}
|
|
3445
|
+
}
|
|
3446
|
+
safeResolve();
|
|
2609
3447
|
});
|
|
3448
|
+
|
|
3449
|
+
// Fallback timeout in case close callback doesn't fire
|
|
3450
|
+
setTimeout(() => {
|
|
3451
|
+
safeResolve();
|
|
3452
|
+
}, 2000);
|
|
2610
3453
|
} else {
|
|
2611
|
-
|
|
3454
|
+
safeResolve();
|
|
2612
3455
|
}
|
|
2613
3456
|
});
|
|
2614
3457
|
}
|