@o2vend/theme-cli 1.0.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/README.md +425 -0
  2. package/assets/Logo_o2vend.png +0 -0
  3. package/assets/favicon.png +0 -0
  4. package/assets/logo-white.png +0 -0
  5. package/bin/o2vend +42 -0
  6. package/config/widget-map.json +50 -0
  7. package/lib/commands/check.js +201 -0
  8. package/lib/commands/generate.js +33 -0
  9. package/lib/commands/init.js +214 -0
  10. package/lib/commands/optimize.js +216 -0
  11. package/lib/commands/package.js +208 -0
  12. package/lib/commands/serve.js +105 -0
  13. package/lib/commands/validate.js +191 -0
  14. package/lib/lib/api-client.js +357 -0
  15. package/lib/lib/dev-server.js +2618 -0
  16. package/lib/lib/file-watcher.js +80 -0
  17. package/lib/lib/hot-reload.js +106 -0
  18. package/lib/lib/liquid-engine.js +822 -0
  19. package/lib/lib/liquid-filters.js +671 -0
  20. package/lib/lib/mock-api-server.js +989 -0
  21. package/lib/lib/mock-data.js +1468 -0
  22. package/lib/lib/widget-service.js +321 -0
  23. package/package.json +70 -0
  24. package/test-theme/README.md +27 -0
  25. package/test-theme/assets/async-sections.js +446 -0
  26. package/test-theme/assets/cart-drawer.js +463 -0
  27. package/test-theme/assets/cart-manager.js +223 -0
  28. package/test-theme/assets/checkout-price-handler.js +368 -0
  29. package/test-theme/assets/components.css +4629 -0
  30. package/test-theme/assets/delivery-zone.css +299 -0
  31. package/test-theme/assets/delivery-zone.js +396 -0
  32. package/test-theme/assets/logo.png +0 -0
  33. package/test-theme/assets/sections.css +48 -0
  34. package/test-theme/assets/theme.css +3500 -0
  35. package/test-theme/assets/theme.js +3745 -0
  36. package/test-theme/config/settings_data.json +292 -0
  37. package/test-theme/config/settings_schema.json +1050 -0
  38. package/test-theme/layout/theme.liquid +195 -0
  39. package/test-theme/locales/en.default.json +260 -0
  40. package/test-theme/sections/content-fallback.liquid +53 -0
  41. package/test-theme/sections/content.liquid +57 -0
  42. package/test-theme/sections/footer-fallback.liquid +328 -0
  43. package/test-theme/sections/footer.liquid +278 -0
  44. package/test-theme/sections/header-fallback.liquid +1805 -0
  45. package/test-theme/sections/header.liquid +1145 -0
  46. package/test-theme/sections/hero-fallback.liquid +212 -0
  47. package/test-theme/sections/hero.liquid +136 -0
  48. package/test-theme/snippets/account-sidebar.liquid +200 -0
  49. package/test-theme/snippets/add-to-cart-modal.liquid +484 -0
  50. package/test-theme/snippets/breadcrumbs.liquid +134 -0
  51. package/test-theme/snippets/cart-drawer.liquid +467 -0
  52. package/test-theme/snippets/delivery-zone-city-selector.liquid +79 -0
  53. package/test-theme/snippets/delivery-zone-modal.liquid +337 -0
  54. package/test-theme/snippets/delivery-zone-search.liquid +78 -0
  55. package/test-theme/snippets/icon.liquid +105 -0
  56. package/test-theme/snippets/login-modal.liquid +346 -0
  57. package/test-theme/snippets/mega-menu.liquid +812 -0
  58. package/test-theme/snippets/news-thumbnail.liquid +187 -0
  59. package/test-theme/snippets/pagination.liquid +120 -0
  60. package/test-theme/snippets/price.liquid +92 -0
  61. package/test-theme/snippets/product-card-related.liquid +78 -0
  62. package/test-theme/snippets/product-card-simple.liquid +41 -0
  63. package/test-theme/snippets/product-card.liquid +697 -0
  64. package/test-theme/snippets/rating.liquid +85 -0
  65. package/test-theme/snippets/skeleton-collection-grid.liquid +114 -0
  66. package/test-theme/snippets/skeleton-product-card.liquid +124 -0
  67. package/test-theme/snippets/skeleton-product-grid.liquid +34 -0
  68. package/test-theme/snippets/social-sharing.liquid +185 -0
  69. package/test-theme/templates/account/dashboard.liquid +401 -0
  70. package/test-theme/templates/account/loyalty-redemption.liquid +405 -0
  71. package/test-theme/templates/account/loyalty.liquid +588 -0
  72. package/test-theme/templates/account/order-detail.liquid +230 -0
  73. package/test-theme/templates/account/orders.liquid +349 -0
  74. package/test-theme/templates/account/profile.liquid +758 -0
  75. package/test-theme/templates/account/register.liquid +232 -0
  76. package/test-theme/templates/account/return-orders.liquid +348 -0
  77. package/test-theme/templates/account/store-credit.liquid +464 -0
  78. package/test-theme/templates/account/subscriptions.liquid +601 -0
  79. package/test-theme/templates/account/wishlist.liquid +419 -0
  80. package/test-theme/templates/address-book.liquid +1092 -0
  81. package/test-theme/templates/categories.liquid +452 -0
  82. package/test-theme/templates/checkout.liquid +4511 -0
  83. package/test-theme/templates/error.liquid +384 -0
  84. package/test-theme/templates/index.liquid +11 -0
  85. package/test-theme/templates/login.liquid +185 -0
  86. package/test-theme/templates/order-confirmation.liquid +720 -0
  87. package/test-theme/templates/page.liquid +297 -0
  88. package/test-theme/templates/product-detail.liquid +4363 -0
  89. package/test-theme/templates/products.liquid +518 -0
  90. package/test-theme/templates/search.liquid +922 -0
  91. package/test-theme/theme.json.example +19 -0
  92. package/test-theme/widgets/brand-carousel.liquid +676 -0
  93. package/test-theme/widgets/brand.liquid +245 -0
  94. package/test-theme/widgets/carousel.liquid +843 -0
  95. package/test-theme/widgets/category-list-carousel.liquid +656 -0
  96. package/test-theme/widgets/category-list.liquid +340 -0
  97. package/test-theme/widgets/category.liquid +475 -0
  98. package/test-theme/widgets/discount-time.liquid +176 -0
  99. package/test-theme/widgets/footer-menu.liquid +695 -0
  100. package/test-theme/widgets/footer.liquid +179 -0
  101. package/test-theme/widgets/gallery.liquid +271 -0
  102. package/test-theme/widgets/header-menu.liquid +932 -0
  103. package/test-theme/widgets/header.liquid +159 -0
  104. package/test-theme/widgets/html.liquid +214 -0
  105. package/test-theme/widgets/news.liquid +217 -0
  106. package/test-theme/widgets/product-canvas.liquid +235 -0
  107. package/test-theme/widgets/product-carousel.liquid +502 -0
  108. package/test-theme/widgets/product.liquid +45 -0
  109. package/test-theme/widgets/recently-viewed.liquid +26 -0
  110. package/test-theme/widgets/shared/product-grid.liquid +339 -0
  111. package/test-theme/widgets/simple-product.liquid +42 -0
  112. package/test-theme/widgets/single-product.liquid +610 -0
  113. package/test-theme/widgets/spacebar-carousel.liquid +663 -0
  114. package/test-theme/widgets/spacebar.liquid +279 -0
  115. package/test-theme/widgets/splash.liquid +378 -0
  116. package/test-theme/widgets/testimonial-carousel.liquid +709 -0
