@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,518 @@
1
+ {% layout 'layout/theme' %}
2
+ {% comment %}
3
+ O2VEND Default Theme - Products Template
4
+ {% endcomment %}
5
+
6
+ <!-- Breadcrumb Navigation Removed for Default Style -->
7
+
8
+ <!-- Collection Header -->
9
+ <section class="collection-header">
10
+ <div class="collection-header-wrapper">
11
+ <div class="collection-header-inner">
12
+ <div class="collection-header-left">
13
+ <h1 class="collection-title collection-title--compact">{{ collection.title }}</h1>
14
+ {% if collection.description %}
15
+ <div class="collection-description">
16
+ {{ collection.description }}
17
+ </div>
18
+ {% endif %}
19
+ </div>
20
+ <div class="collection-header-right">
21
+ <span class="collection-count">{{ collection.totalProducts | default: collection.products.size }} products</span>
22
+ </div>
23
+ </div>
24
+ </div>
25
+ </section>
26
+
27
+ <!-- Collection Toolbar -->
28
+ <section class="collection-toolbar">
29
+ <div class="container">
30
+ <div class="toolbar-content">
31
+ <!-- Filters -->
32
+ <div class="filters-section">
33
+ <button class="btn btn-outline filter-toggle" id="filter-toggle" aria-label="Filter products">
34
+ <svg class="icon-filter" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
35
+ <polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon>
36
+ </svg>
37
+ <span>Filters</span>
38
+ </button>
39
+
40
+ <div class="filter-dropdown" id="filter-dropdown">
41
+ <div class="filter-header">
42
+ <h3 class="filter-title">Filter Products</h3>
43
+ <button class="filter-close" id="filter-close">×</button>
44
+ </div>
45
+
46
+ <div class="filter-content">
47
+ <div class="filter-group">
48
+ <h4 class="filter-group-title">PRICE RANGE</h4>
49
+ <div class="price-range">
50
+ <div class="price-inputs">
51
+ <input type="number" id="price-min" placeholder="Min" min="0" class="price-input">
52
+ <span class="price-separator">-</span>
53
+ <input type="number" id="price-max" placeholder="Max" min="0" class="price-input">
54
+ </div>
55
+ </div>
56
+ </div>
57
+
58
+ <div class="filter-group">
59
+ <h4 class="filter-group-title">AVAILABILITY</h4>
60
+ <div class="filter-options">
61
+ <label class="filter-option">
62
+ <input type="checkbox" name="availability" value="in-stock" class="filter-checkbox">
63
+ <span class="filter-option-text">In Stock</span>
64
+ </label>
65
+ <label class="filter-option">
66
+ <input type="checkbox" name="availability" value="on-sale" class="filter-checkbox">
67
+ <span class="filter-option-text">On Sale</span>
68
+ </label>
69
+ </div>
70
+ </div>
71
+ </div>
72
+
73
+ <div class="filter-actions">
74
+ <button class="btn btn-ghost" id="clear-filters">Clear All</button>
75
+ <button class="btn btn-primary" id="apply-filters">Apply Filters</button>
76
+ </div>
77
+ </div>
78
+ </div>
79
+
80
+ <!-- Sort -->
81
+ <div class="sort-section">
82
+ <div class="sort-dropdown" id="sort-dropdown">
83
+ <button class="btn btn-outline sort-toggle" id="sort-toggle" aria-haspopup="listbox" aria-expanded="false" aria-label="Sort products">
84
+ <svg class="icon-sort" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
85
+ <path d="M3 6h18M7 12h10M11 18h2"></path>
86
+ </svg>
87
+ <span id="sort-toggle-label">Sort: Featured</span>
88
+ <svg class="icon-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
89
+ <polyline points="6 9 12 15 18 9"></polyline>
90
+ </svg>
91
+ </button>
92
+ <ul class="sort-menu" id="sort-menu" role="listbox">
93
+ <li class="sort-option" role="option" data-value="featured" data-label="Featured">Featured</li>
94
+ <li class="sort-option" role="option" data-value="price-asc" data-label="Price, low to high">Price, low to high</li>
95
+ <li class="sort-option" role="option" data-value="price-desc" data-label="Price, high to low">Price, high to low</li>
96
+ <li class="sort-option" role="option" data-value="newest" data-label="Date, new to old">Date, new to old</li>
97
+ </ul>
98
+ </div>
99
+ </div>
100
+ </div>
101
+ </div>
102
+ </section>
103
+
104
+ <!-- Products Section -->
105
+ {% hook 'product_list_before' %}
106
+ <section class="collection-products">
107
+ <div class="container">
108
+ <!-- Products Grid -->
109
+ <div class="products-container">
110
+ <div class="products-grid" id="products-grid">
111
+ {% for product in collection.products %}
112
+ {% hook 'product_card_before' %}
113
+ {% include 'snippets/product-card', product: product %}
114
+ {% hook 'product_card_after' %}
115
+ {% endfor %}
116
+ </div>
117
+
118
+ <!-- No Products Message -->
119
+ {% if collection.products.size == 0 %}
120
+ <div class="no-products">
121
+ <div class="no-products-content">
122
+ <h3 class="no-products-title">No products found</h3>
123
+ <p class="no-products-description">Try adjusting your filters or browse our other collections.</p>
124
+ <div class="no-products-actions">
125
+ <button class="btn btn-primary" id="clear-all-filters">Clear All Filters</button>
126
+ <a href="/collections" class="btn btn-outline">Browse Collections</a>
127
+ </div>
128
+ </div>
129
+ </div>
130
+ {% endif %}
131
+ </div>
132
+
133
+ <!-- Pagination -->
134
+ {% include 'snippets/pagination', pagination: pagination %}
135
+ </div>
136
+ </section>
137
+ {% hook 'product_list_after' %}
138
+
139
+ <script>
140
+ // Collection page specific JavaScript - Server-side filtering & sorting
141
+ document.addEventListener('DOMContentLoaded', function() {
142
+ // Parse URL parameters
143
+ const urlParams = new URLSearchParams(window.location.search);
144
+
145
+ // Helper function to build and navigate to filtered URL
146
+ function applyFiltersToURL() {
147
+ const params = new URLSearchParams(window.location.search);
148
+
149
+ // Price filter
150
+ const priceMin = document.getElementById('price-min');
151
+ const priceMax = document.getElementById('price-max');
152
+
153
+ if (priceMin && priceMin.value) {
154
+ params.set('price_min', priceMin.value);
155
+ } else {
156
+ params.delete('price_min');
157
+ }
158
+
159
+ if (priceMax && priceMax.value) {
160
+ params.set('price_max', priceMax.value);
161
+ } else {
162
+ params.delete('price_max');
163
+ }
164
+
165
+ // Availability filter
166
+ const inStockCheckbox = document.querySelector('input[name="availability"][value="in-stock"]');
167
+ if (inStockCheckbox && inStockCheckbox.checked) {
168
+ params.set('in_stock', 'true');
169
+ } else {
170
+ params.delete('in_stock');
171
+ }
172
+
173
+ const onSaleCheckbox = document.querySelector('input[name="availability"][value="on-sale"]');
174
+ if (onSaleCheckbox && onSaleCheckbox.checked) {
175
+ params.set('on_sale', 'true');
176
+ } else {
177
+ params.delete('on_sale');
178
+ }
179
+
180
+ // Brand filter
181
+ const brandCheckboxes = document.querySelectorAll('input[name="brand"]:checked');
182
+ if (brandCheckboxes.length > 0) {
183
+ const selectedBrands = Array.from(brandCheckboxes).map(cb => cb.value);
184
+ params.set('brand', selectedBrands.join(','));
185
+ } else {
186
+ params.delete('brand');
187
+ }
188
+
189
+ // Reset to page 1 when filters change
190
+ params.set('page', '1');
191
+
192
+ // Navigate to new URL
193
+ window.location.href = `${window.location.pathname}?${params.toString()}`;
194
+ }
195
+
196
+ // Initialize filters from URL parameters
197
+ function initializeFiltersFromURL() {
198
+ // Price filters
199
+ const priceMin = document.getElementById('price-min');
200
+ const priceMax = document.getElementById('price-max');
201
+ const priceRangeMin = document.getElementById('price-range-min');
202
+ const priceRangeMax = document.getElementById('price-range-max');
203
+
204
+ if (urlParams.has('price_min')) {
205
+ const minValue = urlParams.get('price_min');
206
+ if (priceMin) priceMin.value = minValue;
207
+ if (priceRangeMin) priceRangeMin.value = minValue;
208
+ }
209
+
210
+ if (urlParams.has('price_max')) {
211
+ const maxValue = urlParams.get('price_max');
212
+ if (priceMax) priceMax.value = maxValue;
213
+ if (priceRangeMax) priceRangeMax.value = maxValue;
214
+ }
215
+
216
+ // Availability filters
217
+ const inStockCheckbox = document.querySelector('input[name="availability"][value="in-stock"]');
218
+ if (inStockCheckbox && urlParams.has('in_stock')) {
219
+ inStockCheckbox.checked = true;
220
+ }
221
+
222
+ const onSaleCheckbox = document.querySelector('input[name="availability"][value="on-sale"]');
223
+ if (onSaleCheckbox && urlParams.has('on_sale')) {
224
+ onSaleCheckbox.checked = true;
225
+ }
226
+
227
+ // Brand filters
228
+ if (urlParams.has('brand')) {
229
+ const selectedBrands = urlParams.get('brand').split(',');
230
+ selectedBrands.forEach(brand => {
231
+ const checkbox = document.querySelector(`input[name="brand"][value="${brand}"]`);
232
+ if (checkbox) checkbox.checked = true;
233
+ });
234
+ }
235
+
236
+ // Sort initialization - Reconstruct dropdown value from sort and order parameters
237
+ const sortToggleLabel = document.getElementById('sort-toggle-label');
238
+ const resultsSort = document.getElementById('results-sort');
239
+ const sortMenu = document.getElementById('sort-menu');
240
+
241
+ if (urlParams.has('sort')) {
242
+ const sortParam = urlParams.get('sort');
243
+ const orderParam = urlParams.get('order') || 'desc'; // Default to 'desc' if not specified
244
+ let dropdownValue = 'featured';
245
+ let sortLabel = 'Featured';
246
+
247
+ // Reconstruct the dropdown value format based on URL parameters
248
+ if (sortParam === 'created' && orderParam === 'desc') {
249
+ dropdownValue = 'newest';
250
+ sortLabel = 'Date, new to old';
251
+ } else if (sortParam === 'price' && orderParam === 'asc') {
252
+ dropdownValue = 'price-asc';
253
+ sortLabel = 'Price, low to high';
254
+ } else if (sortParam === 'price' && orderParam === 'desc') {
255
+ dropdownValue = 'price-desc';
256
+ sortLabel = 'Price, high to low';
257
+ } else if (sortParam === 'featured') {
258
+ // featured doesn't depend on order parameter
259
+ dropdownValue = 'featured';
260
+ sortLabel = 'Featured';
261
+ }
262
+
263
+ // Update the sort toggle label
264
+ if (sortToggleLabel) {
265
+ sortToggleLabel.textContent = `Sort: ${sortLabel}`;
266
+ }
267
+
268
+ // Update the results sort label
269
+ if (resultsSort) {
270
+ resultsSort.textContent = `Sorted by ${sortLabel}`;
271
+ }
272
+
273
+ // Mark the selected option as active
274
+ if (sortMenu) {
275
+ const sortOptions = sortMenu.querySelectorAll('.sort-option');
276
+ sortOptions.forEach(option => {
277
+ option.classList.remove('active', 'selected');
278
+ if (option.getAttribute('data-value') === dropdownValue) {
279
+ option.classList.add('active', 'selected');
280
+ option.setAttribute('aria-selected', 'true');
281
+ } else {
282
+ option.setAttribute('aria-selected', 'false');
283
+ }
284
+ });
285
+ }
286
+ } else {
287
+ // No sort parameter, show default
288
+ if (sortToggleLabel) {
289
+ sortToggleLabel.textContent = 'Sort: Featured';
290
+ }
291
+ if (resultsSort) {
292
+ resultsSort.textContent = 'Sorted by Featured';
293
+ }
294
+ // Mark featured as selected
295
+ if (sortMenu) {
296
+ const featuredOption = sortMenu.querySelector('.sort-option[data-value="featured"]');
297
+ if (featuredOption) {
298
+ featuredOption.classList.add('active', 'selected');
299
+ featuredOption.setAttribute('aria-selected', 'true');
300
+ }
301
+ }
302
+ }
303
+
304
+ // Show reset button if any filters are active
305
+ const resetFiltersBtn = document.getElementById('reset-filters');
306
+ if (resetFiltersBtn && (urlParams.has('price_min') || urlParams.has('price_max') ||
307
+ urlParams.has('in_stock') || urlParams.has('on_sale') || urlParams.has('brand'))) {
308
+ resetFiltersBtn.style.display = 'block';
309
+ }
310
+ }
311
+
312
+ // Initialize filters on page load
313
+ initializeFiltersFromURL();
314
+
315
+ // Filter functionality
316
+ const filterToggle = document.getElementById('filter-toggle');
317
+ const filterDropdown = document.getElementById('filter-dropdown');
318
+ const filterClose = document.getElementById('filter-close');
319
+ const clearFiltersBtn = document.getElementById('clear-filters');
320
+ const applyFiltersBtn = document.getElementById('apply-filters');
321
+ const resetFiltersBtn = document.getElementById('reset-filters');
322
+ const clearAllFiltersBtn = document.getElementById('clear-all-filters');
323
+
324
+ // Toggle filter dropdown
325
+ if (filterToggle && filterDropdown) {
326
+ filterToggle.addEventListener('click', function() {
327
+ filterDropdown.classList.toggle('active');
328
+ document.body.classList.toggle('filter-open');
329
+ });
330
+ }
331
+
332
+ // Close filter dropdown
333
+ if (filterClose) {
334
+ filterClose.addEventListener('click', function() {
335
+ filterDropdown.classList.remove('active');
336
+ document.body.classList.remove('filter-open');
337
+ });
338
+ }
339
+
340
+ // Close filter dropdown when clicking outside
341
+ document.addEventListener('click', function(e) {
342
+ if (filterDropdown && !filterDropdown.contains(e.target) && !filterToggle.contains(e.target)) {
343
+ filterDropdown.classList.remove('active');
344
+ document.body.classList.remove('filter-open');
345
+ }
346
+ });
347
+
348
+ // Clear filters (but keep sort)
349
+ function clearAllFilters() {
350
+ // Navigate to page without filter parameters (but keep sort if present)
351
+ const params = new URLSearchParams(window.location.search);
352
+ params.delete('price_min');
353
+ params.delete('price_max');
354
+ params.delete('in_stock');
355
+ params.delete('on_sale');
356
+ params.delete('brand');
357
+ params.delete('page');
358
+ // Keep sort and order parameters
359
+
360
+ window.location.href = window.location.pathname + (params.toString() ? '?' + params.toString() : '');
361
+ }
362
+
363
+ if (clearFiltersBtn) {
364
+ clearFiltersBtn.addEventListener('click', clearAllFilters);
365
+ }
366
+
367
+ if (clearAllFiltersBtn) {
368
+ clearAllFiltersBtn.addEventListener('click', clearAllFilters);
369
+ }
370
+
371
+ if (resetFiltersBtn) {
372
+ resetFiltersBtn.addEventListener('click', clearAllFilters);
373
+ }
374
+
375
+ // Apply filters - Submit to server
376
+ if (applyFiltersBtn) {
377
+ applyFiltersBtn.addEventListener('click', function() {
378
+ applyFiltersToURL();
379
+ });
380
+ }
381
+
382
+ // Sorting functionality - custom dropdown
383
+ const sortDropdown = document.getElementById('sort-dropdown');
384
+ const sortToggle = document.getElementById('sort-toggle');
385
+ const sortMenu = document.getElementById('sort-menu');
386
+ const sortLabel = document.getElementById('sort-toggle-label');
387
+
388
+ function applySort(sortValue) {
389
+ const params = new URLSearchParams(window.location.search);
390
+ if (sortValue && sortValue !== 'featured') {
391
+ const parts = sortValue.split('-');
392
+ if (parts.length === 2) {
393
+ params.set('sort', parts[0]);
394
+ params.set('order', parts[1]);
395
+ } else if (sortValue === 'newest') {
396
+ params.set('sort', 'created');
397
+ params.set('order', 'desc');
398
+ }
399
+ } else {
400
+ params.set('sort', 'featured');
401
+ params.set('order', 'desc');
402
+ }
403
+ params.set('page', '1');
404
+ window.location.href = `${window.location.pathname}?${params.toString()}`;
405
+ }
406
+
407
+ if (sortToggle && sortMenu) {
408
+ sortToggle.addEventListener('click', function() {
409
+ sortDropdown.classList.toggle('open');
410
+ const expanded = sortToggle.getAttribute('aria-expanded') === 'true';
411
+ sortToggle.setAttribute('aria-expanded', (!expanded).toString());
412
+ });
413
+
414
+ sortMenu.querySelectorAll('.sort-option').forEach(option => {
415
+ option.addEventListener('click', function() {
416
+ const value = this.getAttribute('data-value');
417
+ const label = this.getAttribute('data-label') || this.textContent;
418
+
419
+ // Update sort toggle label
420
+ if (sortLabel) {
421
+ sortLabel.textContent = `Sort: ${label}`;
422
+ }
423
+
424
+ // Update results sort text
425
+ const resultsSort = document.getElementById('results-sort');
426
+ if (resultsSort) {
427
+ resultsSort.textContent = `Sorted by ${label}`;
428
+ }
429
+
430
+ // Update active state
431
+ sortMenu.querySelectorAll('.sort-option').forEach(opt => {
432
+ opt.classList.remove('active', 'selected');
433
+ opt.setAttribute('aria-selected', 'false');
434
+ });
435
+ this.classList.add('active', 'selected');
436
+ this.setAttribute('aria-selected', 'true');
437
+
438
+ sortDropdown.classList.remove('open');
439
+ applySort(value);
440
+ });
441
+ });
442
+
443
+ // Close on outside click
444
+ document.addEventListener('click', function(e) {
445
+ if (!sortDropdown.contains(e.target)) {
446
+ sortDropdown.classList.remove('open');
447
+ sortToggle.setAttribute('aria-expanded', 'false');
448
+ }
449
+ });
450
+ }
451
+
452
+ // View toggle functionality (client-side only for UI)
453
+ const viewBtns = document.querySelectorAll('.view-btn');
454
+ const productsGrid = document.getElementById('products-grid');
455
+
456
+ viewBtns.forEach(btn => {
457
+ btn.addEventListener('click', function() {
458
+ const view = this.dataset.view;
459
+
460
+ // Update active button
461
+ viewBtns.forEach(b => b.classList.remove('active'));
462
+ this.classList.add('active');
463
+
464
+ // Update grid class
465
+ if (productsGrid) {
466
+ productsGrid.dataset.view = view;
467
+ productsGrid.className = `products-grid products-${view}`;
468
+ }
469
+
470
+ // Save preference to localStorage
471
+ localStorage.setItem('product-view-preference', view);
472
+ });
473
+ });
474
+
475
+ // Restore view preference
476
+ const savedView = localStorage.getItem('product-view-preference');
477
+ if (savedView && productsGrid) {
478
+ const viewBtn = document.querySelector(`.view-btn[data-view="${savedView}"]`);
479
+ if (viewBtn) {
480
+ viewBtns.forEach(b => b.classList.remove('active'));
481
+ viewBtn.classList.add('active');
482
+ productsGrid.dataset.view = savedView;
483
+ productsGrid.className = `products-grid products-${savedView}`;
484
+ }
485
+ }
486
+
487
+ // Price range slider sync
488
+ const priceMin = document.getElementById('price-min');
489
+ const priceMax = document.getElementById('price-max');
490
+ const priceRangeMin = document.getElementById('price-range-min');
491
+ const priceRangeMax = document.getElementById('price-range-max');
492
+
493
+ if (priceMin && priceRangeMin) {
494
+ priceMin.addEventListener('input', function() {
495
+ priceRangeMin.value = this.value;
496
+ });
497
+ }
498
+
499
+ if (priceMax && priceRangeMax) {
500
+ priceMax.addEventListener('input', function() {
501
+ priceRangeMax.value = this.value;
502
+ });
503
+ }
504
+
505
+ if (priceRangeMin && priceMin) {
506
+ priceRangeMin.addEventListener('input', function() {
507
+ priceMin.value = this.value;
508
+ });
509
+ }
510
+
511
+ if (priceRangeMax && priceMax) {
512
+ priceRangeMax.addEventListener('input', function() {
513
+ priceMax.value = this.value;
514
+ });
515
+ }
516
+ });
517
+ </script>
518
+