@o2vend/theme-cli 1.0.37 → 1.0.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/lib/lib/dev-server.js +309 -40
  2. package/lib/lib/liquid-engine.js +3 -1
  3. package/lib/lib/mock-data.js +36 -124
  4. package/lib/lib/widget-service.js +12 -4
  5. package/package.json +1 -1
  6. package/test-theme/assets/async-sections.js +32 -24
  7. package/test-theme/assets/cart-drawer.js +20 -22
  8. package/test-theme/assets/cart-manager.js +1 -15
  9. package/test-theme/assets/checkout-price-handler.js +12 -11
  10. package/test-theme/assets/checkout.css +1415 -0
  11. package/test-theme/assets/checkout.js +3174 -0
  12. package/test-theme/assets/components.css +178 -29
  13. package/test-theme/assets/delivery-zone.js +1 -1
  14. package/test-theme/assets/product-detail.css +1050 -0
  15. package/test-theme/assets/product-detail.js +2940 -0
  16. package/test-theme/assets/theme.css +95 -120
  17. package/test-theme/assets/theme.js +781 -186
  18. package/test-theme/layout/theme.liquid +91 -17
  19. package/test-theme/sections/content.liquid +64 -57
  20. package/test-theme/sections/footer-fallback.liquid +57 -7
  21. package/test-theme/sections/footer.liquid +63 -12
  22. package/test-theme/sections/header-fallback.liquid +41 -41
  23. package/test-theme/sections/header.liquid +41 -51
  24. package/test-theme/sections/hero-fallback.liquid +1 -1
  25. package/test-theme/sections/hero.liquid +159 -136
  26. package/test-theme/snippets/account-sidebar.liquid +121 -29
  27. package/test-theme/snippets/add-to-cart-modal.liquid +258 -206
  28. package/test-theme/snippets/breadcrumbs.liquid +98 -11
  29. package/test-theme/snippets/cart-drawer.liquid +93 -0
  30. package/test-theme/snippets/delivery-zone-city-selector.liquid +101 -15
  31. package/test-theme/snippets/delivery-zone-modal.liquid +529 -84
  32. package/test-theme/snippets/delivery-zone-search.liquid +104 -18
  33. package/test-theme/snippets/login-modal.liquid +269 -82
  34. package/test-theme/snippets/mega-menu.liquid +130 -43
  35. package/test-theme/snippets/news-thumbnail.liquid +120 -28
  36. package/test-theme/snippets/pagination.liquid +1 -1
  37. package/test-theme/snippets/price.liquid +100 -9
  38. package/test-theme/snippets/product-card-related.liquid +22 -4
  39. package/test-theme/snippets/product-card-simple.liquid +521 -25
  40. package/test-theme/snippets/product-card.liquid +145 -232
  41. package/test-theme/snippets/rating.liquid +100 -9
  42. package/test-theme/snippets/skeleton-collection-grid.liquid +94 -8
  43. package/test-theme/snippets/skeleton-product-card.liquid +102 -16
  44. package/test-theme/snippets/skeleton-product-grid.liquid +87 -1
  45. package/test-theme/snippets/social-sharing.liquid +133 -32
  46. package/test-theme/templates/account/dashboard.liquid +30 -0
  47. package/test-theme/templates/account/loyalty-redemption.liquid +29 -28
  48. package/test-theme/templates/account/loyalty.liquid +45 -43
  49. package/test-theme/templates/account/order-detail.liquid +15 -8
  50. package/test-theme/templates/account/orders.liquid +189 -35
  51. package/test-theme/templates/account/profile.liquid +509 -114
  52. package/test-theme/templates/account/register.liquid +18 -8
  53. package/test-theme/templates/account/return-orders.liquid +31 -30
  54. package/test-theme/templates/account/store-credit.liquid +27 -26
  55. package/test-theme/templates/account/subscriptions.liquid +22 -5
  56. package/test-theme/templates/account/wishlist.liquid +88 -19
  57. package/test-theme/templates/address-book.liquid +166 -69
  58. package/test-theme/templates/categories.liquid +90 -30
  59. package/test-theme/templates/checkout.liquid +137 -3834
  60. package/test-theme/templates/error.liquid +23 -21
  61. package/test-theme/templates/index.liquid +29 -0
  62. package/test-theme/templates/login.liquid +33 -6
  63. package/test-theme/templates/order-confirmation.liquid +67 -9
  64. package/test-theme/templates/page.liquid +418 -206
  65. package/test-theme/templates/product-detail.liquid +124 -3878
  66. package/test-theme/templates/products.liquid +155 -30
  67. package/test-theme/templates/search.liquid +739 -225
  68. package/test-theme/widgets/brand-carousel.liquid +102 -82
  69. package/test-theme/widgets/brand.liquid +78 -50
  70. package/test-theme/widgets/carousel.liquid +253 -121
  71. package/test-theme/widgets/category-list-carousel.liquid +32 -8
  72. package/test-theme/widgets/category-list.liquid +21 -6
  73. package/test-theme/widgets/category.liquid +104 -37
  74. package/test-theme/widgets/discount-time.liquid +326 -119
  75. package/test-theme/widgets/footer-menu.liquid +115 -23
  76. package/test-theme/widgets/footer.liquid +118 -5
  77. package/test-theme/widgets/gallery.liquid +29 -5
  78. package/test-theme/widgets/header-menu.liquid +25 -13
  79. package/test-theme/widgets/header.liquid +64 -26
  80. package/test-theme/widgets/html.liquid +29 -6
  81. package/test-theme/widgets/news.liquid +6 -0
  82. package/test-theme/widgets/product-canvas.liquid +20 -12
  83. package/test-theme/widgets/product-carousel.liquid +118 -56
  84. package/test-theme/widgets/shared/product-grid.liquid +12 -0
  85. package/test-theme/widgets/single-product.liquid +688 -250
  86. package/test-theme/widgets/spacebar-carousel.liquid +39 -10
  87. package/test-theme/widgets/spacebar.liquid +77 -6
  88. package/test-theme/widgets/splash.liquid +40 -30
  89. package/test-theme/widgets/testimonial-carousel.liquid +111 -67