@@ -0,0 +1,1092 @@
1
+ {% layout 'layout/theme' %}
2
+
3
+ <section class="account-page">
4
+ <div class="container">
5
+ <div class="account-header">
6
+ <h1 class="account-title">My Addresses</h1>
7
+ <p class="account-subtitle">
8
+ Manage your saved shipping and billing addresses
9
+ </p>
10
+ </div>
11
+
12
+ <div class="account-layout">
13
+
14
+ <!-- Sidebar -->
15
+ <aside class="account-sidebar">
16
+ {% render 'snippets/account-sidebar' %}
17
+ </aside>
18
+
19
+ <!-- Content -->
20
+ <div class="account-content">
21
+
22
+ <div class="account-section-header">
23
+ <h2 class="account-section-title">Saved Addresses</h2>
24
+
25
+ <button class="btn btn-primary btn-sm" id="add-address-btn">
26
+ + Add Address
27
+ </button>
28
+ </div>
29
+
30
+ <div class="addresses-list">
31
+ {% if addresses and addresses.size > 0 %}
32
+ {% for address in addresses %}
33
+ <div class="address-card">
34
+
35
+ <div class="address-card-header">
36
+ <div>
37
+ <h4 class="address-name">{{ address.contactName }}</h4>
38
+ <span class="address-type-badge">
39
+ {{ address.addressType }}
40
+ </span>
41
+ </div>
42
+
43
+ {% if address.isDefaultShipping or address.isDefaultBilling %}
44
+ <span class="default-badge">
45
+ {% if address.isDefaultShipping %}Default Shipping{% endif %}
46
+ {% if address.isDefaultBilling %}Default Billing{% endif %}
47
+ </span>
48
+ {% endif %}
49
+ </div>
50
+
51
+ <div class="address-card-body">
52
+ <p>{{ address.addressLine1 }}</p>
53
+
54
+ {% if address.addressLine2 %}
55
+ <p>{{ address.addressLine2 }}</p>
56
+ {% endif %}
57
+
58
+ <p>
59
+ {{ address.city }}
60
+ {% if address.districtName %}, {{ address.districtName }}{% endif %}
61
+ </p>
62
+
63
+ <p>{{ address.stateOrProvinceName }} – {{ address.zipCode }}</p>
64
+ <p>{{ address.countryName }}</p>
65
+
66
+ {% if address.phone %}
67
+ <p class="address-phone">📞 {{ address.phone }}</p>
68
+ {% endif %}
69
+ </div>
70
+
71
+ <div class="address-card-actions">
72
+ <button class="btn btn-outline btn-sm edit-address" data-address-id="{{ address.id }}">Edit</button>
73
+ <button class="btn btn-outline-danger btn-sm delete-address" data-address-id="{{ address.id }}">
74
+ <span class="btn-text">Delete</span>
75
+ <span class="btn-loading" style="display: none;">
76
+ <span class="btn-spinner"></span>
77
+ <span>Deleting...</span>
78
+ </span>
79
+ </button>
80
+ </div>
81
+
82
+ </div>
83
+ {% endfor %}
84
+ {% else %}
85
+ <div class="empty-state">
86
+ <div class="empty-icon">📍</div>
87
+ <h3>No Saved Addresses</h3>
88
+ <p>Add an address to make checkout faster.</p>
89
+ <button class="btn btn-primary" id="add-first-address-btn"> + Add Address</button>
90
+ </div>
91
+ {% endif %}
92
+ </div>
93
+
94
+ </div>
95
+ </div>
96
+ </div>
97
+ </section>
98
+
99
+ <style>
100
+ :root {
101
+ --color-primary: #2563eb;
102
+ --color-primary-hover: #1d4ed8;
103
+ --color-primary-light: #dbeafe;
104
+ --color-success: #10b981;
105
+ --color-success-light: #d1fae5;
106
+ --color-danger: #ef4444;
107
+ --color-danger-hover: #dc2626;
108
+ --color-danger-light: #fee2e2;
109
+ --color-text: #1f2937;
110
+ --color-text-light: #6b7280;
111
+ --color-background: #f9fafb;
112
+ --color-card-bg: #ffffff;
113
+ --color-border: #e5e7eb;
114
+ --color-border-light: #f3f4f6;
115
+ --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
116
+ --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
117
+ --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
118
+ --radius-md: 0.5rem;
119
+ --radius-lg: 0.75rem;
120
+ --spacing-xs: 0.5rem;
121
+ --spacing-sm: 1rem;
122
+ --spacing-md: 1.5rem;
123
+ --spacing-lg: 2rem;
124
+ --spacing-xl: 3rem;
125
+ }
126
+ /* Page */
127
+ .account-page {
128
+ padding: 3rem 1rem;
129
+ background: #f9fafb;
130
+ }
131
+
132
+ .container {
133
+ max-width: 1200px;
134
+ margin: 0 auto;
135
+ padding: 0 var(--spacing-sm);
136
+ }
137
+
138
+ /* Header */
139
+ .account-header {
140
+ text-align: center;
141
+ margin-bottom: 3rem;
142
+ }
143
+
144
+ .account-title {
145
+ font-size: 2.5rem;
146
+ font-weight: 700;
147
+ }
148
+
149
+ .account-subtitle {
150
+ color: #6b7280;
151
+ }
152
+
153
+ /* Layout */
154
+ .account-layout {
155
+ display: grid;
156
+ grid-template-columns: 280px 1fr;
157
+ gap: 2rem;
158
+ }
159
+
160
+ .account-sidebar,
161
+ .account-content {
162
+ background: #fff;
163
+ border-radius: 14px;
164
+ padding: 1.75rem;
165
+ border: 1px solid #e5e7eb;
166
+ }
167
+
168
+ /* Section header */
169
+ .account-section-header {
170
+ display: flex;
171
+ justify-content: space-between;
172
+ align-items: center;
173
+ margin-bottom: 2rem;
174
+ }
175
+
176
+ .account-section-title {
177
+ font-size: 1.6rem;
178
+ font-weight: 600;
179
+ }
180
+
181
+ /* Address Grid */
182
+ .addresses-list {
183
+ display: grid;
184
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
185
+ gap: 1.5rem;
186
+ }
187
+
188
+ /* Address Card */
189
+ .address-card {
190
+ background: #f9fafb;
191
+ border: 1px solid #e5e7eb;
192
+ border-radius: 14px;
193
+ padding: 1.5rem;
194
+ display: flex;
195
+ flex-direction: column;
196
+ transition: all .25s ease;
197
+ }
198
+
199
+ .address-card:hover {
200
+ background: #fff;
201
+ box-shadow: 0 10px 25px rgba(0,0,0,.06);
202
+ transform: translateY(-3px);
203
+ }
204
+
205
+ /* Card Header */
206
+ .address-card-header {
207
+ display: flex;
208
+ justify-content: space-between;
209
+ margin-bottom: .75rem;
210
+ }
211
+
212
+ .address-name {
213
+ font-size: 1.5rem;
214
+ font-weight: 600;
215
+ }
216
+
217
+ /* Badges */
218
+ .address-type-badge {
219
+ font-size: 1.1rem;
220
+ padding: 5px;
221
+ border-radius: 5px;
222
+ background:black;
223
+ color:white;
224
+ font-weight: 600;
225
+ }
226
+
227
+ .default-badge {
228
+ background: #dcfce7;
229
+ color: #166534;
230
+ font-size: .7rem;
231
+ padding: .3rem .6rem;
232
+ border-radius: 999px;
233
+ font-weight: 600;
234
+ }
235
+
236
+ /* Body */
237
+ .address-card-body p {
238
+ margin: .25rem 0;
239
+ color: #374151;
240
+ font-size: 1.5rem;
241
+ }
242
+
243
+ .address-phone {
244
+ margin-top: .5rem;
245
+ font-weight: 500;
246
+ }
247
+
248
+ /* Actions */
249
+ .address-card-actions {
250
+ display: flex;
251
+ gap: 1.75rem;
252
+ justify-content: flex-end;
253
+ }
254
+
255
+ /* Buttons */
256
+ .btn {
257
+ padding: 10px;
258
+ border-radius: 5px;
259
+ font-size: 1.5rem;;
260
+ cursor: pointer;
261
+ }
262
+
263
+ .btn-primary {
264
+
265
+ border: none;
266
+ }
267
+
268
+ .btn-outline {
269
+ background: transparent;
270
+ border: 1px solid #d1d5db;
271
+ }
272
+
273
+ .btn-outline-danger {
274
+ background: transparent;
275
+ border: 1px solid #fecaca;
276
+ color: #dc2626;
277
+ }
278
+
279
+ /* Empty State */
280
+ .empty-state {
281
+ grid-column: 1 / -1;
282
+ text-align: center;
283
+ padding: 4rem 2rem;
284
+ border: 2px dashed #e5e7eb;
285
+ border-radius: 14px;
286
+ }
287
+
288
+ .empty-icon {
289
+ font-size: 3rem;
290
+ margin-bottom: 1rem;
291
+ }
292
+
293
+ /* Mobile */
294
+ @media (max-width: 768px) {
295
+ .account-layout {
296
+ grid-template-columns: 1fr;
297
+ }
298
+
299
+ .account-section-header {
300
+ flex-direction: column;
301
+ align-items: flex-start;
302
+ gap: 1rem;
303
+ }
304
+
305
+ .addresses-list {
306
+ grid-template-columns: 1fr;
307
+ }
308
+ }
309
+
310
+ /* Address Modal Styles */
311
+ .address-modal {
312
+ position: fixed;
313
+ top: 0;
314
+ left: 0;
315
+ width: 100%;
316
+ height: 100%;
317
+ z-index: 1050;
318
+ display: none;
319
+ }
320
+
321
+ .address-modal.show {
322
+ display: block;
323
+ }
324
+
325
+ .address-modal-overlay {
326
+ position: absolute;
327
+ top: 0;
328
+ left: 0;
329
+ width: 100%;
330
+ height: 100%;
331
+ background: rgba(0, 0, 0, 0.5);
332
+ cursor: pointer;
333
+ }
334
+
335
+ .address-modal-content {
336
+ position: absolute;
337
+ top: 50%;
338
+ left: 50%;
339
+ transform: translate(-50%, -50%);
340
+ background: white;
341
+ border-radius: 14px;
342
+ padding: 2rem;
343
+ max-width: 600px;
344
+ width: 90%;
345
+ max-height: 90vh;
346
+ overflow-y: auto;
347
+ box-shadow: 0 20px 25px rgba(0, 0, 0, 0.15);
348
+ }
349
+
350
+ .address-modal-header {
351
+ display: flex;
352
+ justify-content: space-between;
353
+ align-items: center;
354
+ margin-bottom: 1.5rem;
355
+ border-bottom: 1px solid #d1d5db;
356
+ }
357
+
358
+ .address-modal-close {
359
+ background: none;
360
+ border: none;
361
+ font-size: 2rem;
362
+ cursor: pointer;
363
+ color: #6b7280;
364
+ line-height: 1;
365
+ padding: 0;
366
+ width: 32px;
367
+ height: 32px;
368
+ display: flex;
369
+ align-items: center;
370
+ justify-content: center;
371
+ border-radius: 6px;
372
+ transition: background 0.2s;
373
+ }
374
+
375
+ .address-modal-close:hover {
376
+ background: #f3f4f6;
377
+ }
378
+
379
+ .address-form {
380
+ display: flex;
381
+ flex-direction: column;
382
+ gap: 1rem;
383
+ }
384
+
385
+ .form-group {
386
+ display: flex;
387
+ flex-direction: column;
388
+ gap: 0.5rem;
389
+ }
390
+
391
+ .form-label {
392
+ font-size: 1.7rem;
393
+ font-weight: 500;
394
+ color: #374151;
395
+ }
396
+
397
+ .form-input {
398
+ padding: 0.75rem;
399
+ border: 1px solid #d1d5db;
400
+ border-radius: 8px;
401
+ font-size: 1.5rem;
402
+ transition: border-color 0.2s;
403
+ }
404
+
405
+ /* intl-tel-input styling for address book */
406
+ #address-phone {
407
+ padding-left: 3.5rem;
408
+ }
409
+
410
+ .iti {
411
+ width: 100%;
412
+ }
413
+
414
+ .iti__flag-container {
415
+ z-index: 1;
416
+ }
417
+
418
+ .iti__selected-flag {
419
+ padding: 0 0.75rem 0 0.5rem;
420
+ border-right: 1px solid #d1d5db;
421
+ }
422
+
423
+ .iti__selected-flag:hover {
424
+ background-color: #f9fafb;
425
+ }
426
+
427
+ .iti__country-list {
428
+ z-index: 1051;
429
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
430
+ border: 1px solid #d1d5db;
431
+ border-radius: 8px;
432
+ max-height: 200px;
433
+ overflow-y: auto;
434
+ }
435
+
436
+ .iti__country {
437
+ padding: 0.5rem 0.75rem;
438
+ }
439
+
440
+ .iti__country:hover,
441
+ .iti__country.iti__highlight {
442
+ background-color: #dbeafe;
443
+ }
444
+
445
+ .form-input:focus {
446
+ outline: none;
447
+ border-color: #2563eb;
448
+ box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
449
+ }
450
+
451
+ .form-row {
452
+ display: grid;
453
+ grid-template-columns: 1fr 1fr;
454
+ gap: 1rem;
455
+ }
456
+
457
+ .form-checkbox {
458
+ display: flex;
459
+ align-items: center;
460
+ gap: 0.5rem;
461
+ cursor: pointer;
462
+ }
463
+
464
+ .form-checkbox input[type="checkbox"] {
465
+ width: 18px;
466
+ height: 18px;
467
+ cursor: pointer;
468
+ }
469
+
470
+ .form-actions {
471
+ display: flex;
472
+ gap: 0.75rem;
473
+ justify-content: flex-end;
474
+ margin-top: 1rem;
475
+ }
476
+
477
+ /* Button loading states */
478
+ .btn.loading {
479
+ opacity: 0.7;
480
+ cursor: not-allowed;
481
+ pointer-events: none;
482
+ }
483
+
484
+ .btn-loading {
485
+ display: flex;
486
+ align-items: center;
487
+ gap: 0.5rem;
488
+ }
489
+
490
+ .btn-spinner {
491
+ width: 14px;
492
+ height: 14px;
493
+ border: 2px solid currentColor;
494
+ border-top-color: transparent;
495
+ border-radius: 50%;
496
+ animation: spin 0.6s linear infinite;
497
+ }
498
+
499
+ @keyframes spin {
500
+ to {
501
+ transform: rotate(360deg);
502
+ }
503
+ }
504
+
505
+ .btn-sm .btn-spinner {
506
+ width: 12px;
507
+ height: 12px;
508
+ border-width: 1.5px;
509
+ }
510
+
511
+ @media (max-width: 768px) {
512
+ .form-row {
513
+ grid-template-columns: 1fr;
514
+ }
515
+
516
+ .address-modal-content {
517
+ width: 95%;
518
+ padding: 1.5rem;
519
+ }
520
+ }
521
+
522
+ </style>
523
+
524
+ <!-- Address Form Modal -->
525
+ <div class="address-modal" id="address-modal">
526
+ <div class="address-modal-overlay" data-address-modal-close></div>
527
+ <div class="address-modal-content">
528
+ <div class="address-modal-header">
529
+ <h3 id="address-modal-title">Add New Address</h3>
530
+ <button class="address-modal-close" data-address-modal-close aria-label="Close">×</button>
531
+ </div>
532
+ <form class="address-form" id="address-form" action="/webstoreapi/addresses" method="post">
533
+ <input type="hidden" name="addressId" id="address-id">
534
+
535
+ <div class="form-group">
536
+ <label for="address-first-name" class="form-label">First Name</label>
537
+ <input type="text" id="address-first-name" name="firstName" class="form-input" required>
538
+ </div>
539
+
540
+ <div class="form-group">
541
+ <label for="address-last-name" class="form-label">Last Name</label>
542
+ <input type="text" id="address-last-name" name="lastName" class="form-input">
543
+ </div>
544
+
545
+ <div class="form-group">
546
+ <label for="address-address" class="form-label">Address Line1</label>
547
+ <input type="text" id="address-address" name="address" class="form-input" required>
548
+ </div>
549
+
550
+ <div class="form-group">
551
+ <label for="address-address2" class="form-label">Address Line 2</label>
552
+ <input type="text" id="address-address2" name="address2" class="form-input">
553
+ </div>
554
+
555
+ <div class="form-group">
556
+ <label for="address-city" class="form-label">City</label>
557
+ <input type="text" id="address-city" name="city" class="form-input" required>
558
+ </div>
559
+
560
+ <div class="form-row">
561
+ <div class="form-group">
562
+ <label for="address-state" class="form-label">State/Province</label>
563
+ <select id="address-state" name="state" class="form-input" required>
564
+ <option value="">Select a state</option>
565
+ </select>
566
+ </div>
567
+
568
+ <div class="form-group">
569
+ <label for="address-zip" class="form-label">ZIP/Postal Code</label>
570
+ <input type="text" id="address-zip" name="zip" class="form-input" required>
571
+ </div>
572
+ </div>
573
+
574
+ <div class="form-group">
575
+ <label for="address-country" class="form-label">Country</label>
576
+ <select id="address-country" name="country" class="form-input" required>
577
+ <option value="">Select a country</option>
578
+ {% if countries and countries.size > 0 %}
579
+ {% for country in countries %}
580
+ <option value="{{ country.id }}" data-country-code2="{{ country.code2 | default: country.code }}">{{ country.name }}</option>
581
+ {% endfor %}
582
+ {% else %}
583
+ <option value="US">United States</option>
584
+ <option value="CA">Canada</option>
585
+ <option value="GB">United Kingdom</option>
586
+ <option value="AU">Australia</option>
587
+ <option value="IN">India</option>
588
+ {% endif %}
589
+ </select>
590
+ </div>
591
+
592
+ <div class="form-group">
593
+ <label for="address-phone" class="form-label">Phone</label>
594
+ <input type="tel" id="address-phone" name="phone" class="form-input">
595
+ </div>
596
+
597
+ <div class="form-group">
598
+ <label class="form-checkbox">
599
+ <input type="checkbox" name="isDefault" id="address-is-default">
600
+ <span>Set as default address</span>
601
+ </label>
602
+ </div>
603
+
604
+ <div class="form-actions">
605
+ <button type="button" class="btn btn-outline" data-address-modal-close>Cancel</button>
606
+ <button type="submit" class="btn btn-primary" id="save-address-btn">
607
+ <span class="btn-text">Save Address</span>
608
+ <span class="btn-loading" style="display: none;">
609
+ <span class="btn-spinner"></span>
610
+ <span>Saving...</span>
611
+ </span>
612
+ </button>
613
+ </div>
614
+ </form>
615
+ </div>
616
+ </div>
617
+
618
+ <script>
619
+ // Button loading utility function
620
+ function setButtonLoading(button, loading, loadingText = null) {
621
+ if (!button) return;
622
+
623
+ const btnText = button.querySelector('.btn-text');
624
+ const btnLoading = button.querySelector('.btn-loading');
625
+
626
+ if (loading) {
627
+ button.disabled = true;
628
+ button.classList.add('loading');
629
+ if (btnText) btnText.style.display = 'none';
630
+ if (btnLoading) {
631
+ btnLoading.style.display = 'flex';
632
+ if (loadingText && btnLoading.querySelector('span:last-child')) {
633
+ btnLoading.querySelector('span:last-child').textContent = loadingText;
634
+ }
635
+ } else if (loadingText) {
636
+ button.textContent = loadingText;
637
+ }
638
+ } else {
639
+ button.disabled = false;
640
+ button.classList.remove('loading');
641
+ if (btnText) btnText.style.display = 'inline';
642
+ if (btnLoading) btnLoading.style.display = 'none';
643
+ }
644
+ }
645
+
646
+ document.addEventListener('DOMContentLoaded', function() {
647
+ const modal = document.getElementById('address-modal');
648
+ const openButtons = document.querySelectorAll('#add-address-btn, #add-first-address-btn, .edit-address');
649
+ const closeButtons = document.querySelectorAll('[data-address-modal-close]');
650
+ const form = document.getElementById('address-form');
651
+ const addresses = {% if addresses %}{{ addresses | json }}{% else %}[]{% endif %};
652
+ const COUNTRIES_DATA = {% if countries %}{{ countries | json }}{% else %}[]{% endif %};
653
+ let addressPhoneIti = null;
654
+
655
+ // Helper function to get states for a country
656
+ function getStatesForCountry(countryIdentifier) {
657
+ if (!countryIdentifier) return null;
658
+
659
+ // Try to parse as number (country ID)
660
+ const countryId = parseInt(countryIdentifier, 10);
661
+ const isNumericId = !isNaN(countryId);
662
+
663
+ if (!Array.isArray(COUNTRIES_DATA) || COUNTRIES_DATA.length === 0) {
664
+ return null;
665
+ }
666
+
667
+ // Find country by ID or code
668
+ const country = COUNTRIES_DATA.find(c => {
669
+ if (isNumericId) {
670
+ const cId = c.id || c.countryId;
671
+ if (cId === countryId || String(cId) === String(countryId)) {
672
+ return true;
673
+ }
674
+ }
675
+
676
+ const cCode2 = c.code2 || '';
677
+ const cCode = c.code || '';
678
+ const cCountryCode = c.countryCode || '';
679
+
680
+ return cCode2 === countryIdentifier ||
681
+ cCode === countryIdentifier ||
682
+ cCountryCode === countryIdentifier ||
683
+ String(c.id) === String(countryIdentifier);
684
+ });
685
+
686
+ if (country) {
687
+ if (country.statesOrProvinces && Array.isArray(country.statesOrProvinces)) {
688
+ return country.statesOrProvinces;
689
+ }
690
+ if (country.states && Array.isArray(country.states)) {
691
+ return country.states;
692
+ }
693
+ }
694
+
695
+ return null;
696
+ }
697
+
698
+ // Populate states dropdown based on selected country
699
+ function populateStates(countrySelect, stateSelect, selectedStateId = null, selectedStateName = null) {
700
+ if (!stateSelect || !countrySelect) {
701
+ return;
702
+ }
703
+
704
+ const countryIdentifier = countrySelect.value;
705
+
706
+ // Clear existing options
707
+ stateSelect.innerHTML = '<option value="">Select a state</option>';
708
+
709
+ if (!countryIdentifier) {
710
+ return;
711
+ }
712
+
713
+ // Get states for the selected country
714
+ const countryStates = getStatesForCountry(countryIdentifier);
715
+
716
+ if (!countryStates || !Array.isArray(countryStates) || countryStates.length === 0) {
717
+ // If no states found, convert to text input
718
+ const formGroup = stateSelect.closest('.form-group');
719
+ if (formGroup) {
720
+ let textInput = document.getElementById('address-state-text');
721
+ if (!textInput) {
722
+ textInput = document.createElement('input');
723
+ textInput.type = 'text';
724
+ textInput.id = 'address-state-text';
725
+ textInput.name = 'state';
726
+ textInput.className = 'form-input';
727
+ textInput.required = true;
728
+ textInput.placeholder = 'Enter state/province';
729
+ formGroup.appendChild(textInput);
730
+ }
731
+ stateSelect.style.display = 'none';
732
+ stateSelect.removeAttribute('required');
733
+ textInput.style.display = 'block';
734
+ textInput.required = true;
735
+ if (selectedStateName) {
736
+ textInput.value = selectedStateName;
737
+ } else if (selectedStateId) {
738
+ textInput.value = selectedStateId;
739
+ }
740
+ }
741
+ return;
742
+ }
743
+
744
+ // Show select and hide text input if it exists
745
+ const textInput = document.getElementById('address-state-text');
746
+ if (textInput) {
747
+ textInput.style.display = 'none';
748
+ textInput.removeAttribute('required');
749
+ }
750
+ stateSelect.style.display = 'block';
751
+ stateSelect.required = true;
752
+
753
+ // Populate the dropdown
754
+ countryStates.forEach(state => {
755
+ const option = document.createElement('option');
756
+ const stateId = state.id || state.stateOrProvinceId || state.stateId;
757
+ const stateName = state.name || state.label || '';
758
+
759
+ // Use stateId as the value (required by API)
760
+ option.value = stateId ? String(stateId) : '';
761
+ option.textContent = stateName;
762
+ if (stateId) {
763
+ option.dataset.stateId = String(stateId);
764
+ }
765
+
766
+ // Check if this should be selected
767
+ if (selectedStateId) {
768
+ const selectedStateStr = String(selectedStateId).toLowerCase();
769
+ const stateIdStr = stateId ? String(stateId).toLowerCase() : '';
770
+
771
+ if (stateIdStr && stateIdStr === selectedStateStr) {
772
+ option.selected = true;
773
+ }
774
+ }
775
+
776
+ // Also check by name if provided
777
+ if (!option.selected && selectedStateName) {
778
+ const selectedStateNameStr = String(selectedStateName).toLowerCase();
779
+ const stateNameStr = String(stateName).toLowerCase();
780
+
781
+ if (stateNameStr === selectedStateNameStr || stateNameStr.includes(selectedStateNameStr)) {
782
+ option.selected = true;
783
+ }
784
+ }
785
+
786
+ stateSelect.appendChild(option);
787
+ });
788
+ }
789
+
790
+ // Initialize intl-tel-input for address phone
791
+ function initializeAddressPhoneInput() {
792
+ const phoneInput = document.getElementById('address-phone');
793
+ if (phoneInput && typeof intlTelInput !== 'undefined') {
794
+ // Destroy existing instance if any
795
+ if (addressPhoneIti) {
796
+ addressPhoneIti.destroy();
797
+ addressPhoneIti = null;
798
+ }
799
+
800
+ // Get country code from country select
801
+ const countrySelect = document.getElementById('address-country');
802
+ let initialCountry = 'auto';
803
+
804
+ if (countrySelect && countrySelect.value) {
805
+ const countryCode = countrySelect.options[countrySelect.selectedIndex]?.getAttribute('data-country-code2') ||
806
+ countrySelect.value.toLowerCase();
807
+ initialCountry = countryCode.toLowerCase();
808
+ }
809
+
810
+ // Initialize intl-tel-input
811
+ addressPhoneIti = intlTelInput(phoneInput, {
812
+ utilsScript: 'https://cdn.jsdelivr.net/npm/intl-tel-input@23.0.0/build/js/utils.js',
813
+ initialCountry: initialCountry,
814
+ preferredCountries: ['us', 'gb', 'ca', 'au', 'in'],
815
+ separateDialCode: true,
816
+ nationalMode: false
817
+ });
818
+
819
+ // Update country when address country changes
820
+ if (countrySelect) {
821
+ countrySelect.addEventListener('change', function() {
822
+ const countryCode = this.options[this.selectedIndex]?.getAttribute('data-country-code2') ||
823
+ this.value.toLowerCase();
824
+ if (addressPhoneIti && countryCode) {
825
+ addressPhoneIti.setCountry(countryCode.toLowerCase());
826
+ }
827
+
828
+ // Populate states when country changes
829
+ const stateSelect = document.getElementById('address-state');
830
+ if (stateSelect) {
831
+ populateStates(countrySelect, stateSelect);
832
+ }
833
+ });
834
+ }
835
+ }
836
+ }
837
+
838
+ function openModal(addressId = null) {
839
+ const countrySelect = document.getElementById('address-country');
840
+ const stateSelect = document.getElementById('address-state');
841
+
842
+ if (addressId) {
843
+ // Load address data for editing
844
+ const address = addresses.find(a => a.id == addressId);
845
+ if (address) {
846
+ document.getElementById('address-modal-title').textContent = 'Edit Address';
847
+ document.getElementById('address-id').value = addressId;
848
+ document.getElementById('address-first-name').value = address.contactName || '';
849
+ document.getElementById('address-last-name').value = address.lastName || '';
850
+ document.getElementById('address-address').value = address.addressLine1 || '';
851
+ document.getElementById('address-address2').value = address.addressLine2 || '';
852
+ document.getElementById('address-city').value = address.city || '';
853
+ document.getElementById('address-zip').value = address.zipCode || '';
854
+
855
+ // Set country - try to find matching country by ID or code
856
+ let selectedCountryId = address.countryId || '';
857
+ if (!selectedCountryId && address.countryCode && Array.isArray(COUNTRIES_DATA)) {
858
+ // Find country by code if we don't have countryId
859
+ const country = COUNTRIES_DATA.find(c =>
860
+ c.code2 === address.countryCode ||
861
+ c.code === address.countryCode ||
862
+ c.countryCode === address.countryCode
863
+ );
864
+ if (country) {
865
+ selectedCountryId = country.id;
866
+ }
867
+ }
868
+
869
+ if (countrySelect && selectedCountryId) {
870
+ countrySelect.value = String(selectedCountryId);
871
+ }
872
+
873
+ document.getElementById('address-phone').value = address.phone || '';
874
+ document.getElementById('address-is-default').checked = address.isDefaultShipping || address.isDefaultBilling || false;
875
+
876
+ // Update form action for PUT request
877
+ form.action = `/webstoreapi/addresses/${addressId}`;
878
+ form.setAttribute('data-method', 'put');
879
+
880
+ // Populate states after country is set (with a small delay to ensure DOM is updated)
881
+ if (countrySelect && stateSelect && selectedCountryId) {
882
+ const stateOrProvinceId = address.stateOrProvinceId || null;
883
+ const stateOrProvinceName = address.stateOrProvinceName || null;
884
+ setTimeout(() => {
885
+ populateStates(countrySelect, stateSelect, stateOrProvinceId, stateOrProvinceName);
886
+ }, 50);
887
+ }
888
+ }
889
+ } else {
890
+ document.getElementById('address-modal-title').textContent = 'Add New Address';
891
+ document.getElementById('address-id').value = '';
892
+ form.reset();
893
+ form.action = '/webstoreapi/addresses';
894
+ form.removeAttribute('data-method');
895
+
896
+ // Reset state dropdown
897
+ if (stateSelect) {
898
+ stateSelect.innerHTML = '<option value="">Select a state</option>';
899
+ }
900
+ }
901
+ modal.classList.add('show');
902
+ document.body.style.overflow = 'hidden';
903
+
904
+ // Initialize phone input after modal is shown
905
+ setTimeout(function() {
906
+ initializeAddressPhoneInput();
907
+ // Set phone number if editing (after initialization completes)
908
+ if (addressId) {
909
+ setTimeout(function() {
910
+ const address = addresses.find(a => a.id == addressId);
911
+ if (address && address.phone && addressPhoneIti) {
912
+ addressPhoneIti.setNumber(address.phone);
913
+ }
914
+ }, 150);
915
+ }
916
+ }, 100);
917
+ }
918
+
919
+ function closeModal() {
920
+ modal.classList.remove('show');
921
+ document.body.style.overflow = '';
922
+ form.reset();
923
+ // Destroy phone input instance when modal closes
924
+ if (addressPhoneIti) {
925
+ addressPhoneIti.destroy();
926
+ addressPhoneIti = null;
927
+ }
928
+ }
929
+
930
+ openButtons.forEach(btn => {
931
+ btn.addEventListener('click', function() {
932
+ const addressId = this.dataset.addressId || null;
933
+ openModal(addressId);
934
+ });
935
+ });
936
+
937
+ closeButtons.forEach(btn => {
938
+ btn.addEventListener('click', closeModal);
939
+ });
940
+
941
+ // Handle form submission
942
+ form.addEventListener('submit', async function(e) {
943
+ e.preventDefault();
944
+
945
+ const submitBtn = document.getElementById('save-address-btn');
946
+ setButtonLoading(submitBtn, true, 'Saving...');
947
+
948
+ const formData = new FormData(form);
949
+ const addressId = formData.get('addressId');
950
+ const method = form.getAttribute('data-method')?.toUpperCase() || 'POST';
951
+ const url = method === 'PUT' ? `/webstoreapi/addresses/${addressId}` : '/webstoreapi/addresses';
952
+
953
+ // Get phone number from intl-tel-input if available
954
+ let phoneNumber = formData.get('phone');
955
+ if (addressPhoneIti) {
956
+ const fullPhoneNumber = addressPhoneIti.getNumber();
957
+ if (fullPhoneNumber) {
958
+ // Remove leading + sign
959
+ phoneNumber = fullPhoneNumber.replace(/^\+/, '');
960
+ }
961
+ }
962
+
963
+ // Derive country/state identifiers and names for API
964
+ const countrySelectEl = document.getElementById('address-country');
965
+ const stateSelectEl = document.getElementById('address-state');
966
+ const stateTextInput = document.getElementById('address-state-text');
967
+
968
+ let countryCode = formData.get('country') || '';
969
+ let countryId = null;
970
+ let stateOrProvinceId = null;
971
+ let stateOrProvinceName = null;
972
+
973
+ if (countrySelectEl && countrySelectEl.value) {
974
+ const selectedOption = countrySelectEl.options[countrySelectEl.selectedIndex];
975
+ const parsedCountryId = parseInt(countrySelectEl.value, 10);
976
+ if (!Number.isNaN(parsedCountryId)) {
977
+ countryId = parsedCountryId;
978
+ }
979
+ const optionCode = selectedOption?.getAttribute('data-country-code2');
980
+ if (optionCode) {
981
+ countryCode = optionCode;
982
+ }
983
+ }
984
+
985
+ if (stateSelectEl && stateSelectEl.style.display !== 'none') {
986
+ const stateValue = stateSelectEl.value;
987
+ if (stateValue) {
988
+ const parsedStateId = parseInt(stateValue, 10);
989
+ if (!Number.isNaN(parsedStateId)) {
990
+ stateOrProvinceId = parsedStateId;
991
+ }
992
+ }
993
+ const selectedStateOption = stateSelectEl.options[stateSelectEl.selectedIndex];
994
+ if (selectedStateOption) {
995
+ stateOrProvinceName = selectedStateOption.textContent.trim();
996
+ }
997
+ } else if (stateTextInput && stateTextInput.style.display !== 'none') {
998
+ stateOrProvinceName = stateTextInput.value;
999
+ }
1000
+
1001
+ const data = {
1002
+ firstName: formData.get('firstName'),
1003
+ lastName: formData.get('lastName'),
1004
+ addressLine1: formData.get('address'),
1005
+ addressLine2: formData.get('address2'),
1006
+ city: formData.get('city'),
1007
+ stateOrProvinceId: stateOrProvinceId,
1008
+ stateOrProvinceName: stateOrProvinceName,
1009
+ zipCode: formData.get('zip'),
1010
+ countryId: countryId,
1011
+ countryCode: countryCode,
1012
+ phone: phoneNumber,
1013
+ isDefaultShipping: formData.get('isDefault') === 'on',
1014
+ isDefaultBilling: formData.get('isDefault') === 'on'
1015
+ };
1016
+
1017
+ try {
1018
+ const response = await fetch(url, {
1019
+ method: method,
1020
+ headers: {
1021
+ 'Content-Type': 'application/json',
1022
+ 'X-Requested-With': 'XMLHttpRequest'
1023
+ },
1024
+ body: JSON.stringify(data)
1025
+ });
1026
+
1027
+ if (response.ok) {
1028
+ const responseData = await response.json();
1029
+ const savedAddressId = responseData.data?.id || addressId;
1030
+
1031
+ // Handle setting default address if checkbox is checked
1032
+ const isDefault = formData.get('isDefault') === 'on';
1033
+ if (isDefault && savedAddressId) {
1034
+ try {
1035
+ // Set as default shipping address
1036
+ await fetch(`/webstoreapi/addresses/${savedAddressId}/set-default?addressType=Shipping`, {
1037
+ method: 'PUT',
1038
+ headers: {
1039
+ 'X-Requested-With': 'XMLHttpRequest'
1040
+ }
1041
+ });
1042
+ } catch (defaultError) {
1043
+ console.warn('Failed to set default address:', defaultError);
1044
+ }
1045
+ }
1046
+
1047
+ // Reload page to show updated addresses
1048
+ window.location.reload();
1049
+ } else {
1050
+ const error = await response.json();
1051
+ setButtonLoading(submitBtn, false);
1052
+ alert('Error saving address: ' + (error.error || error.message || 'Unknown error'));
1053
+ }
1054
+ } catch (error) {
1055
+ console.error('Error saving address:', error);
1056
+ setButtonLoading(submitBtn, false);
1057
+ alert('Error saving address. Please try again.');
1058
+ }
1059
+ });
1060
+
1061
+ // Handle delete
1062
+ document.querySelectorAll('.delete-address').forEach(btn => {
1063
+ btn.addEventListener('click', async function() {
1064
+ if (confirm('Are you sure you want to delete this address?')) {
1065
+ const addressId = this.dataset.addressId;
1066
+ setButtonLoading(this, true, 'Deleting...');
1067
+
1068
+ try {
1069
+ const response = await fetch(`/webstoreapi/addresses/${addressId}`, {
1070
+ method: 'DELETE',
1071
+ headers: {
1072
+ 'X-Requested-With': 'XMLHttpRequest'
1073
+ }
1074
+ });
1075
+
1076
+ if (response.ok) {
1077
+ window.location.reload();
1078
+ } else {
1079
+ const error = await response.json();
1080
+ setButtonLoading(this, false);
1081
+ alert('Error deleting address: ' + (error.error || error.message || 'Unknown error'));
1082
+ }
1083
+ } catch (error) {
1084
+ console.error('Error deleting address:', error);
1085
+ setButtonLoading(this, false);
1086
+ alert('Error deleting address. Please try again.');
1087
+ }
1088
+ }
1089
+ });
1090
+ });
1091
+ });
1092
+ </script>