@o2vend/theme-cli 1.0.32
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/README.md +425 -0
- package/assets/Logo_o2vend.png +0 -0
- package/assets/favicon.png +0 -0
- package/assets/logo-white.png +0 -0
- package/bin/o2vend +42 -0
- package/config/widget-map.json +50 -0
- package/lib/commands/check.js +201 -0
- package/lib/commands/generate.js +33 -0
- package/lib/commands/init.js +214 -0
- package/lib/commands/optimize.js +216 -0
- package/lib/commands/package.js +208 -0
- package/lib/commands/serve.js +105 -0
- package/lib/commands/validate.js +191 -0
- package/lib/lib/api-client.js +357 -0
- package/lib/lib/dev-server.js +2618 -0
- package/lib/lib/file-watcher.js +80 -0
- package/lib/lib/hot-reload.js +106 -0
- package/lib/lib/liquid-engine.js +822 -0
- package/lib/lib/liquid-filters.js +671 -0
- package/lib/lib/mock-api-server.js +989 -0
- package/lib/lib/mock-data.js +1468 -0
- package/lib/lib/widget-service.js +321 -0
- package/package.json +70 -0
- package/test-theme/README.md +27 -0
- package/test-theme/assets/async-sections.js +446 -0
- package/test-theme/assets/cart-drawer.js +463 -0
- package/test-theme/assets/cart-manager.js +223 -0
- package/test-theme/assets/checkout-price-handler.js +368 -0
- package/test-theme/assets/components.css +4629 -0
- package/test-theme/assets/delivery-zone.css +299 -0
- package/test-theme/assets/delivery-zone.js +396 -0
- package/test-theme/assets/logo.png +0 -0
- package/test-theme/assets/sections.css +48 -0
- package/test-theme/assets/theme.css +3500 -0
- package/test-theme/assets/theme.js +3745 -0
- package/test-theme/config/settings_data.json +292 -0
- package/test-theme/config/settings_schema.json +1050 -0
- package/test-theme/layout/theme.liquid +195 -0
- package/test-theme/locales/en.default.json +260 -0
- package/test-theme/sections/content-fallback.liquid +53 -0
- package/test-theme/sections/content.liquid +57 -0
- package/test-theme/sections/footer-fallback.liquid +328 -0
- package/test-theme/sections/footer.liquid +278 -0
- package/test-theme/sections/header-fallback.liquid +1805 -0
- package/test-theme/sections/header.liquid +1145 -0
- package/test-theme/sections/hero-fallback.liquid +212 -0
- package/test-theme/sections/hero.liquid +136 -0
- package/test-theme/snippets/account-sidebar.liquid +200 -0
- package/test-theme/snippets/add-to-cart-modal.liquid +484 -0
- package/test-theme/snippets/breadcrumbs.liquid +134 -0
- package/test-theme/snippets/cart-drawer.liquid +467 -0
- package/test-theme/snippets/delivery-zone-city-selector.liquid +79 -0
- package/test-theme/snippets/delivery-zone-modal.liquid +337 -0
- package/test-theme/snippets/delivery-zone-search.liquid +78 -0
- package/test-theme/snippets/icon.liquid +105 -0
- package/test-theme/snippets/login-modal.liquid +346 -0
- package/test-theme/snippets/mega-menu.liquid +812 -0
- package/test-theme/snippets/news-thumbnail.liquid +187 -0
- package/test-theme/snippets/pagination.liquid +120 -0
- package/test-theme/snippets/price.liquid +92 -0
- package/test-theme/snippets/product-card-related.liquid +78 -0
- package/test-theme/snippets/product-card-simple.liquid +41 -0
- package/test-theme/snippets/product-card.liquid +697 -0
- package/test-theme/snippets/rating.liquid +85 -0
- package/test-theme/snippets/skeleton-collection-grid.liquid +114 -0
- package/test-theme/snippets/skeleton-product-card.liquid +124 -0
- package/test-theme/snippets/skeleton-product-grid.liquid +34 -0
- package/test-theme/snippets/social-sharing.liquid +185 -0
- package/test-theme/templates/account/dashboard.liquid +401 -0
- package/test-theme/templates/account/loyalty-redemption.liquid +405 -0
- package/test-theme/templates/account/loyalty.liquid +588 -0
- package/test-theme/templates/account/order-detail.liquid +230 -0
- package/test-theme/templates/account/orders.liquid +349 -0
- package/test-theme/templates/account/profile.liquid +758 -0
- package/test-theme/templates/account/register.liquid +232 -0
- package/test-theme/templates/account/return-orders.liquid +348 -0
- package/test-theme/templates/account/store-credit.liquid +464 -0
- package/test-theme/templates/account/subscriptions.liquid +601 -0
- package/test-theme/templates/account/wishlist.liquid +419 -0
- package/test-theme/templates/address-book.liquid +1092 -0
- package/test-theme/templates/categories.liquid +452 -0
- package/test-theme/templates/checkout.liquid +4511 -0
- package/test-theme/templates/error.liquid +384 -0
- package/test-theme/templates/index.liquid +11 -0
- package/test-theme/templates/login.liquid +185 -0
- package/test-theme/templates/order-confirmation.liquid +720 -0
- package/test-theme/templates/page.liquid +297 -0
- package/test-theme/templates/product-detail.liquid +4363 -0
- package/test-theme/templates/products.liquid +518 -0
- package/test-theme/templates/search.liquid +922 -0
- package/test-theme/theme.json.example +19 -0
- package/test-theme/widgets/brand-carousel.liquid +676 -0
- package/test-theme/widgets/brand.liquid +245 -0
- package/test-theme/widgets/carousel.liquid +843 -0
- package/test-theme/widgets/category-list-carousel.liquid +656 -0
- package/test-theme/widgets/category-list.liquid +340 -0
- package/test-theme/widgets/category.liquid +475 -0
- package/test-theme/widgets/discount-time.liquid +176 -0
- package/test-theme/widgets/footer-menu.liquid +695 -0
- package/test-theme/widgets/footer.liquid +179 -0
- package/test-theme/widgets/gallery.liquid +271 -0
- package/test-theme/widgets/header-menu.liquid +932 -0
- package/test-theme/widgets/header.liquid +159 -0
- package/test-theme/widgets/html.liquid +214 -0
- package/test-theme/widgets/news.liquid +217 -0
- package/test-theme/widgets/product-canvas.liquid +235 -0
- package/test-theme/widgets/product-carousel.liquid +502 -0
- package/test-theme/widgets/product.liquid +45 -0
- package/test-theme/widgets/recently-viewed.liquid +26 -0
- package/test-theme/widgets/shared/product-grid.liquid +339 -0
- package/test-theme/widgets/simple-product.liquid +42 -0
- package/test-theme/widgets/single-product.liquid +610 -0
- package/test-theme/widgets/spacebar-carousel.liquid +663 -0
- package/test-theme/widgets/spacebar.liquid +279 -0
- package/test-theme/widgets/splash.liquid +378 -0
- package/test-theme/widgets/testimonial-carousel.liquid +709 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async Section Loader
|
|
3
|
+
* Loads non-critical page sections asynchronously to improve initial page load time
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
(function() {
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const AsyncSectionLoader = {
|
|
10
|
+
config: {},
|
|
11
|
+
retryAttempts: 3,
|
|
12
|
+
retryDelay: 1000,
|
|
13
|
+
cache: new Map(),
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Initialize the async section loader
|
|
17
|
+
* @param {Object} sectionsConfig - Configuration object with section endpoints
|
|
18
|
+
*/
|
|
19
|
+
init(sectionsConfig) {
|
|
20
|
+
this.config = sectionsConfig || {};
|
|
21
|
+
console.log('[AsyncSectionLoader] Initializing with config:', this.config);
|
|
22
|
+
|
|
23
|
+
// Find all async sections in the page
|
|
24
|
+
const asyncSections = document.querySelectorAll('[data-async-section]');
|
|
25
|
+
|
|
26
|
+
if (asyncSections.length === 0) {
|
|
27
|
+
console.log('[AsyncSectionLoader] No async sections found');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log(`[AsyncSectionLoader] Found ${asyncSections.length} async sections`);
|
|
32
|
+
|
|
33
|
+
// Load each section
|
|
34
|
+
asyncSections.forEach(section => {
|
|
35
|
+
this.loadSection(section);
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Load a specific section
|
|
41
|
+
* @param {HTMLElement} sectionElement - The section DOM element
|
|
42
|
+
*/
|
|
43
|
+
async loadSection(sectionElement) {
|
|
44
|
+
const sectionName = sectionElement.getAttribute('data-async-section');
|
|
45
|
+
const sectionConfig = this.config[sectionName];
|
|
46
|
+
|
|
47
|
+
if (!sectionConfig) {
|
|
48
|
+
console.warn(`[AsyncSectionLoader] No config found for section: ${sectionName}`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
console.log(`[AsyncSectionLoader] Loading section: ${sectionName}`);
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
// Get data from API
|
|
56
|
+
const data = await this.fetchSectionData(
|
|
57
|
+
sectionConfig.endpoint,
|
|
58
|
+
sectionConfig.params || {}
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Render the section
|
|
62
|
+
this.renderSection(sectionElement, data, sectionName);
|
|
63
|
+
|
|
64
|
+
// Trigger custom event
|
|
65
|
+
this.triggerLoadedEvent(sectionElement, sectionName, data);
|
|
66
|
+
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error(`[AsyncSectionLoader] Failed to load section ${sectionName}:`, error);
|
|
69
|
+
this.handleError(sectionElement, sectionName, error);
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Fetch section data from API with retry logic
|
|
75
|
+
* @param {string} endpoint - API endpoint
|
|
76
|
+
* @param {Object} params - Query parameters
|
|
77
|
+
* @param {number} attempt - Current retry attempt
|
|
78
|
+
* @returns {Promise<Object>} Section data
|
|
79
|
+
*/
|
|
80
|
+
async fetchSectionData(endpoint, params = {}, attempt = 1) {
|
|
81
|
+
// Build query string
|
|
82
|
+
const queryString = new URLSearchParams(params).toString();
|
|
83
|
+
const url = queryString ? `${endpoint}?${queryString}` : endpoint;
|
|
84
|
+
|
|
85
|
+
// Check cache first
|
|
86
|
+
if (this.cache.has(url)) {
|
|
87
|
+
console.log(`[AsyncSectionLoader] Using cached data for: ${url}`);
|
|
88
|
+
return this.cache.get(url);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
console.log(`[AsyncSectionLoader] Fetching: ${url} (attempt ${attempt})`);
|
|
93
|
+
|
|
94
|
+
const response = await fetch(url, {
|
|
95
|
+
method: 'GET',
|
|
96
|
+
headers: {
|
|
97
|
+
'Accept': 'application/json',
|
|
98
|
+
'X-Requested-With': 'XMLHttpRequest'
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (!response.ok) {
|
|
103
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const data = await response.json();
|
|
107
|
+
|
|
108
|
+
// Cache the response
|
|
109
|
+
this.cache.set(url, data);
|
|
110
|
+
|
|
111
|
+
return data;
|
|
112
|
+
|
|
113
|
+
} catch (error) {
|
|
114
|
+
// Retry logic
|
|
115
|
+
if (attempt < this.retryAttempts) {
|
|
116
|
+
console.warn(`[AsyncSectionLoader] Retry ${attempt}/${this.retryAttempts} for ${url}`);
|
|
117
|
+
await this.delay(this.retryDelay * attempt);
|
|
118
|
+
return this.fetchSectionData(endpoint, params, attempt + 1);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Render section with data
|
|
127
|
+
* @param {HTMLElement} sectionElement - The section DOM element
|
|
128
|
+
* @param {Object} data - Section data from API
|
|
129
|
+
* @param {string} sectionName - Section name
|
|
130
|
+
*/
|
|
131
|
+
renderSection(sectionElement, data, sectionName) {
|
|
132
|
+
console.log(`[AsyncSectionLoader] Rendering section: ${sectionName}`, data);
|
|
133
|
+
|
|
134
|
+
// Different rendering strategies based on section type
|
|
135
|
+
switch (sectionName) {
|
|
136
|
+
case 'products':
|
|
137
|
+
this.renderProductGrid(sectionElement, data);
|
|
138
|
+
break;
|
|
139
|
+
case 'categories':
|
|
140
|
+
case 'collections':
|
|
141
|
+
this.renderCollectionGrid(sectionElement, data);
|
|
142
|
+
break;
|
|
143
|
+
case 'related-products':
|
|
144
|
+
case 'relatedProducts':
|
|
145
|
+
this.renderRelatedProducts(sectionElement, data);
|
|
146
|
+
break;
|
|
147
|
+
default:
|
|
148
|
+
console.warn(`[AsyncSectionLoader] Unknown section type: ${sectionName}`);
|
|
149
|
+
this.renderGenericSection(sectionElement, data);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Add fade-in animation
|
|
153
|
+
sectionElement.classList.add('async-loaded');
|
|
154
|
+
|
|
155
|
+
// Re-initialize any theme functionality for new content
|
|
156
|
+
// Note: Cart functionality now uses event delegation, so no need to re-initialize
|
|
157
|
+
if (window.Theme && window.Theme.initProductActions) {
|
|
158
|
+
window.Theme.initProductActions();
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Render product grid
|
|
164
|
+
*/
|
|
165
|
+
renderProductGrid(sectionElement, data) {
|
|
166
|
+
const products = data.products || data.data || [];
|
|
167
|
+
|
|
168
|
+
if (products.length === 0) {
|
|
169
|
+
sectionElement.innerHTML = this.getEmptyStateHTML('products');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const gridHTML = `
|
|
174
|
+
<div class="section-header">
|
|
175
|
+
<h2 class="section-title">Featured Products</h2>
|
|
176
|
+
<p class="section-subtitle">Handpicked items that our customers love</p>
|
|
177
|
+
</div>
|
|
178
|
+
<div class="products-grid">
|
|
179
|
+
${products.map(product => this.renderProductCard(product)).join('')}
|
|
180
|
+
</div>
|
|
181
|
+
<div class="section-footer">
|
|
182
|
+
<a href="/products" class="btn btn-outline btn-lg">View All Products</a>
|
|
183
|
+
</div>
|
|
184
|
+
`;
|
|
185
|
+
|
|
186
|
+
sectionElement.innerHTML = gridHTML;
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Render collection grid
|
|
191
|
+
*/
|
|
192
|
+
renderCollectionGrid(sectionElement, data) {
|
|
193
|
+
const collections = data.data || data.collections || [];
|
|
194
|
+
|
|
195
|
+
if (collections.length === 0) {
|
|
196
|
+
sectionElement.innerHTML = this.getEmptyStateHTML('collections');
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const gridHTML = `
|
|
201
|
+
<div class="section-header">
|
|
202
|
+
<h2 class="section-title">Shop by Collection</h2>
|
|
203
|
+
<p class="section-subtitle">Curated collections to help you find exactly what you're looking for</p>
|
|
204
|
+
</div>
|
|
205
|
+
<div class="collections-grid">
|
|
206
|
+
${collections.map(collection => this.renderCollectionCard(collection)).join('')}
|
|
207
|
+
</div>
|
|
208
|
+
`;
|
|
209
|
+
|
|
210
|
+
sectionElement.innerHTML = gridHTML;
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Render related products
|
|
215
|
+
*/
|
|
216
|
+
renderRelatedProducts(sectionElement, data) {
|
|
217
|
+
const products = data.products || data.data || [];
|
|
218
|
+
|
|
219
|
+
if (products.length === 0) {
|
|
220
|
+
sectionElement.remove();
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const gridHTML = `
|
|
225
|
+
<div class="section-header">
|
|
226
|
+
<h2 class="section-title">You May Also Like</h2>
|
|
227
|
+
<p class="section-subtitle">Complete your look with these items</p>
|
|
228
|
+
</div>
|
|
229
|
+
<div class="products-grid products-grid-4">
|
|
230
|
+
${products.map(product => this.renderProductCard(product, 'related')).join('')}
|
|
231
|
+
</div>
|
|
232
|
+
`;
|
|
233
|
+
|
|
234
|
+
sectionElement.innerHTML = gridHTML;
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Render a single product card
|
|
239
|
+
* Matches the structure of snippets/product-card.liquid
|
|
240
|
+
*/
|
|
241
|
+
renderProductCard(product, variant = 'default') {
|
|
242
|
+
const imageUrl = product.images?.[0] || product.thumbnailImage || product.image || '/assets/default/placeholder-product.png';
|
|
243
|
+
const price = this.formatPrice(product.prices?.priceString || product.prices?.price || product.price);
|
|
244
|
+
const comparePrice = product.prices?.mrp && product.prices.mrp > product.prices.price
|
|
245
|
+
? this.formatPrice(product.prices.mrpString || product.prices.mrp)
|
|
246
|
+
: null;
|
|
247
|
+
const slug = product.slug || product.handle || product.id;
|
|
248
|
+
const available = product.stockQuantity > 0 || product.inStock || product.available !== false;
|
|
249
|
+
const showSaleBadge = product.prices?.mrp && product.prices.mrp > product.prices.price;
|
|
250
|
+
|
|
251
|
+
return `
|
|
252
|
+
<div class="product-card"
|
|
253
|
+
data-price="${product.prices?.price || product.price}"
|
|
254
|
+
data-name="${this.escapeHtml((product.name || product.title || '').toLowerCase())}"
|
|
255
|
+
data-availability="${available ? 'in-stock' : 'out-of-stock'}"
|
|
256
|
+
data-brand="${this.escapeHtml((product.brandName || product.vendor || '').toLowerCase())}">
|
|
257
|
+
|
|
258
|
+
<div class="product-image-container">
|
|
259
|
+
<a href="/${slug}" class="product-image-link">
|
|
260
|
+
<img src="${imageUrl}"
|
|
261
|
+
alt="${this.escapeHtml(product.name || product.title)}"
|
|
262
|
+
class="product-image"
|
|
263
|
+
loading="lazy">
|
|
264
|
+
</a>
|
|
265
|
+
|
|
266
|
+
<!-- Product Badges -->
|
|
267
|
+
<div class="product-badges">
|
|
268
|
+
${showSaleBadge ? '<span class="product-badge product-badge-sale">Sale</span>' : ''}
|
|
269
|
+
</div>
|
|
270
|
+
|
|
271
|
+
<!-- Product Actions -->
|
|
272
|
+
<div class="product-actions">
|
|
273
|
+
<button class="product-action-btn quick-view-btn" data-product-id="${product.productId || product.id}" aria-label="Quick view">
|
|
274
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
275
|
+
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
|
|
276
|
+
<circle cx="12" cy="12" r="3"></circle>
|
|
277
|
+
</svg>
|
|
278
|
+
</button>
|
|
279
|
+
<button class="product-action-btn wishlist-btn" data-product-id="${product.productId || product.id}" aria-label="Add to wishlist">
|
|
280
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
281
|
+
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path>
|
|
282
|
+
</svg>
|
|
283
|
+
</button>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
<div class="product-content">
|
|
288
|
+
<h3 class="product-title">
|
|
289
|
+
<a href="/${slug}">
|
|
290
|
+
${this.escapeHtml(product.name || product.title)}
|
|
291
|
+
</a>
|
|
292
|
+
</h3>
|
|
293
|
+
|
|
294
|
+
<div class="product-price">
|
|
295
|
+
<span class="product-price-current">${price}</span>
|
|
296
|
+
${comparePrice ? `<span class="product-price-original">${comparePrice}</span>` : ''}
|
|
297
|
+
</div>
|
|
298
|
+
|
|
299
|
+
${product.shortDescription ? `<p class="product-description">${this.truncate(product.shortDescription, 80)}</p>` : ''}
|
|
300
|
+
|
|
301
|
+
<div class="product-actions-bottom">
|
|
302
|
+
<button class="btn btn-primary add-to-cart-btn" data-product-id="${product.productId || product.id}" ${!available ? 'disabled' : ''}>
|
|
303
|
+
${available ? 'Add to Cart' : 'Out of Stock'}
|
|
304
|
+
</button>
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
`;
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Render a single collection card
|
|
313
|
+
*/
|
|
314
|
+
renderCollectionCard(collection) {
|
|
315
|
+
const imageUrl = collection.image || '/assets/default/placeholder-collection.png';
|
|
316
|
+
const slug = collection.slug || collection.handle || collection.id;
|
|
317
|
+
const productCount = collection.productCount || collection.products_count || 0;
|
|
318
|
+
|
|
319
|
+
return `
|
|
320
|
+
<div class="collection-card">
|
|
321
|
+
<div class="collection-image-container">
|
|
322
|
+
<img src="${imageUrl}" alt="${this.escapeHtml(collection.title || collection.name)}" loading="lazy">
|
|
323
|
+
<div class="collection-overlay">
|
|
324
|
+
<a href="/collections/${slug}" class="collection-link">View Collection</a>
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
<div class="collection-content">
|
|
328
|
+
<h3 class="collection-title">
|
|
329
|
+
<a href="/collections/${slug}">${this.escapeHtml(collection.title || collection.name)}</a>
|
|
330
|
+
</h3>
|
|
331
|
+
${collection.description ? `<p class="collection-description">${this.truncate(collection.description, 120)}</p>` : ''}
|
|
332
|
+
<div class="collection-meta">
|
|
333
|
+
<span class="collection-count">${productCount} products</span>
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
`;
|
|
338
|
+
},
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Render generic section (fallback)
|
|
342
|
+
*/
|
|
343
|
+
renderGenericSection(sectionElement, data) {
|
|
344
|
+
console.log('[AsyncSectionLoader] Using generic renderer for:', data);
|
|
345
|
+
sectionElement.innerHTML = `<pre>${JSON.stringify(data, null, 2)}</pre>`;
|
|
346
|
+
},
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Get empty state HTML
|
|
350
|
+
*/
|
|
351
|
+
getEmptyStateHTML(type) {
|
|
352
|
+
const messages = {
|
|
353
|
+
products: {
|
|
354
|
+
title: 'No Products Found',
|
|
355
|
+
message: 'Check back soon for new products!'
|
|
356
|
+
},
|
|
357
|
+
collections: {
|
|
358
|
+
title: 'No Collections Available',
|
|
359
|
+
message: 'We are working on adding new collections.'
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const msg = messages[type] || messages.products;
|
|
364
|
+
|
|
365
|
+
return `
|
|
366
|
+
<div class="empty-state">
|
|
367
|
+
<h3>${msg.title}</h3>
|
|
368
|
+
<p>${msg.message}</p>
|
|
369
|
+
</div>
|
|
370
|
+
`;
|
|
371
|
+
},
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Handle section loading error
|
|
375
|
+
*/
|
|
376
|
+
handleError(sectionElement, sectionName, error) {
|
|
377
|
+
console.error(`[AsyncSectionLoader] Error in section ${sectionName}:`, error);
|
|
378
|
+
|
|
379
|
+
sectionElement.innerHTML = `
|
|
380
|
+
<div class="section-error">
|
|
381
|
+
<p>Unable to load this section. <button class="btn-link" onclick="location.reload()">Refresh page</button></p>
|
|
382
|
+
</div>
|
|
383
|
+
`;
|
|
384
|
+
|
|
385
|
+
sectionElement.classList.add('async-error');
|
|
386
|
+
},
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Trigger custom event when section is loaded
|
|
390
|
+
*/
|
|
391
|
+
triggerLoadedEvent(sectionElement, sectionName, data) {
|
|
392
|
+
const event = new CustomEvent('async-section-loaded', {
|
|
393
|
+
detail: { sectionName, data },
|
|
394
|
+
bubbles: true
|
|
395
|
+
});
|
|
396
|
+
sectionElement.dispatchEvent(event);
|
|
397
|
+
console.log(`[AsyncSectionLoader] Triggered event for: ${sectionName}`);
|
|
398
|
+
},
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Utility: Delay promise
|
|
402
|
+
*/
|
|
403
|
+
delay(ms) {
|
|
404
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
405
|
+
},
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Utility: Format price
|
|
409
|
+
*/
|
|
410
|
+
formatPrice(price) {
|
|
411
|
+
if (typeof price === 'number') {
|
|
412
|
+
return new Intl.NumberFormat('en-US', {
|
|
413
|
+
style: 'currency',
|
|
414
|
+
currency: 'USD'
|
|
415
|
+
}).format(price / 100);
|
|
416
|
+
}
|
|
417
|
+
return price;
|
|
418
|
+
},
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Utility: Escape HTML
|
|
422
|
+
*/
|
|
423
|
+
escapeHtml(text) {
|
|
424
|
+
const div = document.createElement('div');
|
|
425
|
+
div.textContent = text;
|
|
426
|
+
return div.innerHTML;
|
|
427
|
+
},
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Utility: Truncate text
|
|
431
|
+
*/
|
|
432
|
+
truncate(text, length) {
|
|
433
|
+
// Strip HTML tags first
|
|
434
|
+
const strippedText = text.replace(/<[^>]*>/g, '');
|
|
435
|
+
if (strippedText.length <= length) return this.escapeHtml(strippedText);
|
|
436
|
+
return this.escapeHtml(strippedText.substring(0, length)) + '...';
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
// Make available globally
|
|
441
|
+
window.AsyncSectionLoader = AsyncSectionLoader;
|
|
442
|
+
|
|
443
|
+
console.log('[AsyncSectionLoader] Module loaded');
|
|
444
|
+
|
|
445
|
+
})();
|
|
446
|
+
|