@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,4363 @@
1
+ {% layout 'layout/theme' %}
2
+ {% comment %}
3
+ O2VEND Default Theme - Product Detail Page
4
+ Inspired by Shopify Horizon Theme
5
+ {% endcomment %}
6
+
7
+ <!-- Product Main Section -->
8
+ {% hook 'product_before' %}
9
+ <section class="product-main horizon-style">
10
+ <div class="product-container">
11
+ <!-- Product Gallery -->
12
+ {% hook 'product_images_before' %}
13
+ <div class="product-gallery">
14
+ <div class="gallery-main">
15
+ {% if product.images and product.images.size > 0 %}
16
+ {% for image in product.images %}
17
+ <img
18
+ src="{{ image.url | default: image }}"
19
+ alt="{{ product.name | default: product.title }} - {{ forloop.index }}"
20
+ class="gallery-main-image {% if forloop.first %}active{% endif %}"
21
+ data-index="{{ forloop.index0 }}"
22
+ id="mainProductImage"
23
+ loading="{% if forloop.first %}eager{% else %}lazy{% endif %}"
24
+ >
25
+ {% endfor %}
26
+ {% elsif product.thumbnailImage %}
27
+ {% assign thumbImage = product.thumbnailImage %}
28
+ <img
29
+ src="{{ thumbImage.url | default: thumbImage }}"
30
+ alt="{{ product.name | default: product.title }}"
31
+ class="gallery-main-image active"
32
+ id="mainProductImage"
33
+ loading="eager"
34
+ >
35
+ {% else %}
36
+ <div class="gallery-placeholder">
37
+ <svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
38
+ <rect x="16" y="16" width="32" height="32" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
39
+ <path d="M16 24L24 32L32 24L40 32L48 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
40
+ </svg>
41
+ </div>
42
+ {% endif %}
43
+
44
+ <!-- Full Screen Button -->
45
+ {% if product.images and product.images.size > 0 %}
46
+ <button class="gallery-zoom-btn" id="galleryZoomBtn" aria-label="Open image in full screen">
47
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
48
+ <path d="M6 3H3V6M14 3H17V6M6 17H3V14M14 17H17V14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
49
+ </svg>
50
+ </button>
51
+ {% endif %}
52
+ </div>
53
+
54
+ <!-- Thumbnails -->
55
+ {% if product.images and product.images.size > 1 %}
56
+ <div class="gallery-thumbnails" id="galleryThumbnails">
57
+ {% for image in product.images limit: 8 %}
58
+ {% assign imageUrl = image.url | default: image %}
59
+ <button
60
+ class="gallery-thumbnail {% if forloop.first %}active{% endif %}"
61
+ data-image="{{ imageUrl }}"
62
+ data-index="{{ forloop.index0 }}"
63
+ aria-label="View image {{ forloop.index }}"
64
+ >
65
+ <img src="{{ imageUrl }}" alt="{{ product.name }} - {{ forloop.index }}" loading="lazy">
66
+ </button>
67
+ {% endfor %}
68
+ </div>
69
+ {% endif %}
70
+ </div>
71
+ {% hook 'product_images_after' %}
72
+
73
+ <!-- Product Info -->
74
+ <div class="product-info">
75
+ <!-- Vendor/Brand -->
76
+ {% comment %}Show vendor - Check settings with proper boolean handling{% endcomment %}
77
+ {% liquid
78
+ assign vendor_val = settings.show_vendor
79
+ if vendor_val == blank or vendor_val == null
80
+ assign vendor_val = true
81
+ endif
82
+ assign show_vendor = false
83
+ if vendor_val == true
84
+ assign show_vendor = true
85
+ endif
86
+ if vendor_val == 'true'
87
+ assign show_vendor = true
88
+ endif
89
+ if vendor_val == 1
90
+ assign show_vendor = true
91
+ endif
92
+ %}
93
+ {% if show_vendor %}
94
+ {% if product.vendor %}
95
+ <div class="product-vendor">
96
+ {{ product.vendor }}
97
+ </div>
98
+ {% elsif product.brand %}
99
+ <div class="product-vendor">
100
+ {{ product.brand.name }}
101
+ </div>
102
+ {% endif %}
103
+ {% endif %}
104
+
105
+ <!-- Product Title -->
106
+ {% hook 'product_title_before' %}
107
+ <h1 class="product-title">{{ product.name | default: product.title }}</h1>
108
+ {% hook 'product_title_after' %}
109
+
110
+ {% if product.shortDescription and product.shortDescription != blank %}
111
+ <div class="product-short-description">
112
+ {{ product.shortDescription }}
113
+ </div>
114
+ {% endif %}
115
+
116
+ <!-- Price -->
117
+ {% hook 'product_price_before' %}
118
+ <div class="product-price-wrapper">
119
+
120
+ <span class="price-current" id="productPrice">
121
+ {{ product.prices.price | money_with_settings: shop.settings }}
122
+ </span>
123
+ {% if product.prices.mrp and product.prices.mrp > product.prices.prices.mrp %}
124
+ <span class="price-compare">
125
+ {{ product.prices.mrp | money_with_settings: shop.settings }}
126
+ </span>
127
+ {% endif %}
128
+ </div>
129
+ {% hook 'product_price_after' %}
130
+
131
+ <!-- Product Form -->
132
+ {% hook 'product_form_before' %}
133
+ <form class="product-form" id="productForm" data-product-id="{{ product.productId }}">
134
+ <!-- variants/Options -->
135
+ {% hook 'product_variants_before' %}
136
+ {% if product.variations and product.variations.size > 0 %}
137
+ <div id="productOptionsContainer">
138
+ {% assign optionNames = '' %}
139
+ {% for o in product.variants.first.options %}
140
+ {% unless optionNames contains o.optionName %}
141
+ {% assign optionNames = optionNames | append: o.optionName | append: ',' %}
142
+ {% endunless %}
143
+ {% endfor %}
144
+ {% assign optionNameList = optionNames | split: ',' %}
145
+
146
+ {% for groupName in optionNameList %}
147
+ {% if groupName != '' %}
148
+
149
+ <div class="product-option">
150
+ <label class="option-label">{{ groupName }}</label>
151
+ <div class="option-values">
152
+
153
+ {% assign seenValues = '' %}
154
+
155
+ {% for variation in product.variants %}
156
+ {% for opt in variation.options %}
157
+ {% if opt.optionName == groupName %}
158
+
159
+ {% unless seenValues contains opt.value %}
160
+ {% assign seenValues = seenValues | append: opt.value | append: ',' %}
161
+
162
+ <button
163
+ type="button"
164
+ class="product-option-btn"
165
+ data-option-key="{{ groupName | downcase | replace: ' ', '-' }}"
166
+ data-option-value="{{ opt.value }}"
167
+ data-display-type="{{ opt.displayType }}"
168
+ >
169
+ {{ opt.value }}
170
+ </button>
171
+
172
+ {% endunless %}
173
+ {% endif %}
174
+ {% endfor %}
175
+ {% endfor %}
176
+ </div>
177
+ </div>
178
+
179
+ {% endif %}
180
+ {% endfor %}
181
+ </div>
182
+ {% endif %}
183
+ <!-- Combinations -->
184
+ {% if product.combinations and product.combinations.size > 0 %}
185
+ <div id="comboContainer"></div>
186
+ <div id="combinationValidationMsg"></div>
187
+ {% endif %}
188
+ <!-- Subscriptions -->
189
+ <!-- Subscriptions -->
190
+ {% if product.subscriptions and product.subscriptions.size > 0 %}
191
+ <div class="product-option subscription-option">
192
+ <div id="subscriptionInfoMessage"></div>
193
+ <div id="subscriptionPlanContainer" class="subscription-list"></div>
194
+ <div id="frequencyContainer"></div>
195
+ <div class="sub-modern-box">
196
+
197
+ <div class="sub-row">
198
+ <span class="sub-label">Shipping</span>
199
+ <select id="shippingMethod" class="sub-input" aria-label="Select shipping method">
200
+ <option value="">Select</option>
201
+ </select>
202
+ <div id="shippingMethodDetails"></div>
203
+ </div>
204
+
205
+ <div class="sub-row">
206
+ <span class="sub-label">Start Date <span class="required-indicator">*</span></span>
207
+ <div class="flex-column">
208
+ <input
209
+ type="date"
210
+ id="startDate"
211
+ class="sub-input"
212
+ aria-label="Subscription start date (required)"
213
+ aria-required="true"
214
+ aria-describedby="startDateHelp"
215
+ onchange="calculateDeliverables()"
216
+ >
217
+ <small id="startDateHelp" class="help-text">
218
+ Select when your subscription should start
219
+ </small>
220
+ </div>
221
+ </div>
222
+
223
+ <div class="sub-row">
224
+ <span class="sub-label">End Date <span class="required-indicator">*</span></span>
225
+ <div class="flex-column">
226
+ <input
227
+ type="date"
228
+ id="endDate"
229
+ class="sub-input"
230
+ aria-label="Subscription end date (required)"
231
+ aria-required="true"
232
+ aria-describedby="endDateHelp"
233
+ onchange="calculateDeliverables()"
234
+ >
235
+ <small id="endDateHelp" class="help-text">
236
+ Select when your subscription should end
237
+ </small>
238
+ </div>
239
+ </div>
240
+
241
+ <div class="sub-row">
242
+ <span class="sub-label">Deliverables</span>
243
+ <span class="deliver-pill liveOrderCount">0</span>
244
+ </div>
245
+
246
+ <span id="subscriptionValidationMsg"></span>
247
+
248
+ </div>
249
+
250
+ </div>
251
+ {% endif %}
252
+ {% hook 'product_variants_after' %}
253
+
254
+ <!-- Quantity Selector -->
255
+ {% if product.productType != 90 %}
256
+ {% hook 'product_quantity_before' %}
257
+ <div class="product-option quantity-option">
258
+ <label class="option-label" for="quantity">Quantity</label>
259
+ <div class="quantity-wrapper">
260
+ <button type="button" class="quantity-btn quantity-decrease" aria-label="Decrease quantity">−</button>
261
+ <input
262
+ type="number"
263
+ id="quantity"
264
+ name="quantity"
265
+ value="1"
266
+ min="1"
267
+ max="{{ product.stockQuantity | default: 99 }}"
268
+ class="quantity-input"
269
+ aria-label="Quantity"
270
+ readonly
271
+ >
272
+ <button type="button" class="quantity-btn quantity-increase" aria-label="Increase quantity">+</button>
273
+ </div>
274
+ </div>
275
+ {% hook 'product_quantity_after' %}
276
+ {% endif %}
277
+ <!-- Action Button -->
278
+ {% hook 'product_add_to_cart_before' %}
279
+ <div class="product-actions">
280
+ <button
281
+ type="submit"
282
+ class="btn btn-primary btn-add-to-cart"
283
+ id="addToCartBtn"
284
+ {% unless product.inStock or product.available %}disabled{% endunless %}
285
+ >
286
+ <span class="btn-text">
287
+ {% if product.inStock or product.available %}
288
+ {% if product.productType == 90 %}
289
+ Subscribe
290
+ {% else %}
291
+ Add to Cart
292
+ {% endif %}
293
+ {% else %}
294
+ Out of Stock
295
+ {% endif %}
296
+ </span>
297
+ </button>
298
+ </div>
299
+ {% hook 'product_add_to_cart_after' %}
300
+
301
+ <!-- Added to Cart Message -->
302
+ <div class="cart-message" id="cartMessage">
303
+ <span class="cart-message-text"></span>
304
+ </div>
305
+ </form>
306
+ {% hook 'product_form_after' %}
307
+ </div>
308
+ </div>
309
+ </section>
310
+ {% hook 'product_after' %}
311
+
312
+ <!-- Product Attributes Section -->
313
+ {% assign hasDescription = false %}
314
+ {% if product.description or product.htmlContent %}
315
+ {% assign hasDescription = true %}
316
+ {% endif %}
317
+
318
+ {% assign hasAttributes = false %}
319
+ {% if product.attributes and product.attributes.size > 0 %}
320
+ {% assign hasAttributes = true %}
321
+ {% endif %}
322
+ {% if hasDescription or hasAttributes %}
323
+ <section class="product-attributes-section">
324
+ <div class="description-container">
325
+
326
+ {% if hasAttributes %}
327
+ <!-- Group attributes by attributeGroupName -->
328
+ {% assign availableAttributegroups = "" %}
329
+
330
+ <div class="attributes-tabs-wrapper">
331
+ <!-- Tab Navigation -->
332
+ <ul class="attributes-nav" role="tablist">
333
+ <!-- Description Tab -->
334
+ {% if hasDescription %}
335
+ <li class="attributes-nav-item">
336
+ <button
337
+ class="attributes-tab-link active"
338
+ data-tab="attr-description"
339
+ role="tab"
340
+ aria-selected="true"
341
+ aria-controls="attr-description">
342
+ Description
343
+ </button>
344
+ </li>
345
+ {% endif %}
346
+
347
+ {% for attr in product.attributes %}
348
+ {% unless availableAttributegroups contains attr.attributeGroupName %}
349
+ {% assign availableAttributegroups = availableAttributegroups | append: attr.attributeGroupName | append: "," %}
350
+
351
+ <li class="attributes-nav-item">
352
+ <button
353
+ class="attributes-tab-link {% unless hasDescription %}{% if forloop.first %}active{% endif %}{% endunless %}"
354
+ data-tab="attr-{{ attr.attributeGroupName | handleize }}"
355
+ role="tab"
356
+ aria-selected="{% unless hasDescription %}{% if forloop.first %}true{% else %}false{% endif %}{% else %}false{% endunless %}"
357
+ aria-controls="attr-{{ attr.attributeGroupName | handleize }}">
358
+ {{ attr.attributeGroupName }}
359
+ </button>
360
+ </li>
361
+ {% endunless %}
362
+ {% endfor %}
363
+ </ul>
364
+
365
+ <!-- Tab Content -->
366
+ <div class="attributes-tab-content">
367
+ <!-- Description Tab Pane -->
368
+ {% if hasDescription %}
369
+ <div
370
+ class="attributes-tab-pane active"
371
+ id="attr-description"
372
+ role="tabpanel">
373
+ <div class="description-content">
374
+ {% if product.htmlContent %}
375
+ {{ product.htmlContent }}
376
+ {% elsif product.description %}
377
+ {{ product.description | newline_to_br }}
378
+ {% else %}
379
+ <p>No description available.</p>
380
+ {% endif %}
381
+ </div>
382
+ </div>
383
+ {% endif %}
384
+
385
+ {% assign availableAttributegroups = "" %}
386
+
387
+ {% for attr in product.attributes %}
388
+ {% unless availableAttributegroups contains attr.attributeGroupName %}
389
+ {% assign availableAttributegroups = availableAttributegroups | append: attr.attributeGroupName | append: "," %}
390
+
391
+ <div
392
+ class="attributes-tab-pane {% unless hasDescription %}{% if forloop.first %}active{% endif %}{% endunless %}"
393
+ id="attr-{{ attr.attributeGroupName | handleize }}"
394
+ role="tabpanel">
395
+
396
+ <div class="attributes-grid">
397
+ {% for inner_attr in product.attributes %}
398
+ {% if inner_attr.attributeGroupName == attr.attributeGroupName %}
399
+ <div class="attributes-card" data-attribute-name="{{ inner_attr.name }}" data-base-value="{{ inner_attr.value }}">
400
+ <div class="attribute-icon" aria-hidden="true">
401
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
402
+ <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="1" />
403
+ <path d="M12 8v8M8 12h8" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
404
+ </svg>
405
+ </div>
406
+ <div class="attribute-info">
407
+ <div class="attribute-name">{{ inner_attr.name }}</div>
408
+ <div class="attribute-value-text">{{ inner_attr.value }}</div>
409
+ </div>
410
+ </div>
411
+ {% endif %}
412
+ {% endfor %}
413
+ </div>
414
+ </div>
415
+ {% endunless %}
416
+ {% endfor %}
417
+ </div>
418
+ </div>
419
+ {% elsif hasDescription %}
420
+ <!-- No attributes, just show description without tabs -->
421
+ <div class="description-content">
422
+ {% if product.htmlContent %}
423
+ {{ product.htmlContent }}
424
+ {% elsif product.description %}
425
+ {{ product.description | newline_to_br }}
426
+ {% else %}
427
+ <p>No description available.</p>
428
+ {% endif %}
429
+ </div>
430
+ {% endif %}
431
+ </div>
432
+ </section>
433
+ {% endif %}
434
+
435
+
436
+ <script>
437
+ window.productVariants = {{ product.variations | default: product.variants | json }};
438
+ window.productAttributes = {{ product.attributes | json }};
439
+ </script>
440
+
441
+ <!-- Related Products -->
442
+ {% hook 'product_related_before' %}
443
+ {% if relatedProducts and relatedProducts.size > 0 %}
444
+ <section class="related-products-section">
445
+ <div class="section-container">
446
+ <h2 class="section-title">You may also like</h2>
447
+ <div class="products-grid">
448
+ {% for related in relatedProducts limit: 4 %}
449
+ {% include 'snippets/product-card', product: related %}
450
+ {% endfor %}
451
+ </div>
452
+ </div>
453
+ </section>
454
+ {% endif %}
455
+ {% hook 'product_related_after' %}
456
+
457
+ <!-- Full Screen Image Gallery Modal -->
458
+ <div class="gallery-modal" id="galleryModal">
459
+ <div class="gallery-modal-overlay"></div>
460
+ <div class="gallery-modal-content">
461
+ <button class="gallery-modal-close" id="galleryModalClose" aria-label="Close gallery">
462
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
463
+ <path d="M18 6L6 18M6 6L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
464
+ </svg>
465
+ </button>
466
+ <button class="gallery-modal-nav gallery-modal-prev" id="galleryModalPrev" aria-label="Previous image">
467
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
468
+ <path d="M15 18L9 12L15 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
469
+ </svg>
470
+ </button>
471
+ <button class="gallery-modal-nav gallery-modal-next" id="galleryModalNext" aria-label="Next image">
472
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
473
+ <path d="M9 18L15 12L9 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
474
+ </svg>
475
+ </button>
476
+ <img class="gallery-modal-image" id="galleryModalImage" src="" alt="">
477
+ <div class="gallery-modal-counter" id="galleryModalCounter"></div>
478
+ </div>
479
+ </div>
480
+
481
+ <!-- Product Data for JavaScript -->
482
+ <script type="application/json" id="productData">
483
+ {
484
+ "id": {{ product.productId }},
485
+ "productId": {{ product.productId | default: product.productId }},
486
+ "name": {{ product.name | default: product.title | json }},
487
+ "price": {{ product.prices.price }},
488
+ "mrp": {{ product.prices.mrp | default: 0 }},
489
+ "inStock": {{ product.inStock | default: product.available | default: true }},
490
+ "available": {{ product.available | default: product.inStock | default: true }},
491
+ "stockQuantity": {{ product.stockQuantity | default: 0 }},
492
+ "images": [
493
+ {% if product.images %}
494
+ {% for image in product.images %}
495
+ {% if forloop.index0 > 0 %},{% endif %}
496
+ {% if image.url %}"{{ image.url }}"{% else %}{{ image | json }}{% endif %}
497
+ {% endfor %}
498
+ {% endif %}
499
+ ],
500
+ "productType": {{ product.productType }},
501
+ "additionalData": {{ product.additionalData | default:null }},
502
+ "combinations": {{ product.combinations | default: product.combinations | default: '[]' | json }},
503
+ "subscriptions": {{ product.subscriptions | default:'[]' | json }},
504
+ "variants": {{ product.variations | default: product.variations | default: '[]' | json }},
505
+ "shippingMethods": {{ product.shippingMethods | default: '[]' | json }}
506
+ }
507
+ </script>
508
+
509
+ <!-- Inline Styles for Horizon-Inspired Product Page -->
510
+ <style>
511
+ /* Horizon-Inspired Product Page Styles */
512
+ .sub-modern-box {
513
+ border: 1px solid {{ settings.color_border }};
514
+ background: {{ settings.color_background }};
515
+ padding: {{ settings.spacing_small }}px {{ settings.spacing_element }}px;
516
+ border-radius: {{ settings.border_radius_small }}px;
517
+ font-family: system-ui, sans-serif;
518
+ max-width: 360px;
519
+ font-size: {{ settings.font_size_base | minus: 1 }}px;
520
+ }
521
+
522
+ .sub-row {
523
+ display: flex;
524
+ align-items: center;
525
+ justify-content: flex-start;
526
+ margin-bottom: {{ settings.spacing_small }}px;
527
+ gap: {{ settings.spacing_small | plus: 4 }}px;
528
+ }
529
+
530
+ .sub-label {
531
+ font-weight: {{ settings.font_weight_medium }};
532
+ color: {{ settings.color_text }};
533
+ width: 110px;
534
+ font-size: {{ settings.font_size_base | minus: 1 }}px;
535
+ }
536
+
537
+ .sub-input {
538
+ height: 30px;
539
+ font-size: {{ settings.font_size_base | minus: 1 }}px;
540
+ border: 1px solid {{ settings.color_border }};
541
+ border-radius: {{ settings.border_radius_small }}px;
542
+ padding: 3px {{ settings.spacing_small }}px;
543
+ flex: 1;
544
+ }
545
+
546
+ .deliver-pill {
547
+ background: {{ settings.color_surface }};
548
+ padding: 3px {{ settings.spacing_small }}px;
549
+ border-radius: {{ settings.border_radius_medium | plus: 2 }}px;
550
+ font-weight: 600;
551
+ font-size: {{ settings.font_size_base | minus: 1 }}px;
552
+ display: inline-block;
553
+ }
554
+
555
+ #subscriptionValidationMsg,
556
+ #combinationValidationMsg {
557
+ margin-top: var(--spacing-element, 6px);
558
+ color: var(--color-error, #b70000);
559
+ font-size: var(--text-sm, 13px);
560
+ display: none;
561
+ padding: var(--spacing-element, 8px) var(--spacing-component, 12px);
562
+ background-color: var(--color-error-light, #ffe6e6);
563
+ border-radius: var(--border-radius-small, 4px);
564
+ border-left: 3px solid var(--color-error, #b70000);
565
+ line-height: var(--line-height-base, 1.4);
566
+ }
567
+
568
+ #subscriptionValidationMsg:not(:empty),
569
+ #combinationValidationMsg:not(:empty) {
570
+ display: block;
571
+ }
572
+
573
+ #subscriptionInfoMessage {
574
+ display: none;
575
+ margin-bottom: var(--spacing-component, 12px);
576
+ padding: var(--spacing-element, 8px) var(--spacing-component, 12px);
577
+ background-color: var(--color-info-light, #f0f9ff);
578
+ border-left: 3px solid var(--color-info, #3b82f6);
579
+ border-radius: var(--border-radius-small, 4px);
580
+ font-size: var(--text-sm, 13px);
581
+ color: var(--color-info-dark, #1e40af);
582
+ }
583
+
584
+ #subscriptionInfoMessage:not(:empty) {
585
+ display: block;
586
+ }
587
+
588
+ #shippingMethodDetails {
589
+ margin-top: var(--spacing-element, 8px);
590
+ font-size: var(--text-xs, 12px);
591
+ color: var(--color-text-muted, #666);
592
+ display: none;
593
+ }
594
+
595
+ #shippingMethodDetails:not(:empty) {
596
+ display: block;
597
+ }
598
+
599
+ .required-indicator {
600
+ color: var(--color-error, #b70000);
601
+ }
602
+
603
+ .help-text {
604
+ font-size: var(--text-xs, 11px);
605
+ color: var(--color-text-muted, #666);
606
+ margin-top: var(--spacing-element, 2px);
607
+ }
608
+
609
+ .flex-column {
610
+ flex: 1;
611
+ display: flex;
612
+ flex-direction: column;
613
+ }
614
+
615
+ .subscription-item-content {
616
+ display: flex;
617
+ align-items: center;
618
+ gap: var(--spacing-element, 10px);
619
+ flex: 1;
620
+ }
621
+
622
+ .subscription-item-right {
623
+ text-align: right;
624
+ }
625
+
626
+ .subscription-item-actions {
627
+ display: flex;
628
+ align-items: center;
629
+ gap: var(--spacing-element, 5px);
630
+ justify-content: flex-end;
631
+ }
632
+
633
+ .subscription-item-meta {
634
+ margin-top: var(--spacing-element, 5px);
635
+ }
636
+
637
+ .subscription-included {
638
+ font-size: var(--text-xs, 11px);
639
+ color: var(--color-text-muted, #666);
640
+ font-weight: var(--font-weight-normal, normal);
641
+ }
642
+
643
+ .subscription-spec {
644
+ font-size: var(--text-xs, 12px);
645
+ color: var(--color-text-muted, #666);
646
+ }
647
+
648
+ .subscription-item-title {
649
+ margin: 0;
650
+ }
651
+
652
+ .info-message-content {
653
+ line-height: var(--line-height-base, 1.6);
654
+ }
655
+
656
+ .freq-ui {
657
+ padding: {{ settings.spacing_small }}px 15px;
658
+ border: 1px solid {{ settings.color_border }};
659
+ border-radius: 5px;
660
+ background: {{ settings.color_surface }};
661
+ cursor: pointer;
662
+ transition: 0.2s;
663
+ font-size: {{ settings.font_size_base }}px;
664
+ }
665
+
666
+ .freq-ui.selected {
667
+ background: {{ settings.color_border }};
668
+ color: {{ settings.color_text }};
669
+ }
670
+
671
+ .freq-ui:hover {
672
+ background: {{ settings.color_surface }};
673
+ }
674
+
675
+ .weekly-days {
676
+ display: flex;
677
+ flex-wrap: wrap;
678
+ gap: 10px;
679
+ }
680
+
681
+
682
+ .subscription-item {
683
+ border: 1px solid {{ settings.color_border }};
684
+ padding: 10px;
685
+ margin-bottom: 10px;
686
+ border-radius: 5px;
687
+ display: flex;
688
+ align-items: center;
689
+ justify-content: space-between;
690
+ transition: background 0.3s;
691
+ }
692
+
693
+ /* Highlight selected item */
694
+ .subscription-item.selected {
695
+ background-color: {{ settings.color_border }};
696
+ border-color: {{ settings.color_primary }};
697
+ }
698
+
699
+ /* Ensure checkbox is visible */
700
+ .subscription-item input.sub-select {
701
+ width: 18px !important;
702
+ height: 18px !important;
703
+ cursor: pointer;
704
+ appearance: checkbox !important;
705
+ -webkit-appearance: checkbox !important;
706
+ }
707
+
708
+ .subscription-item input.sub-select-required {
709
+ cursor: not-allowed;
710
+ opacity: 0.6;
711
+ }
712
+
713
+ .combo-group {
714
+ display: flex;
715
+ gap: 10px;
716
+ margin-bottom: 15px;
717
+ }
718
+
719
+ .combo-card {
720
+ border: 1px solid {{ settings.color_border }};
721
+ border-radius: {{ settings.border_radius_medium }}px;
722
+ padding: 5px;
723
+ text-align: center;
724
+ width: 150px;
725
+ cursor: pointer;
726
+ position: relative;
727
+ }
728
+
729
+ .combo-card.selected {
730
+ border-color: {{ settings.color_error }};
731
+ }
732
+
733
+ .combo-card.selected .combo-checkbox {
734
+ color: {{ settings.color_error }};
735
+ background: {{ settings.color_error }};
736
+ }
737
+
738
+ .combo-image {
739
+ position: relative;
740
+ }
741
+
742
+ .combo-image img {
743
+ width: 100%;
744
+ border-radius: {{ settings.border_radius_medium }}px;
745
+ }
746
+
747
+ .combo-checkbox {
748
+ position: absolute;
749
+ top: 5px;
750
+ right: 5px;
751
+ background: white;
752
+ border-radius: 3px;
753
+ padding: 2px;
754
+ height:16px;
755
+ width:16px;
756
+ }
757
+
758
+ .combo-info {
759
+ margin-top: 5px;
760
+ }
761
+
762
+
763
+ .product-option-btn {
764
+ border: 1px solid {{ settings.color_border }};
765
+ background: {{ settings.color_background }};
766
+ padding: {{ settings.spacing_small }}px {{ settings.spacing_element | minus: 2 }}px;
767
+ margin: {{ settings.spacing_xsmall }}px;
768
+ border-radius: {{ settings.border_radius_small }}px;
769
+ cursor: pointer;
770
+ position: relative;
771
+ z-index: 10;
772
+ pointer-events: auto;
773
+ transition: .2s;
774
+ }
775
+
776
+ .product-option-btn.selected {
777
+ background: {{ settings.color_text }};
778
+ color: {{ settings.color_background }};
779
+ border-color: {{ settings.color_text }};
780
+ }
781
+
782
+ .product-option-btn[data-display-type="color"] {
783
+ width: 32px;
784
+ height: 32px;
785
+ border-radius: 50%;
786
+ font-size: 0;
787
+ padding: 0;
788
+ border: 2px solid #aaa;
789
+ background-color: currentColor;
790
+ }
791
+
792
+ .product-option-btn[data-display-type="color"].selected {
793
+ border: 2px solid {{ settings.color_text }} !important;
794
+ }
795
+ .product-main.horizon-style {
796
+ padding: 0;
797
+ margin: 0;
798
+ }
799
+
800
+ .product-container {
801
+ max-width: {{ settings.container_width }}px;
802
+ margin: 0 auto;
803
+ padding: 0 {{ settings.container_padding }}px;
804
+ display: grid;
805
+ grid-template-columns: 1fr 1fr;
806
+ gap: {{ settings.spacing_section }}px;
807
+ align-items: start;
808
+ padding-top: {{ settings.spacing_component | plus: 16 }}px;
809
+ padding-bottom: {{ settings.spacing_section }}px;
810
+ }
811
+
812
+ /* Product Gallery */
813
+ .product-gallery {
814
+ position: sticky;
815
+ top: 20px;
816
+ }
817
+
818
+ .gallery-main {
819
+ position: relative;
820
+ margin-bottom: {{ settings.spacing_element }}px;
821
+ background: {{ settings.color_background }};
822
+ border-radius: var(--border-radius-medium);
823
+ overflow: hidden;
824
+ }
825
+
826
+ .gallery-main-image {
827
+ width: 100%;
828
+ height: auto;
829
+ display: none;
830
+ opacity: 0;
831
+ transition: opacity 0.3s ease;
832
+ }
833
+
834
+ .gallery-main-image.active {
835
+ display: block;
836
+ opacity: 1;
837
+ }
838
+
839
+ .gallery-placeholder {
840
+ width: 100%;
841
+ aspect-ratio: 1;
842
+ display: flex;
843
+ align-items: center;
844
+ justify-content: center;
845
+ background: {{ settings.color_surface }};
846
+ color: {{ settings.color_text_muted }};
847
+ }
848
+
849
+ .gallery-zoom-btn {
850
+ position: absolute;
851
+ bottom: {{ settings.spacing_element }}px;
852
+ right: {{ settings.spacing_element }}px;
853
+ width: 48px;
854
+ height: 48px;
855
+ border-radius: 50%;
856
+ background: rgba(255, 255, 255, 0.9);
857
+ border: 1px solid rgba(0, 0, 0, 0.1);
858
+ display: flex;
859
+ align-items: center;
860
+ justify-content: center;
861
+ cursor: pointer;
862
+ transition: all 0.2s ease;
863
+ z-index: 10;
864
+ color: {{ settings.color_text }};
865
+ backdrop-filter: blur(10px);
866
+ }
867
+
868
+ .gallery-zoom-btn:hover {
869
+ background: rgba(255, 255, 255, 1);
870
+ transform: scale(1.05);
871
+ }
872
+
873
+ .gallery-thumbnails {
874
+ display: grid;
875
+ grid-template-columns: repeat(8, 1fr);
876
+ gap: {{ settings.spacing_small }}px;
877
+ }
878
+
879
+ .gallery-thumbnail {
880
+ aspect-ratio: 1;
881
+ border: 1px solid transparent;
882
+ border-radius: 0;
883
+ overflow: hidden;
884
+ cursor: pointer;
885
+ transition: all 0.2s ease;
886
+ padding: 0;
887
+ background: {{ settings.color_background }};
888
+ position: relative;
889
+ }
890
+
891
+ .gallery-thumbnail::after {
892
+ content: '';
893
+ position: absolute;
894
+ inset: 0;
895
+ border: 2px solid transparent;
896
+ transition: border-color 0.2s ease;
897
+ }
898
+
899
+ .gallery-thumbnail:hover::after,
900
+ .gallery-thumbnail.active::after {
901
+ border-color: {{ settings.color_text }};
902
+ }
903
+
904
+ .gallery-thumbnail img {
905
+ width: 100%;
906
+ height: 100%;
907
+ object-fit: cover;
908
+ display: block;
909
+ }
910
+
911
+ /* Product Info */
912
+ .product-info {
913
+ display: flex;
914
+ flex-direction: column;
915
+ gap: {{ settings.spacing_component }}px;
916
+ padding-top: 0;
917
+ }
918
+
919
+ .product-vendor {
920
+ color: {{ settings.color_text_muted }};
921
+ font-size: {{ settings.font_size_base | minus: 1 }}px;
922
+ text-transform: uppercase;
923
+ letter-spacing: {{ settings.letter_spacing_uppercase }}em;
924
+ font-weight: {{ settings.font_weight_medium }};
925
+ margin-bottom: -{{ settings.spacing_element }}px;
926
+ }
927
+
928
+ .product-title {
929
+ font-size: {{ settings.font_size_heading }}px;
930
+ font-weight: {{ settings.font_weight_normal }};
931
+ line-height: {{ settings.line_height_heading }};
932
+ color: {{ settings.color_text }};
933
+ margin: 0;
934
+ letter-spacing: {{ settings.letter_spacing_heading }}em;
935
+ }
936
+
937
+ .product-price-wrapper {
938
+ display: flex;
939
+ align-items: baseline;
940
+ gap: {{ settings.spacing_small | plus: 4 }}px;
941
+ }
942
+
943
+ .price-current {
944
+ font-size: {{ settings.font_size_heading | times: 0.75 }}px;
945
+ font-weight: {{ settings.font_weight_normal }};
946
+ color: {{ settings.color_text }};
947
+ letter-spacing: -0.01em;
948
+ }
949
+
950
+ .price-compare {
951
+ font-size: {{ settings.font_size_heading | times: 0.5625 }}px;
952
+ color: {{ settings.color_text_light }};
953
+ text-decoration: line-through;
954
+ font-weight: {{ settings.font_weight_normal }};
955
+ }
956
+
957
+ /* Product Form */
958
+ .product-form {
959
+ display: flex;
960
+ flex-direction: column;
961
+ gap: {{ settings.spacing_large }}px;
962
+ padding-top: {{ settings.spacing_large }}px;
963
+ border-top: 1px solid {{ settings.color_border }};
964
+ }
965
+
966
+ #productOptionsContainer:empty {
967
+ display: none;
968
+ }
969
+
970
+ .product-option {
971
+ display: flex;
972
+ flex-direction: column;
973
+ gap: {{ settings.spacing_small | plus: 4 }}px;
974
+ }
975
+
976
+ .option-label {
977
+ font-weight: {{ settings.font_weight_medium }};
978
+ color: {{ settings.color_text }};
979
+ font-size: {{ settings.font_size_base | minus: 1 }}px;
980
+ text-transform: uppercase;
981
+ letter-spacing: {{ settings.letter_spacing_uppercase }}em;
982
+ }
983
+
984
+ .option-values {
985
+ display: flex;
986
+ flex-wrap: wrap;
987
+ gap: {{ settings.spacing_small }}px;
988
+ }
989
+
990
+ /* Color Swatches */
991
+ .option-value-color {
992
+ width: 40px;
993
+ height: 40px;
994
+ border-radius: 50%;
995
+ border: 1px solid rgba(0, 0, 0, 0.1);
996
+ cursor: pointer;
997
+ transition: all 0.2s ease;
998
+ position: relative;
999
+ padding: 0;
1000
+ overflow: hidden;
1001
+ background-size: cover;
1002
+ background-position: center;
1003
+ }
1004
+
1005
+ .option-value-color:hover:not(.disabled) {
1006
+ transform: scale(1.1);
1007
+ border-color: rgba(0, 0, 0, 0.2);
1008
+ }
1009
+
1010
+ .option-value-color.selected {
1011
+ border-color: {{ settings.color_text }};
1012
+ border-width: 2px;
1013
+ }
1014
+
1015
+ .option-value-color.selected::after {
1016
+ content: '';
1017
+ position: absolute;
1018
+ inset: -{{ settings.spacing_xsmall }}px;
1019
+ border: 2px solid {{ settings.color_text }};
1020
+ border-radius: 50%;
1021
+ }
1022
+
1023
+ .option-value-color.disabled {
1024
+ opacity: 0.4;
1025
+ cursor: not-allowed;
1026
+ position: relative;
1027
+ }
1028
+
1029
+ .option-value-color.disabled::after {
1030
+ content: '';
1031
+ position: absolute;
1032
+ top: 50%;
1033
+ left: 0;
1034
+ right: 0;
1035
+ height: 1px;
1036
+ background: {{ settings.color_error }};
1037
+ transform: rotate(45deg);
1038
+ }
1039
+
1040
+ /* Size/Text Options */
1041
+ .option-value-text {
1042
+ padding: 10px 20px;
1043
+ border: 1px solid {{ settings.color_border }};
1044
+ border-radius: 0;
1045
+ background: {{ settings.color_background }};
1046
+ cursor: pointer;
1047
+ transition: all 0.2s ease;
1048
+ font-weight: {{ settings.font_weight_normal }};
1049
+ color: {{ settings.color_text }};
1050
+ font-size: {{ settings.font_size_base }}px;
1051
+ min-width: 60px;
1052
+ text-align: center;
1053
+ border-radius: var(--border-radius-small);
1054
+ }
1055
+
1056
+ .option-value-text:hover:not(.disabled) {
1057
+ border-color: {{ settings.color_text }};
1058
+ background: {{ settings.color_surface }};
1059
+ }
1060
+
1061
+ .option-value-text.selected {
1062
+ border-color: {{ settings.color_text }};
1063
+ background: {{ settings.color_text }};
1064
+ color: {{ settings.color_background }};
1065
+ }
1066
+
1067
+ .option-value-text.disabled {
1068
+ opacity: 0.4;
1069
+ cursor: not-allowed;
1070
+ text-decoration: line-through;
1071
+ }
1072
+
1073
+ /* Quantity Selector */
1074
+ .quantity-option {
1075
+ width: fit-content;
1076
+ }
1077
+
1078
+ .quantity-wrapper {
1079
+ display: flex;
1080
+ align-items: center;
1081
+ border: 1px solid {{ settings.color_border }};
1082
+ border-radius: var(--border-radius-medium);
1083
+ width: fit-content;
1084
+ overflow: hidden;
1085
+ background: {{ settings.color_background }};
1086
+ }
1087
+
1088
+ .quantity-btn {
1089
+ width: 44px;
1090
+ height: 44px;
1091
+ border: none;
1092
+ background: transparent;
1093
+ cursor: pointer;
1094
+ display: flex;
1095
+ align-items: center;
1096
+ justify-content: center;
1097
+ transition: background 0.2s ease;
1098
+ color: {{ settings.color_text }};
1099
+ font-size: 20px;
1100
+ font-weight: 300;
1101
+ line-height: 1;
1102
+ }
1103
+
1104
+ .quantity-btn:hover:not(:disabled) {
1105
+ background: {{ settings.color_surface }};
1106
+ }
1107
+
1108
+ .quantity-btn:disabled {
1109
+ opacity: 0.4;
1110
+ cursor: not-allowed;
1111
+ }
1112
+
1113
+ .quantity-input {
1114
+ width: 60px;
1115
+ height: 44px;
1116
+ border: none;
1117
+ border-left: 1px solid {{ settings.color_border }};
1118
+ border-right: 1px solid {{ settings.color_border }};
1119
+ text-align: center;
1120
+ font-weight: {{ settings.font_weight_normal }};
1121
+ font-size: {{ settings.font_size_base }}px;
1122
+ background: {{ settings.color_background }};
1123
+ color: {{ settings.color_text }};
1124
+ -moz-appearance: textfield;
1125
+ }
1126
+
1127
+ .quantity-input::-webkit-inner-spin-button,
1128
+ .quantity-input::-webkit-outer-spin-button {
1129
+ -webkit-appearance: none;
1130
+ margin: 0;
1131
+ }
1132
+
1133
+ /* Action Button */
1134
+ .product-actions {
1135
+ display: flex;
1136
+ gap: {{ settings.spacing_small | plus: 4 }}px;
1137
+ flex-direction: column;
1138
+ width: 100%;
1139
+ position:relative;
1140
+ opacity:unset;
1141
+ }
1142
+
1143
+ .btn {
1144
+ padding: {{ settings.spacing_element }}px {{ settings.spacing_large }}px;
1145
+ border-radius: var(--border-radius-medium);
1146
+ font-weight: {{ settings.font_weight_medium }};
1147
+ font-size: {{ settings.font_size_base }}px;
1148
+ cursor: pointer;
1149
+ transition: all 0.2s ease;
1150
+ display: inline-flex;
1151
+ align-items: center;
1152
+ justify-content: center;
1153
+ gap: {{ settings.spacing_small }}px;
1154
+ border: 1px solid transparent;
1155
+ text-transform: uppercase;
1156
+ letter-spacing: {{ settings.letter_spacing_uppercase }}em;
1157
+ white-space: nowrap;
1158
+ width: 100%;
1159
+ }
1160
+
1161
+ .btn-primary {
1162
+ background: {{ settings.color_text }};
1163
+ color: {{ settings.color_background }};
1164
+ border-color: {{ settings.color_text }};
1165
+ }
1166
+
1167
+ .btn-primary:hover:not(:disabled) {
1168
+ background: {{ settings.color_primary_dark }};
1169
+ border-color: {{ settings.color_primary_dark }};
1170
+ }
1171
+
1172
+ .btn-primary:disabled {
1173
+ opacity: 0.4;
1174
+ cursor: not-allowed;
1175
+ }
1176
+
1177
+ .btn-primary.loading {
1178
+ opacity: 0.7;
1179
+ cursor: wait;
1180
+ }
1181
+
1182
+ .cart-message {
1183
+ padding: var(--spacing-component, 12px) var(--spacing-component, 16px);
1184
+ color: var(--color-success, #059669);
1185
+ font-size: var(--text-base, 14px);
1186
+ font-weight: var(--font-weight-normal, 500);
1187
+ text-transform: none;
1188
+ letter-spacing: 0;
1189
+ animation: fadeIn var(--transition, 0.3s) var(--ease-out, ease);
1190
+ border-radius: var(--border-radius-small, 4px);
1191
+ margin-top: var(--spacing-component, 12px);
1192
+ line-height: var(--line-height-base, 1.5);
1193
+ }
1194
+
1195
+ .cart-message.error {
1196
+ color: var(--color-error, #b70000);
1197
+ background-color: var(--color-error-light, #ffe6e6);
1198
+ border: 1px solid var(--color-error, #b70000);
1199
+ }
1200
+
1201
+ @keyframes fadeIn {
1202
+ from {
1203
+ opacity: 0;
1204
+ transform: translateY(-4px);
1205
+ }
1206
+ to {
1207
+ opacity: 1;
1208
+ transform: translateY(0);
1209
+ }
1210
+ }
1211
+
1212
+ /* Product Description Short */
1213
+ .product-description-short {
1214
+ color: {{ settings.color_text_muted }};
1215
+ font-size: {{ settings.font_size_base }}px;
1216
+ line-height: {{ settings.line_height_base }};
1217
+ padding-top: {{ settings.spacing_large }}px;
1218
+ border-top: 1px solid {{ settings.color_border }};
1219
+ }
1220
+
1221
+ /* Product Description Section */
1222
+ .product-description-section {
1223
+ padding: {{ settings.spacing_section }}px 0;
1224
+ border-top: 1px solid {{ settings.color_border }};
1225
+ }
1226
+
1227
+ .description-container {
1228
+ max-width: 800px;
1229
+ margin: 0 auto;
1230
+ padding: 0 {{ settings.container_padding }}px;
1231
+ }
1232
+
1233
+ .product-description-content {
1234
+ line-height: 1.8;
1235
+ color: {{ settings.color_text_muted }};
1236
+ font-size: 15px;
1237
+ }
1238
+
1239
+ .product-description-content h1,
1240
+ .product-description-content h2,
1241
+ .product-description-content h3 {
1242
+ color: {{ settings.color_text }};
1243
+ font-weight: {{ settings.font_weight_normal }};
1244
+ margin-top: {{ settings.spacing_large }}px;
1245
+ margin-bottom: {{ settings.spacing_element }}px;
1246
+ letter-spacing: -0.01em;
1247
+ }
1248
+
1249
+ .product-description-content p {
1250
+ margin-bottom: {{ settings.spacing_element }}px;
1251
+ }
1252
+
1253
+ /* Related Products */
1254
+ .related-products-section {
1255
+ padding: {{ settings.spacing_section }}px 0;
1256
+ border-top: 1px solid {{ settings.color_border }};
1257
+ }
1258
+
1259
+ .section-container {
1260
+ max-width: {{ settings.container_width }}px;
1261
+ margin: 0 auto;
1262
+ padding: 0 {{ settings.container_padding }}px;
1263
+ }
1264
+
1265
+ .section-title {
1266
+ font-size: {{ settings.font_size_heading | times: 0.75 }}px;
1267
+ font-weight: {{ settings.font_weight_normal }};
1268
+ color: {{ settings.color_text }};
1269
+ margin: 0 0 48px 0;
1270
+ text-align: center;
1271
+ letter-spacing: -0.01em;
1272
+ }
1273
+
1274
+ .products-grid {
1275
+ display: grid;
1276
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
1277
+ gap: {{ settings.spacing_large }}px;
1278
+ }
1279
+
1280
+ /* Full Screen Gallery Modal */
1281
+ .gallery-modal {
1282
+ display: none;
1283
+ position: fixed;
1284
+ top: 0;
1285
+ left: 0;
1286
+ right: 0;
1287
+ bottom: 0;
1288
+ z-index: 9999;
1289
+ }
1290
+
1291
+ .gallery-modal.active {
1292
+ display: block;
1293
+ }
1294
+
1295
+ .gallery-modal-overlay {
1296
+ position: absolute;
1297
+ top: 0;
1298
+ left: 0;
1299
+ right: 0;
1300
+ bottom: 0;
1301
+ background: rgba(0, 0, 0, 0.95);
1302
+ }
1303
+
1304
+ .gallery-modal-content {
1305
+ position: relative;
1306
+ width: 100%;
1307
+ height: 100%;
1308
+ display: flex;
1309
+ align-items: center;
1310
+ justify-content: center;
1311
+ padding: {{ settings.spacing_section }}px;
1312
+ }
1313
+
1314
+ .gallery-modal-image {
1315
+ max-width: 100%;
1316
+ max-height: 90vh;
1317
+ object-fit: contain;
1318
+ }
1319
+
1320
+ .gallery-modal-close,
1321
+ .gallery-modal-nav {
1322
+ position: absolute;
1323
+ width: 48px;
1324
+ height: 48px;
1325
+ border-radius: 50%;
1326
+ background: rgba(255, 255, 255, 0.1);
1327
+ border: 1px solid rgba(255, 255, 255, 0.2);
1328
+ display: flex;
1329
+ align-items: center;
1330
+ justify-content: center;
1331
+ cursor: pointer;
1332
+ transition: all 0.2s ease;
1333
+ z-index: 10000;
1334
+ color: #fff;
1335
+ backdrop-filter: blur(10px);
1336
+ }
1337
+
1338
+ .gallery-modal-close:hover,
1339
+ .gallery-modal-nav:hover {
1340
+ background: rgba(255, 255, 255, 0.2);
1341
+ transform: scale(1.1);
1342
+ }
1343
+
1344
+ .gallery-modal-close {
1345
+ top: {{ settings.spacing_large }}px;
1346
+ right: {{ settings.spacing_large }}px;
1347
+ }
1348
+
1349
+ .gallery-modal-nav {
1350
+ top: 50%;
1351
+ transform: translateY(-50%);
1352
+ }
1353
+
1354
+ .gallery-modal-prev {
1355
+ left: {{ settings.spacing_large }}px;
1356
+ }
1357
+
1358
+ .gallery-modal-next {
1359
+ right: {{ settings.spacing_large }}px;
1360
+ }
1361
+
1362
+ .gallery-modal-counter {
1363
+ position: absolute;
1364
+ bottom: {{ settings.spacing_large }}px;
1365
+ left: 50%;
1366
+ transform: translateX(-50%);
1367
+ color: {{ settings.color_background }};
1368
+ font-size: {{ settings.font_size_base }}px;
1369
+ background: rgba(0, 0, 0, 0.5);
1370
+ padding: {{ settings.spacing_small }}px {{ settings.spacing_element }}px;
1371
+ border-radius: 20px;
1372
+ backdrop-filter: blur(10px);
1373
+ }
1374
+
1375
+ .sr-only {
1376
+ position: absolute;
1377
+ width: 1px;
1378
+ height: 1px;
1379
+ padding: 0;
1380
+ margin: -1px;
1381
+ overflow: hidden;
1382
+ clip: rect(0, 0, 0, 0);
1383
+ white-space: nowrap;
1384
+ border-width: 0;
1385
+ }
1386
+
1387
+ /* Product Attributes Section */
1388
+ .product-attributes-section {
1389
+ padding: 72px 0 40px;
1390
+ border-top: 1px solid {{ settings.color_border }};
1391
+ }
1392
+
1393
+ .attributes-tabs-wrapper {
1394
+ margin-top: 28px;
1395
+ }
1396
+
1397
+ .attributes-nav {
1398
+ display: flex;
1399
+ gap: 10px;
1400
+ list-style: none;
1401
+ margin: -138px 31px 18px 0;
1402
+ padding: 0;
1403
+
1404
+ }
1405
+
1406
+ .attributes-tab-link {
1407
+ padding: 10px 7px;
1408
+ background: transparent;
1409
+ color: {{ settings.color_text_muted }};
1410
+ font-weight: 600;
1411
+ font-size: 12px;
1412
+ cursor: pointer;
1413
+ border: none;
1414
+ border-bottom: 3px solid transparent;
1415
+ text-transform: uppercase;
1416
+ letter-spacing: 0.6px;
1417
+ transition: color .18s ease, border-color .18s ease;
1418
+ }
1419
+
1420
+ .attributes-tab-link:hover { color: {{ settings.color_text }}; }
1421
+ .attributes-tab-link.active { color: {{ settings.color_text }}; border-bottom-color: {{ settings.color_text }}; }
1422
+
1423
+ .attributes-tab-content { padding-top: 20px; }
1424
+
1425
+ .attributes-tab-pane { display: none; }
1426
+ .attributes-tab-pane.active { display: block; }
1427
+
1428
+ .attributes-grid {
1429
+ display: grid;
1430
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
1431
+ gap: 18px;
1432
+ }
1433
+
1434
+ .attributes-card {
1435
+ display: flex;
1436
+ gap: {{ settings.spacing_small | plus: 4 }}px;
1437
+ padding: {{ settings.spacing_element }}px;
1438
+ border: 1px solid {{ settings.color_border }};
1439
+ border-radius: {{ settings.border_radius_medium }}px;
1440
+ background: {{ settings.color_surface }};
1441
+ align-items: flex-start;
1442
+ transition: box-shadow .18s ease, transform .12s ease, background .18s ease;
1443
+ }
1444
+
1445
+ .attributes-card:hover { box-shadow: 0 6px 18px rgba(16,24,40,0.06); transform: translateY(-2px); background: {{ settings.color_background }}; }
1446
+
1447
+ .attribute-icon { width: 40px; height: 40px; border-radius: {{ settings.border_radius_medium }}px; display:flex; align-items:center; justify-content:center; background:{{ settings.color_surface }}; color:{{ settings.color_text_muted }}; flex-shrink:0; }
1448
+
1449
+ .attribute-info { min-width:0; }
1450
+ .attribute-name { font-size:11px; text-transform:uppercase; color:{{ settings.color_text }}; font-weight:{{ settings.font_weight_bold }}; margin-bottom:6px; }
1451
+ .attribute-value-text { font-size:{{ settings.font_size_base }}px; color:{{ settings.color_text_muted }}; line-height:1.4; word-break:break-word; }
1452
+
1453
+ /* Product Specifications Section */
1454
+ .product-specifications-section {
1455
+ padding: {{ settings.spacing_section }}px 0;
1456
+ border-top: 1px solid {{ settings.color_border }};
1457
+ }
1458
+
1459
+ .specifications-container {
1460
+ margin-top: {{ settings.spacing_large }}px;
1461
+ }
1462
+
1463
+ /* Responsive Design */
1464
+ @media (max-width: 1024px) {
1465
+ .product-container {
1466
+ grid-template-columns: 1fr;
1467
+ gap: {{ settings.spacing_component | times: 2 }}px;
1468
+ padding-top: {{ settings.spacing_component }}px;
1469
+ padding-bottom: {{ settings.spacing_component | times: 2 }}px;
1470
+ }
1471
+
1472
+ .product-gallery {
1473
+ position: static;
1474
+ }
1475
+
1476
+ .gallery-thumbnails {
1477
+ grid-template-columns: repeat(6, 1fr);
1478
+ }
1479
+ }
1480
+
1481
+ @media (max-width: 768px) {
1482
+ .product-container {
1483
+ padding: 0 {{ settings.spacing_element }}px;
1484
+ padding-top: {{ settings.spacing_element }}px;
1485
+ padding-bottom: {{ settings.spacing_large }}px;
1486
+ gap: {{ settings.spacing_large }}px;
1487
+ }
1488
+
1489
+ .product-title {
1490
+ font-size: {{ settings.font_size_heading | times: 0.875 }}px;
1491
+ }
1492
+
1493
+ .price-current {
1494
+ font-size: {{ settings.font_size_heading | times: 0.625 }}px;
1495
+ }
1496
+
1497
+ .gallery-thumbnails {
1498
+ grid-template-columns: repeat(4, 1fr);
1499
+ }
1500
+
1501
+ .gallery-modal-content {
1502
+ padding: {{ settings.spacing_element }}px;
1503
+ }
1504
+
1505
+ .gallery-modal-close,
1506
+ .gallery-modal-nav {
1507
+ width: 40px;
1508
+ height: 40px;
1509
+ }
1510
+
1511
+ .gallery-modal-close {
1512
+ top: {{ settings.spacing_element }}px;
1513
+ right: {{ settings.spacing_element }}px;
1514
+ }
1515
+
1516
+ .gallery-modal-prev {
1517
+ left: {{ settings.spacing_element }}px;
1518
+ }
1519
+
1520
+ .gallery-modal-next {
1521
+ right: {{ settings.spacing_element }}px;
1522
+ }
1523
+
1524
+ .products-grid {
1525
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
1526
+ gap: {{ settings.spacing_component }}px;
1527
+ }
1528
+
1529
+ .product-description-section,
1530
+ .related-products-section {
1531
+ padding: {{ settings.spacing_component | times: 2 }}px 0;
1532
+ }
1533
+
1534
+ .description-container,
1535
+ .section-container {
1536
+ padding: 0 {{ settings.spacing_element }}px;
1537
+ }
1538
+ }
1539
+
1540
+ @media (max-width: 480px) {
1541
+ .product-title {
1542
+ font-size: {{ settings.font_size_heading | times: 0.75 }}px;
1543
+ }
1544
+
1545
+ .gallery-thumbnails {
1546
+ grid-template-columns: repeat(4, 1fr);
1547
+ gap: {{ settings.border_radius_small }}px;
1548
+ }
1549
+
1550
+ .products-grid {
1551
+ grid-template-columns: 1fr 1fr;
1552
+ gap: {{ settings.spacing_element }}px;
1553
+ }
1554
+ }
1555
+ </style>
1556
+
1557
+ <!-- Product Page JavaScript -->
1558
+ <script>
1559
+ (function() {
1560
+ 'use strict';
1561
+
1562
+ // Get product data
1563
+ const productDataEl = document.getElementById('productData');
1564
+ const productData = productDataEl ? JSON.parse(productDataEl.textContent) : {};
1565
+ productData.combinations.forEach(c => {
1566
+ try {
1567
+ const gd = c.group?.groupDetail;
1568
+
1569
+ c.groupName = gd?.groupName || "Default";
1570
+ c.minimumSelectable = gd?.minimumSelectable || 1;
1571
+ c.maximumSelectable = gd?.maximumSelectable || 1;
1572
+ c.isOptional = gd?.isOptional || false;
1573
+ } catch (err) {
1574
+ c.groupName = "Default";
1575
+ c.minimumSelectable = 1;
1576
+ c.maximumSelectable = 1;
1577
+ c.isOptional = false;
1578
+ }
1579
+ });
1580
+ // Product state
1581
+ let selectedOptions = {};
1582
+ let currentVariant = null;
1583
+ let currentImageIndex = 0;
1584
+ let moneyFormat = "{{ shop.settings.currencySymbol }}";
1585
+ // DOM elements
1586
+ let mainImages, thumbnails, productForm, addToCartBtn, quantityInput, priceElement;
1587
+ let optionsContainer, galleryModal, galleryModalImage, galleryModalClose;
1588
+ let galleryModalPrev, galleryModalNext, galleryModalCounter, galleryZoomBtn, cartMessage;
1589
+
1590
+ // Helper function to format money
1591
+ function formatMoney(cents) {
1592
+ return moneyFormat + (cents).toFixed(2);
1593
+ }
1594
+ let bundleSelections = {};
1595
+ let selectedSubscription = null;
1596
+
1597
+ // Helper function to show subscription validation errors
1598
+ function showSubscriptionError(message) {
1599
+ const errorContainer = document.getElementById("subscriptionValidationMsg");
1600
+ if (errorContainer) {
1601
+ errorContainer.textContent = message;
1602
+ errorContainer.style.display = message ? "block" : "none";
1603
+ }
1604
+ }
1605
+
1606
+ // Helper function to clear subscription errors
1607
+ function clearSubscriptionError() {
1608
+ showSubscriptionError("");
1609
+ }
1610
+
1611
+ // Helper function to show combination validation errors
1612
+ function showCombinationError(message) {
1613
+ const errorContainer = document.getElementById("combinationValidationMsg");
1614
+ if (errorContainer) {
1615
+ errorContainer.textContent = message;
1616
+ errorContainer.style.display = message ? "block" : "none";
1617
+ // Auto-hide after 5 seconds
1618
+ if (message) {
1619
+ setTimeout(() => {
1620
+ showCombinationError("");
1621
+ }, 5000);
1622
+ }
1623
+ }
1624
+ }
1625
+
1626
+ // Helper function to calculate shipping start date based on shippingDays only
1627
+ function calculateShippingStartDate(shippingDays) {
1628
+ const now = new Date();
1629
+ const shippingStartDate = new Date();
1630
+
1631
+ // Simply add shippingDays to today's date
1632
+ // If shippingDays = 1, start date = tomorrow
1633
+ // If shippingDays = 2, start date = today + 2 days, etc.
1634
+ shippingStartDate.setDate(now.getDate() + shippingDays);
1635
+ shippingStartDate.setHours(0, 0, 0, 0);
1636
+
1637
+ return shippingStartDate;
1638
+ }
1639
+
1640
+ // Helper function to calculate correct end date based on start date, expected orders count, and frequency
1641
+ function calculateCorrectEndDate(startDateStr, expectedOrdersCount, addData) {
1642
+ if (!startDateStr || !expectedOrdersCount) return null;
1643
+
1644
+ const startDate = new Date(startDateStr);
1645
+ startDate.setHours(0, 0, 0, 0);
1646
+
1647
+ const predefinedFrequencyData = addData.frequencyData || {};
1648
+ const predefinedFrequency = addData.frequency || {};
1649
+
1650
+ // Get frequency from predefined data
1651
+ const freqOption = predefinedFrequency.selectedOption || predefinedFrequencyData.selectedOption || 'daily';
1652
+ let freqOptionNormalized = freqOption.toLowerCase();
1653
+ let selectedFreq = 'Daily';
1654
+
1655
+ if (predefinedFrequencyData.isDailyFrequency) {
1656
+ selectedFreq = 'Daily';
1657
+ } else if (predefinedFrequencyData.isWeeklyFrequency) {
1658
+ selectedFreq = 'Weekly';
1659
+ } else if (predefinedFrequencyData.isMonthlyFrequency) {
1660
+ selectedFreq = 'Monthly';
1661
+ } else if (predefinedFrequencyData.isAlterNativeFrequency) {
1662
+ selectedFreq = 'Alternate Days';
1663
+ }
1664
+
1665
+ // Get frequency details
1666
+ let freqDetails = {};
1667
+ if (selectedFreq === 'Weekly') {
1668
+ freqDetails.days = predefinedFrequencyData.weeklyFreVals || ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
1669
+ } else if (selectedFreq === 'Alternate Days') {
1670
+ freqDetails.days = predefinedFrequencyData.weeklyFreVal || 2;
1671
+ } else if (selectedFreq === 'Monthly') {
1672
+ freqDetails.day = predefinedFrequencyData.day || 1;
1673
+ }
1674
+
1675
+ // Calculate end date based on frequency and expected orders count
1676
+ let endDate = new Date(startDate);
1677
+
1678
+ if (selectedFreq === 'Daily') {
1679
+ // Daily: end date = start date + (expectedOrdersCount - 1) days
1680
+ endDate.setDate(startDate.getDate() + (expectedOrdersCount - 1));
1681
+ } else if (selectedFreq === 'Alternate Days') {
1682
+ // Alternate Days: calculate backwards from expected count
1683
+ const step = freqDetails.days || 2;
1684
+ let currentDate = new Date(startDate);
1685
+ let count = 0;
1686
+ while (count < expectedOrdersCount) {
1687
+ count++;
1688
+ if (count < expectedOrdersCount) {
1689
+ currentDate.setDate(currentDate.getDate() + step);
1690
+ }
1691
+ }
1692
+ endDate = currentDate;
1693
+ } else if (selectedFreq === 'Weekly') {
1694
+ // Weekly: find the date that gives us exactly expectedOrdersCount occurrences
1695
+ const selectedDays = freqDetails.days || ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
1696
+ let currentDate = new Date(startDate);
1697
+ let count = 0;
1698
+
1699
+ // Find the end date that gives us exactly expectedOrdersCount
1700
+ while (count < expectedOrdersCount) {
1701
+ let day = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"][currentDate.getDay()];
1702
+ if (selectedDays.includes(day)) {
1703
+ count++;
1704
+ if (count === expectedOrdersCount) {
1705
+ endDate = new Date(currentDate);
1706
+ break;
1707
+ }
1708
+ }
1709
+ currentDate.setDate(currentDate.getDate() + 1);
1710
+
1711
+ // Safety check to prevent infinite loop
1712
+ if (currentDate.getTime() - startDate.getTime() > 365 * 24 * 60 * 60 * 1000) {
1713
+ return null;
1714
+ }
1715
+ }
1716
+ } else if (selectedFreq === 'Monthly') {
1717
+ // Monthly: calculate based on day of month
1718
+ const dom = freqDetails.day || 1;
1719
+ let currentDate = new Date(startDate);
1720
+
1721
+ // Set to first occurrence of the day in start month
1722
+ currentDate.setDate(dom);
1723
+ if (currentDate < startDate) {
1724
+ currentDate.setMonth(currentDate.getMonth() + 1);
1725
+ currentDate.setDate(dom);
1726
+ }
1727
+
1728
+ let count = 0;
1729
+ while (count < expectedOrdersCount) {
1730
+ const lastDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0).getDate();
1731
+ const validDay = Math.min(dom, lastDayOfMonth);
1732
+ const validDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), validDay);
1733
+
1734
+ if (validDate >= startDate) {
1735
+ count++;
1736
+ if (count === expectedOrdersCount) {
1737
+ endDate = validDate;
1738
+ break;
1739
+ }
1740
+ }
1741
+
1742
+ currentDate.setMonth(currentDate.getMonth() + 1);
1743
+ currentDate.setDate(validDay);
1744
+
1745
+ // Safety check
1746
+ if (currentDate.getTime() - startDate.getTime() > 365 * 24 * 60 * 60 * 1000) {
1747
+ return null;
1748
+ }
1749
+ }
1750
+ }
1751
+
1752
+ return endDate;
1753
+ }
1754
+
1755
+ // Helper function to parse time string (e.g., "14:30", "2:30 PM", or ISO date string)
1756
+ function parseTimeString(timeStr) {
1757
+ if (!timeStr) return null;
1758
+
1759
+ // Handle ISO date strings (e.g., "1970-01-01T07:32:00+00:00" or "1970-01-01T07:32:00.000Z")
1760
+ if (timeStr.includes('T') || timeStr.includes('Z') || timeStr.includes('+')) {
1761
+ try {
1762
+ const date = new Date(timeStr);
1763
+ if (!isNaN(date.getTime())) {
1764
+ return {
1765
+ hours: date.getUTCHours(),
1766
+ minutes: date.getUTCMinutes()
1767
+ };
1768
+ }
1769
+ } catch (e) {
1770
+ // Fall through to other parsing methods
1771
+ }
1772
+ }
1773
+
1774
+ // Try HH:MM format first
1775
+ const match24 = timeStr.match(/^(\d{1,2}):(\d{2})$/);
1776
+ if (match24) {
1777
+ return {
1778
+ hours: parseInt(match24[1], 10),
1779
+ minutes: parseInt(match24[2], 10)
1780
+ };
1781
+ }
1782
+
1783
+ // Try 12-hour format with AM/PM
1784
+ const match12 = timeStr.match(/^(\d{1,2}):(\d{2})\s*(AM|PM)$/i);
1785
+ if (match12) {
1786
+ let hours = parseInt(match12[1], 10);
1787
+ const minutes = parseInt(match12[2], 10);
1788
+ const ampm = match12[3].toUpperCase();
1789
+
1790
+ if (ampm === 'PM' && hours !== 12) hours += 12;
1791
+ if (ampm === 'AM' && hours === 12) hours = 0;
1792
+
1793
+ return { hours, minutes };
1794
+ }
1795
+
1796
+ return null;
1797
+ }
1798
+
1799
+ // Helper function to format time for display
1800
+ function formatTimeDisplay(timeStr) {
1801
+ const time = parseTimeString(timeStr);
1802
+ if (!time) return timeStr;
1803
+
1804
+ const hours = time.hours;
1805
+ const minutes = time.minutes;
1806
+ const ampm = hours >= 12 ? 'PM' : 'AM';
1807
+ const displayHours = hours % 12 || 12;
1808
+ const displayMinutes = minutes.toString().padStart(2, '0');
1809
+
1810
+ return `${displayHours}:${displayMinutes} ${ampm}`;
1811
+ }
1812
+
1813
+ // Initialize shipping methods dropdown
1814
+ function initializeShippingMethods() {
1815
+ const shippingSelect = document.getElementById('shippingMethod');
1816
+ const shippingDetailsDiv = document.getElementById('shippingMethodDetails');
1817
+
1818
+ if (!shippingSelect) return;
1819
+
1820
+ // Clear existing options except "Select"
1821
+ while (shippingSelect.options.length > 1) {
1822
+ shippingSelect.remove(1);
1823
+ }
1824
+
1825
+ // Get shipping methods from productData
1826
+ const shippingMethods = productData.shippingMethods || [];
1827
+
1828
+ if (shippingMethods.length === 0) {
1829
+ // No shipping methods available
1830
+ shippingSelect.innerHTML = '<option value="">No shipping methods available</option>';
1831
+ return;
1832
+ }
1833
+
1834
+ // Populate dropdown with shipping methods
1835
+ shippingMethods.forEach((method, index) => {
1836
+ try {
1837
+ // Parse the data JSON string if it's a string
1838
+ let methodData = {};
1839
+ if (method.data) {
1840
+ if (typeof method.data === 'string') {
1841
+ methodData = JSON.parse(method.data);
1842
+ } else {
1843
+ methodData = method.data;
1844
+ }
1845
+ }
1846
+
1847
+ const description = methodData.description || `Shipping Method ${index + 1}`;
1848
+ const shippingDays = methodData.shippingDays || 1;
1849
+ const deliveryDays = methodData.deliveryDays || 0;
1850
+ const shippingTiming = methodData.shippingTiming || '';
1851
+ const deliveryTiming = methodData.deliveryTiming || '';
1852
+ const orderCutOffTiming = methodData.orderCutOffTiming || '';
1853
+
1854
+ // Create option element
1855
+ const option = document.createElement('option');
1856
+ option.value = method.shippingClassId || index;
1857
+ option.textContent = description;
1858
+ option.dataset.shippingDays = shippingDays;
1859
+ option.dataset.deliveryDays = deliveryDays;
1860
+ option.dataset.shippingTiming = shippingTiming;
1861
+ option.dataset.deliveryTiming = deliveryTiming;
1862
+ option.dataset.orderCutOffTiming = orderCutOffTiming;
1863
+ option.dataset.methodIndex = index;
1864
+
1865
+ shippingSelect.appendChild(option);
1866
+ } catch (error) {
1867
+ console.warn('Error parsing shipping method data:', error, method);
1868
+ }
1869
+ });
1870
+
1871
+ // Add change handler to update start date and end date
1872
+ shippingSelect.addEventListener('change', function() {
1873
+ const selectedOption = this.options[this.selectedIndex];
1874
+
1875
+ if (!selectedOption || !selectedOption.value) {
1876
+ if (shippingDetailsDiv) {
1877
+ shippingDetailsDiv.style.display = 'none';
1878
+ }
1879
+ return;
1880
+ }
1881
+
1882
+ const shippingDays = parseInt(selectedOption.dataset.shippingDays) || 1;
1883
+
1884
+ // Calculate shipping start date (only based on shippingDays)
1885
+ const shippingStartDate = calculateShippingStartDate(shippingDays);
1886
+
1887
+ // Update start date input field
1888
+ const startDateInput = document.getElementById('startDate');
1889
+ const endDateInput = document.getElementById('endDate');
1890
+
1891
+ if (startDateInput) {
1892
+ const formatDate = (date) => {
1893
+ const year = date.getFullYear();
1894
+ const month = String(date.getMonth() + 1).padStart(2, '0');
1895
+ const day = String(date.getDate()).padStart(2, '0');
1896
+ return `${year}-${month}-${day}`;
1897
+ };
1898
+
1899
+ // Temporarily enable if disabled to set value (for predefined subscriptions)
1900
+ const wasStartDisabled = startDateInput.disabled;
1901
+ const wasEndDisabled = endDateInput ? endDateInput.disabled : false;
1902
+
1903
+ if (wasStartDisabled) {
1904
+ startDateInput.disabled = false;
1905
+ }
1906
+ startDateInput.value = formatDate(shippingStartDate);
1907
+ if (wasStartDisabled) {
1908
+ startDateInput.disabled = true;
1909
+ }
1910
+
1911
+ // Check if this is a predefined subscription - if so, calculate end date
1912
+ let addData = {};
1913
+ if (productData.additionalData) {
1914
+ try {
1915
+ addData = typeof(productData.additionalData) == "string" ? JSON.parse(productData.additionalData) : productData.additionalData;
1916
+ } catch (e) {
1917
+ console.warn("Error parsing additionalData:", e);
1918
+ }
1919
+ }
1920
+
1921
+ const isScheduleByCustomer = addData.settings?.isScheduleByCustomer ?? true;
1922
+ const hasPredefinedFrequency = !isScheduleByCustomer && (addData.frequency || addData.frequencyData);
1923
+ const predefinedFrequency = addData.frequency || {};
1924
+ const expectedOrdersCount = hasPredefinedFrequency ? (predefinedFrequency.ordersCount) : null;
1925
+
1926
+ // For predefined subscriptions, calculate end date based on start date and ordersCount
1927
+ if (expectedOrdersCount !== null && expectedOrdersCount !== undefined && endDateInput) {
1928
+ const correctEndDate = calculateCorrectEndDate(
1929
+ startDateInput.value,
1930
+ expectedOrdersCount,
1931
+ addData
1932
+ );
1933
+
1934
+ if (correctEndDate) {
1935
+ if (wasEndDisabled) {
1936
+ endDateInput.disabled = false;
1937
+ }
1938
+ endDateInput.value = formatDate(correctEndDate);
1939
+ if (wasEndDisabled) {
1940
+ endDateInput.disabled = true;
1941
+ }
1942
+ }
1943
+ }
1944
+
1945
+ // Trigger change event to recalculate deliverables if it's a subscription
1946
+ if (typeof calculateDeliverables === 'function') {
1947
+ setTimeout(() => {
1948
+ calculateDeliverables();
1949
+ }, 100);
1950
+ }
1951
+ }
1952
+
1953
+ // Hide shipping details div (no need to show delivery info)
1954
+ if (shippingDetailsDiv) {
1955
+ shippingDetailsDiv.style.display = 'none';
1956
+ }
1957
+ });
1958
+
1959
+ // Auto-select shipping method based on shippingClassId for predefined subscriptions
1960
+ let addData = {};
1961
+ if (productData.additionalData) {
1962
+ try {
1963
+ addData = typeof(productData.additionalData) == "string" ? JSON.parse(productData.additionalData) : productData.additionalData;
1964
+ } catch (e) {
1965
+ console.warn("Error parsing additionalData:", e);
1966
+ }
1967
+ }
1968
+
1969
+ const isScheduleByCustomer = addData.settings?.isScheduleByCustomer ?? true;
1970
+ const shippingClassId = addData.settings?.shippingClassId;
1971
+
1972
+ // If predefined subscription and shippingClassId exists, auto-select matching shipping method
1973
+ if (!isScheduleByCustomer && shippingClassId !== null && shippingClassId !== undefined) {
1974
+ // Find matching shipping method
1975
+ for (let i = 0; i < shippingSelect.options.length; i++) {
1976
+ const option = shippingSelect.options[i];
1977
+ if (option.value == shippingClassId || parseInt(option.value) === parseInt(shippingClassId)) {
1978
+ shippingSelect.selectedIndex = i;
1979
+ // Trigger change event to auto-set dates
1980
+ shippingSelect.dispatchEvent(new Event('change'));
1981
+ break;
1982
+ }
1983
+ }
1984
+ }
1985
+ }
1986
+
1987
+ // Helper function to show user-friendly success/error messages
1988
+ // Delegate to the global Theme notification system so that
1989
+ // product detail uses the same bottom-center toast as other entry points.
1990
+ function showUserMessage(message, type = 'success') {
1991
+ if (window.Theme && typeof window.Theme.showNotification === 'function') {
1992
+ window.Theme.showNotification(message, type, 3000);
1993
+ return;
1994
+ }
1995
+
1996
+ // Fallback to inline cartMessage banner if Theme is not available
1997
+ const cartMessage = document.getElementById('cartMessage');
1998
+ const cartMessageText = cartMessage ? cartMessage.querySelector('.cart-message-text') : null;
1999
+
2000
+ if (cartMessage && cartMessageText) {
2001
+ cartMessageText.textContent = message;
2002
+
2003
+ // Update styling based on message type
2004
+ if (type === 'success') {
2005
+ cartMessage.style.color = '#059669';
2006
+ cartMessage.style.backgroundColor = 'transparent';
2007
+ cartMessage.style.border = 'none';
2008
+ } else if (type === 'error') {
2009
+ cartMessage.style.color = '#b70000';
2010
+ cartMessage.style.backgroundColor = '#ffe6e6';
2011
+ cartMessage.style.border = '1px solid #b70000';
2012
+ cartMessage.style.borderRadius = '4px';
2013
+ cartMessage.style.padding = '12px';
2014
+ }
2015
+
2016
+ cartMessage.style.display = 'block';
2017
+
2018
+ // Auto-hide after appropriate time
2019
+ const hideDelay = type === 'success' ? 3000 : 5000;
2020
+ setTimeout(() => {
2021
+ cartMessage.style.display = 'none';
2022
+ }, hideDelay);
2023
+ }
2024
+ }
2025
+
2026
+ // Validate subscription before submission
2027
+ function validateSubscription() {
2028
+ // Check if this is a subscription product
2029
+ if (productData.productType != 90) {
2030
+ return { valid: true };
2031
+ }
2032
+
2033
+ // Check if at least one subscription item is selected (only if customer can choose)
2034
+ let addData = {};
2035
+ if (productData.additionalData) {
2036
+ try {
2037
+ addData = typeof(productData.additionalData) == "string"
2038
+ ? JSON.parse(productData.additionalData)
2039
+ : productData.additionalData;
2040
+ } catch (e) {
2041
+ console.warn("Invalid additionalData JSON", e);
2042
+ }
2043
+ }
2044
+
2045
+ const isScheduleByCustomer = addData.settings?.isScheduleByCustomer ?? true;
2046
+ const isProductChoiceEnabled = addData.settings?.isProductChoiceEnabled ?? true;
2047
+ const allProductsRequired = !isScheduleByCustomer || !isProductChoiceEnabled;
2048
+
2049
+ // If customer can choose products, validate selection
2050
+ if (!allProductsRequired) {
2051
+ const selectedSubs = document.querySelectorAll(".subscription-item.selected");
2052
+ if (!selectedSubs || selectedSubs.length === 0) {
2053
+ return {
2054
+ valid: false,
2055
+ message: "Please select at least one subscription item"
2056
+ };
2057
+ }
2058
+
2059
+ // Check minimum subscription items requirement
2060
+ const minSelectable = addData.minimumSelectable || addData.settings?.minimumSelectable || addData.settings?.minSubscriptionProducts || 1;
2061
+ if (selectedSubs.length < minSelectable) {
2062
+ return {
2063
+ valid: false,
2064
+ message: `Please select at least ${minSelectable} subscription item(s)`
2065
+ };
2066
+ }
2067
+ }
2068
+
2069
+ // Check if frequency is selected (skip if predefined)
2070
+ const hasPredefinedFrequency = !isScheduleByCustomer && (addData.frequency || addData.frequencyData);
2071
+
2072
+ if (!hasPredefinedFrequency) {
2073
+ if (!selectedFrequency) {
2074
+ return {
2075
+ valid: false,
2076
+ message: "Please select a subscription frequency"
2077
+ };
2078
+ }
2079
+
2080
+ // Check if weekly frequency has at least one day selected
2081
+ if (selectedFrequency === "Weekly") {
2082
+ if (!frequencyDetails.days || frequencyDetails.days.length === 0) {
2083
+ return {
2084
+ valid: false,
2085
+ message: "Please select at least one day for weekly delivery"
2086
+ };
2087
+ }
2088
+ }
2089
+ }
2090
+
2091
+ // Validate dates
2092
+ const dateValidation = validateDates();
2093
+ if (!dateValidation.valid) {
2094
+ return dateValidation;
2095
+ }
2096
+
2097
+ // If there's a predefined frequency with ordersCount, validate that calculated count matches
2098
+ const predefinedFrequencyData = addData.frequencyData || {};
2099
+ const expectedOrdersCount = hasPredefinedFrequency ? (addData.frequency?.ordersCount) : null;
2100
+ if (expectedOrdersCount !== null && expectedOrdersCount !== undefined) {
2101
+ const liveOrderCount = document.querySelector(".liveOrderCount");
2102
+ const calculatedCount = liveOrderCount ? parseInt(liveOrderCount.textContent) : 0;
2103
+ if (calculatedCount !== expectedOrdersCount) {
2104
+ return {
2105
+ valid: false,
2106
+ message: `The selected dates result in ${calculatedCount} deliveries, but this subscription requires exactly ${expectedOrdersCount} deliveries. Please adjust your dates.`
2107
+ };
2108
+ }
2109
+ }
2110
+
2111
+ return { valid: true };
2112
+ }
2113
+
2114
+ // Helper function to validate dates
2115
+ function validateDates() {
2116
+ const startDateInput = document.getElementById("startDate");
2117
+ const endDateInput = document.getElementById("endDate");
2118
+
2119
+ if (!startDateInput || !endDateInput) return { valid: true };
2120
+
2121
+ const startDate = startDateInput.value;
2122
+ const endDate = endDateInput.value;
2123
+ const today = new Date();
2124
+ today.setHours(0, 0, 0, 0);
2125
+
2126
+ // Check if start date is provided
2127
+ if (!startDate) {
2128
+ return { valid: false, message: "Please select a start date" };
2129
+ }
2130
+
2131
+ // Check if end date is provided
2132
+ if (!endDate) {
2133
+ return { valid: false, message: "Please select an end date" };
2134
+ }
2135
+
2136
+ const start = new Date(startDate);
2137
+ start.setHours(0, 0, 0, 0);
2138
+ const end = new Date(endDate);
2139
+ end.setHours(0, 0, 0, 0);
2140
+
2141
+ // Check if start date is in the past
2142
+ if (start < today) {
2143
+ return { valid: false, message: "Start date cannot be in the past" };
2144
+ }
2145
+
2146
+ // Check if end date is before start date
2147
+ if (end < start) {
2148
+ return { valid: false, message: "End date must be after start date" };
2149
+ }
2150
+
2151
+ return { valid: true };
2152
+ }
2153
+
2154
+ // Set minimum date to today for date inputs
2155
+ function initializeDateInputs() {
2156
+ const startDateInput = document.getElementById("startDate");
2157
+ const endDateInput = document.getElementById("endDate");
2158
+
2159
+ // Check if we have predefined frequency data with dates
2160
+ let addData = {};
2161
+ if (productData.additionalData) {
2162
+ try {
2163
+ addData = typeof(productData.additionalData) == "string" ? JSON.parse(productData.additionalData) : productData.additionalData;
2164
+ } catch (e) {
2165
+ console.warn("Invalid additionalData JSON", e);
2166
+ }
2167
+ }
2168
+
2169
+ const isScheduleByCustomerValue = addData.settings?.isScheduleByCustomer ?? true;
2170
+ const hasPredefinedFrequency = !isScheduleByCustomerValue && (addData.frequency || addData.frequencyData);
2171
+ const predefinedFrequency = addData.frequency || {};
2172
+ const predefinedFrequencyData = addData.frequencyData || {};
2173
+ const ordersCount = predefinedFrequencyData.ordersCount || predefinedFrequency.ordersCount;
2174
+
2175
+ // If predefined subscription, disable date inputs
2176
+ if (!isScheduleByCustomerValue) {
2177
+ if (startDateInput) {
2178
+ startDateInput.disabled = true;
2179
+ startDateInput.style.backgroundColor = '#f5f5f5';
2180
+ startDateInput.style.cursor = 'not-allowed';
2181
+ startDateInput.setAttribute('readonly', 'readonly');
2182
+ }
2183
+ if (endDateInput) {
2184
+ endDateInput.disabled = true;
2185
+ endDateInput.style.backgroundColor = '#f5f5f5';
2186
+ endDateInput.style.cursor = 'not-allowed';
2187
+ endDateInput.setAttribute('readonly', 'readonly');
2188
+ }
2189
+ }
2190
+
2191
+ // For predefined subscriptions, dates will be auto-set when shipping method is selected
2192
+ // No need to set dates here - they will be set by the shipping method change handler
2193
+
2194
+ if (startDateInput && !startDateInput.disabled) {
2195
+ // Use local date formatting to avoid timezone issues
2196
+ const today = new Date();
2197
+ today.setHours(0, 0, 0, 0);
2198
+ const todayStr = today.getFullYear() + '-' + String(today.getMonth() + 1).padStart(2, '0') + '-' + String(today.getDate()).padStart(2, '0');
2199
+ startDateInput.setAttribute('min', todayStr);
2200
+ startDateInput.addEventListener('change', function() {
2201
+ const validation = validateDates();
2202
+ if (!validation.valid) {
2203
+ showSubscriptionError(validation.message);
2204
+ return;
2205
+ }
2206
+
2207
+ // Update end date min to be start date
2208
+ if (endDateInput && this.value) {
2209
+ endDateInput.setAttribute('min', this.value);
2210
+ }
2211
+
2212
+ // Check if this is a predefined subscription - if so, recalculate end date
2213
+ let addData = {};
2214
+ if (productData.additionalData) {
2215
+ try {
2216
+ addData = typeof(productData.additionalData) == "string" ? JSON.parse(productData.additionalData) : productData.additionalData;
2217
+ } catch (e) {
2218
+ console.warn("Error parsing additionalData:", e);
2219
+ }
2220
+ }
2221
+
2222
+ const isScheduleByCustomer = addData.settings?.isScheduleByCustomer ?? true;
2223
+ const hasPredefinedFrequency = !isScheduleByCustomer && (addData.frequency || addData.frequencyData);
2224
+ const predefinedFrequency = addData.frequency || {};
2225
+ const expectedOrdersCount = hasPredefinedFrequency ? (predefinedFrequency.ordersCount) : null;
2226
+
2227
+ // For predefined subscriptions, recalculate end date to match deliverables
2228
+ if (expectedOrdersCount !== null && expectedOrdersCount !== undefined && endDateInput) {
2229
+ const correctEndDate = calculateCorrectEndDate(
2230
+ this.value,
2231
+ expectedOrdersCount,
2232
+ addData
2233
+ );
2234
+
2235
+ if (correctEndDate) {
2236
+ const formatDate = (date) => {
2237
+ const year = date.getFullYear();
2238
+ const month = String(date.getMonth() + 1).padStart(2, '0');
2239
+ const day = String(date.getDate()).padStart(2, '0');
2240
+ return `${year}-${month}-${day}`;
2241
+ };
2242
+
2243
+ endDateInput.value = formatDate(correctEndDate);
2244
+ }
2245
+ }
2246
+
2247
+ clearSubscriptionError();
2248
+ calculateDeliverables();
2249
+ });
2250
+ }
2251
+
2252
+ if (endDateInput && !endDateInput.disabled) {
2253
+ endDateInput.addEventListener('change', function() {
2254
+ const validation = validateDates();
2255
+ if (!validation.valid) {
2256
+ showSubscriptionError(validation.message);
2257
+ return;
2258
+ }
2259
+
2260
+ // Check if this is a predefined subscription that requires exact deliverables
2261
+ let addData = {};
2262
+ if (productData.additionalData) {
2263
+ try {
2264
+ addData = typeof(productData.additionalData) == "string" ? JSON.parse(productData.additionalData) : productData.additionalData;
2265
+ } catch (e) {
2266
+ console.warn("Error parsing additionalData:", e);
2267
+ }
2268
+ }
2269
+
2270
+ const isScheduleByCustomer = addData.settings?.isScheduleByCustomer ?? true;
2271
+ const hasPredefinedFrequency = !isScheduleByCustomer && (addData.frequency || addData.frequencyData);
2272
+ const predefinedFrequency = addData.frequency || {};
2273
+ const expectedOrdersCount = hasPredefinedFrequency ? (predefinedFrequency.ordersCount) : null;
2274
+
2275
+ // For predefined subscriptions, enforce exact deliverables count
2276
+ if (expectedOrdersCount !== null && expectedOrdersCount !== undefined) {
2277
+ // Calculate deliverables with current dates
2278
+ calculateDeliverables();
2279
+
2280
+ // Wait a moment for calculation to complete, then check
2281
+ setTimeout(() => {
2282
+ const liveOrderCount = document.querySelector(".liveOrderCount");
2283
+ const calculatedCount = liveOrderCount ? parseInt(liveOrderCount.textContent) : 0;
2284
+
2285
+ if (calculatedCount !== expectedOrdersCount) {
2286
+ // End date doesn't match required deliverables - reset it
2287
+ const correctEndDate = calculateCorrectEndDate(
2288
+ startDateInput.value,
2289
+ expectedOrdersCount,
2290
+ addData
2291
+ );
2292
+
2293
+ if (correctEndDate) {
2294
+ const formatDate = (date) => {
2295
+ const year = date.getFullYear();
2296
+ const month = String(date.getMonth() + 1).padStart(2, '0');
2297
+ const day = String(date.getDate()).padStart(2, '0');
2298
+ return `${year}-${month}-${day}`;
2299
+ };
2300
+
2301
+ endDateInput.value = formatDate(correctEndDate);
2302
+
2303
+ // Recalculate with correct end date
2304
+ calculateDeliverables();
2305
+
2306
+ // Show validation message
2307
+ showSubscriptionError(`End Date must match ${expectedOrdersCount} deliveries. The date has been adjusted.`);
2308
+ } else {
2309
+ showSubscriptionError(`End Date must match ${expectedOrdersCount} deliveries. Please adjust your dates.`);
2310
+ }
2311
+ } else {
2312
+ clearSubscriptionError();
2313
+ }
2314
+ }, 50);
2315
+ } else {
2316
+ // Not a predefined subscription, allow normal calculation
2317
+ clearSubscriptionError();
2318
+ calculateDeliverables();
2319
+ }
2320
+ });
2321
+ }
2322
+ }
2323
+
2324
+ function renderSubscriptionUI() {
2325
+ const container = document.getElementById("subscriptionPlanContainer");
2326
+ if (!container || !productData.subscriptions) {
2327
+ container.innerHTML = "<p>No subscription items available.</p>";
2328
+ return;
2329
+ }
2330
+
2331
+ const subscriptions = productData.subscriptions;
2332
+
2333
+ let addData = {};
2334
+ if (productData.additionalData) {
2335
+ try {
2336
+ addData = typeof(productData.additionalData) == "string" ? JSON.parse(productData.additionalData) : productData.additionalData;
2337
+ } catch (e) {
2338
+ console.warn("Invalid additionalData JSON", e);
2339
+ }
2340
+ }
2341
+
2342
+ // Check for minimumSelectable in additionalSettings (try root level first, then settings)
2343
+ const minSelectable = addData.minimumSelectable || addData.settings?.minimumSelectable || addData.settings?.minSubscriptionProducts || 1;
2344
+ const maxSelectable = addData.maximumSelectable || addData.settings?.maximumSelectable || addData.settings?.maxSubscriptionProducts || 1;
2345
+ const quantityChangeAllowed = addData.settings?.isQuantityChangeAllowed ?? true;
2346
+ const isScheduleByCustomer = addData.settings?.isScheduleByCustomer ?? true;
2347
+ const isProductChoiceEnabled = addData.settings?.isProductChoiceEnabled ?? true;
2348
+
2349
+ container.innerHTML = "";
2350
+ let selectedCount = 0;
2351
+
2352
+ // If isScheduleByCustomer is false, customer cannot choose - all products are included
2353
+ const allProductsRequired = !isScheduleByCustomer || !isProductChoiceEnabled;
2354
+
2355
+ // Show info message if all products are required (will be updated with frequency details later)
2356
+ const infoMessageEl = document.getElementById("subscriptionInfoMessage");
2357
+ if (infoMessageEl && allProductsRequired) {
2358
+ // Build product list with quantities
2359
+ let productList = subscriptions.map(sub => {
2360
+ const qty = sub.quantity || minSelectable;
2361
+ return `${sub.name || 'Product'} (Qty: ${qty})`;
2362
+ }).join(', ');
2363
+
2364
+ // Initial message (will be updated with frequency details)
2365
+ infoMessageEl.innerHTML = `
2366
+ <div class="info-message-content">
2367
+ <strong>You will get the below products in this subscription with their respective quantities:</strong><br>
2368
+ ${productList}
2369
+ </div>
2370
+ `;
2371
+ infoMessageEl.style.display = "block";
2372
+ } else if (infoMessageEl) {
2373
+ infoMessageEl.style.display = "none";
2374
+ }
2375
+
2376
+ subscriptions.forEach((sub) => {
2377
+ let quantity = sub.quantity || minSelectable;
2378
+
2379
+ const subDiv = document.createElement("div");
2380
+ subDiv.className = "subscription-item";
2381
+ subDiv.dataset.subId = sub.productId;
2382
+
2383
+ // If all products are required, checkbox should be checked and disabled
2384
+ const isRequired = allProductsRequired;
2385
+
2386
+ const checkboxClass = isRequired ? 'sub-select sub-select-required' : 'sub-select';
2387
+ subDiv.innerHTML = `
2388
+ <div class="subscription-item-content">
2389
+ <input
2390
+ type="checkbox"
2391
+ class="${checkboxClass}"
2392
+ aria-label="${isRequired ? 'Included' : 'Select'} ${sub.name || 'subscription item'}"
2393
+ id="sub-checkbox-${sub.productId}"
2394
+ ${isRequired ? 'checked disabled' : ''}
2395
+ >
2396
+ <div>
2397
+ <input type="hidden" class="sub-id" value="${sub.productId}">
2398
+ <h4 class="subscription-item-title">${sub.name}${isRequired ? ' <span class="subscription-included">(Included)</span>' : ''}</h4>
2399
+ <div class="subscription-spec">${sub.specification || ''}</div>
2400
+ <div class="subscription-item-meta">
2401
+ Price: <span class="sub-price">${formatMoney(sub.prices.price)}</span>
2402
+ </div>
2403
+ </div>
2404
+ </div>
2405
+
2406
+ <div class="subscription-item-right">
2407
+ <div class="subscription-item-actions">
2408
+ <button
2409
+ type="button"
2410
+ class="qty-btn minus"
2411
+ aria-label="Decrease quantity for ${sub.name || 'item'}"
2412
+ >−</button>
2413
+ <span class="qty-value" aria-live="polite">${quantity}</span>
2414
+ <button
2415
+ type="button"
2416
+ class="qty-btn plus"
2417
+ aria-label="Increase quantity for ${sub.name || 'item'}"
2418
+ >+</button>
2419
+ </div>
2420
+ <div class="subscription-item-meta">
2421
+ Total: <span class="sub-total">${formatMoney(sub.prices.price * quantity)}</span>
2422
+ </div>
2423
+ </div>
2424
+ `;
2425
+
2426
+ const qtyValueEl = subDiv.querySelector(".qty-value");
2427
+ const totalEl = subDiv.querySelector(".sub-total");
2428
+ const plusBtn = subDiv.querySelector(".plus");
2429
+ const minusBtn = subDiv.querySelector(".minus");
2430
+ const checkbox = subDiv.querySelector(".sub-select");
2431
+
2432
+ const updateTotal = () => {
2433
+ totalEl.textContent = formatMoney(sub.prices.price * quantity);
2434
+ updateSubscriptionPriceUI();
2435
+ };
2436
+
2437
+
2438
+
2439
+ //=====================================
2440
+ // QUANTITY BEHAVIOR
2441
+ //=====================================
2442
+ if (quantityChangeAllowed === false) {
2443
+ // ❗ disable buttons completely
2444
+ plusBtn.disabled = true;
2445
+ minusBtn.disabled = true;
2446
+ }
2447
+ else {
2448
+ plusBtn.addEventListener("click", () => {
2449
+ // Sanitize: ensure quantity is a positive integer
2450
+ quantity = Math.max(1, Math.floor(quantity) + 1);
2451
+ qtyValueEl.textContent = quantity;
2452
+ updateTotal();
2453
+ });
2454
+
2455
+ minusBtn.addEventListener("click", () => {
2456
+ // Sanitize: ensure quantity doesn't go below minimum
2457
+ const newQuantity = Math.max(minSelectable, Math.floor(quantity) - 1);
2458
+ if (newQuantity < quantity) {
2459
+ quantity = newQuantity;
2460
+ qtyValueEl.textContent = quantity;
2461
+ updateTotal();
2462
+ }
2463
+ });
2464
+ }
2465
+ //=====================================
2466
+ // CHECKBOX SELECTION (line items)
2467
+ //=====================================
2468
+
2469
+ // If all products are required, mark as selected and don't allow changes
2470
+ if (isRequired) {
2471
+ checkbox.checked = true;
2472
+ subDiv.classList.add("selected");
2473
+ selectedCount++;
2474
+ } else {
2475
+ checkbox.addEventListener("change", () => {
2476
+ if (checkbox.checked) {
2477
+ if (selectedCount >= maxSelectable) {
2478
+ checkbox.checked = false;
2479
+ showSubscriptionError(`You can select only ${maxSelectable} subscription item${maxSelectable > 1 ? 's' : ''}.`);
2480
+ return;
2481
+ }
2482
+ selectedCount++;
2483
+ subDiv.classList.add("selected");
2484
+ clearSubscriptionError();
2485
+ } else {
2486
+ selectedCount--;
2487
+ subDiv.classList.remove("selected");
2488
+ }
2489
+ updateSubscriptionPriceUI();
2490
+ });
2491
+ }
2492
+
2493
+ container.appendChild(subDiv);
2494
+ });
2495
+
2496
+ // Auto-select items based on isScheduleByCustomer
2497
+ if (allProductsRequired) {
2498
+ // If isScheduleByCustomer is false, select ALL items
2499
+ const subscriptionItems = container.querySelectorAll(".subscription-item");
2500
+ subscriptionItems.forEach(item => {
2501
+ const checkbox = item.querySelector(".sub-select");
2502
+ if (checkbox && !checkbox.checked) {
2503
+ checkbox.checked = true;
2504
+ item.classList.add("selected");
2505
+ selectedCount++;
2506
+ }
2507
+ });
2508
+ if (subscriptionItems.length > 0) {
2509
+ updateSubscriptionPriceUI();
2510
+ }
2511
+ } else if (minSelectable > 0 && subscriptions.length > 0) {
2512
+ // Otherwise, auto-select minimum required items
2513
+ const subscriptionItems = container.querySelectorAll(".subscription-item");
2514
+ let selected = 0;
2515
+ for (let i = 0; i < subscriptionItems.length && selected < minSelectable; i++) {
2516
+ const checkbox = subscriptionItems[i].querySelector(".sub-select");
2517
+ if (checkbox && !checkbox.checked) {
2518
+ checkbox.checked = true;
2519
+ subscriptionItems[i].classList.add("selected");
2520
+ selected++;
2521
+ selectedCount++;
2522
+ }
2523
+ }
2524
+ if (selected > 0) {
2525
+ updateSubscriptionPriceUI();
2526
+ }
2527
+ }
2528
+
2529
+ // Check if frequency is predefined (isScheduleByCustomer is false and frequency exists)
2530
+ const hasPredefinedFrequency = !isScheduleByCustomer && (addData.frequency || addData.frequencyData);
2531
+
2532
+ if (hasPredefinedFrequency) {
2533
+ // Use predefined frequency data
2534
+ const predefinedFrequency = addData.frequency || {};
2535
+ const predefinedFrequencyData = addData.frequencyData || {};
2536
+
2537
+ // Set frequency from predefined data
2538
+ const freqOption = predefinedFrequency.selectedOption || predefinedFrequencyData.selectedOption || 'daily';
2539
+ let freqOptionNormalized = freqOption.toLowerCase();
2540
+
2541
+ // Map frequency options
2542
+ if (freqOptionNormalized === 'daily' || predefinedFrequencyData.isDailyFrequency) {
2543
+ selectedFrequency = 'Daily';
2544
+ } else if (freqOptionNormalized === 'weekly' || predefinedFrequencyData.isWeeklyFrequency) {
2545
+ selectedFrequency = 'Weekly';
2546
+ } else if (freqOptionNormalized === 'monthly' || predefinedFrequencyData.isMonthlyFrequency) {
2547
+ selectedFrequency = 'Monthly';
2548
+ } else if (freqOptionNormalized === 'alternate' || freqOptionNormalized === 'alternate days' || predefinedFrequencyData.isAlterNativeFrequency) {
2549
+ selectedFrequency = 'Alternate Days';
2550
+ } else {
2551
+ selectedFrequency = freqOption.charAt(0).toUpperCase() + freqOption.slice(1);
2552
+ }
2553
+
2554
+ // Use ordersCount from frequencyData if available
2555
+ const ordersCount = predefinedFrequencyData.ordersCount || predefinedFrequency.ordersCount;
2556
+
2557
+ // Set frequencyDetails for calculation
2558
+ // Handle Weekly frequency - ensure days array is properly set
2559
+ let weeklyDays = null;
2560
+ if (selectedFrequency === 'Weekly') {
2561
+ // weeklyFreVals should be an array of day names, or we default to all days
2562
+ if (predefinedFrequencyData.weeklyFreVals && Array.isArray(predefinedFrequencyData.weeklyFreVals) && predefinedFrequencyData.weeklyFreVals.length > 0) {
2563
+ weeklyDays = predefinedFrequencyData.weeklyFreVals;
2564
+ } else {
2565
+ // Default to all days if not specified (once per week)
2566
+ weeklyDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
2567
+ }
2568
+ }
2569
+
2570
+ frequencyDetails = {
2571
+ type: selectedFrequency,
2572
+ days: weeklyDays || (selectedFrequency === 'Alternate Days' ? (predefinedFrequencyData.weeklyFreVal || 2) : null),
2573
+ day: predefinedFrequencyData.day || (selectedFrequency === 'Monthly' ? (predefinedFrequencyData.day || 1) : null)
2574
+ };
2575
+
2576
+ // Display frequency UI with predefined selection
2577
+ // Render frequency UI with default selection from predefined data
2578
+ renderFrequencyUI(selectedFrequency, frequencyDetails);
2579
+
2580
+ // Update info message with frequency details
2581
+ updateSubscriptionInfoMessage(selectedFrequency, ordersCount, frequencyDetails);
2582
+
2583
+ // Calculate deliverables based on dates (will validate against ordersCount)
2584
+ setTimeout(() => {
2585
+ calculateDeliverables();
2586
+ }, 100);
2587
+ } else {
2588
+ // Render frequency UI for customer selection
2589
+ renderFrequencyUI();
2590
+ }
2591
+
2592
+ initializeDateInputs();
2593
+ }
2594
+
2595
+ // Helper function to update subscription info message with frequency details
2596
+ function updateSubscriptionInfoMessage(frequency, ordersCount, frequencyDetails) {
2597
+ const infoMessageEl = document.getElementById("subscriptionInfoMessage");
2598
+ if (!infoMessageEl || infoMessageEl.style.display === "none") return;
2599
+
2600
+ let addData = {};
2601
+ if (productData.additionalData) {
2602
+ try {
2603
+ addData = typeof(productData.additionalData) == "string" ? JSON.parse(productData.additionalData) : productData.additionalData;
2604
+ } catch (e) {
2605
+ console.warn("Invalid additionalData JSON", e);
2606
+ }
2607
+ }
2608
+
2609
+ const isScheduleByCustomer = addData.settings?.isScheduleByCustomer ?? true;
2610
+ const isProductChoiceEnabled = addData.settings?.isProductChoiceEnabled ?? true;
2611
+ const allProductsRequired = !isScheduleByCustomer || !isProductChoiceEnabled;
2612
+
2613
+ if (allProductsRequired && productData.subscriptions) {
2614
+ // Build product list with quantities
2615
+ const minSelectable = addData.minimumSelectable || addData.settings?.minimumSelectable || addData.settings?.minSubscriptionProducts || 1;
2616
+ let productList = productData.subscriptions.map(sub => {
2617
+ const qty = sub.quantity || minSelectable;
2618
+ return `${sub.name || 'Product'} (Qty: ${qty})`;
2619
+ }).join(', ');
2620
+
2621
+ // Build frequency text with details
2622
+ let frequencyText = '';
2623
+ if (frequency) {
2624
+ let frequencyDetail = '';
2625
+ if (frequencyDetails) {
2626
+ if (frequency === 'Alternate Days' && frequencyDetails.days) {
2627
+ frequencyDetail = ` (every ${frequencyDetails.days} days)`;
2628
+ } else if (frequency === 'Weekly' && frequencyDetails.days && Array.isArray(frequencyDetails.days) && frequencyDetails.days.length > 0) {
2629
+ frequencyDetail = ` (${frequencyDetails.days.join(', ')} each week)`;
2630
+ } else if (frequency === 'Monthly' && frequencyDetails.day) {
2631
+ frequencyDetail = ` (day ${frequencyDetails.day} of each month)`;
2632
+ }
2633
+ }
2634
+
2635
+ }
2636
+
2637
+ infoMessageEl.innerHTML = `
2638
+ <div class="info-message-content">
2639
+ <strong>You will get the below products in this subscription with their respective quantities:</strong><br>
2640
+ </div>
2641
+ `;
2642
+ }
2643
+ }
2644
+
2645
+
2646
+
2647
+ let selectedFrequency = null;
2648
+ let frequencyDetails = {};
2649
+
2650
+ function renderFrequencyUI(defaultFrequency, defaultFrequencyDetails) {
2651
+ const container = document.getElementById("frequencyContainer");
2652
+ if (!container) return;
2653
+ container.innerHTML = "";
2654
+
2655
+ const frequencyOptions = ["Daily", "Alternate Days", "Weekly", "Monthly"];
2656
+
2657
+ const title = document.createElement("h4");
2658
+ title.innerHTML = "Select Subscription Frequency: <span class='required-indicator'>*</span>";
2659
+ title.style.marginBottom = "10px";
2660
+ container.appendChild(title);
2661
+
2662
+ const frequencyOptionsContainer = document.createElement("div");
2663
+ frequencyOptionsContainer.style.display = "flex";
2664
+ frequencyOptionsContainer.style.flexWrap = "wrap";
2665
+ frequencyOptionsContainer.style.gap = "10px";
2666
+ container.appendChild(frequencyOptionsContainer);
2667
+
2668
+ const detailsContainer = document.createElement("div");
2669
+ detailsContainer.id = "frequencyDetailsContainer";
2670
+ detailsContainer.style.marginTop = "15px";
2671
+ container.appendChild(detailsContainer);
2672
+
2673
+ // Check if this is a predefined frequency (read-only)
2674
+ const isPredefined = defaultFrequency !== null && defaultFrequency !== undefined;
2675
+
2676
+ frequencyOptions.forEach(option => {
2677
+ const btn = document.createElement("button");
2678
+ btn.type = "button";
2679
+ btn.textContent = option;
2680
+ btn.className = "freq-ui";
2681
+ btn.setAttribute("aria-label", `Select ${option} frequency`);
2682
+
2683
+ // Pre-select if this is the default frequency
2684
+ if (isPredefined && option === defaultFrequency) {
2685
+ btn.classList.add("selected");
2686
+ selectedFrequency = option;
2687
+ // Set frequencyDetails from predefined data
2688
+ if (defaultFrequencyDetails) {
2689
+ frequencyDetails = JSON.parse(JSON.stringify(defaultFrequencyDetails));
2690
+ }
2691
+ }
2692
+
2693
+ // If predefined, disable buttons (read-only display)
2694
+ if (isPredefined) {
2695
+ btn.disabled = true;
2696
+ btn.style.opacity = option === defaultFrequency ? "1" : "0.5";
2697
+ btn.style.cursor = "not-allowed";
2698
+ } else {
2699
+ // Allow selection for non-predefined frequencies
2700
+ btn.addEventListener("click", () => {
2701
+ selectedFrequency = option;
2702
+ frequencyDetails = {};
2703
+
2704
+ frequencyOptionsContainer.querySelectorAll("button").forEach(b => b.classList.remove("selected"));
2705
+ btn.classList.add("selected");
2706
+
2707
+ renderFrequencyDetails(option, detailsContainer);
2708
+ calculateDeliverables();
2709
+ });
2710
+ }
2711
+
2712
+ frequencyOptionsContainer.appendChild(btn);
2713
+ });
2714
+
2715
+ // Render frequency details if default frequency is set
2716
+ if (isPredefined && defaultFrequency) {
2717
+ renderFrequencyDetails(defaultFrequency, detailsContainer, defaultFrequencyDetails, isPredefined);
2718
+ }
2719
+ }
2720
+
2721
+ function renderFrequencyDetails(frequency, container, predefinedDetails, isPredefined) {
2722
+ container.innerHTML = "";
2723
+
2724
+ if (frequency === "Daily") {
2725
+ container.innerHTML = `<p>Occurs every day.</p>`;
2726
+ if (!isPredefined) {
2727
+ frequencyDetails.type = "Daily";
2728
+ }
2729
+
2730
+ } else if (frequency === "Alternate Days") {
2731
+ const wrapper = document.createElement("div");
2732
+ wrapper.style.display = "flex";
2733
+ wrapper.style.alignItems = "center";
2734
+ wrapper.style.gap = "10px";
2735
+
2736
+ const label = document.createElement("span");
2737
+ label.textContent = "Every";
2738
+
2739
+ const input = document.createElement("input");
2740
+ input.type = "number";
2741
+ input.min = 1;
2742
+ input.value = predefinedDetails?.days || 2;
2743
+ input.className = "freq-ui";
2744
+ input.style.width = "60px";
2745
+ input.style.textAlign = "center";
2746
+ input.setAttribute("aria-label", "Number of days between deliveries");
2747
+
2748
+ if (isPredefined) {
2749
+ input.disabled = true;
2750
+ input.style.backgroundColor = "#f5f5f5";
2751
+ input.style.cursor = "not-allowed";
2752
+ } else {
2753
+ input.addEventListener("input", () => {
2754
+ frequencyDetails.type = "Alternate Days";
2755
+ frequencyDetails.days = parseInt(input.value) || 2;
2756
+ calculateDeliverables();
2757
+ });
2758
+ }
2759
+
2760
+ const text = document.createElement("span");
2761
+ text.textContent = "days";
2762
+
2763
+ wrapper.appendChild(label);
2764
+ wrapper.appendChild(input);
2765
+ wrapper.appendChild(text);
2766
+ container.appendChild(wrapper);
2767
+
2768
+ if (!isPredefined) {
2769
+ frequencyDetails.type = "Alternate Days";
2770
+ frequencyDetails.days = parseInt(input.value) || 2;
2771
+ }
2772
+
2773
+ } else if (frequency === "Weekly") {
2774
+ const days = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"];
2775
+ const wrapper = document.createElement("div");
2776
+ wrapper.className = "weekly-days";
2777
+
2778
+ // Use predefined days if available, otherwise empty array
2779
+ const predefinedDays = predefinedDetails?.days || [];
2780
+ if (!isPredefined) {
2781
+ frequencyDetails.days = [];
2782
+ }
2783
+
2784
+ days.forEach(day => {
2785
+ const btn = document.createElement("button");
2786
+ btn.type = "button";
2787
+ btn.textContent = day;
2788
+ btn.className = "freq-ui";
2789
+ btn.setAttribute("aria-label", `Select ${day} for weekly delivery`);
2790
+
2791
+ // Pre-select days from predefined data
2792
+ if (isPredefined && predefinedDays.includes(day)) {
2793
+ btn.classList.add("selected");
2794
+ if (!frequencyDetails.days) frequencyDetails.days = [];
2795
+ if (!frequencyDetails.days.includes(day)) {
2796
+ frequencyDetails.days.push(day);
2797
+ }
2798
+ }
2799
+
2800
+ if (isPredefined) {
2801
+ btn.disabled = true;
2802
+ btn.style.opacity = predefinedDays.includes(day) ? "1" : "0.5";
2803
+ btn.style.cursor = "not-allowed";
2804
+ } else {
2805
+ btn.addEventListener("click", () => {
2806
+ if (frequencyDetails.days.includes(day)) {
2807
+ frequencyDetails.days = frequencyDetails.days.filter(d => d !== day);
2808
+ btn.classList.remove("selected");
2809
+ } else {
2810
+ frequencyDetails.days.push(day);
2811
+ btn.classList.add("selected");
2812
+ }
2813
+ calculateDeliverables();
2814
+ });
2815
+ }
2816
+
2817
+ wrapper.appendChild(btn);
2818
+ });
2819
+
2820
+ container.appendChild(wrapper);
2821
+
2822
+ } else if (frequency === "Monthly") {
2823
+ const wrapper = document.createElement("div");
2824
+ wrapper.style.display = "flex";
2825
+ wrapper.style.alignItems = "center";
2826
+ wrapper.style.gap = "10px";
2827
+
2828
+ const label = document.createElement("span");
2829
+ label.textContent = "Day of month:";
2830
+
2831
+ const input = document.createElement("input");
2832
+ input.type = "number";
2833
+ input.min = 1;
2834
+ input.max = 31;
2835
+ input.value = predefinedDetails?.day || 1;
2836
+ input.className = "freq-ui";
2837
+ input.style.width = "60px";
2838
+ input.style.textAlign = "center";
2839
+ input.setAttribute("aria-label", "Day of month for monthly delivery");
2840
+
2841
+ if (isPredefined) {
2842
+ input.disabled = true;
2843
+ input.style.backgroundColor = "#f5f5f5";
2844
+ input.style.cursor = "not-allowed";
2845
+ } else {
2846
+ input.addEventListener("input", () => {
2847
+ frequencyDetails.type = "Monthly";
2848
+ frequencyDetails.day = parseInt(input.value) || 1;
2849
+ calculateDeliverables();
2850
+ });
2851
+ }
2852
+
2853
+ wrapper.appendChild(label);
2854
+ wrapper.appendChild(input);
2855
+ container.appendChild(wrapper);
2856
+
2857
+ if (!isPredefined) {
2858
+ frequencyDetails.type = "Monthly";
2859
+ frequencyDetails.day = parseInt(input.value) || 1;
2860
+ }
2861
+ }
2862
+ }
2863
+
2864
+ // -------------------------------------
2865
+ // DELIVERY COUNT CALCULATOR
2866
+ // -------------------------------------
2867
+ function calculateDeliverables() {
2868
+ try {
2869
+ // Check if we have predefined frequency with ordersCount
2870
+ let addData = {};
2871
+ if (productData.additionalData) {
2872
+ try {
2873
+ addData = typeof(productData.additionalData) == "string" ? JSON.parse(productData.additionalData) : productData.additionalData;
2874
+ } catch (e) {
2875
+ console.warn("Error parsing additionalData:", e);
2876
+ }
2877
+ }
2878
+
2879
+ const isScheduleByCustomer = addData.settings?.isScheduleByCustomer ?? true;
2880
+ const hasPredefinedFrequency = !isScheduleByCustomer && (addData.frequency || addData.frequencyData);
2881
+ const predefinedFrequencyData = addData.frequencyData || {};
2882
+ const predefinedFrequency = addData.frequency || {};
2883
+
2884
+ // Check if there's a predefined ordersCount to validate against
2885
+ const expectedOrdersCount = hasPredefinedFrequency ? (predefinedFrequency.ordersCount) : null;
2886
+
2887
+ const startDateInput = document.getElementById("startDate");
2888
+ const endDateInput = document.getElementById("endDate");
2889
+
2890
+ if (!startDateInput || !endDateInput) return;
2891
+
2892
+ const start = startDateInput.value;
2893
+ const end = endDateInput.value;
2894
+
2895
+ // Validate dates before calculating
2896
+ const dateValidation = validateDates();
2897
+ if (!dateValidation.valid) {
2898
+ showSubscriptionError(dateValidation.message);
2899
+ const liveOrderCount = document.querySelector(".liveOrderCount");
2900
+ if (liveOrderCount) liveOrderCount.textContent = "0";
2901
+ return;
2902
+ }
2903
+
2904
+ clearSubscriptionError();
2905
+
2906
+ // If predefined frequency, always get selectedFrequency from predefined data
2907
+ let currentSelectedFrequency = selectedFrequency;
2908
+ if (hasPredefinedFrequency) {
2909
+ // Always use predefined frequency data to ensure accuracy
2910
+ const freqOption = predefinedFrequency.selectedOption || predefinedFrequencyData.selectedOption || 'daily';
2911
+ let freqOptionNormalized = freqOption.toLowerCase();
2912
+ if (predefinedFrequencyData.isDailyFrequency) {
2913
+ currentSelectedFrequency = 'Daily';
2914
+ } else if (predefinedFrequencyData.isWeeklyFrequency) {
2915
+ currentSelectedFrequency = 'Weekly';
2916
+ } else if (predefinedFrequencyData.isMonthlyFrequency) {
2917
+ currentSelectedFrequency = 'Monthly';
2918
+ } else if (predefinedFrequencyData.isAlterNativeFrequency) {
2919
+ currentSelectedFrequency = 'Alternate Days';
2920
+ } else {
2921
+ // Fallback to option name
2922
+ currentSelectedFrequency = freqOption.charAt(0).toUpperCase() + freqOption.slice(1);
2923
+ }
2924
+ }
2925
+
2926
+ if (!start || !end || !currentSelectedFrequency) {
2927
+ const liveOrderCount = document.querySelector(".liveOrderCount");
2928
+ if (liveOrderCount) liveOrderCount.textContent = "0";
2929
+ return;
2930
+ }
2931
+
2932
+ let count = 0;
2933
+ let startDate = new Date(start);
2934
+ let endDate = new Date(end);
2935
+
2936
+ // Normalize to midnight
2937
+ startDate.setHours(0,0,0,0);
2938
+ endDate.setHours(0,0,0,0);
2939
+
2940
+ // Get frequencyDetails for predefined frequencies if not set globally
2941
+ let currentFrequencyDetails = frequencyDetails || {};
2942
+ if (hasPredefinedFrequency && (!currentFrequencyDetails || Object.keys(currentFrequencyDetails).length === 0)) {
2943
+ // Handle Weekly frequency - ensure days array is properly set
2944
+ let weeklyDays = null;
2945
+ if (currentSelectedFrequency === 'Weekly') {
2946
+ // weeklyFreVals should be an array of day names, or we default to all days
2947
+ if (predefinedFrequencyData.weeklyFreVals && Array.isArray(predefinedFrequencyData.weeklyFreVals) && predefinedFrequencyData.weeklyFreVals.length > 0) {
2948
+ weeklyDays = predefinedFrequencyData.weeklyFreVals;
2949
+ } else {
2950
+ // Default to all days if not specified
2951
+ weeklyDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
2952
+ }
2953
+ }
2954
+
2955
+ currentFrequencyDetails = {
2956
+ type: currentSelectedFrequency,
2957
+ days: weeklyDays || (currentSelectedFrequency === 'Alternate Days' ? (predefinedFrequencyData.weeklyFreVal || 2) : null),
2958
+ day: predefinedFrequencyData.day || (currentSelectedFrequency === 'Monthly' ? (predefinedFrequencyData.day || 1) : null)
2959
+ };
2960
+ }
2961
+
2962
+ // Also ensure frequencyDetails is set globally for future calculations
2963
+ if (hasPredefinedFrequency && (!frequencyDetails || Object.keys(frequencyDetails).length === 0)) {
2964
+ frequencyDetails = currentFrequencyDetails;
2965
+ }
2966
+
2967
+ // ----------------------------------------
2968
+ // DAILY = every single day including both ends
2969
+ // ----------------------------------------
2970
+ if (currentSelectedFrequency === "Daily") {
2971
+ count = Math.floor((endDate - startDate) / (1000 * 60 * 60 * 24)) + 1;
2972
+ }
2973
+
2974
+ // ----------------------------------------
2975
+ // ALTERNATE DAYS
2976
+ // Deliver every X days from start
2977
+ // Including starting day
2978
+ // ----------------------------------------
2979
+ else if (currentSelectedFrequency === "Alternate Days") {
2980
+ let step = currentFrequencyDetails.days || 2;
2981
+
2982
+ for (let d = new Date(startDate), i = 0; d <= endDate; d.setDate(d.getDate() + step), i++) {
2983
+ count++;
2984
+ }
2985
+ }
2986
+
2987
+ // ----------------------------------------
2988
+ // WEEKLY
2989
+ // Selected specific days of week
2990
+ // ----------------------------------------
2991
+ else if (currentSelectedFrequency === "Weekly") {
2992
+ let selectedDays = currentFrequencyDetails.days || [];
2993
+
2994
+ // If no days specified, default to all days (once per week = 7 days)
2995
+ if (!Array.isArray(selectedDays) || selectedDays.length === 0) {
2996
+ selectedDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
2997
+ }
2998
+
2999
+ // Count occurrences of selected days within the date range
3000
+ for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
3001
+ let day = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"][d.getDay()];
3002
+ if (selectedDays.includes(day)) count++;
3003
+ }
3004
+ }
3005
+
3006
+ // ----------------------------------------
3007
+ // MONTHLY
3008
+ // Day of month (e.g. 5 → 5th every month)
3009
+ // Handles invalid dates (e.g., Feb 31) by using last day of month
3010
+ // ----------------------------------------
3011
+ else if (currentSelectedFrequency === "Monthly") {
3012
+ let dom = currentFrequencyDetails.day || 1;
3013
+
3014
+ // Start from the first occurrence of the day in the start month
3015
+ let currentDate = new Date(startDate);
3016
+ currentDate.setDate(dom);
3017
+
3018
+ // If the date is before start date, move to next month
3019
+ if (currentDate < startDate) {
3020
+ currentDate.setMonth(currentDate.getMonth() + 1);
3021
+ currentDate.setDate(dom);
3022
+ }
3023
+
3024
+ // Count occurrences until we exceed end date
3025
+ while (currentDate <= endDate) {
3026
+ // Check if date is valid (handles cases like Feb 31)
3027
+ const lastDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0).getDate();
3028
+ const validDay = Math.min(dom, lastDayOfMonth);
3029
+
3030
+ // Create valid date
3031
+ const validDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), validDay);
3032
+
3033
+ if (validDate >= startDate && validDate <= endDate) {
3034
+ count++;
3035
+ }
3036
+
3037
+ // Move to next month
3038
+ currentDate.setMonth(currentDate.getMonth() + 1);
3039
+ currentDate.setDate(validDay);
3040
+ }
3041
+ }
3042
+
3043
+ const liveOrderCount = document.querySelector(".liveOrderCount");
3044
+ if (liveOrderCount) {
3045
+ liveOrderCount.textContent = count;
3046
+ }
3047
+
3048
+ // If there's an expected order count, validate against it
3049
+ if (expectedOrdersCount !== null && expectedOrdersCount !== undefined) {
3050
+ if (count !== expectedOrdersCount) {
3051
+ showSubscriptionError(`The selected dates result in ${count} deliveries, but this subscription requires exactly ${expectedOrdersCount} deliveries. Please adjust your dates.`);
3052
+ } else {
3053
+ // Clear any previous errors when count matches
3054
+ clearSubscriptionError();
3055
+ }
3056
+ } else {
3057
+ // If no expected count, clear any errors
3058
+ clearSubscriptionError();
3059
+ }
3060
+
3061
+ updateSubscriptionPriceUI();
3062
+ } catch (error) {
3063
+ console.error("Error calculating deliverables:", error);
3064
+ showSubscriptionError("Error calculating delivery count. Please check your dates.");
3065
+ const liveOrderCount = document.querySelector(".liveOrderCount");
3066
+ if (liveOrderCount) liveOrderCount.textContent = "0";
3067
+ }
3068
+ }
3069
+
3070
+
3071
+ function updateSubscriptionPriceUI() {
3072
+ try {
3073
+ let total = 0;
3074
+ const liveOrderCountEl = document.querySelector(".liveOrderCount");
3075
+ let deliverables = liveOrderCountEl ? parseInt(liveOrderCountEl.textContent) || 1 : 1;
3076
+
3077
+ // Parse additionalData if needed
3078
+ let additionalData = {};
3079
+ try {
3080
+ additionalData = typeof(productData.additionalData) == "string"
3081
+ ? JSON.parse(productData.additionalData || "{}")
3082
+ : productData.additionalData;
3083
+ } catch (e) {
3084
+ console.warn("Error parsing additionalData:", e);
3085
+ }
3086
+
3087
+ let items = [];
3088
+ // Must explicitly check IsSubscriptionPrice
3089
+ if (additionalData.IsSubscriptionPrice === true) {
3090
+ const isScheduleByCustomer = additionalData.settings?.isScheduleByCustomer ?? true;
3091
+ const isProductChoiceEnabled = additionalData.settings?.isProductChoiceEnabled ?? true;
3092
+ const allProductsRequired = !isScheduleByCustomer || !isProductChoiceEnabled;
3093
+
3094
+ // If isScheduleByCustomer is false, include ALL subscription products
3095
+ if (allProductsRequired) {
3096
+ // Get all subscription products from productData
3097
+ const allSubscriptions = productData.subscriptions || [];
3098
+ allSubscriptions.forEach(sub => {
3099
+ const quantity = sub.quantity || 1;
3100
+ const price = parseFloat(sub.prices?.price || sub.price || 0);
3101
+ const subId = parseInt(sub.productId || sub.id || 0);
3102
+
3103
+ if (subId > 0 && price >= 0) {
3104
+ items.push({
3105
+ id: subId,
3106
+ name: sub.name || '',
3107
+ quantity: quantity,
3108
+ price: price,
3109
+ orderTotal: price * quantity
3110
+ });
3111
+ total += price * quantity;
3112
+ }
3113
+ });
3114
+
3115
+ // For predefined subscriptions, check if orderTotal is already provided
3116
+ // If orderTotal exists, it's already the total for all deliveries, don't multiply
3117
+ const predefinedOrderTotal = additionalData.subscriptionDetails?.orderTotal;
3118
+ if (predefinedOrderTotal) {
3119
+ // Use the predefined orderTotal directly (already includes all deliveries)
3120
+ deliverables = Math.max(1, deliverables);
3121
+ total = total * deliverables;
3122
+ total = total;
3123
+ } else {
3124
+ // If no predefined orderTotal, calculate from perOrderPrice if available
3125
+ const perOrderPrice = additionalData.settings?.perOrderPrice;
3126
+ if (perOrderPrice) {
3127
+ total = parseFloat(perOrderPrice) || total;
3128
+ // Multiply by deliverables only if using perOrderPrice
3129
+ deliverables = Math.max(1, deliverables);
3130
+ total = total * deliverables;
3131
+ } else {
3132
+ // Fallback: multiply calculated total by deliverables
3133
+ deliverables = Math.max(1, deliverables);
3134
+ total = total * deliverables;
3135
+ }
3136
+ }
3137
+ } else {
3138
+ // ⛳ find ALL selected subscriptions (customer can choose)
3139
+ const selectedSubs = document.querySelectorAll(".subscription-item.selected");
3140
+
3141
+ selectedSubs.forEach(subItem => {
3142
+ const idEl = subItem.querySelector(".sub-id");
3143
+ const priceEl = subItem.querySelector(".sub-price");
3144
+ const qtyEl = subItem.querySelector(".qty-value");
3145
+ const nameEl = subItem.querySelector("h4");
3146
+
3147
+ // Sanitize and validate inputs
3148
+ let price = parseFloat(priceEl.textContent.replace(/[^\d.-]/g, '')) || 0;
3149
+ let qty = Math.max(1, parseInt(qtyEl.textContent) || 1); // Ensure positive integer
3150
+ let subId = parseInt(idEl.value) || 0;
3151
+ let name = nameEl ? nameEl.textContent.replace(/\s*\(Included\)/g, '').trim() : '';
3152
+
3153
+ if (subId > 0 && price >= 0 && qty > 0) {
3154
+ items.push({
3155
+ id: subId,
3156
+ name: name,
3157
+ quantity: qty,
3158
+ price: price,
3159
+ orderTotal: price * qty
3160
+ });
3161
+ total += price * qty;
3162
+ }
3163
+ });
3164
+
3165
+ // For customer-selectable subscriptions, multiply by deliverables
3166
+ deliverables = Math.max(1, deliverables);
3167
+ total = total * deliverables;
3168
+ }
3169
+
3170
+ } else {
3171
+ total = productData.price || 0;
3172
+ // For non-subscription products, multiply by deliverables
3173
+ deliverables = Math.max(1, deliverables);
3174
+ total = total * deliverables;
3175
+ }
3176
+
3177
+ if (priceElement) {
3178
+ priceElement.textContent = formatMoney(total);
3179
+ }
3180
+ updateSubscriptionData(items, deliverables, total);
3181
+ } catch (error) {
3182
+ console.error("Error updating subscription price UI:", error);
3183
+ showSubscriptionError("Error calculating price. Please refresh the page.");
3184
+ }
3185
+ }
3186
+
3187
+ function updateSubscriptionData(items, deliverables, priceTotal) {
3188
+ try {
3189
+ const startDateInput = document.getElementById("startDate");
3190
+ const endDateInput = document.getElementById("endDate");
3191
+
3192
+ if (!startDateInput || !endDateInput) return;
3193
+
3194
+ let startDate = startDateInput.value;
3195
+ let endDate = endDateInput.value;
3196
+
3197
+ // Validate and sanitize dates
3198
+ if (!startDate || !endDate) return;
3199
+
3200
+ // ensure ISO dates
3201
+ let isoStart = startDate ? new Date(startDate).toISOString() : null;
3202
+ let isoEnd = endDate ? new Date(endDate).toISOString() : null;
3203
+
3204
+ // Validate dates are valid
3205
+ if (isoStart && isNaN(new Date(isoStart).getTime())) {
3206
+ console.warn("Invalid start date");
3207
+ return;
3208
+ }
3209
+ if (isoEnd && isNaN(new Date(isoEnd).getTime())) {
3210
+ console.warn("Invalid end date");
3211
+ return;
3212
+ }
3213
+
3214
+ // Get original settings from additionalData to preserve isScheduleByCustomer and other settings
3215
+ let originalAddData = {};
3216
+ try {
3217
+ const originalDataEl = document.getElementById('productData');
3218
+ if (originalDataEl) {
3219
+ const originalProductData = JSON.parse(originalDataEl.textContent);
3220
+ if (originalProductData.additionalData) {
3221
+ originalAddData = typeof(originalProductData.additionalData) == "string"
3222
+ ? JSON.parse(originalProductData.additionalData || "{}")
3223
+ : originalProductData.additionalData;
3224
+ }
3225
+ }
3226
+ } catch (e) {
3227
+ console.warn("Error parsing original additionalData:", e);
3228
+ }
3229
+
3230
+ let subscriptionPayload = {
3231
+ QuantityVariation: [],
3232
+ IsCombinationPrice: false,
3233
+
3234
+ settings: {
3235
+ isScheduleByCustomer: originalAddData.settings?.isScheduleByCustomer ?? true,
3236
+ typeOfOrder: originalAddData.settings?.typeOfOrder ?? 10,
3237
+ // Set shipping class based on order count (deliverables)
3238
+ // Use original shippingClassId if available, otherwise calculate based on order count
3239
+ // Default to 3 if no order count, otherwise use order count (capped at reasonable max)
3240
+ shippingClassId: originalAddData.settings?.shippingClassId || (deliverables > 0 ? Math.max(3, Math.min(deliverables, 50)) : 3),
3241
+ offersSetting: originalAddData.settings?.offersSetting || [],
3242
+ perOrderPrice: deliverables > 0 ? priceTotal / deliverables : priceTotal,
3243
+ nextBillDate: originalAddData.settings?.nextBillDate || null,
3244
+ startDate: isoStart,
3245
+ endDate: isoEnd,
3246
+ isProductChoiceEnabled: originalAddData.settings?.isProductChoiceEnabled ?? true,
3247
+ isQuantityChangeAllowed: originalAddData.settings?.isQuantityChangeAllowed ?? true,
3248
+ minSubscriptionProducts: originalAddData.settings?.minSubscriptionProducts ?? 1,
3249
+ maxSubscriptionProducts: originalAddData.settings?.maxSubscriptionProducts ?? 100
3250
+ },
3251
+
3252
+ subscriptionDetails: {
3253
+ shippingFeeAmount: originalAddData.subscriptionDetails?.shippingFeeAmount || 0,
3254
+ paymentFeeAmount: originalAddData.subscriptionDetails?.paymentFeeAmount || 0,
3255
+ roundOff: originalAddData.subscriptionDetails?.roundOff || 0,
3256
+ discountAmount: originalAddData.subscriptionDetails?.discountAmount || 0,
3257
+ subscriptionCoupon: originalAddData.subscriptionDetails?.subscriptionCoupon || {},
3258
+ orderTotal: priceTotal.toFixed(2)
3259
+ },
3260
+
3261
+ IsSubscriptionPrice: true,
3262
+
3263
+ paymentSettings: {
3264
+ detectPaymentFromLP: true,
3265
+ createInvoiceWithoutPayment: true
3266
+ },
3267
+
3268
+ QuantityManualInput: false,
3269
+
3270
+ items: items,
3271
+
3272
+ frequency: originalAddData.frequency || {
3273
+ selectedOption: selectedFrequency?.toLowerCase(),
3274
+ timeFre: originalAddData.frequency?.timeFre || null,
3275
+ ordersCount: deliverables
3276
+ },
3277
+ frequencyData: {
3278
+ ...(originalAddData.frequencyData || {}),
3279
+ selectedOption: selectedFrequency?.toLowerCase() || originalAddData.frequencyData?.selectedOption,
3280
+ ordersCount: deliverables || originalAddData.frequencyData?.ordersCount || originalAddData.frequency?.ordersCount,
3281
+ startDate: isoStart || originalAddData.frequencyData?.startDate,
3282
+ endDate: isoEnd || originalAddData.frequencyData?.endDate,
3283
+ isDailyFrequency: (selectedFrequency == "Daily") || originalAddData.frequencyData?.isDailyFrequency || false,
3284
+ isWeeklyFrequency: (selectedFrequency == "Weekly") || originalAddData.frequencyData?.isWeeklyFrequency || false,
3285
+ isAlterNativeFrequency: (selectedFrequency == "Alternate Days") || originalAddData.frequencyData?.isAlterNativeFrequency || false,
3286
+ isMonthlyFrequency: (selectedFrequency == "Monthly") || originalAddData.frequencyData?.isMonthlyFrequency || false,
3287
+ isYearlyFrequency: originalAddData.frequencyData?.isYearlyFrequency || false
3288
+ }
3289
+ };
3290
+
3291
+ // 🧠 replace original product additional data
3292
+ productData.additionalData = subscriptionPayload;
3293
+
3294
+ console.log("UPDATED additionalData:", subscriptionPayload);
3295
+ } catch (error) {
3296
+ console.error("Error updating subscription data:", error);
3297
+ showSubscriptionError("Error updating subscription settings. Please try again.");
3298
+ }
3299
+ }
3300
+
3301
+
3302
+ function toggleComboSelection(btn) {
3303
+ const group = btn.dataset.group;
3304
+ const productId = btn.dataset.productId;
3305
+ const item = productData.combinations.find(x => x.productId == productId);
3306
+
3307
+ if (!bundleSelections[group]) bundleSelections[group] = [];
3308
+
3309
+ const selected = bundleSelections[group];
3310
+
3311
+ // deselect
3312
+ if (selected.includes(productId)) {
3313
+ bundleSelections[group] = selected.filter(x => x !== productId);
3314
+ btn.classList.remove("selected");
3315
+ updateBundlePriceUI();
3316
+ return;
3317
+ }
3318
+
3319
+ // ENFORCE MAX RULE
3320
+ if (selected.length >= item.maximumSelectable) {
3321
+ showCombinationError(`You can select only ${item.maximumSelectable} item${item.maximumSelectable > 1 ? 's' : ''} from "${item.groupName}".`);
3322
+ return;
3323
+ }
3324
+
3325
+ // select
3326
+ bundleSelections[group].push(productId);
3327
+ btn.classList.add("selected");
3328
+
3329
+ updateBundlePriceUI();
3330
+ }
3331
+
3332
+ //-------------------------------
3333
+ // VALIDATE BEFORE CART ADDING
3334
+ //-------------------------------
3335
+ function validateBundle() {
3336
+ for (const group in bundleSelections) {
3337
+ const items = productData.combinations.filter(x => x.groupName == group);
3338
+ const min = items[0].minimumSelectable;
3339
+ const selectedCount = bundleSelections[group].length;
3340
+
3341
+ if (selectedCount < min) {
3342
+ showCombinationError(`Please select at least ${min} item${min > 1 ? 's' : ''} from "${group}".`);
3343
+ return false;
3344
+ }
3345
+ }
3346
+ return true;
3347
+ }
3348
+
3349
+ function updateBundlePriceUI() {
3350
+ let total = 0;
3351
+ const items = [];
3352
+
3353
+ // Don't interfere with subscription products (productType == 90)
3354
+ const isSubscriptionProduct = productData.productType == 90;
3355
+
3356
+ // Parse additionalData (string → JSON)
3357
+ let additionalData = {};
3358
+ try {
3359
+ additionalData = typeof(productData.additionalData) == "string" ? JSON.parse(productData.additionalData || "{}") :productData.additionalData;
3360
+ } catch (e) {
3361
+ additionalData = {};
3362
+ }
3363
+
3364
+ // Check if this is a combination product
3365
+ const hasCombinations = productData.combinations && productData.combinations.length > 0;
3366
+
3367
+ if (hasCombinations && Object.keys(bundleSelections).length > 0) {
3368
+ // Combination-based pricing - build Items array
3369
+ for (const g in bundleSelections) {
3370
+ bundleSelections[g].forEach(productId => {
3371
+ const item = productData.combinations.find(c => c.productId == productId);
3372
+ if (item) {
3373
+ const quantity = item.quantity || 1;
3374
+ total += item.prices.price * quantity;
3375
+
3376
+ // Add to Items array for additionalSettings
3377
+ items.push({
3378
+ Id: parseInt(item.productId),
3379
+ Name: item.name || '',
3380
+ Price: parseFloat(item.prices.price || 0),
3381
+ Quantity: parseInt(quantity)
3382
+ });
3383
+ }
3384
+ });
3385
+ }
3386
+
3387
+ // Only update additionalData for combination products (not subscription products)
3388
+ if (!isSubscriptionProduct) {
3389
+ // Update additionalData with only Items structure
3390
+ productData.additionalData = {
3391
+ Items: items
3392
+ };
3393
+ }
3394
+ } else if (hasCombinations && !isSubscriptionProduct) {
3395
+ // Use main product price
3396
+ total = productData.prices.price || 0;
3397
+
3398
+ // Clear additionalData if no combinations selected (only for non-subscription products)
3399
+ productData.additionalData = {
3400
+ Items: []
3401
+ };
3402
+ } else {
3403
+ // Use main product price or existing pricing logic
3404
+ if (additionalData.IsCombinationPrice === true && hasCombinations) {
3405
+ // Combination-based pricing from existing data
3406
+ for (const g in bundleSelections) {
3407
+ bundleSelections[g].forEach(productId => {
3408
+ const item = productData.combinations.find(c => c.productId == productId);
3409
+ if (item) {
3410
+ total += item.prices.price * (item.quantity || 1);
3411
+ }
3412
+ });
3413
+ }
3414
+ } else {
3415
+ // Use main product price
3416
+ total = productData.prices.price || 0;
3417
+ }
3418
+ }
3419
+
3420
+ if (priceElement) {
3421
+ priceElement.textContent = formatMoney(total);
3422
+ }
3423
+ }
3424
+
3425
+ // Build option groups from variants
3426
+ function buildOptionGroups() {
3427
+ const variants = productData.variants || productData.variants || [];
3428
+ const combinations = productData.combinations || [];
3429
+ const subscriptions = productData.subscriptions || [];
3430
+ if ((!variants || variants.length === 0) && (!combinations || combinations.length === 0) && (!subscriptions || subscriptions.length === 0)) return null;
3431
+
3432
+ if(combinations && combinations.length > 0){
3433
+ renderCombinationUI();
3434
+ }
3435
+
3436
+ if(subscriptions && subscriptions.length > 0){
3437
+ renderSubscriptionUI();
3438
+ }
3439
+ if (variants || variants.length > 0) {
3440
+ const optionGroups = {};
3441
+
3442
+ // Process each variation
3443
+ variants.forEach(variation => {
3444
+ const options = variation.options || [];
3445
+
3446
+
3447
+ options.forEach(option => {
3448
+ const optionName = (option.optionName || 'Option').toLowerCase();
3449
+ const cleanName = optionName.replace(/[^a-z]/g, ''); // Remove non-alphabetic chars
3450
+
3451
+ // Map common option names
3452
+ let mappedName = cleanName;
3453
+ if (cleanName.includes('color') || cleanName.includes('colour')) {
3454
+ mappedName = 'color';
3455
+ } else if (cleanName.includes('size')) {
3456
+ mappedName = 'size';
3457
+ }
3458
+
3459
+ if (!optionGroups[mappedName]) {
3460
+ optionGroups[mappedName] = {
3461
+ name: mappedName === 'color' ? 'Color' : (mappedName === 'size' ? 'Size' : option.optionName || 'Option'),
3462
+ type: option.displayType || (mappedName === 'color' ? 'color' : 'text'),
3463
+ values: new Map()
3464
+ };
3465
+ }
3466
+
3467
+ const value = option.value || '';
3468
+ if (!optionGroups[mappedName].values.has(value)) {
3469
+ optionGroups[mappedName].values.set(value, {
3470
+ value: value,
3471
+ available: variation.inStock !== false && variation.available !== false,
3472
+ images: variation.images || [],
3473
+ combinationId: combinations.length > 0 ? combinations[0].productId : variation.productId
3474
+ });
3475
+ }
3476
+ });
3477
+ });
3478
+
3479
+ return optionGroups;
3480
+ }
3481
+ }
3482
+
3483
+ // Render option groups
3484
+ function renderOptionGroups() {
3485
+ const optionGroups = buildOptionGroups();
3486
+ if (!optionGroups || Object.keys(optionGroups).length === 0) return;
3487
+
3488
+ optionsContainer.innerHTML = '';
3489
+
3490
+ // Sort options: color first, then size, then others
3491
+ const sortedKeys = Object.keys(optionGroups).sort((a, b) => {
3492
+ const order = { color: 1, size: 2 };
3493
+ return (order[a] || 99) - (order[b] || 99);
3494
+ });
3495
+
3496
+ sortedKeys.forEach((key, groupIndex) => {
3497
+ const group = optionGroups[key];
3498
+ const optionDiv = document.createElement('div');
3499
+ optionDiv.className = 'product-option';
3500
+
3501
+ const label = document.createElement('label');
3502
+ label.className = 'option-label';
3503
+ label.textContent = group.name;
3504
+ optionDiv.appendChild(label);
3505
+
3506
+ const valuesDiv = document.createElement('div');
3507
+ valuesDiv.className = 'option-values';
3508
+
3509
+ const valuesArray = Array.from(group.values.values());
3510
+ valuesArray.forEach((valueObj, index) => {
3511
+ const isFirst = groupIndex === 0 && index === 0;
3512
+ if (isFirst && !selectedOptions[key]) {
3513
+ selectedOptions[key] = valueObj.value;
3514
+ }
3515
+
3516
+ const button = document.createElement('button');
3517
+ button.type = 'button';
3518
+ button.className = `option-value option-value-${group.type} ${(selectedOptions[key] === valueObj.value) ? 'selected' : ''} ${!valueObj.available ? 'disabled' : ''}`;
3519
+ button.dataset.optionKey = key;
3520
+ button.dataset.optionValue = valueObj.value;
3521
+ button.dataset.combinationId = valueObj.combinationId;
3522
+ button.dataset.available = valueObj.available;
3523
+
3524
+ if (group.type === 'color') {
3525
+ // Color swatch
3526
+ const colorValue = valueObj.value.toLowerCase().trim();
3527
+ const colorMap = {
3528
+ 'red': '#ef4444',
3529
+ 'blue': '#3b82f6',
3530
+ 'green': '#10b981',
3531
+ 'yellow': '#fbbf24',
3532
+ 'black': '#000000',
3533
+ 'white': '#ffffff',
3534
+ 'gray': '#6b7280',
3535
+ 'grey': '#6b7280',
3536
+ 'pink': '#ec4899',
3537
+ 'purple': '#a855f7',
3538
+ 'orange': '#f97316',
3539
+ 'brown': '#92400e',
3540
+ 'navy': '#1e3a8a',
3541
+ 'tan': '#d4a574',
3542
+ 'beige': '#f5f5dc',
3543
+ 'cream': '#fffdd0',
3544
+ 'pumice': '#c8c5b9'
3545
+ };
3546
+
3547
+ const color = colorMap[colorValue] || colorValue;
3548
+ button.style.backgroundColor = color;
3549
+ button.style.borderColor = (color === '#ffffff' || color === '#fffdd0' || color === '#f5f5dc') ? 'rgba(0, 0, 0, 0.2)' : 'rgba(0, 0, 0, 0.1)';
3550
+
3551
+ // Add screen reader text
3552
+ const srText = document.createElement('span');
3553
+ srText.className = 'sr-only';
3554
+ srText.textContent = valueObj.value;
3555
+ button.appendChild(srText);
3556
+ } else {
3557
+ // Text/Size button
3558
+ button.textContent = valueObj.value;
3559
+ }
3560
+
3561
+ if (!valueObj.available) {
3562
+ button.disabled = true;
3563
+ }
3564
+
3565
+ valuesDiv.appendChild(button);
3566
+ });
3567
+
3568
+ optionDiv.appendChild(valuesDiv);
3569
+ optionsContainer.appendChild(optionDiv);
3570
+ });
3571
+
3572
+ // Find initial variant
3573
+ findMatchingVariant();
3574
+ }
3575
+
3576
+
3577
+ function renderCombinationUI() {
3578
+ const combos = productData.combinations || [];
3579
+ if (combos.length === 0) return;
3580
+
3581
+ const grouped = {};
3582
+ combos.forEach(c => {
3583
+ if (!grouped[c.groupName]) grouped[c.groupName] = [];
3584
+ grouped[c.groupName].push(c);
3585
+ });
3586
+
3587
+ const container = document.getElementById("comboContainer");
3588
+ container.innerHTML = "";
3589
+
3590
+ Object.keys(grouped).forEach(groupName => {
3591
+ const items = grouped[groupName];
3592
+
3593
+ const header = document.createElement("div");
3594
+ header.className = "combo-group-header";
3595
+ header.innerHTML = `<strong>${groupName}</strong> (Select ${items[0].minimumSelectable}-${items[0].maximumSelectable})`;
3596
+ container.appendChild(header);
3597
+
3598
+ const groupDiv = document.createElement("div");
3599
+ groupDiv.className = "combo-group";
3600
+
3601
+ items.forEach(item => {
3602
+ const card = document.createElement("div");
3603
+ card.className = "combo-card";
3604
+ card.dataset.group = groupName;
3605
+ card.dataset.productId = item.productId;
3606
+ card.dataset.price = item.prices.price;
3607
+
3608
+ card.innerHTML = `
3609
+ <div class="combo-image">
3610
+ <img src="${item.thumbnailImage1.url}" alt="${item.name}" />
3611
+ <div class="combo-checkbox">
3612
+ <label>
3613
+ <input type="checkbox" />
3614
+ <span class="checkbox-custom"></span>
3615
+ </label>
3616
+ </div>
3617
+ </div>
3618
+ <div class="combo-info">
3619
+ <span class="combo-name">${item.name} × 1</span>
3620
+ <span class="combo-price">${formatMoney(item.prices.price)}</span>
3621
+ </div>`;
3622
+
3623
+ card.addEventListener("click", () => toggleComboSelection(card));
3624
+ groupDiv.appendChild(card);
3625
+ });
3626
+
3627
+ container.appendChild(groupDiv);
3628
+ });
3629
+
3630
+ // ⬇️ AUTO SELECT MINIMUM REQUIRED CARDS IN EACH GROUP
3631
+ Object.keys(grouped).forEach(groupName => {
3632
+ const items = grouped[groupName];
3633
+ const minSelectable = items[0]?.minimumSelectable || 1;
3634
+
3635
+ // Get all cards for this group
3636
+ const groupCards = Array.from(container.querySelectorAll(`.combo-card[data-group="${groupName}"]`));
3637
+
3638
+ // Select minimum required items
3639
+ let selected = 0;
3640
+ for (let i = 0; i < groupCards.length && selected < minSelectable; i++) {
3641
+ const card = groupCards[i];
3642
+ if (card && !card.classList.contains("selected")) {
3643
+ card.classList.add("selected");
3644
+ const checkbox = card.querySelector('input[type="checkbox"]');
3645
+ if (checkbox) {
3646
+ checkbox.checked = true;
3647
+ }
3648
+
3649
+ if (!bundleSelections[groupName]) bundleSelections[groupName] = [];
3650
+ bundleSelections[groupName].push(card.dataset.productId);
3651
+ selected++;
3652
+ }
3653
+ }
3654
+ });
3655
+
3656
+ updateBundlePriceUI();
3657
+ }
3658
+
3659
+
3660
+ // Find matching variant based on selected options
3661
+ function findMatchingVariant() {
3662
+ const variants = productData.variants || productData.variants || [];
3663
+
3664
+ // If no variants, use base product
3665
+ if (variants.length === 0) {
3666
+ currentVariant = {
3667
+ productId: productData.productId,
3668
+ price: productData.price,
3669
+ mrp: productData.mrp,
3670
+ inStock: productData.inStock,
3671
+ available: productData.available
3672
+ };
3673
+ updateVariantUI();
3674
+ return;
3675
+ }
3676
+
3677
+ // Find matching variation
3678
+ for (const variation of variants) {
3679
+ const options = variation.options || [];
3680
+ let matches = true;
3681
+
3682
+ for (const [key, value] of Object.entries(selectedOptions)) {
3683
+ const hasMatchingOption = options.some(opt => {
3684
+ const optName = (opt.optionName || 'Option').toLowerCase().replace(/[^a-z]/g, '');
3685
+ let mappedName = optName;
3686
+ if (optName.includes('color') || optName.includes('colour')) {
3687
+ mappedName = 'color';
3688
+ } else if (optName.includes('size')) {
3689
+ mappedName = 'size';
3690
+ }
3691
+ const optValue = opt.value || '';
3692
+ return mappedName === key && optValue === value;
3693
+ });
3694
+
3695
+ if (!hasMatchingOption) {
3696
+ matches = false;
3697
+ break;
3698
+ }
3699
+ }
3700
+
3701
+ if (matches) {
3702
+ currentVariant = {
3703
+ productId: variation.productId,
3704
+ price: variation.prices.price || productData.price,
3705
+ mrp: variation.prices.mrp || productData.mrp,
3706
+ inStock: variation.inStock !== false,
3707
+ available: variation.available !== false,
3708
+ images: variation.images || [],
3709
+ stockQuantity: variation.stockQuantity || 0
3710
+ };
3711
+ updateVariantUI();
3712
+ return;
3713
+ }
3714
+ }
3715
+
3716
+ // If no match found, use first variation
3717
+ if (variants.length > 0) {
3718
+ const firstVar = variants[0];
3719
+ currentVariant = {
3720
+ productId: firstVar.productId,
3721
+ price: firstVar.prices.price || productData.price,
3722
+ mrp: firstVar.prices.mrp || productData.mrp,
3723
+ inStock: firstVar.inStock !== false,
3724
+ available: firstVar.available !== false,
3725
+ images: firstVar.images || [],
3726
+ stockQuantity: firstVar.stockQuantity || 0
3727
+ };
3728
+ } else {
3729
+ currentVariant = {
3730
+ productId: productData.productId,
3731
+ price: productData.price,
3732
+ mrp: productData.mrp,
3733
+ inStock: productData.inStock,
3734
+ available: productData.available
3735
+ };
3736
+ }
3737
+
3738
+ updateVariantUI();
3739
+ }
3740
+
3741
+ // Update UI based on selected variant
3742
+ // Update product attributes based on current variant
3743
+ function updateAttributesUI() {
3744
+ if (!currentVariant) return;
3745
+
3746
+ try {
3747
+ const variants = productData.variants || [];
3748
+ const matchingVariant = variants.find(v => v.productId === currentVariant.productId);
3749
+
3750
+ // Select all attribute cards
3751
+ const attributeCards = document.querySelectorAll('.attributes-card[data-attribute-name]');
3752
+ if (!attributeCards || attributeCards.length === 0) return;
3753
+
3754
+ // Build map of possible attribute values from the variant
3755
+ const attributeValueMap = {};
3756
+
3757
+ if (matchingVariant) {
3758
+ // variantAttributes
3759
+ if (matchingVariant.variantAttributes && Array.isArray(matchingVariant.variantAttributes)) {
3760
+ matchingVariant.variantAttributes.forEach(a => {
3761
+ const name = a.name || a.attributeName;
3762
+ if (name) attributeValueMap[name] = a.value;
3763
+ });
3764
+ }
3765
+
3766
+ // options
3767
+ if (matchingVariant.options && Array.isArray(matchingVariant.options)) {
3768
+ matchingVariant.options.forEach(o => {
3769
+ const name = o.optionName || o.name;
3770
+ if (name) attributeValueMap[name] = o.value;
3771
+ });
3772
+ }
3773
+
3774
+ // attributes root
3775
+ if (matchingVariant.attributes && Array.isArray(matchingVariant.attributes)) {
3776
+ matchingVariant.attributes.forEach(a => {
3777
+ const name = a.name || a.attributeName;
3778
+ if (name) attributeValueMap[name] = a.value;
3779
+ });
3780
+ }
3781
+
3782
+ // additionalData
3783
+ if (matchingVariant.additionalData) {
3784
+ let add = matchingVariant.additionalData;
3785
+ if (typeof add === 'string') {
3786
+ try { add = JSON.parse(add); } catch(e) { add = null; }
3787
+ }
3788
+ if (add && add.attributes && Array.isArray(add.attributes)) {
3789
+ add.attributes.forEach(a => {
3790
+ const name = a.name || a.attributeName;
3791
+ if (name) attributeValueMap[name] = a.value;
3792
+ });
3793
+ }
3794
+ }
3795
+ }
3796
+
3797
+ // Update DOM cards
3798
+ attributeCards.forEach(card => {
3799
+ const name = card.getAttribute('data-attribute-name');
3800
+ const base = card.getAttribute('data-base-value');
3801
+ const valueEl = card.querySelector('.attribute-value-text');
3802
+ if (!valueEl) return;
3803
+
3804
+ // exact or case-insensitive match
3805
+ let newVal = null;
3806
+ if (attributeValueMap[name]) newVal = attributeValueMap[name];
3807
+ else {
3808
+ for (const k in attributeValueMap) {
3809
+ if (k && k.toLowerCase() === (name || '').toLowerCase()) { newVal = attributeValueMap[k]; break; }
3810
+ }
3811
+ }
3812
+
3813
+ if (newVal !== null && newVal !== undefined) {
3814
+ valueEl.textContent = newVal;
3815
+ valueEl.setAttribute('data-current-value', newVal);
3816
+ } else if (base !== null && base !== undefined) {
3817
+ valueEl.textContent = base;
3818
+ valueEl.setAttribute('data-current-value', base);
3819
+ }
3820
+ });
3821
+ } catch (err) {
3822
+ console.error('Error updating attributes UI', err);
3823
+ }
3824
+ }
3825
+
3826
+ function updateVariantUI() {
3827
+ if (!currentVariant) return;
3828
+
3829
+ // Update price
3830
+ if (priceElement) {
3831
+ priceElement.textContent = formatMoney(currentVariant.price);
3832
+ }
3833
+
3834
+ // Update availability
3835
+ if (addToCartBtn) {
3836
+ addToCartBtn.disabled = !currentVariant.available;
3837
+ const btnText = addToCartBtn.querySelector('.btn-text');
3838
+ if (btnText) {
3839
+ if(productData.productType == 90){
3840
+ btnText.textContent = 'Subscribe';
3841
+ }else{
3842
+ btnText.textContent = currentVariant.available ? 'Add to Cart' : 'Out of Stock';
3843
+ }
3844
+ }
3845
+ }
3846
+
3847
+ // Update images if variant has specific images
3848
+ if (currentVariant.images && currentVariant.images.length > 0 && mainImages && mainImages.length > 0) {
3849
+ const firstVariantImage = currentVariant.images[0];
3850
+ const variantImageUrl = typeof firstVariantImage === 'string'
3851
+ ? firstVariantImage
3852
+ : (firstVariantImage.url || firstVariantImage.Url || firstVariantImage);
3853
+
3854
+ // Try to find in parent images first (for gallery consistency)
3855
+ const allImages = productData.images || [];
3856
+ const imageIndex = allImages.findIndex(img => {
3857
+ const imgUrl = typeof img === 'string' ? img : (img.url || img.Url || img);
3858
+ return imgUrl === variantImageUrl;
3859
+ });
3860
+
3861
+ if (imageIndex >= 0) {
3862
+ // Variant image exists in parent images - use index-based switching
3863
+ switchToImage(imageIndex);
3864
+ } else {
3865
+ // Variant image is unique - directly update image sources
3866
+ mainImages.forEach(img => {
3867
+ if (img.tagName === 'IMG') {
3868
+ img.src = variantImageUrl;
3869
+ img.alt = currentVariant.title || productData.title || '';
3870
+ }
3871
+ });
3872
+ // Update thumbnails if needed
3873
+ if (thumbnails && thumbnails.length > 0) {
3874
+ thumbnails.forEach(thumb => {
3875
+ const thumbImg = thumb.querySelector('img');
3876
+ if (thumbImg) {
3877
+ thumbImg.src = variantImageUrl;
3878
+ }
3879
+ });
3880
+ }
3881
+ }
3882
+ }
3883
+
3884
+ // Update quantity max
3885
+ if (quantityInput && currentVariant.stockQuantity) {
3886
+ quantityInput.max = currentVariant.stockQuantity;
3887
+ }
3888
+
3889
+ // Update product attributes based on variant
3890
+ updateAttributesUI();
3891
+ }
3892
+
3893
+ // Switch to image by index
3894
+ function switchToImage(index) {
3895
+ if (!mainImages || mainImages.length === 0) return;
3896
+
3897
+ mainImages.forEach((img, i) => {
3898
+ if (i === index) {
3899
+ img.classList.add('active');
3900
+ currentImageIndex = index;
3901
+ } else {
3902
+ img.classList.remove('active');
3903
+ }
3904
+ });
3905
+
3906
+ updateThumbnails();
3907
+ }
3908
+
3909
+ // Update thumbnails active state
3910
+ function updateThumbnails() {
3911
+ if (!thumbnails || thumbnails.length === 0) return;
3912
+ thumbnails.forEach((thumb, index) => {
3913
+ if (parseInt(thumb.dataset.index) === currentImageIndex) {
3914
+ thumb.classList.add('active');
3915
+ } else {
3916
+ thumb.classList.remove('active');
3917
+ }
3918
+ });
3919
+ }
3920
+
3921
+ document.addEventListener("click", function(e){
3922
+ let btn = e.target.closest(".product-option-btn");
3923
+ if(!btn) return;
3924
+
3925
+ let key = btn.dataset.optionKey;
3926
+
3927
+ document.querySelectorAll(`.product-option-btn[data-option-key="${key}"]`)
3928
+ .forEach(b => b.classList.remove("selected"));
3929
+
3930
+ btn.classList.add("selected");
3931
+ findMatchingVariant()
3932
+ });
3933
+
3934
+ // Initialize on DOM ready
3935
+ document.addEventListener('DOMContentLoaded', function() {
3936
+ // Initialize DOM elements
3937
+ mainImages = document.querySelectorAll('.gallery-main-image');
3938
+ thumbnails = document.querySelectorAll('.gallery-thumbnail');
3939
+ productForm = document.getElementById('productForm');
3940
+ addToCartBtn = document.getElementById('addToCartBtn');
3941
+ quantityInput = document.getElementById('quantity');
3942
+ priceElement = document.getElementById('productPrice');
3943
+ optionsContainer = document.getElementById('productOptionsContainer');
3944
+ galleryModal = document.getElementById('galleryModal');
3945
+ galleryModalImage = document.getElementById('galleryModalImage');
3946
+ galleryModalClose = document.getElementById('galleryModalClose');
3947
+ galleryModalPrev = document.getElementById('galleryModalPrev');
3948
+ galleryModalNext = document.getElementById('galleryModalNext');
3949
+ galleryModalCounter = document.getElementById('galleryModalCounter');
3950
+ galleryZoomBtn = document.getElementById('galleryZoomBtn');
3951
+ cartMessage = document.getElementById('cartMessage');
3952
+
3953
+ // Render option groups
3954
+ renderOptionGroups();
3955
+
3956
+ // Initialize shipping methods
3957
+ initializeShippingMethods();
3958
+
3959
+ // Initialize Product Attributes Tabs
3960
+ const attributeTabLinks = document.querySelectorAll('.attributes-tab-link');
3961
+ attributeTabLinks.forEach(link => {
3962
+ link.addEventListener('click', function(e) {
3963
+ e.preventDefault();
3964
+ const tabId = this.getAttribute('data-tab');
3965
+
3966
+ // Remove active class from all links and panes
3967
+ attributeTabLinks.forEach(l => l.classList.remove('active'));
3968
+ document.querySelectorAll('.attributes-tab-pane').forEach(pane => pane.classList.remove('active'));
3969
+
3970
+ // Add active class to clicked link and corresponding pane
3971
+ this.classList.add('active');
3972
+ this.setAttribute('aria-selected', 'true');
3973
+
3974
+ const activePane = document.getElementById(tabId);
3975
+ if (activePane) {
3976
+ activePane.classList.add('active');
3977
+ }
3978
+ });
3979
+ });
3980
+
3981
+ // Image Gallery Thumbnails
3982
+ if (thumbnails && thumbnails.length > 0) {
3983
+ thumbnails.forEach(thumbnail => {
3984
+ thumbnail.addEventListener('click', function() {
3985
+ const imageIndex = parseInt(this.dataset.index);
3986
+ switchToImage(imageIndex);
3987
+ });
3988
+ });
3989
+ }
3990
+
3991
+ // Option selection
3992
+ document.addEventListener('click', function(e) {
3993
+ const optionBtn = e.target.closest('.option-value');
3994
+ if (!optionBtn || optionBtn.disabled) return;
3995
+
3996
+ const optionKey = optionBtn.dataset.optionKey;
3997
+ const optionValue = optionBtn.dataset.optionValue;
3998
+
3999
+ if (!optionKey || !optionValue) return;
4000
+
4001
+ // Deselect other options in same group
4002
+ const optionGroup = optionBtn.closest('.product-option');
4003
+ optionGroup.querySelectorAll('.option-value').forEach(btn => {
4004
+ btn.classList.remove('selected');
4005
+ });
4006
+
4007
+ // Select clicked option
4008
+ optionBtn.classList.add('selected');
4009
+
4010
+ // Update selected options
4011
+ selectedOptions[optionKey] = optionValue;
4012
+
4013
+ // Find matching variant
4014
+ findMatchingVariant();
4015
+
4016
+ // Explicitly update attributes immediately
4017
+ setTimeout(() => {
4018
+ updateAttributesUI();
4019
+ }, 50);
4020
+ });
4021
+
4022
+ // Quantity Controls
4023
+ const decreaseBtn = document.querySelector('.quantity-decrease');
4024
+ const increaseBtn = document.querySelector('.quantity-increase');
4025
+
4026
+ if (decreaseBtn && quantityInput) {
4027
+ decreaseBtn.addEventListener('click', () => {
4028
+ const val = parseInt(quantityInput.value) || 1;
4029
+ if (val > 1) {
4030
+ quantityInput.value = val - 1;
4031
+ }
4032
+ });
4033
+ }
4034
+
4035
+ if (increaseBtn && quantityInput) {
4036
+ increaseBtn.addEventListener('click', () => {
4037
+ const val = parseInt(quantityInput.value) || 1;
4038
+ const max = parseInt(quantityInput.max) || 99;
4039
+ if (val < max) {
4040
+ quantityInput.value = val + 1;
4041
+ }
4042
+ });
4043
+ }
4044
+
4045
+ // Add to Cart
4046
+ if (productForm && addToCartBtn) {
4047
+ productForm.addEventListener('submit', async function(e) {
4048
+ e.preventDefault();
4049
+
4050
+ if (addToCartBtn.disabled || addToCartBtn.classList.contains('loading')) return;
4051
+
4052
+ const productId = currentVariant ? currentVariant.productId : productData.productId;
4053
+ const quantity = quantityInput != null && quantityInput.value != null && quantityInput.value != ""? parseInt(quantityInput.value) : 1;
4054
+
4055
+ addToCartBtn.classList.add('loading');
4056
+ const btnText = addToCartBtn.querySelector('.btn-text');
4057
+ if (btnText) {
4058
+ btnText.textContent = 'Adding...';
4059
+ }
4060
+
4061
+ try {
4062
+ // Validate subscription before submission
4063
+ if (productData.productType == 90) {
4064
+ const validation = validateSubscription();
4065
+ if (!validation.valid) {
4066
+ showSubscriptionError(validation.message);
4067
+ addToCartBtn.classList.remove('loading');
4068
+ if (btnText) {
4069
+ btnText.textContent = productData.productType == 90 ? 'Subscribe' : 'Add to Cart';
4070
+ }
4071
+ return;
4072
+ }
4073
+ clearSubscriptionError();
4074
+ }
4075
+
4076
+ // Validate combination products before submission
4077
+ const hasCombinations = productData.combinations && productData.combinations.length > 0;
4078
+ if (hasCombinations) {
4079
+ const bundleValidation = validateBundle();
4080
+ if (!bundleValidation) {
4081
+ addToCartBtn.classList.remove('loading');
4082
+ if (btnText) {
4083
+ btnText.textContent = 'Add to Cart';
4084
+ }
4085
+ return;
4086
+ }
4087
+ }
4088
+
4089
+ // Store variation image in localStorage before adding to cart
4090
+ if (currentVariant && currentVariant.images && currentVariant.images.length > 0) {
4091
+ const firstVariantImage = currentVariant.images[0];
4092
+ const variantImageUrl = typeof firstVariantImage === 'string'
4093
+ ? firstVariantImage
4094
+ : (firstVariantImage.url || firstVariantImage.Url || firstVariantImage);
4095
+
4096
+ if (variantImageUrl) {
4097
+ // Store variation image for this productId
4098
+ const imageKey = `variantImage_${productId}`;
4099
+ try {
4100
+ localStorage.setItem(imageKey, variantImageUrl);
4101
+ } catch (e) {
4102
+ console.warn('Failed to store variant image in localStorage:', e);
4103
+ }
4104
+ }
4105
+ }
4106
+
4107
+ let bodyData = {
4108
+ productId: parseInt(productId),
4109
+ quantity: quantity
4110
+ };
4111
+
4112
+ // If product type is subscription → attach Additional Settings
4113
+ if (productData.productType == 90) {
4114
+ try {
4115
+ bodyData.additionalSettings = typeof(productData.additionalData) == "string"
4116
+ ? productData.additionalData
4117
+ : JSON.stringify(productData.additionalData);
4118
+ } catch(e) {
4119
+ console.warn("Invalid additionalData JSON");
4120
+ showSubscriptionError("Error preparing subscription data. Please try again.");
4121
+ addToCartBtn.classList.remove('loading');
4122
+ if (btnText) {
4123
+ btnText.textContent = productData.productType == 90 ? 'Subscribe' : 'Add to Cart';
4124
+ }
4125
+ return;
4126
+ }
4127
+ }
4128
+ // If product has combinations (and is NOT a subscription) → attach Additional Settings with Items array
4129
+ else {
4130
+ const hasCombinations = productData.combinations && productData.combinations.length > 0;
4131
+ if (hasCombinations) {
4132
+ try {
4133
+ // Ensure bundle selections are up to date
4134
+ updateBundlePriceUI();
4135
+
4136
+ // Get the Items array from additionalData
4137
+ const combinationData = productData.additionalData || {};
4138
+ const items = combinationData.Items || [];
4139
+
4140
+ // Only include additionalSettings if there are selected items
4141
+ if (items.length > 0) {
4142
+ bodyData.additionalSettings = JSON.stringify({
4143
+ Items: items
4144
+ });
4145
+ }
4146
+ } catch(e) {
4147
+ console.warn("Error preparing combination data:", e);
4148
+ addToCartBtn.classList.remove('loading');
4149
+ if (btnText) {
4150
+ btnText.textContent = 'Add to Cart';
4151
+ }
4152
+ return;
4153
+ }
4154
+ }
4155
+ }
4156
+
4157
+ const response = await fetch('/webstoreapi/carts/add', {
4158
+ method: 'POST',
4159
+ headers: {
4160
+ 'Content-Type': 'application/json',
4161
+ 'X-Requested-With': 'XMLHttpRequest'
4162
+ },
4163
+ body: JSON.stringify(bodyData)
4164
+ });
4165
+
4166
+ const data = await response.json();
4167
+
4168
+ // Check if authentication is required
4169
+ if (data.requiresAuth) {
4170
+ // Open login modal using the same pattern as widgets
4171
+ if (window.Theme && window.Theme.openLoginModal) {
4172
+ window.Theme.openLoginModal();
4173
+ } else if (window.CartManager && window.CartManager.openLoginModal) {
4174
+ window.CartManager.openLoginModal();
4175
+ } else {
4176
+ // Fallback: trigger login modal via data attribute
4177
+ const loginTrigger = document.querySelector('[data-login-modal-trigger]');
4178
+ if (loginTrigger) {
4179
+ loginTrigger.click();
4180
+ }
4181
+ }
4182
+
4183
+ // Reset button state
4184
+ if (btnText) {
4185
+ btnText.textContent = productData.productType == 90 ? 'Subscribe' : 'Add to Cart';
4186
+ }
4187
+ addToCartBtn.classList.remove('loading');
4188
+ return;
4189
+ }
4190
+
4191
+ if (data.success) {
4192
+ // Fetch cart count after successful add to ensure instant update (like other pages)
4193
+ // Use CartManager to get cart count, which uses /carts/quantity API
4194
+ let cartCount = 0;
4195
+ try {
4196
+ if (window.CartManager && typeof window.CartManager.getCartCount === 'function') {
4197
+ // Force refresh to get the latest count after adding item
4198
+ cartCount = await window.CartManager.getCartCount(true);
4199
+ // Update cart data with the fetched count
4200
+ data.data = data.data || {};
4201
+ data.data.itemCount = cartCount;
4202
+ // Dispatch cart updated event to update all badges instantly
4203
+ if (window.CartManager && typeof window.CartManager.dispatchCartUpdated === 'function') {
4204
+ window.CartManager.dispatchCartUpdated({ itemCount: cartCount, cart: data.data });
4205
+ }
4206
+ } else {
4207
+ // Fallback to direct fetch if CartManager not available
4208
+ const countResponse = await fetch('/webstoreapi/carts/quantity', {
4209
+ method: 'GET',
4210
+ credentials: 'same-origin',
4211
+ headers: { 'Accept': 'application/json' }
4212
+ });
4213
+ if (countResponse.ok) {
4214
+ const countData = await countResponse.json();
4215
+ if (countData.success && countData.data) {
4216
+ cartCount = countData.data.cartQuantity || 0;
4217
+ data.data = data.data || {};
4218
+ data.data.itemCount = cartCount;
4219
+ // Fallback: update badges manually if CartManager not available
4220
+ const countElements = document.querySelectorAll('[data-cart-count]');
4221
+ countElements.forEach(element => {
4222
+ element.textContent = cartCount;
4223
+ element.setAttribute('data-cart-count', cartCount.toString());
4224
+ if (cartCount > 0) {
4225
+ element.removeAttribute('style');
4226
+ } else {
4227
+ const isDrawerTitle = element.closest('.cart-drawer-title');
4228
+ if (!isDrawerTitle) {
4229
+ element.style.display = 'none';
4230
+ }
4231
+ }
4232
+ });
4233
+ }
4234
+ }
4235
+ }
4236
+ } catch (e) {
4237
+ console.warn('Failed to fetch cart count after add:', e);
4238
+ // If we have itemCount in the response, use it as fallback
4239
+ if (data.data && (data.data.itemCount !== undefined || data.data.items)) {
4240
+ cartCount = data.data.itemCount || (data.data.items ? data.data.items.length : 0);
4241
+ }
4242
+ }
4243
+
4244
+ // Show success message - match widget format
4245
+ const successMessage = productData.productType == 90
4246
+ ? 'Subscription added to cart successfully!'
4247
+ : 'Product added to cart!';
4248
+ showUserMessage(successMessage, 'success');
4249
+
4250
+ if (btnText) {
4251
+ btnText.textContent = productData.productType == 90 ? 'Subscribe' : 'Add to Cart';
4252
+ }
4253
+ addToCartBtn.classList.remove('loading');
4254
+
4255
+ // Clear any validation errors
4256
+ clearSubscriptionError();
4257
+ showCombinationError("");
4258
+
4259
+ // Update cart UI with the latest data (includes total and count)
4260
+ if (window.Theme && typeof window.Theme.updateCartUI === 'function') {
4261
+ window.Theme.updateCartUI(data.data);
4262
+ } else if (window.theme && typeof window.theme.updateCartUI === 'function') {
4263
+ window.theme.updateCartUI(data.data);
4264
+ }
4265
+ } else {
4266
+ throw new Error(data.error || 'Failed to add to cart');
4267
+ }
4268
+ } catch (error) {
4269
+ console.error('Error adding to cart:', error);
4270
+
4271
+ // Show user-friendly error message
4272
+ const errorMessage = error.message || 'Failed to add item to cart. Please try again.';
4273
+ showUserMessage(errorMessage, 'error');
4274
+
4275
+ if (btnText) {
4276
+ btnText.textContent = productData.productType == 90 ? 'Subscribe' : 'Add to Cart';
4277
+ }
4278
+ addToCartBtn.classList.remove('loading');
4279
+ }
4280
+ });
4281
+ }
4282
+
4283
+ // Full Screen Gallery
4284
+ if (galleryZoomBtn && galleryModal) {
4285
+ const images = productData.images || [];
4286
+
4287
+ // Process images to extract URLs
4288
+ const imageUrls = images.map(img => {
4289
+ return typeof img === 'string' ? img : (img.url || img);
4290
+ });
4291
+
4292
+ if (imageUrls.length > 0) {
4293
+ galleryZoomBtn.style.display = 'flex';
4294
+
4295
+ galleryZoomBtn.addEventListener('click', () => {
4296
+ currentImageIndex = 0;
4297
+ galleryModalImage.src = imageUrls[currentImageIndex];
4298
+ updateGalleryModal();
4299
+ galleryModal.classList.add('active');
4300
+ document.body.style.overflow = 'hidden';
4301
+ });
4302
+
4303
+ galleryModalPrev.addEventListener('click', () => {
4304
+ if (currentImageIndex > 0) {
4305
+ currentImageIndex--;
4306
+ } else {
4307
+ currentImageIndex = imageUrls.length - 1;
4308
+ }
4309
+ galleryModalImage.src = imageUrls[currentImageIndex];
4310
+ updateGalleryModal();
4311
+ });
4312
+
4313
+ galleryModalNext.addEventListener('click', () => {
4314
+ if (currentImageIndex < imageUrls.length - 1) {
4315
+ currentImageIndex++;
4316
+ } else {
4317
+ currentImageIndex = 0;
4318
+ }
4319
+ galleryModalImage.src = imageUrls[currentImageIndex];
4320
+ updateGalleryModal();
4321
+ });
4322
+
4323
+ function updateGalleryModal() {
4324
+ galleryModalCounter.textContent = `${currentImageIndex + 1} / ${imageUrls.length}`;
4325
+ }
4326
+ } else {
4327
+ if (galleryZoomBtn) galleryZoomBtn.style.display = 'none';
4328
+ }
4329
+
4330
+ galleryModalClose.addEventListener('click', () => {
4331
+ galleryModal.classList.remove('active');
4332
+ document.body.style.overflow = '';
4333
+ });
4334
+
4335
+ galleryModal.addEventListener('click', (e) => {
4336
+ if (e.target === galleryModal || e.target.classList.contains('gallery-modal-overlay')) {
4337
+ galleryModal.classList.remove('active');
4338
+ document.body.style.overflow = '';
4339
+ }
4340
+ });
4341
+
4342
+ // Keyboard navigation
4343
+ document.addEventListener('keydown', (e) => {
4344
+ if (!galleryModal.classList.contains('active')) return;
4345
+
4346
+ if (e.key === 'Escape') {
4347
+ galleryModal.classList.remove('active');
4348
+ document.body.style.overflow = '';
4349
+ } else if (e.key === 'ArrowLeft' && galleryModalPrev) {
4350
+ galleryModalPrev.click();
4351
+ } else if (e.key === 'ArrowRight' && galleryModalNext) {
4352
+ galleryModalNext.click();
4353
+ }
4354
+ });
4355
+ }
4356
+ });
4357
+ })();
4358
+
4359
+ </script>
4360
+
4361
+
4362
+
4363
+