@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.
Files changed (116) hide show
  1. package/README.md +425 -0
  2. package/assets/Logo_o2vend.png +0 -0
  3. package/assets/favicon.png +0 -0
  4. package/assets/logo-white.png +0 -0
  5. package/bin/o2vend +42 -0
  6. package/config/widget-map.json +50 -0
  7. package/lib/commands/check.js +201 -0
  8. package/lib/commands/generate.js +33 -0
  9. package/lib/commands/init.js +214 -0
  10. package/lib/commands/optimize.js +216 -0
  11. package/lib/commands/package.js +208 -0
  12. package/lib/commands/serve.js +105 -0
  13. package/lib/commands/validate.js +191 -0
  14. package/lib/lib/api-client.js +357 -0
  15. package/lib/lib/dev-server.js +2618 -0
  16. package/lib/lib/file-watcher.js +80 -0
  17. package/lib/lib/hot-reload.js +106 -0
  18. package/lib/lib/liquid-engine.js +822 -0
  19. package/lib/lib/liquid-filters.js +671 -0
  20. package/lib/lib/mock-api-server.js +989 -0
  21. package/lib/lib/mock-data.js +1468 -0
  22. package/lib/lib/widget-service.js +321 -0
  23. package/package.json +70 -0
  24. package/test-theme/README.md +27 -0
  25. package/test-theme/assets/async-sections.js +446 -0
  26. package/test-theme/assets/cart-drawer.js +463 -0
  27. package/test-theme/assets/cart-manager.js +223 -0
  28. package/test-theme/assets/checkout-price-handler.js +368 -0
  29. package/test-theme/assets/components.css +4629 -0
  30. package/test-theme/assets/delivery-zone.css +299 -0
  31. package/test-theme/assets/delivery-zone.js +396 -0
  32. package/test-theme/assets/logo.png +0 -0
  33. package/test-theme/assets/sections.css +48 -0
  34. package/test-theme/assets/theme.css +3500 -0
  35. package/test-theme/assets/theme.js +3745 -0
  36. package/test-theme/config/settings_data.json +292 -0
  37. package/test-theme/config/settings_schema.json +1050 -0
  38. package/test-theme/layout/theme.liquid +195 -0
  39. package/test-theme/locales/en.default.json +260 -0
  40. package/test-theme/sections/content-fallback.liquid +53 -0
  41. package/test-theme/sections/content.liquid +57 -0
  42. package/test-theme/sections/footer-fallback.liquid +328 -0
  43. package/test-theme/sections/footer.liquid +278 -0
  44. package/test-theme/sections/header-fallback.liquid +1805 -0
  45. package/test-theme/sections/header.liquid +1145 -0
  46. package/test-theme/sections/hero-fallback.liquid +212 -0
  47. package/test-theme/sections/hero.liquid +136 -0
  48. package/test-theme/snippets/account-sidebar.liquid +200 -0
  49. package/test-theme/snippets/add-to-cart-modal.liquid +484 -0
  50. package/test-theme/snippets/breadcrumbs.liquid +134 -0
  51. package/test-theme/snippets/cart-drawer.liquid +467 -0
  52. package/test-theme/snippets/delivery-zone-city-selector.liquid +79 -0
  53. package/test-theme/snippets/delivery-zone-modal.liquid +337 -0
  54. package/test-theme/snippets/delivery-zone-search.liquid +78 -0
  55. package/test-theme/snippets/icon.liquid +105 -0
  56. package/test-theme/snippets/login-modal.liquid +346 -0
  57. package/test-theme/snippets/mega-menu.liquid +812 -0
  58. package/test-theme/snippets/news-thumbnail.liquid +187 -0
  59. package/test-theme/snippets/pagination.liquid +120 -0
  60. package/test-theme/snippets/price.liquid +92 -0
  61. package/test-theme/snippets/product-card-related.liquid +78 -0
  62. package/test-theme/snippets/product-card-simple.liquid +41 -0
  63. package/test-theme/snippets/product-card.liquid +697 -0
  64. package/test-theme/snippets/rating.liquid +85 -0
  65. package/test-theme/snippets/skeleton-collection-grid.liquid +114 -0
  66. package/test-theme/snippets/skeleton-product-card.liquid +124 -0
  67. package/test-theme/snippets/skeleton-product-grid.liquid +34 -0
  68. package/test-theme/snippets/social-sharing.liquid +185 -0
  69. package/test-theme/templates/account/dashboard.liquid +401 -0
  70. package/test-theme/templates/account/loyalty-redemption.liquid +405 -0
  71. package/test-theme/templates/account/loyalty.liquid +588 -0
  72. package/test-theme/templates/account/order-detail.liquid +230 -0
  73. package/test-theme/templates/account/orders.liquid +349 -0
  74. package/test-theme/templates/account/profile.liquid +758 -0
  75. package/test-theme/templates/account/register.liquid +232 -0
  76. package/test-theme/templates/account/return-orders.liquid +348 -0
  77. package/test-theme/templates/account/store-credit.liquid +464 -0
  78. package/test-theme/templates/account/subscriptions.liquid +601 -0
  79. package/test-theme/templates/account/wishlist.liquid +419 -0
  80. package/test-theme/templates/address-book.liquid +1092 -0
  81. package/test-theme/templates/categories.liquid +452 -0
  82. package/test-theme/templates/checkout.liquid +4511 -0
  83. package/test-theme/templates/error.liquid +384 -0
  84. package/test-theme/templates/index.liquid +11 -0
  85. package/test-theme/templates/login.liquid +185 -0
  86. package/test-theme/templates/order-confirmation.liquid +720 -0
  87. package/test-theme/templates/page.liquid +297 -0
  88. package/test-theme/templates/product-detail.liquid +4363 -0
  89. package/test-theme/templates/products.liquid +518 -0
  90. package/test-theme/templates/search.liquid +922 -0
  91. package/test-theme/theme.json.example +19 -0
  92. package/test-theme/widgets/brand-carousel.liquid +676 -0
  93. package/test-theme/widgets/brand.liquid +245 -0
  94. package/test-theme/widgets/carousel.liquid +843 -0
  95. package/test-theme/widgets/category-list-carousel.liquid +656 -0
  96. package/test-theme/widgets/category-list.liquid +340 -0
  97. package/test-theme/widgets/category.liquid +475 -0
  98. package/test-theme/widgets/discount-time.liquid +176 -0
  99. package/test-theme/widgets/footer-menu.liquid +695 -0
  100. package/test-theme/widgets/footer.liquid +179 -0
  101. package/test-theme/widgets/gallery.liquid +271 -0
  102. package/test-theme/widgets/header-menu.liquid +932 -0
  103. package/test-theme/widgets/header.liquid +159 -0
  104. package/test-theme/widgets/html.liquid +214 -0
  105. package/test-theme/widgets/news.liquid +217 -0
  106. package/test-theme/widgets/product-canvas.liquid +235 -0
  107. package/test-theme/widgets/product-carousel.liquid +502 -0
  108. package/test-theme/widgets/product.liquid +45 -0
  109. package/test-theme/widgets/recently-viewed.liquid +26 -0
  110. package/test-theme/widgets/shared/product-grid.liquid +339 -0
  111. package/test-theme/widgets/simple-product.liquid +42 -0
  112. package/test-theme/widgets/single-product.liquid +610 -0
  113. package/test-theme/widgets/spacebar-carousel.liquid +663 -0
  114. package/test-theme/widgets/spacebar.liquid +279 -0
  115. package/test-theme/widgets/splash.liquid +378 -0
  116. 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
+