@@ -4,6 +4,52 @@
4
4
  Inspired by Shopify Horizon Theme
5
5
  {% endcomment %}
6
6
 
7
+ <style>
8
+ :root {
9
+ --color-primary: {{ settings.color_primary | default: '#000000' }};
10
+ --color-primary-hover: {{ settings.color_primary_dark | default: '#333333' }};
11
+ --color-primary-light: {{ settings.color_primary_light | default: '#666666' }};
12
+ --color-success: {{ settings.color_success | default: '#22c55e' }};
13
+ --color-success-light: {{ settings.color_success | default: '#22c55e' }};
14
+ --color-danger: {{ settings.color_error | default: '#ef4444' }};
15
+ --color-danger-light: {{ settings.color_error | default: '#ef4444' }};
16
+ --color-text: {{ settings.color_text | default: '#000000' }};
17
+ --color-text-light: {{ settings.color_text_muted | default: '#666666' }};
18
+ --color-background: {{ settings.color_background | default: '#ffffff' }};
19
+ --color-card-bg: {{ settings.color_surface | default: '#f5f5f5' }};
20
+ --color-border: {{ settings.color_border | default: '#cccccc' }};
21
+ --color-border-light: {{ settings.color_surface | default: '#f5f5f5' }};
22
+ --shadow-sm: var(--shadow-sm);
23
+ --shadow-md: var(--shadow-md);
24
+ --shadow-lg: var(--shadow-lg);
25
+ --radius-md: var(--radius-md);
26
+ --radius-lg: var(--radius-lg);
27
+ --spacing-xs: var(--space-3);
28
+ --spacing-sm: var(--space-4);
29
+ --spacing-md: var(--space-6);
30
+ --spacing-lg: var(--space-8);
31
+ --spacing-xl: var(--space-12);
32
+ }
33
+ </style>
34
+ <!-- Product Detail Styles -->
35
+ <link rel="stylesheet" href="{{ 'product-detail.css' | asset_url }}">
36
+ <!-- Hero Widgets (if any) -->
37
+ {% assign hero_widgets = widgets.hero %}
38
+ {% if hero_widgets and hero_widgets.size > 0 %}
39
+ <section class="theme-section theme-section--hero" data-section="hero">
40
+ {% for widget in hero_widgets %}
41
+ <div class="theme-widget-wrapper"
42
+ data-widget-id="{{ widget.id }}"
43
+ data-widget-type="{{ widget.type }}"
44
+ data-widget-position="{{ widget.Position | default: widget.position | default: forloop.index }}">
45
+ {% if widget and widget.template_path %}
46
+ {% render widget.template_path, widget: widget, settings: settings, shop: shop, is_hero_first: forloop.first %}
47
+ {% endif %}
48
+ </div>
49
+ {% endfor %}
50
+ </section>
51
+ {% endif %}
52
+
7
53
  <!-- Product Main Section -->
