@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,475 @@
1
+ {% liquid
2
+ assign widget_settings = widget.settings
3
+ assign widget_data = widget.data
4
+
5
+ comment
6
+ Category data is enriched by WidgetService using getCategoryById API
7
+ Based on widget.content.CategoryId
8
+ endcomment
9
+ assign category = widget_data.category
10
+
11
+ comment
12
+ Dynamic Widget: Category comes from widget_data.category (enriched by WidgetService)
13
+ Settings come from widget_settings
14
+ endcomment
15
+ assign background_color = widget_settings.backgroundColor
16
+ assign text_color = widget_settings.textColor | default: '#ffffff'
17
+ assign show_container = widget_settings.showContainer
18
+ assign show_bottom_margin = widget_settings.showWidgetBottomMargin
19
+
20
+
21
+ assign category_slug = category.slug | default: category.Slug | default: category.handle
22
+ assign category_id = category.id | default: category.Id | default: category.categoryId
23
+ assign category_url = category.url | default: category.Url | default: category.link | default: category.Link
24
+
25
+ if category_url == blank
26
+ if category_slug and category_slug != blank
27
+ assign category_url = category_slug
28
+ else
29
+ assign category_url = '#'
30
+ endif
31
+ endif
32
+
33
+ assign category_image = nil
34
+ if category.bannerImage and category.bannerImage.url
35
+ assign category_image = category.bannerImage.url
36
+ elsif category.BannerImage and category.BannerImage.Url
37
+ assign category_image = category.BannerImage.Url
38
+ elsif category.BannerImage and category.BannerImage.url
39
+ assign category_image = category.BannerImage.url
40
+ elsif category.bannerImage and category.bannerImage != blank
41
+ assign category_image = category.bannerImage
42
+ elsif category.BannerImage and category.BannerImage != blank
43
+ assign category_image = category.BannerImage
44
+ elsif category.thumbnailImage and category.thumbnailImage.url
45
+ assign category_image = category.thumbnailImage.url
46
+ elsif category.ThumbnailImage and category.ThumbnailImage.Url
47
+ assign category_image = category.ThumbnailImage.Url
48
+ elsif category.thumbnailImage and category.thumbnailImage != blank
49
+ assign category_image = category.thumbnailImage
50
+ elsif category.image
51
+ assign category_image = category.image
52
+ elsif category.Image
53
+ assign category_image = category.Image
54
+ endif
55
+
56
+ assign category_name = category.name | default: category.Name | default: category.title
57
+ assign category_description = category.description | default: category.Description
58
+ %}
59
+
60
+ {% if category %}
61
+ <section class="widget widget-category-banner" data-widget-id="{{ widget.id }}">
62
+ <a href="{{ category_url }}" class="category-banner{% unless category_image %} category-banner--no-image{% endunless %}">
63
+
64
+ {% if category_image %}
65
+ <!-- With Image: Full-width banner with overlay -->
66
+ <div class="category-banner__bg">
67
+ <img src="{{ category_image }}"
68
+ alt="{{ category_name }}"
69
+ width="1400"
70
+ height="400"
71
+ loading="lazy"
72
+ onerror="this.style.display='none';">
73
+ </div>
74
+ <div class="category-banner__overlay"></div>
75
+ <div class="category-banner__content category-banner__content--overlay">
76
+ <h2 class="category-banner__title">{{ category_name }}</h2>
77
+ {% if category_description and category_description != blank %}
78
+ <p class="category-banner__description">{{ category_description | truncatewords: 30 }}</p>
79
+ {% endif %}
80
+ <span class="category-banner__cta">
81
+ Shop Now
82
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
83
+ <line x1="5" y1="12" x2="19" y2="12"></line>
84
+ <polyline points="12 5 19 12 12 19"></polyline>
85
+ </svg>
86
+ </span>
87
+ </div>
88
+ {% else %}
89
+ <!-- No Image: Clean card-style layout -->
90
+ <div class="category-banner__card">
91
+ <div class="category-banner__icon">
92
+ <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
93
+ <path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"></path>
94
+ <line x1="3" y1="6" x2="21" y2="6"></line>
95
+ <path d="M16 10a4 4 0 0 1-8 0"></path>
96
+ </svg>
97
+ </div>
98
+ <div class="category-banner__text">
99
+ <h2 class="category-banner__title">{{ category_name }}</h2>
100
+ {% if category_description and category_description != blank %}
101
+ <p class="category-banner__description">{{ category_description | truncatewords: 40 }}</p>
102
+ {% endif %}
103
+ </div>
104
+ <span class="category-banner__cta category-banner__cta--solid">
105
+ Shop Now
106
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
107
+ <line x1="5" y1="12" x2="19" y2="12"></line>
108
+ <polyline points="12 5 19 12 12 19"></polyline>
109
+ </svg>
110
+ </span>
111
+ </div>
112
+ {% endif %}
113
+
114
+ </a>
115
+
116
+ <style>
117
+ .widget-category-banner {
118
+ width: 100%;
119
+ padding: 0 {{ settings.container_padding }}px;
120
+ {% if show_bottom_margin == 'Yes' %}
121
+ margin-bottom: {{ settings.spacing_large }}px;
122
+ {% elsif show_bottom_margin == 'No' %}
123
+ margin-bottom: 0;
124
+ {% else %}
125
+ margin-bottom: {{ settings.spacing_component }}px;
126
+ {% endif %}
127
+ }
128
+
129
+ /* ===== WITH IMAGE: Full Banner ===== */
130
+ .category-banner {
131
+ display: block;
132
+ position: relative;
133
+ width: 100%;
134
+ max-width: {{ settings.container_width }}px;
135
+ margin: 0 auto;
136
+ height: 280px;
137
+ overflow: hidden;
138
+ text-decoration: none;
139
+ color: {{ settings.color_background }};
140
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
141
+ transition: box-shadow 0.3s ease, transform 0.3s ease;
142
+ border-radius: var(--border-radius-medium);
143
+ }
144
+
145
+ .category-banner:hover {
146
+ box-shadow: 0 12px 48px rgba(0, 0, 0, 0.18);
147
+ transform: translateY(-2px);
148
+ }
149
+
150
+ .category-banner__bg {
151
+ position: absolute;
152
+ top: 0;
153
+ left: 0;
154
+ right: 0;
155
+ bottom: 0;
156
+ overflow: hidden;
157
+
158
+ }
159
+
160
+ .category-banner__bg img {
161
+ position: absolute;
162
+ top: 0;
163
+ left: 0;
164
+ width: 100%;
165
+ height: 100%;
166
+ object-fit: cover;
167
+ object-position: center;
168
+ transition: transform 0.6s ease;
169
+ }
170
+
171
+ {% comment %}
172
+ .category-banner:hover .category-banner__bg img {
173
+ transform: scale(1.05);
174
+ }
175
+ {% endcomment %}
176
+
177
+ .category-banner__overlay {
178
+ position: absolute;
179
+ top: 0;
180
+ left: 0;
181
+ right: 0;
182
+ bottom: 0;
183
+ pointer-events: none;
184
+ }
185
+
186
+ .category-banner__content--overlay {
187
+ position: relative;
188
+ z-index: 2;
189
+ display: flex;
190
+ flex-direction: column;
191
+ justify-content: center;
192
+ align-items: flex-start;
193
+ height: 280px;
194
+ padding: 40px 56px;
195
+ max-width: 650px;
196
+ }
197
+
198
+ .category-banner__content--overlay .category-banner__title {
199
+ font-size: 40px;
200
+ font-weight: 700;
201
+ margin: 0 0 16px 0;
202
+ line-height: 1.15;
203
+ letter-spacing: -0.025em;
204
+ text-shadow: 0 2px 12px rgba(0, 0, 0, 0.4);
205
+ color: #fff;
206
+ }
207
+
208
+ .category-banner__content--overlay .category-banner__description {
209
+ font-size: 17px;
210
+ line-height: 1.65;
211
+ margin: 0 0 28px 0;
212
+ opacity: 0.95;
213
+ max-width: 500px;
214
+ text-shadow: 0 1px 6px rgba(0, 0, 0, 0.3);
215
+ color: #fff;
216
+ }
217
+
218
+ .category-banner__cta {
219
+ display: inline-flex;
220
+ align-items: center;
221
+ gap: 10px;
222
+ font-size: 14px;
223
+ font-weight: 600;
224
+ text-transform: uppercase;
225
+ letter-spacing: 0.1em;
226
+ padding: 14px 32px;
227
+ background: {{ settings.color_background }};
228
+ color: {{ settings.color_text }};
229
+ transition: all 0.3s ease;
230
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
231
+ border-radius: var(--border-radius-medium);
232
+ }
233
+
234
+ .category-banner:hover .category-banner__cta {
235
+ background: {{ settings.color_text }};
236
+ color: {{ settings.color_background }};
237
+ }
238
+
239
+ .category-banner__cta svg {
240
+ transition: transform 0.3s ease;
241
+ }
242
+
243
+ .category-banner:hover .category-banner__cta svg {
244
+ transform: translateX(5px);
245
+ }
246
+
247
+ /* ===== NO IMAGE: Card Style ===== */
248
+ .category-banner--no-image {
249
+ min-height: auto;
250
+ }
251
+
252
+ .category-banner__card {
253
+ display: flex;
254
+ align-items: center;
255
+ gap: 32px;
256
+ padding: 40px 48px;
257
+ }
258
+
259
+ .category-banner__icon {
260
+ flex-shrink: 0;
261
+ width: 80px;
262
+ height: 80px;
263
+ display: flex;
264
+ align-items: center;
265
+ justify-content: center;
266
+ background: rgba(255, 255, 255, 0.2);
267
+ color: #fff;
268
+ }
269
+
270
+ .category-banner__text {
271
+ flex: 1;
272
+ min-width: 0;
273
+ }
274
+
275
+ .category-banner__card .category-banner__title {
276
+ font-size: 28px;
277
+ font-weight: 700;
278
+ margin: 0 0 8px 0;
279
+ line-height: 1.2;
280
+ color: #fff;
281
+ }
282
+
283
+ .category-banner__card .category-banner__description {
284
+ font-size: 15px;
285
+ line-height: 1.6;
286
+ margin: 0;
287
+ color: rgba(255, 255, 255, 0.9);
288
+ display: -webkit-box;
289
+ -webkit-line-clamp: 2;
290
+ -webkit-box-orient: vertical;
291
+ overflow: hidden;
292
+ }
293
+
294
+ .category-banner__cta--solid {
295
+ flex-shrink: 0;
296
+ background: #fff;
297
+ color: #764ba2;
298
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
299
+ }
300
+
301
+ .category-banner--no-image:hover .category-banner__cta--solid {
302
+ background: #111;
303
+ color: #fff;
304
+ }
305
+
306
+ /* ===== Tablet ===== */
307
+ @media (max-width: 1024px) {
308
+ .widget-category-banner {
309
+ padding: 0 20px;
310
+ }
311
+
312
+ .category-banner {
313
+ height: 240px;
314
+ }
315
+
316
+ .category-banner__content--overlay {
317
+ height: 240px;
318
+ padding: 32px 40px;
319
+ }
320
+
321
+ .category-banner__content--overlay .category-banner__title {
322
+ font-size: 32px;
323
+ }
324
+
325
+ .category-banner__content--overlay .category-banner__description {
326
+ font-size: 15px;
327
+ }
328
+
329
+ .category-banner__card {
330
+ padding: 32px 36px;
331
+ gap: 24px;
332
+ }
333
+
334
+ .category-banner__icon {
335
+ width: 64px;
336
+ height: 64px;
337
+ }
338
+
339
+ .category-banner__icon svg {
340
+ width: 36px;
341
+ height: 36px;
342
+ }
343
+
344
+ .category-banner__card .category-banner__title {
345
+ font-size: 24px;
346
+ }
347
+ }
348
+
349
+ /* ===== Mobile ===== */
350
+ @media (max-width: 768px) {
351
+ .widget-category-banner {
352
+ padding: 0 16px;
353
+ }
354
+
355
+ .category-banner {
356
+ height: 200px;
357
+ }
358
+
359
+
360
+ }
361
+
362
+ .category-banner__content--overlay {
363
+ height: 200px;
364
+ padding: 24px;
365
+ justify-content: flex-end;
366
+ max-width: 100%;
367
+ }
368
+
369
+ .category-banner__content--overlay .category-banner__title {
370
+ font-size: 22px;
371
+ margin-bottom: 12px;
372
+ }
373
+
374
+ .category-banner__content--overlay .category-banner__description {
375
+ display: none;
376
+ }
377
+
378
+ .category-banner__cta {
379
+ font-size: 12px;
380
+ padding: 12px 24px;
381
+ }
382
+
383
+ /* Card style mobile */
384
+ .category-banner--no-image {
385
+ height: auto;
386
+ }
387
+
388
+ .category-banner__card {
389
+ flex-direction: column;
390
+ text-align: center;
391
+ padding: 32px 24px;
392
+ gap: 20px;
393
+ }
394
+
395
+ .category-banner__card .category-banner__title {
396
+ font-size: 22px;
397
+ }
398
+
399
+ .category-banner__card .category-banner__description {
400
+ -webkit-line-clamp: 3;
401
+ }
402
+ }
403
+
404
+ /* ===== Small Mobile ===== */
405
+ @media (max-width: 480px) {
406
+ .widget-category-banner {
407
+ padding: 0 12px;
408
+ }
409
+
410
+ .category-banner {
411
+ height: 180px;
412
+ }
413
+
414
+ .category-banner__content--overlay {
415
+ height: 180px;
416
+ padding: 20px;
417
+ }
418
+
419
+ .category-banner__content--overlay .category-banner__title {
420
+ font-size: 20px;
421
+ }
422
+
423
+ .category-banner__cta {
424
+ font-size: 11px;
425
+ padding: 10px 20px;
426
+ gap: 8px;
427
+ }
428
+
429
+ .category-banner__cta svg {
430
+ width: 14px;
431
+ height: 14px;
432
+ }
433
+
434
+ /* Card style small mobile */
435
+ .category-banner__card {
436
+ padding: 24px 20px;
437
+ gap: 16px;
438
+ }
439
+
440
+ .category-banner__icon {
441
+ width: 56px;
442
+ height: 56px;
443
+ }
444
+
445
+ .category-banner__icon svg {
446
+ width: 28px;
447
+ height: 28px;
448
+ }
449
+
450
+ .category-banner__card .category-banner__title {
451
+ font-size: 18px;
452
+ }
453
+
454
+ .category-banner__card .category-banner__description {
455
+ font-size: 13px;
456
+ -webkit-line-clamp: 2;
457
+ }
458
+ }
459
+ </style>
460
+ </section>
461
+ {% else %}
462
+ <section class="widget widget-category-banner widget-category-banner--empty" data-widget-id="{{ widget.id }}">
463
+ <div style="max-width: {{ settings.container_width }}px; margin: 0 auto; padding: 0 {{ settings.container_padding }}px;">
464
+ <div style="padding: {{ settings.spacing_component | times: 2 }}px; text-align: center;">
465
+ <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="{{ settings.color_text_muted }}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="margin-bottom: {{ settings.spacing_element }}px;">
466
+ <path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"></path>
467
+ <line x1="3" y1="6" x2="21" y2="6"></line>
468
+ <path d="M16 10a4 4 0 0 1-8 0"></path>
469
+ </svg>
470
+ <p style="color: {{ settings.color_text_muted }}; margin: 0; font-size: {{ settings.font_size_base | plus: 1 }}px;">{{ widget_settings.empty_state | default: 'Category not found. Please configure a valid CategoryId.' }}</p>
471
+ </div>
472
+ </div>
473
+ </section>
474
+ {% endif %}
475
+
@@ -0,0 +1,176 @@
1
+ {% liquid
2
+ assign widget_settings = widget.settings
3
+ assign widget_content = widget.content
4
+ assign widget_data = widget.data
5
+
6
+ assign heading = widget_settings.title | default: widget_content.title | default: 'Limited time offer'
7
+ assign subheading = widget_settings.subtitle | default: widget_content.subtitle
8
+ assign end_time = widget_settings.ends_at | default: widget_content.ends_at | default: widget_data.ends_at
9
+ assign highlight_text = widget_settings.highlight | default: widget_content.highlight
10
+ assign background_image = widget_settings.background_image | default: widget_content.background_image
11
+ %}
12
+
13
+ <section class="widget widget-discount-time" data-widget-id="{{ widget.id }}" data-countdown="{{ end_time }}">
14
+ <div class="widget-discount-time__background" {% if background_image %}style="background-image: url('{{ background_image }}');"{% endif %}></div>
15
+ <div class="widget-discount-time__overlay"></div>
16
+ <div class="widget-discount-time__content">
17
+ <h2>{{ heading }}</h2>
18
+ {% if subheading %}
19
+ <p>{{ subheading }}</p>
20
+ {% endif %}
21
+ {% if highlight_text %}
22
+ <span class="widget-discount-time__highlight">{{ highlight_text }}</span>
23
+ {% endif %}
24
+ {% if end_time %}
25
+ <div class="widget-discount-time__countdown" data-countdown-display>
26
+ <div><strong data-countdown-days>00</strong><span>Days</span></div>
27
+ <div><strong data-countdown-hours>00</strong><span>Hours</span></div>
28
+ <div><strong data-countdown-minutes>00</strong><span>Minutes</span></div>
29
+ <div><strong data-countdown-seconds>00</strong><span>Seconds</span></div>
30
+ </div>
31
+ {% endif %}
32
+ {% if widget_settings.cta_label %}
33
+ <a href="{{ widget_settings.cta_link | default: '#' }}" class="widget-discount-time__cta">
34
+ {{ widget_settings.cta_label }}
35
+ </a>
36
+ {% endif %}
37
+ </div>
38
+
39
+ <style>
40
+ .widget-discount-time {
41
+ position: relative;
42
+ padding: 4rem 1rem;
43
+ color: {{ widget_settings.text_color | default: '#ffffff' }};
44
+ overflow: hidden;
45
+ border-radius: var(--border-radius-medium);
46
+ margin: 2rem auto;
47
+ max-width: {{ settings.container_width }}px;
48
+ }
49
+ .widget-discount-time__background {
50
+ position: absolute;
51
+ inset: 0;
52
+ background: linear-gradient(135deg, {{ widget_settings.background_from | default: '#1f2937' }}, {{ widget_settings.background_to | default: '#2563eb' }});
53
+ background-size: cover;
54
+ background-position: center;
55
+ filter: saturate(1.1);
56
+ }
57
+ .widget-discount-time__overlay {
58
+ position: absolute;
59
+ inset: 0;
60
+ background: rgba(15, 23, 42, 0.4);
61
+ }
62
+ .widget-discount-time__content {
63
+ position: relative;
64
+ z-index: 2;
65
+ text-align: center;
66
+ max-width: 640px;
67
+ margin: 0 auto;
68
+ }
69
+ .widget-discount-time h2 {
70
+ font-size: clamp(2rem, 4vw, 2.8rem);
71
+ margin: 0 0 1rem;
72
+ }
73
+ .widget-discount-time p {
74
+ margin: 0 0 1.5rem;
75
+ font-size: 1.1rem;
76
+ opacity: 0.9;
77
+ }
78
+ .widget-discount-time__highlight {
79
+ display: inline-flex;
80
+ align-items: center;
81
+ gap: 0.5rem;
82
+ padding: 0.5rem 1rem;
83
+ border-radius: 999px;
84
+ background: rgba(255, 255, 255, 0.15);
85
+ font-weight: 600;
86
+ margin-bottom: 2rem;
87
+ }
88
+ .widget-discount-time__countdown {
89
+ display: inline-flex;
90
+ gap: 1rem;
91
+ margin: 2.5rem 0;
92
+ padding: 0.5rem 1rem;
93
+ border-radius: 999px;
94
+ background: rgba(255, 255, 255, 0.12);
95
+ backdrop-filter: blur(8px);
96
+ }
97
+ .widget-discount-time__countdown div {
98
+ display: flex;
99
+ flex-direction: column;
100
+ align-items: center;
101
+ min-width: 70px;
102
+ }
103
+ .widget-discount-time__countdown strong {
104
+ font-size: 1.75rem;
105
+ font-weight: 700;
106
+ }
107
+ .widget-discount-time__countdown span {
108
+ font-size: 0.75rem;
109
+ text-transform: uppercase;
110
+ letter-spacing: 0.1em;
111
+ }
112
+ .widget-discount-time__cta {
113
+ display: inline-flex;
114
+ align-items: center;
115
+ gap: 0.5rem;
116
+ padding: 0.85rem 1.75rem;
117
+ border-radius: 999px;
118
+ background: {{ widget_settings.cta_background | default: '#ffffff' }};
119
+ color: {{ widget_settings.cta_text | default: '#111827' }};
120
+ text-decoration: none;
121
+ font-weight: 600;
122
+ }
123
+ @media (max-width: 640px) {
124
+ .widget-discount-time__countdown {
125
+ flex-wrap: wrap;
126
+ justify-content: center;
127
+ gap: 0.75rem;
128
+ }
129
+ .widget-discount-time__countdown div {
130
+ min-width: 60px;
131
+ }
132
+ }
133
+ </style>
134
+
135
+ {% if end_time %}
136
+ <script>
137
+ document.addEventListener('DOMContentLoaded', function() {
138
+ const widget = document.querySelector('[data-widget-id="{{ widget.id }}"]');
139
+ if (!widget) return;
140
+
141
+ const targetDate = new Date(widget.dataset.countdown);
142
+ if (!targetDate.getTime()) return;
143
+
144
+ const parts = {
145
+ days: widget.querySelector('[data-countdown-days]'),
146
+ hours: widget.querySelector('[data-countdown-hours]'),
147
+ minutes: widget.querySelector('[data-countdown-minutes]'),
148
+ seconds: widget.querySelector('[data-countdown-seconds]')
149
+ };
150
+
151
+ const tick = () => {
152
+ const now = new Date();
153
+ let diff = targetDate - now;
154
+ if (diff <= 0) {
155
+ diff = 0;
156
+ clearInterval(timer);
157
+ widget.classList.add('widget-discount-time--expired');
158
+ }
159
+ const seconds = Math.floor((diff / 1000) % 60);
160
+ const minutes = Math.floor((diff / (1000 * 60)) % 60);
161
+ const hours = Math.floor((diff / (1000 * 60 * 60)) % 24);
162
+ const days = Math.floor(diff / (1000 * 60 * 60 * 24));
163
+
164
+ if (parts.days) parts.days.textContent = String(days).padStart(2, '0');
165
+ if (parts.hours) parts.hours.textContent = String(hours).padStart(2, '0');
166
+ if (parts.minutes) parts.minutes.textContent = String(minutes).padStart(2, '0');
167
+ if (parts.seconds) parts.seconds.textContent = String(seconds).padStart(2, '0');
168
+ };
169
+
170
+ tick();
171
+ const timer = setInterval(tick, 1000);
172
+ });
173
+ </script>
174
+ {% endif %}
175
+ </section>
176
+