@o2vend/theme-cli 1.0.32 → 1.0.33
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 +478 -46
- 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,53 @@ 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
|
|
466
|
+
const categoryFilter = 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
|
|
484
|
+
const filteredProducts = context.products.filter(p =>
|
|
485
|
+
p.categoryId === category.id ||
|
|
486
|
+
String(p.categoryId).toLowerCase() === String(category.id).toLowerCase()
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
context.products = filteredProducts;
|
|
490
|
+
console.log(`[PRODUCTS PAGE] Filtered products: ${filteredProducts.length} products in category`);
|
|
491
|
+
|
|
492
|
+
// Update collection context
|
|
493
|
+
context.collection = context.collection || {};
|
|
494
|
+
context.collection.title = category.name || 'Products';
|
|
495
|
+
context.collection.handle = category.handle || categoryFilter;
|
|
496
|
+
context.collection.description = category.description;
|
|
497
|
+
context.collection.totalProducts = filteredProducts.length;
|
|
498
|
+
} else {
|
|
499
|
+
console.warn(`[PRODUCTS PAGE] Category not found: ${categoryFilter}, showing all products`);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Ensure all products have proper URL field if missing
|
|
504
|
+
context.products = (context.products || []).map(product => {
|
|
505
|
+
if (!product.url && !product.link) {
|
|
506
|
+
const handle = product.handle || product.slug || product.id;
|
|
507
|
+
product.url = `/products/${handle}`;
|
|
508
|
+
product.link = `/products/${handle}`;
|
|
509
|
+
}
|
|
510
|
+
return product;
|
|
511
|
+
});
|
|
457
512
|
|
|
458
513
|
// DEBUG: Log context data for /products page
|
|
459
514
|
console.log(`[PRODUCTS PAGE] Context summary:`);
|
|
@@ -468,8 +523,30 @@ class DevServer {
|
|
|
468
523
|
if (!context.products || context.products.length === 0) {
|
|
469
524
|
console.warn('[PRODUCTS PAGE] ⚠️ No products in context, attempting to reload...');
|
|
470
525
|
try {
|
|
471
|
-
const
|
|
526
|
+
const queryParams = {};
|
|
527
|
+
if (categoryFilter) {
|
|
528
|
+
// Find category and filter by categoryId
|
|
529
|
+
const category = context.categories?.find(c =>
|
|
530
|
+
(c.name && c.name.toLowerCase() === categoryFilter.toLowerCase()) ||
|
|
531
|
+
(c.handle && c.handle.toLowerCase() === categoryFilter.toLowerCase())
|
|
532
|
+
);
|
|
533
|
+
if (category) {
|
|
534
|
+
queryParams.categoryId = category.id;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
const productsResponse = await this.apiClient.getProducts({ limit: 50, offset: 0, ...queryParams });
|
|
472
538
|
context.products = productsResponse.products || productsResponse.data?.products || [];
|
|
539
|
+
|
|
540
|
+
// Ensure URLs are set
|
|
541
|
+
context.products = context.products.map(product => {
|
|
542
|
+
if (!product.url && !product.link) {
|
|
543
|
+
const handle = product.handle || product.slug || product.id;
|
|
544
|
+
product.url = `/products/${handle}`;
|
|
545
|
+
product.link = `/products/${handle}`;
|
|
546
|
+
}
|
|
547
|
+
return product;
|
|
548
|
+
});
|
|
549
|
+
|
|
473
550
|
console.log(`[PRODUCTS PAGE] ✅ Reloaded ${context.products.length} products`);
|
|
474
551
|
} catch (error) {
|
|
475
552
|
console.error('[PRODUCTS PAGE] ❌ Failed to load products:', error.message);
|
|
@@ -479,10 +556,14 @@ class DevServer {
|
|
|
479
556
|
if (context.products.length > 0) {
|
|
480
557
|
const sampleProduct = context.products[0];
|
|
481
558
|
const hasStock = 'stock' in sampleProduct || 'quantity' in sampleProduct;
|
|
559
|
+
const hasImage = sampleProduct.thumbnailImage1 || sampleProduct.imageUrl || (sampleProduct.images && sampleProduct.images.length > 0);
|
|
560
|
+
const hasUrl = sampleProduct.url || sampleProduct.link;
|
|
482
561
|
// Use nullish coalescing to show 0 values correctly
|
|
483
562
|
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]
|
|
563
|
+
console.log(`[PRODUCTS PAGE DEBUG] Sample product - Title: ${sampleProduct.title || sampleProduct.name}`);
|
|
564
|
+
console.log(`[PRODUCTS PAGE DEBUG] - Has stock: ${hasStock}, Stock: ${stockValue}`);
|
|
565
|
+
console.log(`[PRODUCTS PAGE DEBUG] - Has image: ${hasImage}, Has URL: ${hasUrl}`);
|
|
566
|
+
console.log(`[PRODUCTS PAGE DEBUG] - URL: ${sampleProduct.url || sampleProduct.link || 'missing'}`);
|
|
486
567
|
|
|
487
568
|
// Check a few more products to see stock distribution
|
|
488
569
|
const stockCounts = context.products.slice(0, 5).map(p => p.stock ?? 0);
|
|
@@ -527,13 +608,20 @@ class DevServer {
|
|
|
527
608
|
if (context.products && context.products.length > 0) {
|
|
528
609
|
context.collection = context.collection || {};
|
|
529
610
|
context.collection.products = context.products;
|
|
530
|
-
context.collection.title
|
|
531
|
-
|
|
532
|
-
|
|
611
|
+
if (!context.collection.title) {
|
|
612
|
+
context.collection.title = categoryFilter ? `${categoryFilter} Products` : 'All Products';
|
|
613
|
+
}
|
|
614
|
+
if (!context.collection.handle) {
|
|
615
|
+
context.collection.handle = categoryFilter || 'all';
|
|
616
|
+
}
|
|
617
|
+
if (!context.collection.totalProducts) {
|
|
618
|
+
context.collection.totalProducts = context.products.length;
|
|
619
|
+
}
|
|
533
620
|
}
|
|
534
621
|
|
|
535
622
|
const html = await renderWithLayout(this.liquid, 'templates/products', context, this.themePath);
|
|
536
623
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
624
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
537
625
|
res.send(html);
|
|
538
626
|
} catch (error) {
|
|
539
627
|
// If products template doesn't exist, try collections template as fallback
|
|
@@ -542,6 +630,7 @@ class DevServer {
|
|
|
542
630
|
const context = await this.buildContext(req, 'collection');
|
|
543
631
|
const html = await renderWithLayout(this.liquid, 'templates/collection', context, this.themePath);
|
|
544
632
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
633
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
545
634
|
res.send(html);
|
|
546
635
|
} catch (fallbackError) {
|
|
547
636
|
next(error);
|
|
@@ -555,7 +644,46 @@ class DevServer {
|
|
|
555
644
|
// Product detail page
|
|
556
645
|
this.app.get('/products/:handle', async (req, res, next) => {
|
|
557
646
|
try {
|
|
558
|
-
const
|
|
647
|
+
const handle = req.params.handle;
|
|
648
|
+
console.log(`[PRODUCT PAGE] Looking up product with handle: ${handle}`);
|
|
649
|
+
|
|
650
|
+
const context = await this.buildContext(req, 'product', { productHandle: handle });
|
|
651
|
+
|
|
652
|
+
// Check if product was found
|
|
653
|
+
if (!context.product) {
|
|
654
|
+
console.warn(`[PRODUCT PAGE] Product not found with handle: ${handle}`);
|
|
655
|
+
// Try to find product by ID if handle looks like an ID
|
|
656
|
+
if (handle.match(/^\d+$/) || handle.startsWith('product-')) {
|
|
657
|
+
const productId = handle.replace(/^product-/, '');
|
|
658
|
+
console.log(`[PRODUCT PAGE] Trying to find product by ID: ${productId}`);
|
|
659
|
+
const allProducts = context.products || [];
|
|
660
|
+
const productById = allProducts.find(p =>
|
|
661
|
+
String(p.id) === productId ||
|
|
662
|
+
String(p.id) === handle ||
|
|
663
|
+
(p.handle && p.handle.includes(productId))
|
|
664
|
+
);
|
|
665
|
+
if (productById) {
|
|
666
|
+
context.product = productById;
|
|
667
|
+
console.log(`[PRODUCT PAGE] Found product by ID: ${productById.title || productById.name}`);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// If still not found, return 404
|
|
672
|
+
if (!context.product) {
|
|
673
|
+
return res.status(404).send(`
|
|
674
|
+
<!DOCTYPE html>
|
|
675
|
+
<html>
|
|
676
|
+
<head><title>Product Not Found</title></head>
|
|
677
|
+
<body>
|
|
678
|
+
<h1>Product Not Found</h1>
|
|
679
|
+
<p>Product with handle "${handle}" was not found.</p>
|
|
680
|
+
<p><a href="/products">Back to Products</a></p>
|
|
681
|
+
</body>
|
|
682
|
+
</html>
|
|
683
|
+
`);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
559
687
|
// Try multiple template names: product-detail, product, product-page
|
|
560
688
|
let html;
|
|
561
689
|
const templateOptions = ['templates/product-detail', 'templates/product', 'templates/product-page'];
|
|
@@ -573,8 +701,10 @@ class DevServer {
|
|
|
573
701
|
throw new Error(`Product template not found. Tried: ${templateOptions.join(', ')}`);
|
|
574
702
|
}
|
|
575
703
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
704
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
576
705
|
res.send(html);
|
|
577
706
|
} catch (error) {
|
|
707
|
+
console.error(`[PRODUCT PAGE] Error rendering product page:`, error);
|
|
578
708
|
next(error);
|
|
579
709
|
}
|
|
580
710
|
});
|
|
@@ -600,6 +730,7 @@ class DevServer {
|
|
|
600
730
|
throw new Error(`Product template not found. Tried: ${templateOptions.join(', ')}`);
|
|
601
731
|
}
|
|
602
732
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
733
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
603
734
|
res.send(html);
|
|
604
735
|
} catch (error) {
|
|
605
736
|
next(error);
|
|
@@ -626,6 +757,7 @@ class DevServer {
|
|
|
626
757
|
throw new Error(`Product template not found. Tried: ${templateOptions.join(', ')}`);
|
|
627
758
|
}
|
|
628
759
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
760
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
629
761
|
res.send(html);
|
|
630
762
|
} catch (error) {
|
|
631
763
|
next(error);
|
|
@@ -652,6 +784,7 @@ class DevServer {
|
|
|
652
784
|
}
|
|
653
785
|
}
|
|
654
786
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
787
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
655
788
|
res.send(html);
|
|
656
789
|
} catch (error) {
|
|
657
790
|
next(error);
|
|
@@ -670,6 +803,39 @@ class DevServer {
|
|
|
670
803
|
if (category) {
|
|
671
804
|
context.category = category;
|
|
672
805
|
context.collection = category;
|
|
806
|
+
|
|
807
|
+
// Filter products by this category
|
|
808
|
+
const filteredProducts = (context.products || []).filter(p =>
|
|
809
|
+
p.categoryId === category.id ||
|
|
810
|
+
String(p.categoryId).toLowerCase() === String(category.id).toLowerCase()
|
|
811
|
+
);
|
|
812
|
+
|
|
813
|
+
// Ensure all products have proper URL field if missing
|
|
814
|
+
filteredProducts.forEach(product => {
|
|
815
|
+
if (!product.url && !product.link) {
|
|
816
|
+
const handle = product.handle || product.slug || product.id;
|
|
817
|
+
product.url = `/products/${handle}`;
|
|
818
|
+
product.link = `/products/${handle}`;
|
|
819
|
+
}
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
context.collection.products = filteredProducts;
|
|
823
|
+
context.collection.totalProducts = filteredProducts.length;
|
|
824
|
+
context.products = filteredProducts; // Also set in products array for consistency
|
|
825
|
+
|
|
826
|
+
console.log(`[COLLECTION PAGE] Category: ${category.name}, Products: ${filteredProducts.length}`);
|
|
827
|
+
} else {
|
|
828
|
+
// No category found, use all products
|
|
829
|
+
context.products = (context.products || []).map(product => {
|
|
830
|
+
if (!product.url && !product.link) {
|
|
831
|
+
const handle = product.handle || product.slug || product.id;
|
|
832
|
+
product.url = `/products/${handle}`;
|
|
833
|
+
product.link = `/products/${handle}`;
|
|
834
|
+
}
|
|
835
|
+
return product;
|
|
836
|
+
});
|
|
837
|
+
context.collection = context.collection || {};
|
|
838
|
+
context.collection.products = context.products;
|
|
673
839
|
}
|
|
674
840
|
|
|
675
841
|
let html;
|
|
@@ -688,6 +854,7 @@ class DevServer {
|
|
|
688
854
|
throw new Error(`Category/Collection template not found. Tried: ${templateOptions.join(', ')}`);
|
|
689
855
|
}
|
|
690
856
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
857
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
691
858
|
res.send(html);
|
|
692
859
|
} catch (error) {
|
|
693
860
|
next(error);
|
|
@@ -714,6 +881,7 @@ class DevServer {
|
|
|
714
881
|
throw new Error(`Categories template not found. Tried: ${templateOptions.join(', ')}`);
|
|
715
882
|
}
|
|
716
883
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
884
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
717
885
|
res.send(html);
|
|
718
886
|
} catch (error) {
|
|
719
887
|
next(error);
|
|
@@ -730,6 +898,39 @@ class DevServer {
|
|
|
730
898
|
if (category) {
|
|
731
899
|
context.category = category;
|
|
732
900
|
context.collection = category;
|
|
901
|
+
|
|
902
|
+
// Filter products by this category
|
|
903
|
+
const filteredProducts = (context.products || []).filter(p =>
|
|
904
|
+
p.categoryId === category.id ||
|
|
905
|
+
String(p.categoryId).toLowerCase() === String(category.id).toLowerCase()
|
|
906
|
+
);
|
|
907
|
+
|
|
908
|
+
// Ensure all products have proper URL field if missing
|
|
909
|
+
filteredProducts.forEach(product => {
|
|
910
|
+
if (!product.url && !product.link) {
|
|
911
|
+
const handle = product.handle || product.slug || product.id;
|
|
912
|
+
product.url = `/products/${handle}`;
|
|
913
|
+
product.link = `/products/${handle}`;
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
context.collection.products = filteredProducts;
|
|
918
|
+
context.collection.totalProducts = filteredProducts.length;
|
|
919
|
+
context.products = filteredProducts; // Also set in products array for consistency
|
|
920
|
+
|
|
921
|
+
console.log(`[CATEGORY PAGE] Category: ${category.name}, Products: ${filteredProducts.length}`);
|
|
922
|
+
} else {
|
|
923
|
+
// No category found, use all products
|
|
924
|
+
context.products = (context.products || []).map(product => {
|
|
925
|
+
if (!product.url && !product.link) {
|
|
926
|
+
const handle = product.handle || product.slug || product.id;
|
|
927
|
+
product.url = `/products/${handle}`;
|
|
928
|
+
product.link = `/products/${handle}`;
|
|
929
|
+
}
|
|
930
|
+
return product;
|
|
931
|
+
});
|
|
932
|
+
context.collection = context.collection || {};
|
|
933
|
+
context.collection.products = context.products;
|
|
733
934
|
}
|
|
734
935
|
|
|
735
936
|
let html;
|
|
@@ -748,6 +949,7 @@ class DevServer {
|
|
|
748
949
|
throw new Error(`Category template not found. Tried: ${templateOptions.join(', ')}`);
|
|
749
950
|
}
|
|
750
951
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
952
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
751
953
|
res.send(html);
|
|
752
954
|
} catch (error) {
|
|
753
955
|
next(error);
|
|
@@ -775,6 +977,7 @@ class DevServer {
|
|
|
775
977
|
html = this.createSimpleBrandsPage(context);
|
|
776
978
|
}
|
|
777
979
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
980
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
778
981
|
res.send(html);
|
|
779
982
|
} catch (error) {
|
|
780
983
|
next(error);
|
|
@@ -813,6 +1016,7 @@ class DevServer {
|
|
|
813
1016
|
html = this.createSimpleBrandPage(context, brand);
|
|
814
1017
|
}
|
|
815
1018
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1019
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
816
1020
|
res.send(html);
|
|
817
1021
|
} catch (error) {
|
|
818
1022
|
next(error);
|
|
@@ -825,6 +1029,7 @@ class DevServer {
|
|
|
825
1029
|
const context = await this.buildContext(req, 'cart');
|
|
826
1030
|
const html = await renderWithLayout(this.liquid, 'templates/cart', context, this.themePath);
|
|
827
1031
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1032
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
828
1033
|
res.send(html);
|
|
829
1034
|
} catch (error) {
|
|
830
1035
|
next(error);
|
|
@@ -837,6 +1042,7 @@ class DevServer {
|
|
|
837
1042
|
const context = await this.buildContext(req, 'search', { query: req.query.q });
|
|
838
1043
|
const html = await renderWithLayout(this.liquid, 'templates/search', context, this.themePath);
|
|
839
1044
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1045
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
840
1046
|
res.send(html);
|
|
841
1047
|
} catch (error) {
|
|
842
1048
|
next(error);
|
|
@@ -890,12 +1096,14 @@ class DevServer {
|
|
|
890
1096
|
try {
|
|
891
1097
|
const html = await renderWithLayout(this.liquid, 'templates/page', context, this.themePath);
|
|
892
1098
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1099
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
893
1100
|
res.send(html);
|
|
894
1101
|
} catch (templateError) {
|
|
895
1102
|
// If no page template, render raw HTML content within layout
|
|
896
1103
|
console.log(`[PAGE] No page template found, rendering raw HTML for ${req.params.handle}`);
|
|
897
1104
|
const html = this.renderLandingPage(context);
|
|
898
1105
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1106
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
899
1107
|
res.send(html);
|
|
900
1108
|
}
|
|
901
1109
|
} catch (error) {
|
|
@@ -934,6 +1142,7 @@ class DevServer {
|
|
|
934
1142
|
|
|
935
1143
|
const html = await renderWithLayout(this.liquid, 'templates/page', context, this.themePath);
|
|
936
1144
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1145
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
937
1146
|
res.send(html);
|
|
938
1147
|
} catch (error) {
|
|
939
1148
|
next(error);
|
|
@@ -950,6 +1159,7 @@ class DevServer {
|
|
|
950
1159
|
|
|
951
1160
|
const html = this.generateDevDashboard({ templates, sections, widgets, snippets });
|
|
952
1161
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1162
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
953
1163
|
res.send(html);
|
|
954
1164
|
} catch (error) {
|
|
955
1165
|
next(error);
|
|
@@ -974,6 +1184,7 @@ class DevServer {
|
|
|
974
1184
|
backUrl: '/dev'
|
|
975
1185
|
});
|
|
976
1186
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1187
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
977
1188
|
res.send(html);
|
|
978
1189
|
} catch (error) {
|
|
979
1190
|
next(error);
|
|
@@ -1033,6 +1244,7 @@ class DevServer {
|
|
|
1033
1244
|
backUrl: '/dev'
|
|
1034
1245
|
});
|
|
1035
1246
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1247
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
1036
1248
|
res.send(html);
|
|
1037
1249
|
} catch (error) {
|
|
1038
1250
|
next(error);
|
|
@@ -1081,6 +1293,7 @@ class DevServer {
|
|
|
1081
1293
|
backUrl: '/dev'
|
|
1082
1294
|
});
|
|
1083
1295
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1296
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
1084
1297
|
res.send(html);
|
|
1085
1298
|
} catch (error) {
|
|
1086
1299
|
next(error);
|
|
@@ -1118,12 +1331,122 @@ class DevServer {
|
|
|
1118
1331
|
const context = await this.buildContext(req, pageType);
|
|
1119
1332
|
const html = await renderWithLayout(this.liquid, `templates/${templateName}`, context, this.themePath);
|
|
1120
1333
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1334
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
1121
1335
|
res.send(html);
|
|
1122
1336
|
} catch (error) {
|
|
1123
1337
|
next(error);
|
|
1124
1338
|
}
|
|
1125
1339
|
});
|
|
1126
1340
|
|
|
1341
|
+
// Catch-all route for category/collection handles at root level (e.g., /toys-and-games)
|
|
1342
|
+
// This handles links from widgets that use category handles directly
|
|
1343
|
+
this.app.get('/:handle', async (req, res, next) => {
|
|
1344
|
+
try {
|
|
1345
|
+
const handle = req.params.handle;
|
|
1346
|
+
|
|
1347
|
+
// Skip if it's a known route path or static file extension
|
|
1348
|
+
const knownPaths = ['assets', 'images', 'api', 'dev', 'favicon.ico', 'products', 'collections', 'categories', 'brands', 'cart', 'search', 'page', 'pages'];
|
|
1349
|
+
const staticExtensions = ['.css', '.js', '.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp', '.woff', '.woff2', '.ttf', '.eot', '.ico'];
|
|
1350
|
+
const hasStaticExtension = staticExtensions.some(ext => handle.toLowerCase().endsWith(ext));
|
|
1351
|
+
|
|
1352
|
+
if (knownPaths.includes(handle) || hasStaticExtension) {
|
|
1353
|
+
return next();
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// Build context to get categories and products
|
|
1357
|
+
const context = await this.buildContext(req, 'collection', { collectionHandle: handle });
|
|
1358
|
+
|
|
1359
|
+
// Try to find category by handle (case-insensitive)
|
|
1360
|
+
const category = context.categories?.find(c =>
|
|
1361
|
+
(c.handle && c.handle.toLowerCase() === handle.toLowerCase()) ||
|
|
1362
|
+
(c.name && c.name.toLowerCase().replace(/\s+/g, '-') === handle.toLowerCase())
|
|
1363
|
+
);
|
|
1364
|
+
|
|
1365
|
+
if (category) {
|
|
1366
|
+
// Found a category - render collection page
|
|
1367
|
+
context.category = category;
|
|
1368
|
+
context.collection = category;
|
|
1369
|
+
|
|
1370
|
+
// Filter products by this category
|
|
1371
|
+
const filteredProducts = (context.products || []).filter(p =>
|
|
1372
|
+
p.categoryId === category.id ||
|
|
1373
|
+
String(p.categoryId).toLowerCase() === String(category.id).toLowerCase()
|
|
1374
|
+
);
|
|
1375
|
+
|
|
1376
|
+
// Ensure all products have proper URL field if missing
|
|
1377
|
+
filteredProducts.forEach(product => {
|
|
1378
|
+
if (!product.url && !product.link) {
|
|
1379
|
+
const productHandle = product.handle || product.slug || product.id;
|
|
1380
|
+
product.url = `/products/${productHandle}`;
|
|
1381
|
+
product.link = `/products/${productHandle}`;
|
|
1382
|
+
}
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
context.collection.products = filteredProducts;
|
|
1386
|
+
context.collection.totalProducts = filteredProducts.length;
|
|
1387
|
+
context.products = filteredProducts; // Also set in products array for consistency
|
|
1388
|
+
|
|
1389
|
+
console.log(`[ROOT COLLECTION] Category: ${category.name}, Products: ${filteredProducts.length}`);
|
|
1390
|
+
|
|
1391
|
+
// Try to render collection template
|
|
1392
|
+
let html;
|
|
1393
|
+
const templateOptions = ['templates/categories', 'templates/category', 'templates/collection', 'templates/products'];
|
|
1394
|
+
for (const template of templateOptions) {
|
|
1395
|
+
try {
|
|
1396
|
+
html = await renderWithLayout(this.liquid, template, context, this.themePath);
|
|
1397
|
+
break;
|
|
1398
|
+
} catch (error) {
|
|
1399
|
+
if (!error.message?.includes('Template not found')) {
|
|
1400
|
+
throw error;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
if (html) {
|
|
1406
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1407
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
1408
|
+
return res.send(html);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
// Not a category - check if it's a product handle
|
|
1413
|
+
const product = context.products?.find(p =>
|
|
1414
|
+
(p.handle && p.handle.toLowerCase() === handle.toLowerCase()) ||
|
|
1415
|
+
(p.slug && p.slug.toLowerCase() === handle.toLowerCase())
|
|
1416
|
+
);
|
|
1417
|
+
|
|
1418
|
+
if (product) {
|
|
1419
|
+
// Found a product - render product detail page
|
|
1420
|
+
const productContext = await this.buildContext(req, 'product', { productHandle: product.handle || product.slug || handle });
|
|
1421
|
+
|
|
1422
|
+
let html;
|
|
1423
|
+
const templateOptions = ['templates/product-detail', 'templates/product', 'templates/product-page'];
|
|
1424
|
+
for (const template of templateOptions) {
|
|
1425
|
+
try {
|
|
1426
|
+
html = await renderWithLayout(this.liquid, template, productContext, this.themePath);
|
|
1427
|
+
break;
|
|
1428
|
+
} catch (error) {
|
|
1429
|
+
if (!error.message?.includes('Template not found')) {
|
|
1430
|
+
throw error;
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
if (html) {
|
|
1436
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1437
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
1438
|
+
return res.send(html);
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
// Not found - pass to next middleware (will eventually 404)
|
|
1443
|
+
next();
|
|
1444
|
+
} catch (error) {
|
|
1445
|
+
// If there's an error, pass to next middleware
|
|
1446
|
+
next(error);
|
|
1447
|
+
}
|
|
1448
|
+
});
|
|
1449
|
+
|
|
1127
1450
|
// Favicon handler
|
|
1128
1451
|
this.app.get('/favicon.ico', (req, res) => {
|
|
1129
1452
|
res.status(204).end(); // No Content
|
|
@@ -1267,10 +1590,13 @@ class DevServer {
|
|
|
1267
1590
|
|
|
1268
1591
|
try {
|
|
1269
1592
|
// Load settings from settings_data.json
|
|
1593
|
+
// Extract 'current' section to match webstore behavior
|
|
1270
1594
|
const settingsPath = path.join(this.themePath, 'config', 'settings_data.json');
|
|
1271
1595
|
if (fs.existsSync(settingsPath)) {
|
|
1272
1596
|
const settingsData = fs.readFileSync(settingsPath, 'utf8');
|
|
1273
|
-
|
|
1597
|
+
const parsedData = JSON.parse(settingsData);
|
|
1598
|
+
// Extract 'current' section like loadThemeSettings() does
|
|
1599
|
+
context.settings = parsedData.current || parsedData.presets?.default || {};
|
|
1274
1600
|
}
|
|
1275
1601
|
} catch (error) {
|
|
1276
1602
|
console.warn('[CONTEXT] Failed to load settings:', error.message);
|
|
@@ -1350,6 +1676,50 @@ class DevServer {
|
|
|
1350
1676
|
// Load products
|
|
1351
1677
|
const productsResponse = await this.apiClient.getProducts({ limit: 50, offset: 0 });
|
|
1352
1678
|
context.products = productsResponse.products || productsResponse.data?.products || [];
|
|
1679
|
+
|
|
1680
|
+
// Ensure all products have proper URL, images, and stock fields
|
|
1681
|
+
context.products = context.products.map((product, index) => {
|
|
1682
|
+
// Ensure URL field exists
|
|
1683
|
+
if (!product.url && !product.link) {
|
|
1684
|
+
const handle = product.handle || product.slug || product.id;
|
|
1685
|
+
product.url = `/products/${handle}`;
|
|
1686
|
+
product.link = `/products/${handle}`;
|
|
1687
|
+
|
|
1688
|
+
// Debug first few products to verify URL generation
|
|
1689
|
+
if (index < 3) {
|
|
1690
|
+
console.log(`[CONTEXT] Product ${index + 1} URL: ${product.url} (handle: ${product.handle || 'none'}, slug: ${product.slug || 'none'}, id: ${product.id || 'none'})`);
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
// Ensure stock fields exist (they should from mock-data, but double-check)
|
|
1694
|
+
if (product.stock === undefined && product.stockQuantity === undefined) {
|
|
1695
|
+
// Calculate from variants if available
|
|
1696
|
+
if (product.variants && product.variants.length > 0) {
|
|
1697
|
+
const totalStock = product.variants.reduce((sum, v) => sum + (v.stock || 0), 0);
|
|
1698
|
+
product.stock = totalStock;
|
|
1699
|
+
product.stockQuantity = totalStock;
|
|
1700
|
+
product.inStock = totalStock > 0;
|
|
1701
|
+
product.available = totalStock > 0;
|
|
1702
|
+
} else {
|
|
1703
|
+
// Default to in stock if no stock info
|
|
1704
|
+
product.stock = 10;
|
|
1705
|
+
product.stockQuantity = 10;
|
|
1706
|
+
product.inStock = true;
|
|
1707
|
+
product.available = true;
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
// Ensure image fields exist (they should from mock-data, but double-check)
|
|
1711
|
+
if (!product.thumbnailImage1 && !product.imageUrl && (!product.images || product.images.length === 0)) {
|
|
1712
|
+
// Fallback to picsum placeholder
|
|
1713
|
+
const imageId = product.id ? String(product.id).replace(/\D/g, '') || '1' : '1';
|
|
1714
|
+
product.imageUrl = `https://picsum.photos/seed/${imageId}/800/800`;
|
|
1715
|
+
product.thumbnailImage1 = {
|
|
1716
|
+
url: product.imageUrl,
|
|
1717
|
+
altText: product.title || product.name || 'Product image'
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1720
|
+
return product;
|
|
1721
|
+
});
|
|
1722
|
+
|
|
1353
1723
|
console.log(`[CONTEXT] Loaded ${context.products.length} products`);
|
|
1354
1724
|
|
|
1355
1725
|
// DEBUG: Verify products have quantity/stock field
|
|
@@ -1446,40 +1816,57 @@ class DevServer {
|
|
|
1446
1816
|
if (pageType === 'product' && extra.productHandle) {
|
|
1447
1817
|
// Load single product data
|
|
1448
1818
|
try {
|
|
1819
|
+
const searchHandle = extra.productHandle.toLowerCase().trim();
|
|
1820
|
+
console.log(`[CONTEXT] Searching for product with handle: ${searchHandle}`);
|
|
1821
|
+
|
|
1449
1822
|
// First try to find in already loaded products
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
(p.
|
|
1453
|
-
(p.
|
|
1454
|
-
|
|
1823
|
+
// Check multiple fields: handle, slug, id, and also check if handle contains the ID
|
|
1824
|
+
let product = context.products?.find(p => {
|
|
1825
|
+
const pHandle = (p.handle || '').toLowerCase().trim();
|
|
1826
|
+
const pSlug = (p.slug || '').toLowerCase().trim();
|
|
1827
|
+
const pId = String(p.id || '').toLowerCase().trim();
|
|
1828
|
+
const pIdStr = String(p.id || '');
|
|
1829
|
+
|
|
1830
|
+
return pHandle === searchHandle ||
|
|
1831
|
+
pSlug === searchHandle ||
|
|
1832
|
+
pId === searchHandle ||
|
|
1833
|
+
(searchHandle.startsWith('product-') && pId === searchHandle.replace('product-', '')) ||
|
|
1834
|
+
(pHandle && searchHandle.includes(pIdStr)) ||
|
|
1835
|
+
(pSlug && searchHandle.includes(pIdStr));
|
|
1836
|
+
});
|
|
1455
1837
|
|
|
1456
1838
|
// If not found, try loading more products
|
|
1457
1839
|
if (!product) {
|
|
1840
|
+
console.log(`[CONTEXT] Product not found in initial list, loading more products...`);
|
|
1458
1841
|
const productsResponse = await this.apiClient.getProducts({ limit: 100 });
|
|
1459
1842
|
const allProducts = productsResponse.products || productsResponse.data?.products || [];
|
|
1460
|
-
product = allProducts.find(p =>
|
|
1461
|
-
(p.handle
|
|
1462
|
-
(p.slug
|
|
1463
|
-
|
|
1464
|
-
|
|
1843
|
+
product = allProducts.find(p => {
|
|
1844
|
+
const pHandle = (p.handle || '').toLowerCase().trim();
|
|
1845
|
+
const pSlug = (p.slug || '').toLowerCase().trim();
|
|
1846
|
+
const pId = String(p.id || '').toLowerCase().trim();
|
|
1847
|
+
const pIdStr = String(p.id || '');
|
|
1848
|
+
|
|
1849
|
+
return pHandle === searchHandle ||
|
|
1850
|
+
pSlug === searchHandle ||
|
|
1851
|
+
pId === searchHandle ||
|
|
1852
|
+
(searchHandle.startsWith('product-') && pId === searchHandle.replace('product-', '')) ||
|
|
1853
|
+
(pHandle && searchHandle.includes(pIdStr)) ||
|
|
1854
|
+
(pSlug && searchHandle.includes(pIdStr));
|
|
1855
|
+
});
|
|
1465
1856
|
}
|
|
1466
1857
|
|
|
1467
1858
|
if (product) {
|
|
1468
1859
|
context.product = product;
|
|
1469
|
-
console.log(`[CONTEXT] Found product: ${product.title || product.name} (handle: ${product.handle || product.slug})`);
|
|
1860
|
+
console.log(`[CONTEXT] ✅ Found product: ${product.title || product.name} (handle: ${product.handle || product.slug || product.id})`);
|
|
1470
1861
|
} 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
|
-
}
|
|
1862
|
+
console.warn(`[CONTEXT] ⚠️ Product not found with handle/slug/id: ${extra.productHandle}`);
|
|
1863
|
+
console.warn(`[CONTEXT] Available product handles: ${(context.products || []).slice(0, 5).map(p => p.handle || p.slug || p.id).join(', ')}`);
|
|
1864
|
+
// Don't use fallback - let the route handler deal with 404
|
|
1477
1865
|
}
|
|
1478
1866
|
} catch (error) {
|
|
1479
|
-
console.
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
}
|
|
1867
|
+
console.error('[CONTEXT] ❌ Failed to load product:', error.message);
|
|
1868
|
+
console.error('[CONTEXT] Error stack:', error.stack);
|
|
1869
|
+
// Don't use fallback on error - let route handle it
|
|
1483
1870
|
}
|
|
1484
1871
|
}
|
|
1485
1872
|
} catch (error) {
|
|
@@ -2586,14 +2973,36 @@ class DevServer {
|
|
|
2586
2973
|
*/
|
|
2587
2974
|
async stop() {
|
|
2588
2975
|
return new Promise((resolve) => {
|
|
2976
|
+
let resolved = false;
|
|
2977
|
+
const safeResolve = () => {
|
|
2978
|
+
if (!resolved) {
|
|
2979
|
+
resolved = true;
|
|
2980
|
+
console.log(chalk.yellow('\n👋 Server stopped'));
|
|
2981
|
+
resolve();
|
|
2982
|
+
}
|
|
2983
|
+
};
|
|
2984
|
+
|
|
2985
|
+
// Increase max listeners to prevent warning if multiple shutdown attempts
|
|
2986
|
+
if (this.server && this.server.setMaxListeners) {
|
|
2987
|
+
this.server.setMaxListeners(20);
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2589
2990
|
// Stop file watcher
|
|
2590
2991
|
if (this.fileWatcher && typeof this.fileWatcher.close === 'function') {
|
|
2591
|
-
|
|
2992
|
+
try {
|
|
2993
|
+
this.fileWatcher.close();
|
|
2994
|
+
} catch (error) {
|
|
2995
|
+
// Ignore errors when closing watcher
|
|
2996
|
+
}
|
|
2592
2997
|
}
|
|
2593
2998
|
|
|
2594
2999
|
// Close Socket.IO
|
|
2595
3000
|
if (this.io && typeof this.io.close === 'function') {
|
|
2596
|
-
|
|
3001
|
+
try {
|
|
3002
|
+
this.io.close();
|
|
3003
|
+
} catch (error) {
|
|
3004
|
+
// Ignore errors when closing Socket.IO
|
|
3005
|
+
}
|
|
2597
3006
|
}
|
|
2598
3007
|
|
|
2599
3008
|
// Stop mock API
|
|
@@ -2601,14 +3010,37 @@ class DevServer {
|
|
|
2601
3010
|
this.mockApi.stop().catch(() => {});
|
|
2602
3011
|
}
|
|
2603
3012
|
|
|
2604
|
-
// Close HTTP server
|
|
3013
|
+
// Close HTTP server - check if it's actually listening/running
|
|
2605
3014
|
if (this.server) {
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
3015
|
+
// Check if server is listening before trying to close
|
|
3016
|
+
const isListening = this.server.listening || false;
|
|
3017
|
+
|
|
3018
|
+
if (!isListening) {
|
|
3019
|
+
// Server is already closed/not running
|
|
3020
|
+
safeResolve();
|
|
3021
|
+
return;
|
|
3022
|
+
}
|
|
3023
|
+
|
|
3024
|
+
// Remove all existing listeners to prevent memory leak warning
|
|
3025
|
+
this.server.removeAllListeners('close');
|
|
3026
|
+
|
|
3027
|
+
// Close the server - use callback instead of event listener to avoid duplicate calls
|
|
3028
|
+
this.server.close((err) => {
|
|
3029
|
+
if (err) {
|
|
3030
|
+
// Ignore ERR_SERVER_NOT_RUNNING errors as they're harmless
|
|
3031
|
+
if (err.code !== 'ERR_SERVER_NOT_RUNNING') {
|
|
3032
|
+
console.error(chalk.red('Error closing server:'), err.message);
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
safeResolve();
|
|
2609
3036
|
});
|
|
3037
|
+
|
|
3038
|
+
// Fallback timeout in case close callback doesn't fire
|
|
3039
|
+
setTimeout(() => {
|
|
3040
|
+
safeResolve();
|
|
3041
|
+
}, 2000);
|
|
2610
3042
|
} else {
|
|
2611
|
-
|
|
3043
|
+
safeResolve();
|
|
2612
3044
|
}
|
|
2613
3045
|
});
|
|
2614
3046
|
}
|
package/lib/lib/mock-data.js
CHANGED
|
@@ -65,6 +65,25 @@ function generateMockProducts(count = 20) {
|
|
|
65
65
|
console.log(`[MOCK DATA] Generating ${count} mock products with variations and stock...`);
|
|
66
66
|
const products = [];
|
|
67
67
|
|
|
68
|
+
// Category mapping - map category names to category IDs (matching generateMockCategories order)
|
|
69
|
+
const categoryMap = {
|
|
70
|
+
'Electronics': 'category-1',
|
|
71
|
+
'Clothing': 'category-2',
|
|
72
|
+
'Accessories': 'category-3',
|
|
73
|
+
'Home & Garden': 'category-4',
|
|
74
|
+
'Home': 'category-4', // Alias for Home & Garden
|
|
75
|
+
'Sports & Fitness': 'category-5',
|
|
76
|
+
'Sports': 'category-5', // Alias for Sports & Fitness
|
|
77
|
+
'Books & Media': 'category-6',
|
|
78
|
+
'Toys & Games': 'category-7',
|
|
79
|
+
'Beauty & Health': 'category-8',
|
|
80
|
+
'Automotive': 'category-9',
|
|
81
|
+
'Food & Beverages': 'category-10',
|
|
82
|
+
'Furniture': 'category-4', // Map Furniture to Home & Garden
|
|
83
|
+
'Office': 'category-6', // Map Office to Books & Media (or could be separate)
|
|
84
|
+
'Footwear': 'category-2' // Map Footwear to Clothing
|
|
85
|
+
};
|
|
86
|
+
|
|
68
87
|
// Product data with images from picsum.photos (reliable placeholder service)
|
|
69
88
|
const productData = [
|
|
70
89
|
{ name: 'Wireless Headphones', category: 'Electronics', imageId: 1 },
|
|
@@ -115,7 +134,13 @@ function generateMockProducts(count = 20) {
|
|
|
115
134
|
const productId = `product-${i + 1}`;
|
|
116
135
|
const basePrice = Math.floor(Math.random() * 50000) + 1000; // $10 to $500 (in cents)
|
|
117
136
|
const comparePrice = basePrice * 1.5;
|
|
118
|
-
const
|
|
137
|
+
const productTemplate = productData[i % productData.length];
|
|
138
|
+
const productName = productTemplate.name;
|
|
139
|
+
const productCategory = productTemplate.category;
|
|
140
|
+
const productImageId = productTemplate.imageId;
|
|
141
|
+
|
|
142
|
+
// Get categoryId from category name using categoryMap
|
|
143
|
+
const categoryId = categoryMap[productCategory] || 'category-1'; // Default to Electronics if not found
|
|
119
144
|
|
|
120
145
|
// Determine stock quantity - mix of low stock, high stock, and out of stock
|
|
121
146
|
const stockType = i % 5;
|
|
@@ -278,18 +303,19 @@ function generateMockProducts(count = 20) {
|
|
|
278
303
|
stock: totalStock,
|
|
279
304
|
stockQuantity: totalStock, // Alternative property for stock
|
|
280
305
|
sku: `SKU-${i + 1}`,
|
|
281
|
-
categoryId:
|
|
306
|
+
categoryId: categoryId, // Use mapped categoryId based on product category
|
|
307
|
+
category: productCategory, // Also include category name for reference
|
|
282
308
|
brandId: `brand-${(i % 8) + 1}`,
|
|
283
309
|
// Primary thumbnail for product cards
|
|
284
310
|
thumbnailImage1: {
|
|
285
|
-
url: `https://picsum.photos/seed/${
|
|
311
|
+
url: `https://picsum.photos/seed/${productImageId}/800/800`,
|
|
286
312
|
altText: productName
|
|
287
313
|
},
|
|
288
|
-
imageUrl: `https://picsum.photos/seed/${
|
|
314
|
+
imageUrl: `https://picsum.photos/seed/${productImageId}/800/800`,
|
|
289
315
|
images: [
|
|
290
316
|
{
|
|
291
317
|
id: `img-${i + 1}-1`,
|
|
292
|
-
url: `https://picsum.photos/seed/${
|
|
318
|
+
url: `https://picsum.photos/seed/${productImageId}/800/800`,
|
|
293
319
|
alt: productName,
|
|
294
320
|
altText: productName,
|
|
295
321
|
width: 800,
|
|
@@ -297,7 +323,7 @@ function generateMockProducts(count = 20) {
|
|
|
297
323
|
},
|
|
298
324
|
{
|
|
299
325
|
id: `img-${i + 1}-2`,
|
|
300
|
-
url: `https://picsum.photos/seed/${
|
|
326
|
+
url: `https://picsum.photos/seed/${productImageId + 100}/800/800`,
|
|
301
327
|
alt: `${productName} - View 2`,
|
|
302
328
|
altText: `${productName} - View 2`,
|
|
303
329
|
width: 800,
|
|
@@ -305,7 +331,7 @@ function generateMockProducts(count = 20) {
|
|
|
305
331
|
},
|
|
306
332
|
{
|
|
307
333
|
id: `img-${i + 1}-3`,
|
|
308
|
-
url: `https://picsum.photos/seed/${
|
|
334
|
+
url: `https://picsum.photos/seed/${productImageId + 200}/800/800`,
|
|
309
335
|
alt: `${productName} - View 3`,
|
|
310
336
|
altText: `${productName} - View 3`,
|
|
311
337
|
width: 800,
|
|
@@ -342,10 +368,22 @@ function generateMockProducts(count = 20) {
|
|
|
342
368
|
const productsWithVariations = products.filter(p => (p.variants?.length || 0) > 1).length;
|
|
343
369
|
const productsWithOptions = products.filter(p => (p.options?.length || 0) > 0).length;
|
|
344
370
|
const outOfStock = products.filter(p => !p.inStock || (p.stock || 0) === 0).length;
|
|
371
|
+
|
|
372
|
+
// Log category distribution
|
|
373
|
+
const categoryDistribution = {};
|
|
374
|
+
products.forEach(p => {
|
|
375
|
+
const cat = p.categoryId || 'unknown';
|
|
376
|
+
categoryDistribution[cat] = (categoryDistribution[cat] || 0) + 1;
|
|
377
|
+
});
|
|
378
|
+
const categorySummary = Object.entries(categoryDistribution)
|
|
379
|
+
.map(([cat, count]) => `${cat}: ${count}`)
|
|
380
|
+
.join(', ');
|
|
381
|
+
|
|
345
382
|
console.log(`[MOCK DATA] ✅ Generated ${products.length} products:`);
|
|
346
383
|
console.log(` - With variations: ${productsWithVariations}`);
|
|
347
384
|
console.log(` - With options: ${productsWithOptions}`);
|
|
348
385
|
console.log(` - Out of stock: ${outOfStock}`);
|
|
386
|
+
console.log(` - Category distribution: ${categorySummary}`);
|
|
349
387
|
|
|
350
388
|
return products;
|
|
351
389
|
}
|