8
54
  {% hook 'product_before' %}
9
55
  <section class="product-main horizon-style">
@@ -19,8 +65,10 @@
19
65
  alt="{{ product.name | default: product.title }} - {{ forloop.index }}"
20
66
  class="gallery-main-image {% if forloop.first %}active{% endif %}"
21
67
  data-index="{{ forloop.index0 }}"
22
- id="mainProductImage"
68
+ {% if forloop.first %}id="mainProductImage"{% endif %}
23
69
  loading="{% if forloop.first %}eager{% else %}lazy{% endif %}"
70
+ decoding="async"
71
+ {% if forloop.first %}fetchpriority="high"{% endif %}
24
72
  >
25
73
  {% endfor %}
26
74
  {% elsif product.thumbnailImage %}
@@ -31,6 +79,8 @@
31
79
  class="gallery-main-image active"
32
80
  id="mainProductImage"
33
81
  loading="eager"
82
+ decoding="async"
83
+ fetchpriority="high"
34
84
  >
35
85
  {% else %}
36
86
  <div class="gallery-placeholder">
@@ -101,10 +151,39 @@
101
151
  </div>
102
152
  {% endif %}
103
153
  {% endif %}
154
+
155
+ <!-- SKU -->
156
+ {% comment %}Show SKU - Check settings with proper boolean handling{% endcomment %}
157
+ {% liquid
158
+ assign sku_val = settings.show_sku
159
+ if sku_val == blank or sku_val == null
160
+ assign sku_val = false
161
+ endif
162
+ assign show_sku = false
163
+ if sku_val == true
164
+ assign show_sku = true
165
+ endif
166
+ if sku_val == 'true'
167
+ assign show_sku = true
168
+ endif
169
+ if sku_val == 1
170
+ assign show_sku = true
171
+ endif
172
+ %}
173
+
104
174
 
105
175
  <!-- Product Title -->
106
176
  {% hook 'product_title_before' %}
107
177
  <h1 class="product-title">{{ product.name | default: product.title }}</h1>
178
+ {% if show_sku %}
179
+ {% assign product_sku = product.sku | default: product.skuCode | default: product.SKU %}
180
+ {% if product_sku and product_sku != blank %}
181
+ <div class="product-sku">
182
+ <span class="product-sku__label">SKU:</span>
183
+ <span class="product-sku__value" id="productSku">{{ product_sku }}</span>
184
+ </div>
185
+ {% endif %}
186
+ {% endif %}
108
187
  {% hook 'product_title_after' %}
109
188
 
110
189
  {% if product.shortDescription and product.shortDescription != blank %}
@@ -114,16 +193,22 @@
114
193
  {% endif %}
115
194
 
116
195
  <!-- Price -->
196
+ {% liquid
197
+ assign show_call_for_pricing = product.showCallForPricing | default: false
198
+ %}
117
199
  {% hook 'product_price_before' %}
118
200
  <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 }}
201
+ {% if show_call_for_pricing %}
202
+ <span class="price-current" id="productPrice">Call for pricing</span>
203
+ {% else %}
204
+ <span class="price-current" id="productPrice">
205
+ {{ product.prices.price | money_with_settings: shop.settings }}
126
206
  </span>
207
+ {% if product.prices.mrp and product.prices.mrp > product.prices.price %}
208
+ <span class="price-compare">
209
+ {{ product.prices.mrp | money_with_settings: shop.settings }}
210
+ </span>
211
+ {% endif %}
127
212
  {% endif %}
128
213
  </div>
129
214
  {% hook 'product_price_after' %}
@@ -257,7 +342,7 @@
257
342
  <div class="product-option quantity-option">
