@o2vend/theme-cli 1.0.37 → 1.0.38
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 +309 -40
- package/lib/lib/liquid-engine.js +3 -1
- package/lib/lib/mock-data.js +36 -124
- package/lib/lib/widget-service.js +12 -4
- package/package.json +1 -1
- package/test-theme/assets/async-sections.js +32 -24
- package/test-theme/assets/cart-drawer.js +20 -22
- package/test-theme/assets/cart-manager.js +1 -15
- package/test-theme/assets/checkout-price-handler.js +12 -11
- package/test-theme/assets/checkout.css +1415 -0
- package/test-theme/assets/checkout.js +3174 -0
- package/test-theme/assets/components.css +178 -29
- package/test-theme/assets/delivery-zone.js +1 -1
- package/test-theme/assets/product-detail.css +1050 -0
- package/test-theme/assets/product-detail.js +2940 -0
- package/test-theme/assets/theme.css +95 -120
- package/test-theme/assets/theme.js +781 -186
- package/test-theme/layout/theme.liquid +91 -17
- package/test-theme/sections/content.liquid +64 -57
- package/test-theme/sections/footer-fallback.liquid +57 -7
- package/test-theme/sections/footer.liquid +63 -12
- package/test-theme/sections/header-fallback.liquid +41 -41
- package/test-theme/sections/header.liquid +41 -51
- package/test-theme/sections/hero-fallback.liquid +1 -1
- package/test-theme/sections/hero.liquid +159 -136
- package/test-theme/snippets/account-sidebar.liquid +121 -29
- package/test-theme/snippets/add-to-cart-modal.liquid +258 -206
- package/test-theme/snippets/breadcrumbs.liquid +98 -11
- package/test-theme/snippets/cart-drawer.liquid +93 -0
- package/test-theme/snippets/delivery-zone-city-selector.liquid +101 -15
- package/test-theme/snippets/delivery-zone-modal.liquid +529 -84
- package/test-theme/snippets/delivery-zone-search.liquid +104 -18
- package/test-theme/snippets/login-modal.liquid +269 -82
- package/test-theme/snippets/mega-menu.liquid +130 -43
- package/test-theme/snippets/news-thumbnail.liquid +120 -28
- package/test-theme/snippets/pagination.liquid +1 -1
- package/test-theme/snippets/price.liquid +100 -9
- package/test-theme/snippets/product-card-related.liquid +22 -4
- package/test-theme/snippets/product-card-simple.liquid +521 -25
- package/test-theme/snippets/product-card.liquid +145 -232
- package/test-theme/snippets/rating.liquid +100 -9
- package/test-theme/snippets/skeleton-collection-grid.liquid +94 -8
- package/test-theme/snippets/skeleton-product-card.liquid +102 -16
- package/test-theme/snippets/skeleton-product-grid.liquid +87 -1
- package/test-theme/snippets/social-sharing.liquid +133 -32
- package/test-theme/templates/account/dashboard.liquid +30 -0
- package/test-theme/templates/account/loyalty-redemption.liquid +29 -28
- package/test-theme/templates/account/loyalty.liquid +45 -43
- package/test-theme/templates/account/order-detail.liquid +15 -8
- package/test-theme/templates/account/orders.liquid +189 -35
- package/test-theme/templates/account/profile.liquid +509 -114
- package/test-theme/templates/account/register.liquid +18 -8
- package/test-theme/templates/account/return-orders.liquid +31 -30
- package/test-theme/templates/account/store-credit.liquid +27 -26
- package/test-theme/templates/account/subscriptions.liquid +22 -5
- package/test-theme/templates/account/wishlist.liquid +88 -19
- package/test-theme/templates/address-book.liquid +166 -69
- package/test-theme/templates/categories.liquid +90 -30
- package/test-theme/templates/checkout.liquid +137 -3834
- package/test-theme/templates/error.liquid +23 -21
- package/test-theme/templates/index.liquid +29 -0
- package/test-theme/templates/login.liquid +33 -6
- package/test-theme/templates/order-confirmation.liquid +67 -9
- package/test-theme/templates/page.liquid +418 -206
- package/test-theme/templates/product-detail.liquid +124 -3878
- package/test-theme/templates/products.liquid +155 -30
- package/test-theme/templates/search.liquid +739 -225
- package/test-theme/widgets/brand-carousel.liquid +102 -82
- package/test-theme/widgets/brand.liquid +78 -50
- package/test-theme/widgets/carousel.liquid +253 -121
- package/test-theme/widgets/category-list-carousel.liquid +32 -8
- package/test-theme/widgets/category-list.liquid +21 -6
- package/test-theme/widgets/category.liquid +104 -37
- package/test-theme/widgets/discount-time.liquid +326 -119
- package/test-theme/widgets/footer-menu.liquid +115 -23
- package/test-theme/widgets/footer.liquid +118 -5
- package/test-theme/widgets/gallery.liquid +29 -5
- package/test-theme/widgets/header-menu.liquid +25 -13
- package/test-theme/widgets/header.liquid +64 -26
- package/test-theme/widgets/html.liquid +29 -6
- package/test-theme/widgets/news.liquid +6 -0
- package/test-theme/widgets/product-canvas.liquid +20 -12
- package/test-theme/widgets/product-carousel.liquid +118 -56
- package/test-theme/widgets/shared/product-grid.liquid +12 -0
- package/test-theme/widgets/single-product.liquid +688 -250
- package/test-theme/widgets/spacebar-carousel.liquid +39 -10
- package/test-theme/widgets/spacebar.liquid +77 -6
- package/test-theme/widgets/splash.liquid +40 -30
- package/test-theme/widgets/testimonial-carousel.liquid +111 -67
package/lib/lib/dev-server.js
CHANGED
|
@@ -356,7 +356,6 @@ class DevServer {
|
|
|
356
356
|
if (fs.existsSync(altPath)) {
|
|
357
357
|
console.log(`[WIDGET] ✅ Using shared widget: ${templateSlug}.liquid`);
|
|
358
358
|
const widgetContent = fs.readFileSync(altPath, 'utf8');
|
|
359
|
-
// Get full context - use current rendering context if available, otherwise use passed context
|
|
360
359
|
const fullContext = liquidEngine.getCurrentContext ? liquidEngine.getCurrentContext() : null;
|
|
361
360
|
const widgetContext = fullContext
|
|
362
361
|
? { ...fullContext, widget: widget }
|
|
@@ -380,7 +379,6 @@ class DevServer {
|
|
|
380
379
|
|
|
381
380
|
console.log(`[WIDGET] ✅ Found widget template: ${widgetPath}`);
|
|
382
381
|
const widgetContent = fs.readFileSync(widgetPath, 'utf8');
|
|
383
|
-
// Get full context - use current rendering context if available
|
|
384
382
|
const fullContext = liquidEngine.getCurrentContext ? liquidEngine.getCurrentContext() : null;
|
|
385
383
|
const widgetContext = fullContext
|
|
386
384
|
? { ...fullContext, widget: widget }
|
|
@@ -1056,6 +1054,19 @@ class DevServer {
|
|
|
1056
1054
|
}
|
|
1057
1055
|
});
|
|
1058
1056
|
|
|
1057
|
+
// Checkout page (use pageType 'checkout' so only header/footer widgets load, not home widgets)
|
|
1058
|
+
this.app.get('/checkout', async (req, res, next) => {
|
|
1059
|
+
try {
|
|
1060
|
+
const context = await this.buildContext(req, 'checkout');
|
|
1061
|
+
const html = await renderWithLayout(this.liquid, 'templates/checkout', context, this.themePath);
|
|
1062
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1063
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
1064
|
+
res.send(html);
|
|
1065
|
+
} catch (error) {
|
|
1066
|
+
next(error);
|
|
1067
|
+
}
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1059
1070
|
// Search page
|
|
1060
1071
|
this.app.get('/search', async (req, res, next) => {
|
|
1061
1072
|
try {
|
|
@@ -1244,7 +1255,7 @@ class DevServer {
|
|
|
1244
1255
|
content: sectionContent,
|
|
1245
1256
|
type: 'section',
|
|
1246
1257
|
name: sectionName,
|
|
1247
|
-
backUrl: '/dev'
|
|
1258
|
+
backUrl: '/dev#sections'
|
|
1248
1259
|
});
|
|
1249
1260
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1250
1261
|
res.setHeader('Cache-Control', 'no-cache');
|
|
@@ -1276,13 +1287,14 @@ class DevServer {
|
|
|
1276
1287
|
}
|
|
1277
1288
|
}
|
|
1278
1289
|
|
|
1279
|
-
// Get mock widget data for this type
|
|
1290
|
+
// Get mock widget data for this type (widgetType is usually template slug, e.g. spacebar-carousel)
|
|
1280
1291
|
const mockWidgets = this.mockApi?.mockData?.widgets || [];
|
|
1281
|
-
const mockWidget = mockWidgets.find(w =>
|
|
1282
|
-
w.type === widgetType ||
|
|
1283
|
-
w.type.toLowerCase() === widgetType.toLowerCase()
|
|
1284
|
-
w.
|
|
1285
|
-
|
|
1292
|
+
const mockWidget = mockWidgets.find(w => {
|
|
1293
|
+
if (w.type === widgetType || w.id?.includes(widgetType)) return true;
|
|
1294
|
+
if (w.type && typeof w.type === 'string' && w.type.toLowerCase().replace(/-/g, '') === widgetType.toLowerCase().replace(/-/g, '')) return true;
|
|
1295
|
+
if (this.widgetService && w.type && this.widgetService.getTemplateSlug(w.type) === widgetType) return true;
|
|
1296
|
+
return false;
|
|
1297
|
+
}) || {
|
|
1286
1298
|
id: `widget-${widgetType}-1`,
|
|
1287
1299
|
type: widgetType,
|
|
1288
1300
|
section: 'content',
|
|
@@ -1299,6 +1311,8 @@ class DevServer {
|
|
|
1299
1311
|
if (this.widgetService) {
|
|
1300
1312
|
widgetToRender = this.widgetService.normalizeWidget(mockWidget);
|
|
1301
1313
|
}
|
|
1314
|
+
// Enrich single widget with products/news/etc so preview has data
|
|
1315
|
+
this.enrichSingleWidget(widgetToRender, context);
|
|
1302
1316
|
|
|
1303
1317
|
const widgetContent = await this.renderWidget(widgetToRender, context);
|
|
1304
1318
|
|
|
@@ -1307,7 +1321,7 @@ class DevServer {
|
|
|
1307
1321
|
content: widgetContent,
|
|
1308
1322
|
type: 'widget',
|
|
1309
1323
|
name: widgetType,
|
|
1310
|
-
backUrl: '/dev'
|
|
1324
|
+
backUrl: '/dev#widgets'
|
|
1311
1325
|
});
|
|
1312
1326
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1313
1327
|
res.setHeader('Cache-Control', 'no-cache');
|
|
@@ -1336,18 +1350,10 @@ class DevServer {
|
|
|
1336
1350
|
}
|
|
1337
1351
|
|
|
1338
1352
|
const context = await this.buildContext(req, 'home');
|
|
1339
|
-
|
|
1340
|
-
// Try to render snippet with common variables
|
|
1341
|
-
const snippetContext = {
|
|
1353
|
+
const snippetContext = this.getSnippetMockContext(snippetName, {
|
|
1342
1354
|
...context,
|
|
1343
|
-
snippet: {
|
|
1344
|
-
|
|
1345
|
-
},
|
|
1346
|
-
// Add common snippet variables
|
|
1347
|
-
product: context.products?.[0] || context.product,
|
|
1348
|
-
collection: context.collections?.[0] || context.collection,
|
|
1349
|
-
cart: context.cart
|
|
1350
|
-
};
|
|
1355
|
+
snippet: { name: snippetName }
|
|
1356
|
+
});
|
|
1351
1357
|
|
|
1352
1358
|
const snippetContent = await this.liquid.renderFile(snippetPath, snippetContext);
|
|
1353
1359
|
|
|
@@ -1356,7 +1362,8 @@ class DevServer {
|
|
|
1356
1362
|
content: snippetContent,
|
|
1357
1363
|
type: 'snippet',
|
|
1358
1364
|
name: snippetName,
|
|
1359
|
-
backUrl: '/dev'
|
|
1365
|
+
backUrl: '/dev#snippets',
|
|
1366
|
+
includeThemeCss: true
|
|
1360
1367
|
});
|
|
1361
1368
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1362
1369
|
res.setHeader('Cache-Control', 'no-cache');
|
|
@@ -1392,7 +1399,7 @@ class DevServer {
|
|
|
1392
1399
|
else if (templateName.includes('search')) pageType = 'search';
|
|
1393
1400
|
else if (templateName.includes('page')) pageType = 'page';
|
|
1394
1401
|
else if (templateName.includes('account') || templateName.includes('login')) pageType = 'page';
|
|
1395
|
-
else if (templateName.includes('checkout')) pageType = '
|
|
1402
|
+
else if (templateName.includes('checkout')) pageType = 'checkout';
|
|
1396
1403
|
|
|
1397
1404
|
const context = await this.buildContext(req, pageType);
|
|
1398
1405
|
const html = await renderWithLayout(this.liquid, `templates/${templateName}`, context, this.themePath);
|
|
@@ -1411,7 +1418,7 @@ class DevServer {
|
|
|
1411
1418
|
const handle = req.params.handle;
|
|
1412
1419
|
|
|
1413
1420
|
// Skip if it's a known route path or static file extension
|
|
1414
|
-
const knownPaths = ['assets', 'images', 'api', 'dev', 'favicon.ico', 'products', 'collections', 'categories', 'brands', 'cart', 'search', 'page', 'pages'];
|
|
1421
|
+
const knownPaths = ['assets', 'images', 'api', 'dev', 'favicon.ico', 'products', 'collections', 'categories', 'brands', 'cart', 'checkout', 'search', 'page', 'pages'];
|
|
1415
1422
|
const staticExtensions = ['.css', '.js', '.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp', '.woff', '.woff2', '.ttf', '.eot', '.ico'];
|
|
1416
1423
|
const hasStaticExtension = staticExtensions.some(ext => handle.toLowerCase().endsWith(ext));
|
|
1417
1424
|
|
|
@@ -1840,7 +1847,7 @@ class DevServer {
|
|
|
1840
1847
|
};
|
|
1841
1848
|
|
|
1842
1849
|
// Load widgets filtered by page type (matches production behavior)
|
|
1843
|
-
const pageIdMap = { home: 'home', product: 'product', collection: 'category', category: 'category', products: 'products', categories: 'categories', brand: 'brand', checkout: 'checkout', page: 'home' };
|
|
1850
|
+
const pageIdMap = { home: 'home', product: 'product', collection: 'category', category: 'category', products: 'products', categories: 'categories', collections: 'categories', brand: 'brand', cart: 'cart', checkout: 'checkout', page: 'home' };
|
|
1844
1851
|
const widgetPageId = pageIdMap[pageType] || 'home';
|
|
1845
1852
|
if (this.widgetService) {
|
|
1846
1853
|
try {
|
|
@@ -2121,7 +2128,7 @@ class DevServer {
|
|
|
2121
2128
|
context.settings.layout_style = 'full-width';
|
|
2122
2129
|
}
|
|
2123
2130
|
|
|
2124
|
-
const pageIdMap = { home: 'home', product: 'product', collection: 'category', category: 'category', products: 'products', categories: 'categories', brand: 'brand', checkout: 'checkout', page: 'home' };
|
|
2131
|
+
const pageIdMap = { home: 'home', product: 'product', collection: 'category', category: 'category', products: 'products', categories: 'categories', collections: 'categories', brand: 'brand', cart: 'cart', checkout: 'checkout', page: 'home' };
|
|
2125
2132
|
const page = pageIdMap[pageType] || 'home';
|
|
2126
2133
|
context.widgets = await webstoreApi.fetchWidgetsBySections(page, webstoreApi.DEFAULT_SECTIONS, opts);
|
|
2127
2134
|
context.widgets = context.widgets || { hero: [], products: [], footer: [], content: [], header: [] };
|
|
@@ -2147,20 +2154,23 @@ class DevServer {
|
|
|
2147
2154
|
|
|
2148
2155
|
try {
|
|
2149
2156
|
const catRes = await webstoreApi.fetchCategories({ limit: 50, offset: 0 }, opts);
|
|
2150
|
-
const raw = catRes?.categories || catRes?.data?.categories || catRes || [];
|
|
2151
|
-
context.categories = raw.map((c) => {
|
|
2157
|
+
const raw = catRes?.categories || catRes?.data?.categories || catRes?.data?.items || catRes?.items || (Array.isArray(catRes) ? catRes : []);
|
|
2158
|
+
context.categories = Array.isArray(raw) ? raw.map((c) => {
|
|
2152
2159
|
const h = (c.handle || (c.name || '').toLowerCase().replace(/\s+/g, '-') || c.id || '').toString().trim() || null;
|
|
2153
2160
|
if (h) {
|
|
2154
2161
|
c.handle = c.handle || h;
|
|
2155
2162
|
c.slug = c.slug || h;
|
|
2156
2163
|
}
|
|
2157
2164
|
return c;
|
|
2158
|
-
});
|
|
2165
|
+
}) : [];
|
|
2159
2166
|
context.collections = context.categories;
|
|
2160
2167
|
} catch (e) {
|
|
2161
2168
|
context.categories = [];
|
|
2162
2169
|
context.collections = [];
|
|
2163
2170
|
}
|
|
2171
|
+
if (pageType === 'collections' && !context.collections) {
|
|
2172
|
+
context.collections = context.categories || [];
|
|
2173
|
+
}
|
|
2164
2174
|
|
|
2165
2175
|
try {
|
|
2166
2176
|
const brandRes = await webstoreApi.fetchBrands({ limit: 50, offset: 0 }, opts);
|
|
@@ -2254,12 +2264,35 @@ class DevServer {
|
|
|
2254
2264
|
}
|
|
2255
2265
|
}
|
|
2256
2266
|
|
|
2267
|
+
// Align with mock: hide Splash on pages (show only in /dev); in mock only, hide Footer type in footer (only FooterMenu)
|
|
2268
|
+
this.filterWidgetsForDisplay(context.widgets, this.mode);
|
|
2269
|
+
|
|
2257
2270
|
// Enrich widgets with products, categories, and brands data
|
|
2258
2271
|
this.enrichWidgetsWithData(context);
|
|
2259
|
-
|
|
2272
|
+
|
|
2260
2273
|
return context;
|
|
2261
2274
|
}
|
|
2262
|
-
|
|
2275
|
+
|
|
2276
|
+
/**
|
|
2277
|
+
* Filter widgets for display: remove Splash from all sections (preview only in /dev).
|
|
2278
|
+
* In mock mode only, remove Footer type from footer section (only FooterMenu in footer). Real mode shows all footer widgets.
|
|
2279
|
+
*/
|
|
2280
|
+
filterWidgetsForDisplay(widgets, mode) {
|
|
2281
|
+
if (!widgets || typeof widgets !== 'object') return;
|
|
2282
|
+
const isMock = mode !== 'real';
|
|
2283
|
+
Object.keys(widgets).forEach(section => {
|
|
2284
|
+
const list = widgets[section];
|
|
2285
|
+
if (!Array.isArray(list)) return;
|
|
2286
|
+
const filtered = list.filter((w) => {
|
|
2287
|
+
const type = (w.type || '').toLowerCase().trim();
|
|
2288
|
+
if (type === 'splash') return false;
|
|
2289
|
+
if (isMock && section === 'footer' && type === 'footer') return false;
|
|
2290
|
+
return true;
|
|
2291
|
+
});
|
|
2292
|
+
widgets[section] = filtered;
|
|
2293
|
+
});
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2263
2296
|
/**
|
|
2264
2297
|
* Enrich widgets with actual data (products, categories, brands)
|
|
2265
2298
|
* This is essential for product carousels, category lists, etc. to display content
|
|
@@ -2349,11 +2382,165 @@ class DevServer {
|
|
|
2349
2382
|
enrichedCount++;
|
|
2350
2383
|
console.log(`[ENRICH] ${widget.type} (${widget.id}): Added ${recentProducts.length} recently viewed products`);
|
|
2351
2384
|
}
|
|
2385
|
+
|
|
2386
|
+
// Enrich News widgets with items (use existing or default)
|
|
2387
|
+
if (type === 'news') {
|
|
2388
|
+
let items = widget.data.items || widget.data.newsItems || widget.data.content?.items || widget.data.content?.Items;
|
|
2389
|
+
if (!items || !Array.isArray(items) || items.length === 0) {
|
|
2390
|
+
items = this.getDefaultMockNewsItems();
|
|
2391
|
+
enrichedCount++;
|
|
2392
|
+
console.log(`[ENRICH] ${widget.type} (${widget.id}): Added ${items.length} news items`);
|
|
2393
|
+
}
|
|
2394
|
+
widget.data.items = items;
|
|
2395
|
+
widget.data.newsItems = items;
|
|
2396
|
+
widget.data.content = widget.data.content || {};
|
|
2397
|
+
widget.data.content.items = items;
|
|
2398
|
+
widget.data.content.Items = items;
|
|
2399
|
+
}
|
|
2352
2400
|
});
|
|
2353
2401
|
});
|
|
2354
2402
|
|
|
2355
2403
|
console.log(`[CONTEXT] ✅ Enriched ${enrichedCount} widgets with data`);
|
|
2356
2404
|
}
|
|
2405
|
+
|
|
2406
|
+
/**
|
|
2407
|
+
* Enrich a single widget with products, categories, brands, or news items.
|
|
2408
|
+
* Used by /dev/widget/:type so the preview has data without being in context.widgets.
|
|
2409
|
+
*/
|
|
2410
|
+
enrichSingleWidget(widget, context) {
|
|
2411
|
+
if (!widget) return;
|
|
2412
|
+
widget.data = widget.data || {};
|
|
2413
|
+
const type = (widget.type || '').toLowerCase();
|
|
2414
|
+
const products = context.products || [];
|
|
2415
|
+
const categories = context.categories || [];
|
|
2416
|
+
const brands = context.brands || [];
|
|
2417
|
+
|
|
2418
|
+
if (type.includes('product') || type === 'featuredproducts' || type === 'bestsellerproducts' || type === 'newproducts' || type === 'simpleproduct' || type === 'singleproduct') {
|
|
2419
|
+
const widgetProducts = this.getProductsForWidget(widget, products);
|
|
2420
|
+
const formatted = this.mode === 'real'
|
|
2421
|
+
? this.normalizeProductsForRealMode(widgetProducts)
|
|
2422
|
+
: this.enrichProductsData(widgetProducts);
|
|
2423
|
+
widget.data.products = formatted;
|
|
2424
|
+
widget.data.Products = formatted;
|
|
2425
|
+
widget.products = formatted;
|
|
2426
|
+
widget.data.content = widget.data.content || {};
|
|
2427
|
+
widget.data.content.products = formatted;
|
|
2428
|
+
widget.data.content.Products = formatted;
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
if (type === 'recentlyviewed' || type === 'recently-viewed') {
|
|
2432
|
+
const recent = products.slice(0, 6);
|
|
2433
|
+
const recentProducts = this.mode === 'real'
|
|
2434
|
+
? this.normalizeProductsForRealMode(recent)
|
|
2435
|
+
: this.enrichProductsData(recent);
|
|
2436
|
+
widget.data.products = recentProducts;
|
|
2437
|
+
widget.data.Products = recentProducts;
|
|
2438
|
+
widget.products = recentProducts;
|
|
2439
|
+
widget.data.content = widget.data.content || {};
|
|
2440
|
+
widget.data.content.products = recentProducts;
|
|
2441
|
+
widget.data.content.Products = recentProducts;
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
if (type.includes('category') || type === 'categorylist' || type === 'categorylistcarousel') {
|
|
2445
|
+
const widgetCategories = categories.slice(0, 10);
|
|
2446
|
+
widget.data.categories = widgetCategories;
|
|
2447
|
+
widget.data.Categories = widgetCategories;
|
|
2448
|
+
widget.categories = widgetCategories;
|
|
2449
|
+
widget.data.content = widget.data.content || {};
|
|
2450
|
+
widget.data.content.categories = widgetCategories;
|
|
2451
|
+
widget.data.content.Categories = widgetCategories;
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
if (type.includes('brand') || type === 'brandcarousel' || type === 'brandlist') {
|
|
2455
|
+
widget.data.brands = brands;
|
|
2456
|
+
widget.data.Brands = brands;
|
|
2457
|
+
widget.brands = brands;
|
|
2458
|
+
widget.data.content = widget.data.content || {};
|
|
2459
|
+
widget.data.content.brands = brands;
|
|
2460
|
+
widget.data.content.Brands = brands;
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
if (type === 'news') {
|
|
2464
|
+
let items = widget.data.items || widget.data.newsItems || widget.data.content?.items || widget.data.content?.Items;
|
|
2465
|
+
if (!items || !Array.isArray(items) || items.length === 0) {
|
|
2466
|
+
items = context.mockNewsItems || this.getDefaultMockNewsItems();
|
|
2467
|
+
}
|
|
2468
|
+
widget.data.items = items;
|
|
2469
|
+
widget.data.newsItems = items;
|
|
2470
|
+
widget.data.content = widget.data.content || {};
|
|
2471
|
+
widget.data.content.items = items;
|
|
2472
|
+
widget.data.content.Items = items;
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
/** Default news items for dev preview when no mock data is present */
|
|
2477
|
+
getDefaultMockNewsItems() {
|
|
2478
|
+
return [
|
|
2479
|
+
{ id: 1, title: 'Welcome to Our Store', slug: 'welcome', excerpt: 'Learn about our new collection and special offers.', thumbnailImage: { url: 'https://picsum.photos/seed/n1/400/220' }, publishedAt: new Date().toISOString() },
|
|
2480
|
+
{ id: 2, title: 'Summer Sale 2025', slug: 'summer-sale', excerpt: 'Up to 50% off on selected items.', thumbnailImage: { url: 'https://picsum.photos/seed/n2/400/220' }, publishedAt: new Date().toISOString() },
|
|
2481
|
+
{ id: 3, title: 'New Arrivals', slug: 'new-arrivals', excerpt: 'Check out the latest products in our catalog.', thumbnailImage: { url: 'https://picsum.photos/seed/n3/400/220' }, publishedAt: new Date().toISOString() },
|
|
2482
|
+
{ id: 4, title: 'Tips & Guides', slug: 'tips-guides', excerpt: 'How to get the most from your purchase.', thumbnailImage: { url: 'https://picsum.photos/seed/n4/400/220' }, publishedAt: new Date().toISOString() }
|
|
2483
|
+
];
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
/**
|
|
2487
|
+
* Build mock context for snippet preview so all snippets have data to render.
|
|
2488
|
+
* Ensures product, collection, cart, news_item, pagination, page, etc. are present.
|
|
2489
|
+
*/
|
|
2490
|
+
getSnippetMockContext(snippetName, baseContext) {
|
|
2491
|
+
const ctx = { ...baseContext };
|
|
2492
|
+
const product = ctx.products?.[0] || ctx.product;
|
|
2493
|
+
const collection = ctx.collections?.[0] || ctx.collection || ctx.categories?.[0];
|
|
2494
|
+
const cart = ctx.cart || { items: [], itemCount: 0, total: 0, subTotal: 0 };
|
|
2495
|
+
|
|
2496
|
+
if (product) {
|
|
2497
|
+
ctx.product = this.mode === 'real' ? product : this.enrichProductData(product);
|
|
2498
|
+
if (!ctx.product.prices) {
|
|
2499
|
+
ctx.product.prices = { price: ctx.product.price || 0, mrp: ctx.product.compareAtPrice || 0, priceString: `$${Number(ctx.product.price || 0).toFixed(2)}`, mrpString: ctx.product.compareAtPrice ? `$${Number(ctx.product.compareAtPrice).toFixed(2)}` : '' };
|
|
2500
|
+
}
|
|
2501
|
+
if (!ctx.product.variations && !ctx.product.variants) {
|
|
2502
|
+
ctx.product.variations = [{ id: 1, productId: ctx.product.id, price: ctx.product.price, inStock: true, title: 'Default' }];
|
|
2503
|
+
ctx.product.variants = ctx.product.variations;
|
|
2504
|
+
}
|
|
2505
|
+
} else {
|
|
2506
|
+
const mockProduct = { id: 1, title: 'Sample Product', name: 'Sample Product', handle: 'sample-product', slug: 'sample-product', url: '/products/sample-product', price: 29.99, compareAtPrice: 39.99, imageUrl: 'https://picsum.photos/seed/p1/400/400', thumbnailImage: 'https://picsum.photos/seed/p1/400/400', prices: { price: 29.99, mrp: 39.99, priceString: '$29.99', mrpString: '$39.99' }, variations: [{ id: 1, productId: 1, price: 29.99, inStock: true, title: 'Default' }], variants: null, rating: 4.5, averageRating: 4.5, reviewCount: 12, reviewsCount: 12 };
|
|
2507
|
+
mockProduct.variants = mockProduct.variations;
|
|
2508
|
+
ctx.product = mockProduct;
|
|
2509
|
+
}
|
|
2510
|
+
if (ctx.product && (ctx.product.rating == null && ctx.product.averageRating == null)) {
|
|
2511
|
+
ctx.product.rating = ctx.product.rating ?? 4.5;
|
|
2512
|
+
ctx.product.averageRating = ctx.product.averageRating ?? ctx.product.rating;
|
|
2513
|
+
}
|
|
2514
|
+
if (ctx.product && (ctx.product.reviewCount == null && ctx.product.reviewsCount == null)) {
|
|
2515
|
+
ctx.product.reviewCount = ctx.product.reviewCount ?? 12;
|
|
2516
|
+
ctx.product.reviewsCount = ctx.product.reviewsCount ?? ctx.product.reviewCount;
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
if (collection) {
|
|
2520
|
+
ctx.collection = { ...collection, title: collection.title || collection.name, handle: collection.handle || collection.slug, url: `/categories/${collection.handle || collection.slug}` };
|
|
2521
|
+
} else {
|
|
2522
|
+
ctx.collection = { id: 1, title: 'Sample Collection', name: 'Sample Collection', handle: 'sample', slug: 'sample', url: '/categories/sample' };
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
ctx.cart = cart && typeof cart === 'object' ? cart : { items: [], itemCount: 0, total: 0, subTotal: 0 };
|
|
2526
|
+
if (!ctx.cart.items) ctx.cart.items = [];
|
|
2527
|
+
if (ctx.cart.items.length === 0 && ctx.product) {
|
|
2528
|
+
ctx.cart.items = [{ id: '1', productId: ctx.product.id, title: ctx.product.title || ctx.product.name, quantity: 1, price: ctx.product.price, image: ctx.product.imageUrl || ctx.product.thumbnailImage }];
|
|
2529
|
+
ctx.cart.itemCount = 1;
|
|
2530
|
+
ctx.cart.total = ctx.product.price;
|
|
2531
|
+
ctx.cart.subTotal = ctx.product.price;
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
ctx.news_item = ctx.news_item || this.getDefaultMockNewsItems()[0];
|
|
2535
|
+
ctx.pagination = ctx.pagination || { totalPages: 5, currentPage: 1, total: 50, pageSize: 10 };
|
|
2536
|
+
ctx.page = ctx.page || { title: 'Sample Page', handle: 'sample-page' };
|
|
2537
|
+
ctx.article = ctx.article || null;
|
|
2538
|
+
ctx.line_item = ctx.cart.items?.[0] || null;
|
|
2539
|
+
if (ctx.shop && !ctx.shop.currency) ctx.shop.currency = 'USD';
|
|
2540
|
+
if (ctx.shop && !ctx.shop.settings) ctx.shop.settings = { currencySymbol: '$' };
|
|
2541
|
+
|
|
2542
|
+
return ctx;
|
|
2543
|
+
}
|
|
2357
2544
|
|
|
2358
2545
|
/**
|
|
2359
2546
|
* Check if a product belongs to a category (for collection/category filtering).
|
|
@@ -2881,10 +3068,14 @@ class DevServer {
|
|
|
2881
3068
|
|
|
2882
3069
|
try {
|
|
2883
3070
|
const templateSlug = this.widgetService.getTemplateSlug(widget.type);
|
|
2884
|
-
|
|
2885
|
-
|
|
3071
|
+
let widgetPath = path.join(this.themePath, 'widgets', `${templateSlug}.liquid`);
|
|
2886
3072
|
if (!fs.existsSync(widgetPath)) {
|
|
2887
|
-
|
|
3073
|
+
const sharedPath = path.join(this.themePath, 'widgets', 'shared', `${templateSlug}.liquid`);
|
|
3074
|
+
if (fs.existsSync(sharedPath)) {
|
|
3075
|
+
widgetPath = sharedPath;
|
|
3076
|
+
} else {
|
|
3077
|
+
return `<div class="error">Widget template not found: ${templateSlug}.liquid</div>`;
|
|
3078
|
+
}
|
|
2888
3079
|
}
|
|
2889
3080
|
|
|
2890
3081
|
const widgetContent = fs.readFileSync(widgetPath, 'utf8');
|
|
@@ -3126,7 +3317,7 @@ class DevServer {
|
|
|
3126
3317
|
</div>
|
|
3127
3318
|
</header>
|
|
3128
3319
|
|
|
3129
|
-
<div class="section">
|
|
3320
|
+
<div class="section" id="templates">
|
|
3130
3321
|
<h2>📄 Templates <span class="section-count">(${templates.length})</span></h2>
|
|
3131
3322
|
${templates.length > 0 ? `
|
|
3132
3323
|
<div class="file-grid">
|
|
@@ -3140,7 +3331,7 @@ class DevServer {
|
|
|
3140
3331
|
` : '<div class="empty">No templates found</div>'}
|
|
3141
3332
|
</div>
|
|
3142
3333
|
|
|
3143
|
-
<div class="section">
|
|
3334
|
+
<div class="section" id="sections">
|
|
3144
3335
|
<h2>🧩 Sections <span class="section-count">(${sections.length})</span></h2>
|
|
3145
3336
|
${sections.length > 0 ? `
|
|
3146
3337
|
<div class="file-grid">
|
|
@@ -3154,7 +3345,7 @@ class DevServer {
|
|
|
3154
3345
|
` : '<div class="empty">No sections found</div>'}
|
|
3155
3346
|
</div>
|
|
3156
3347
|
|
|
3157
|
-
<div class="section">
|
|
3348
|
+
<div class="section" id="widgets">
|
|
3158
3349
|
<h2>🎯 Widgets <span class="section-count">(${widgets.length})</span></h2>
|
|
3159
3350
|
${widgets.length > 0 ? `
|
|
3160
3351
|
<div class="file-grid">
|
|
@@ -3168,7 +3359,7 @@ class DevServer {
|
|
|
3168
3359
|
` : '<div class="empty">No widgets found</div>'}
|
|
3169
3360
|
</div>
|
|
3170
3361
|
|
|
3171
|
-
<div class="section">
|
|
3362
|
+
<div class="section" id="snippets">
|
|
3172
3363
|
<h2>✂️ Snippets <span class="section-count">(${snippets.length})</span></h2>
|
|
3173
3364
|
${snippets.length > 0 ? `
|
|
3174
3365
|
<div class="file-grid">
|
|
@@ -3190,14 +3381,91 @@ class DevServer {
|
|
|
3190
3381
|
<p style="margin-top: 8px; font-size: 0.85em; opacity: 0.7;">Version 1.0.0 | Running on port ${this.port || 3000}</p>
|
|
3191
3382
|
</div>
|
|
3192
3383
|
</div>
|
|
3384
|
+
<script>
|
|
3385
|
+
(function() {
|
|
3386
|
+
var hash = window.location.hash;
|
|
3387
|
+
if (hash) {
|
|
3388
|
+
var el = document.querySelector(hash);
|
|
3389
|
+
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
3390
|
+
}
|
|
3391
|
+
})();
|
|
3392
|
+
</script>
|
|
3193
3393
|
</body>
|
|
3194
3394
|
</html>`;
|
|
3195
3395
|
}
|
|
3196
3396
|
|
|
3397
|
+
/**
|
|
3398
|
+
* Return link tags for theme CSS files that exist (so snippet/section preview gets theme styles).
|
|
3399
|
+
* Includes theme, components, sections, and any other .css in assets (e.g. delivery-zone.css).
|
|
3400
|
+
*/
|
|
3401
|
+
getThemeCssLinkTags() {
|
|
3402
|
+
const assetsDir = path.join(this.themePath, 'assets');
|
|
3403
|
+
const links = [];
|
|
3404
|
+
const seen = new Set();
|
|
3405
|
+
const add = (base) => {
|
|
3406
|
+
if (seen.has(base)) return;
|
|
3407
|
+
seen.add(base);
|
|
3408
|
+
const minPath = path.join(assetsDir, `${base}.min.css`);
|
|
3409
|
+
const cssPath = path.join(assetsDir, `${base}.css`);
|
|
3410
|
+
if (fs.existsSync(minPath)) {
|
|
3411
|
+
links.push(`<link rel="stylesheet" href="/assets/${base}.min.css">`);
|
|
3412
|
+
} else if (fs.existsSync(cssPath)) {
|
|
3413
|
+
links.push(`<link rel="stylesheet" href="/assets/${base}.css">`);
|
|
3414
|
+
}
|
|
3415
|
+
};
|
|
3416
|
+
['theme', 'components', 'sections'].forEach(add);
|
|
3417
|
+
if (fs.existsSync(assetsDir)) {
|
|
3418
|
+
try {
|
|
3419
|
+
fs.readdirSync(assetsDir)
|
|
3420
|
+
.filter((f) => f.endsWith('.css') && !f.endsWith('.min.css'))
|
|
3421
|
+
.forEach((f) => add(f.replace(/\.css$/, '')));
|
|
3422
|
+
} catch (_) { /* ignore */ }
|
|
3423
|
+
}
|
|
3424
|
+
return links.join('\n ');
|
|
3425
|
+
}
|
|
3426
|
+
|
|
3197
3427
|
/**
|
|
3198
3428
|
* Generate preview page HTML
|
|
3199
3429
|
*/
|
|
3200
|
-
generatePreviewPage({ title, content, type, name, backUrl }) {
|
|
3430
|
+
generatePreviewPage({ title, content, type, name, backUrl, includeThemeCss = false }) {
|
|
3431
|
+
const themeCssLinks = includeThemeCss ? this.getThemeCssLinkTags() : '';
|
|
3432
|
+
const modalSnippets = ['login-modal', 'delivery-zone-modal', 'add-to-cart-modal'];
|
|
3433
|
+
const isModalSnippet = type === 'snippet' && name && modalSnippets.includes(name);
|
|
3434
|
+
const modalVisibilityCss = isModalSnippet ? `
|
|
3435
|
+
/* Force modal visible in /dev snippet preview (no trigger to open; overrides [aria-hidden="true"] { display: none }) */
|
|
3436
|
+
.preview-container .login-modal,
|
|
3437
|
+
.preview-container .login-modal[aria-hidden="true"],
|
|
3438
|
+
.preview-container .login-modal.login-modal--active {
|
|
3439
|
+
display: flex !important;
|
|
3440
|
+
align-items: center;
|
|
3441
|
+
justify-content: center;
|
|
3442
|
+
position: fixed;
|
|
3443
|
+
inset: 0;
|
|
3444
|
+
z-index: 9999;
|
|
3445
|
+
visibility: visible !important;
|
|
3446
|
+
}
|
|
3447
|
+
.preview-container .delivery-zone-modal,
|
|
3448
|
+
.preview-container .delivery-zone-modal[aria-hidden="true"],
|
|
3449
|
+
.preview-container .delivery-zone-modal.active {
|
|
3450
|
+
display: flex !important;
|
|
3451
|
+
align-items: center;
|
|
3452
|
+
justify-content: center;
|
|
3453
|
+
position: fixed;
|
|
3454
|
+
inset: 0;
|
|
3455
|
+
z-index: 9999;
|
|
3456
|
+
visibility: visible !important;
|
|
3457
|
+
}
|
|
3458
|
+
.preview-container .add-to-cart-modal,
|
|
3459
|
+
.preview-container .add-to-cart-modal[aria-hidden="true"] {
|
|
3460
|
+
display: flex !important;
|
|
3461
|
+
align-items: center;
|
|
3462
|
+
justify-content: center;
|
|
3463
|
+
position: fixed;
|
|
3464
|
+
inset: 0;
|
|
3465
|
+
z-index: 9999;
|
|
3466
|
+
visibility: visible !important;
|
|
3467
|
+
}
|
|
3468
|
+
` : '';
|
|
3201
3469
|
return `<!DOCTYPE html>
|
|
3202
3470
|
<html lang="en">
|
|
3203
3471
|
<head>
|
|
@@ -3205,7 +3473,7 @@ class DevServer {
|
|
|
3205
3473
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3206
3474
|
<title>${title} - O2VEND Dev</title>
|
|
3207
3475
|
<link rel="icon" type="image/png" href="/o2vend-assets/favicon.png">
|
|
3208
|
-
<style>
|
|
3476
|
+
${themeCssLinks ? ` ${themeCssLinks}\n ` : ''}<style>
|
|
3209
3477
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
3210
3478
|
:root {
|
|
3211
3479
|
--o2vend-primary: #F9A825;
|
|
@@ -3278,6 +3546,7 @@ class DevServer {
|
|
|
3278
3546
|
border-radius: 6px;
|
|
3279
3547
|
color: #c62828;
|
|
3280
3548
|
}
|
|
3549
|
+
${modalVisibilityCss}
|
|
3281
3550
|
</style>
|
|
3282
3551
|
</head>
|
|
3283
3552
|
<body>
|
package/lib/lib/liquid-engine.js
CHANGED
|
@@ -233,9 +233,11 @@ function registerCustomTags(liquid, themePath) {
|
|
|
233
233
|
if (widget.template_path) {
|
|
234
234
|
const widgetPath = path.join(themePath, `${widget.template_path}.liquid`);
|
|
235
235
|
const altPath = path.join(themePath, widget.template_path);
|
|
236
|
+
const baseName = (widget.template_path.replace(/^widgets\//, '').replace(/\.liquid$/, '') || widget.template_path).split('/').pop();
|
|
237
|
+
const sharedPath = path.join(themePath, 'widgets', 'shared', `${baseName}.liquid`);
|
|
236
238
|
|
|
237
239
|
const finalPath = fs.existsSync(widgetPath) ? widgetPath :
|
|
238
|
-
(fs.existsSync(altPath) ? altPath : null);
|
|
240
|
+
(fs.existsSync(altPath) ? altPath : (fs.existsSync(sharedPath) ? sharedPath : null));
|
|
239
241
|
|
|
240
242
|
if (finalPath) {
|
|
241
243
|
const fullContext = currentRenderingContext
|