@o2vend/theme-cli 1.0.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +425 -0
- package/assets/Logo_o2vend.png +0 -0
- package/assets/favicon.png +0 -0
- package/assets/logo-white.png +0 -0
- package/bin/o2vend +42 -0
- package/config/widget-map.json +50 -0
- package/lib/commands/check.js +201 -0
- package/lib/commands/generate.js +33 -0
- package/lib/commands/init.js +214 -0
- package/lib/commands/optimize.js +216 -0
- package/lib/commands/package.js +208 -0
- package/lib/commands/serve.js +105 -0
- package/lib/commands/validate.js +191 -0
- package/lib/lib/api-client.js +357 -0
- package/lib/lib/dev-server.js +2618 -0
- package/lib/lib/file-watcher.js +80 -0
- package/lib/lib/hot-reload.js +106 -0
- package/lib/lib/liquid-engine.js +822 -0
- package/lib/lib/liquid-filters.js +671 -0
- package/lib/lib/mock-api-server.js +989 -0
- package/lib/lib/mock-data.js +1468 -0
- package/lib/lib/widget-service.js +321 -0
- package/package.json +70 -0
- package/test-theme/README.md +27 -0
- package/test-theme/assets/async-sections.js +446 -0
- package/test-theme/assets/cart-drawer.js +463 -0
- package/test-theme/assets/cart-manager.js +223 -0
- package/test-theme/assets/checkout-price-handler.js +368 -0
- package/test-theme/assets/components.css +4629 -0
- package/test-theme/assets/delivery-zone.css +299 -0
- package/test-theme/assets/delivery-zone.js +396 -0
- package/test-theme/assets/logo.png +0 -0
- package/test-theme/assets/sections.css +48 -0
- package/test-theme/assets/theme.css +3500 -0
- package/test-theme/assets/theme.js +3745 -0
- package/test-theme/config/settings_data.json +292 -0
- package/test-theme/config/settings_schema.json +1050 -0
- package/test-theme/layout/theme.liquid +195 -0
- package/test-theme/locales/en.default.json +260 -0
- package/test-theme/sections/content-fallback.liquid +53 -0
- package/test-theme/sections/content.liquid +57 -0
- package/test-theme/sections/footer-fallback.liquid +328 -0
- package/test-theme/sections/footer.liquid +278 -0
- package/test-theme/sections/header-fallback.liquid +1805 -0
- package/test-theme/sections/header.liquid +1145 -0
- package/test-theme/sections/hero-fallback.liquid +212 -0
- package/test-theme/sections/hero.liquid +136 -0
- package/test-theme/snippets/account-sidebar.liquid +200 -0
- package/test-theme/snippets/add-to-cart-modal.liquid +484 -0
- package/test-theme/snippets/breadcrumbs.liquid +134 -0
- package/test-theme/snippets/cart-drawer.liquid +467 -0
- package/test-theme/snippets/delivery-zone-city-selector.liquid +79 -0
- package/test-theme/snippets/delivery-zone-modal.liquid +337 -0
- package/test-theme/snippets/delivery-zone-search.liquid +78 -0
- package/test-theme/snippets/icon.liquid +105 -0
- package/test-theme/snippets/login-modal.liquid +346 -0
- package/test-theme/snippets/mega-menu.liquid +812 -0
- package/test-theme/snippets/news-thumbnail.liquid +187 -0
- package/test-theme/snippets/pagination.liquid +120 -0
- package/test-theme/snippets/price.liquid +92 -0
- package/test-theme/snippets/product-card-related.liquid +78 -0
- package/test-theme/snippets/product-card-simple.liquid +41 -0
- package/test-theme/snippets/product-card.liquid +697 -0
- package/test-theme/snippets/rating.liquid +85 -0
- package/test-theme/snippets/skeleton-collection-grid.liquid +114 -0
- package/test-theme/snippets/skeleton-product-card.liquid +124 -0
- package/test-theme/snippets/skeleton-product-grid.liquid +34 -0
- package/test-theme/snippets/social-sharing.liquid +185 -0
- package/test-theme/templates/account/dashboard.liquid +401 -0
- package/test-theme/templates/account/loyalty-redemption.liquid +405 -0
- package/test-theme/templates/account/loyalty.liquid +588 -0
- package/test-theme/templates/account/order-detail.liquid +230 -0
- package/test-theme/templates/account/orders.liquid +349 -0
- package/test-theme/templates/account/profile.liquid +758 -0
- package/test-theme/templates/account/register.liquid +232 -0
- package/test-theme/templates/account/return-orders.liquid +348 -0
- package/test-theme/templates/account/store-credit.liquid +464 -0
- package/test-theme/templates/account/subscriptions.liquid +601 -0
- package/test-theme/templates/account/wishlist.liquid +419 -0
- package/test-theme/templates/address-book.liquid +1092 -0
- package/test-theme/templates/categories.liquid +452 -0
- package/test-theme/templates/checkout.liquid +4511 -0
- package/test-theme/templates/error.liquid +384 -0
- package/test-theme/templates/index.liquid +11 -0
- package/test-theme/templates/login.liquid +185 -0
- package/test-theme/templates/order-confirmation.liquid +720 -0
- package/test-theme/templates/page.liquid +297 -0
- package/test-theme/templates/product-detail.liquid +4363 -0
- package/test-theme/templates/products.liquid +518 -0
- package/test-theme/templates/search.liquid +922 -0
- package/test-theme/theme.json.example +19 -0
- package/test-theme/widgets/brand-carousel.liquid +676 -0
- package/test-theme/widgets/brand.liquid +245 -0
- package/test-theme/widgets/carousel.liquid +843 -0
- package/test-theme/widgets/category-list-carousel.liquid +656 -0
- package/test-theme/widgets/category-list.liquid +340 -0
- package/test-theme/widgets/category.liquid +475 -0
- package/test-theme/widgets/discount-time.liquid +176 -0
- package/test-theme/widgets/footer-menu.liquid +695 -0
- package/test-theme/widgets/footer.liquid +179 -0
- package/test-theme/widgets/gallery.liquid +271 -0
- package/test-theme/widgets/header-menu.liquid +932 -0
- package/test-theme/widgets/header.liquid +159 -0
- package/test-theme/widgets/html.liquid +214 -0
- package/test-theme/widgets/news.liquid +217 -0
- package/test-theme/widgets/product-canvas.liquid +235 -0
- package/test-theme/widgets/product-carousel.liquid +502 -0
- package/test-theme/widgets/product.liquid +45 -0
- package/test-theme/widgets/recently-viewed.liquid +26 -0
- package/test-theme/widgets/shared/product-grid.liquid +339 -0
- package/test-theme/widgets/simple-product.liquid +42 -0
- package/test-theme/widgets/single-product.liquid +610 -0
- package/test-theme/widgets/spacebar-carousel.liquid +663 -0
- package/test-theme/widgets/spacebar.liquid +279 -0
- package/test-theme/widgets/splash.liquid +378 -0
- package/test-theme/widgets/testimonial-carousel.liquid +709 -0
|
@@ -0,0 +1,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
|
+
|