258
343
  <label class="option-label" for="quantity">Quantity</label>
259
344
  <div class="quantity-wrapper">
260
- <button type="button" class="quantity-btn quantity-decrease" aria-label="Decrease quantity">−</button>
345
+ <button type="button" class="quantity-btn quantity-decrease" aria-label="Decrease quantity">&minus;</button>
261
346
  <input
262
347
  type="number"
263
348
  id="quantity"
@@ -323,7 +408,7 @@
323
408
  <section class="product-attributes-section">
324
409
  <div class="description-container">
325
410
 
326
- {% if hasAttributes %}
411
+ {% if hasAttributes or hasDescription %}
327
412
  <!-- Group attributes by attributeGroupName -->
328
413
  {% assign availableAttributegroups = "" %}
329
414
 
@@ -397,12 +482,6 @@
397
482
  {% for inner_attr in product.attributes %}
398
483
  {% if inner_attr.attributeGroupName == attr.attributeGroupName %}
399
484
  <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
485
  <div class="attribute-info">
407
486
  <div class="attribute-name">{{ inner_attr.name }}</div>
408
487
  <div class="attribute-value-text">{{ inner_attr.value }}</div>
@@ -416,17 +495,6 @@
416
495
  {% endfor %}
417
496
  </div>
418
497
  </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
498
  {% endif %}
431
499
  </div>
432
500
  </section>
@@ -454,6 +522,23 @@
454
522
  {% endif %}
455
523
  {% hook 'product_related_after' %}
456
524
 
525
+ <!-- Content Widgets (if any) -->
526
+ {% assign content_widgets = widgets.content %}
527
+ {% if content_widgets and content_widgets.size > 0 %}
528
+ <section class="theme-section theme-section--content" data-section="content">
529
+ {% for widget in content_widgets %}
530
+ <div class="theme-widget-wrapper"
531
+ data-widget-id="{{ widget.id }}"
532
+ data-widget-type="{{ widget.type }}"
533
+ data-widget-position="{{ widget.Position | default: widget.position | default: forloop.index }}">
534
+ {% if widget and widget.template_path %}
535
+ {% render widget.template_path, widget: widget, settings: settings, shop: shop %}
536
+ {% endif %}
537
+ </div>
538
+ {% endfor %}
539
+ </section>
540
+ {% endif %}
541
+
457
542
  <!-- Full Screen Image Gallery Modal -->
458
543
  <div class="gallery-modal" id="galleryModal">
459
544
  <div class="gallery-modal-overlay"></div>
@@ -486,6 +571,7 @@
486
571
  "name": {{ product.name | default: product.title | json }},
487
572
  "price": {{ product.prices.price }},
488
573
  "mrp": {{ product.prices.mrp | default: 0 }},
574
+ "showCallForPricing": {{ product.showCallForPricing | default: false | json }},
489
575
  "inStock": {{ product.inStock | default: product.available | default: true }},
490
576
  "available": {{ product.available | default: product.inStock | default: true }},
491
577
  "stockQuantity": {{ product.stockQuantity | default: 0 }},
@@ -506,3858 +592,18 @@
506
592
  }
507
593
  </script>
508
594
 
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
595
 
1557
596
  <!-- Product Page JavaScript -->
1558
597
  <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
-
598
+ // Liquid-injected config (minimal inline block for server data)
599
+ const PRODUCT_DATA = {% if product %}{{ product | json }}{% else %}null{% endif %};
600
+ const PRODUCT_VARIANTS = {{ product.variations | default: product.variants | json }};
601
+ const PRODUCT_ATTRIBUTES = {{ product.attributes | json }};
602
+ const SHOP_CURRENCY_SYMBOL = {% if shop.settings.currencySymbol %}{{ shop.settings.currencySymbol | json }}{% else %}''{% endif %};
603
+ </script>
604
+ <script src="{{ 'product-detail.js' | asset_url }}" defer></script>
605
+ <script>
606
+ // Backward compatibility: Keep window.productVariants and window.productAttributes
607
+ window.productVariants = {{ product.variations | default: product.variants | json }};
608
+ window.productAttributes = {{ product.attributes | json }};
4359
609
  </script>
4360
-
4361
-
4362
-
4363
-