@o2vend/theme-cli 1.0.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +425 -0
- package/assets/Logo_o2vend.png +0 -0
- package/assets/favicon.png +0 -0
- package/assets/logo-white.png +0 -0
- package/bin/o2vend +42 -0
- package/config/widget-map.json +50 -0
- package/lib/commands/check.js +201 -0
- package/lib/commands/generate.js +33 -0
- package/lib/commands/init.js +214 -0
- package/lib/commands/optimize.js +216 -0
- package/lib/commands/package.js +208 -0
- package/lib/commands/serve.js +105 -0
- package/lib/commands/validate.js +191 -0
- package/lib/lib/api-client.js +357 -0
- package/lib/lib/dev-server.js +2618 -0
- package/lib/lib/file-watcher.js +80 -0
- package/lib/lib/hot-reload.js +106 -0
- package/lib/lib/liquid-engine.js +822 -0
- package/lib/lib/liquid-filters.js +671 -0
- package/lib/lib/mock-api-server.js +989 -0
- package/lib/lib/mock-data.js +1468 -0
- package/lib/lib/widget-service.js +321 -0
- package/package.json +70 -0
- package/test-theme/README.md +27 -0
- package/test-theme/assets/async-sections.js +446 -0
- package/test-theme/assets/cart-drawer.js +463 -0
- package/test-theme/assets/cart-manager.js +223 -0
- package/test-theme/assets/checkout-price-handler.js +368 -0
- package/test-theme/assets/components.css +4629 -0
- package/test-theme/assets/delivery-zone.css +299 -0
- package/test-theme/assets/delivery-zone.js +396 -0
- package/test-theme/assets/logo.png +0 -0
- package/test-theme/assets/sections.css +48 -0
- package/test-theme/assets/theme.css +3500 -0
- package/test-theme/assets/theme.js +3745 -0
- package/test-theme/config/settings_data.json +292 -0
- package/test-theme/config/settings_schema.json +1050 -0
- package/test-theme/layout/theme.liquid +195 -0
- package/test-theme/locales/en.default.json +260 -0
- package/test-theme/sections/content-fallback.liquid +53 -0
- package/test-theme/sections/content.liquid +57 -0
- package/test-theme/sections/footer-fallback.liquid +328 -0
- package/test-theme/sections/footer.liquid +278 -0
- package/test-theme/sections/header-fallback.liquid +1805 -0
- package/test-theme/sections/header.liquid +1145 -0
- package/test-theme/sections/hero-fallback.liquid +212 -0
- package/test-theme/sections/hero.liquid +136 -0
- package/test-theme/snippets/account-sidebar.liquid +200 -0
- package/test-theme/snippets/add-to-cart-modal.liquid +484 -0
- package/test-theme/snippets/breadcrumbs.liquid +134 -0
- package/test-theme/snippets/cart-drawer.liquid +467 -0
- package/test-theme/snippets/delivery-zone-city-selector.liquid +79 -0
- package/test-theme/snippets/delivery-zone-modal.liquid +337 -0
- package/test-theme/snippets/delivery-zone-search.liquid +78 -0
- package/test-theme/snippets/icon.liquid +105 -0
- package/test-theme/snippets/login-modal.liquid +346 -0
- package/test-theme/snippets/mega-menu.liquid +812 -0
- package/test-theme/snippets/news-thumbnail.liquid +187 -0
- package/test-theme/snippets/pagination.liquid +120 -0
- package/test-theme/snippets/price.liquid +92 -0
- package/test-theme/snippets/product-card-related.liquid +78 -0
- package/test-theme/snippets/product-card-simple.liquid +41 -0
- package/test-theme/snippets/product-card.liquid +697 -0
- package/test-theme/snippets/rating.liquid +85 -0
- package/test-theme/snippets/skeleton-collection-grid.liquid +114 -0
- package/test-theme/snippets/skeleton-product-card.liquid +124 -0
- package/test-theme/snippets/skeleton-product-grid.liquid +34 -0
- package/test-theme/snippets/social-sharing.liquid +185 -0
- package/test-theme/templates/account/dashboard.liquid +401 -0
- package/test-theme/templates/account/loyalty-redemption.liquid +405 -0
- package/test-theme/templates/account/loyalty.liquid +588 -0
- package/test-theme/templates/account/order-detail.liquid +230 -0
- package/test-theme/templates/account/orders.liquid +349 -0
- package/test-theme/templates/account/profile.liquid +758 -0
- package/test-theme/templates/account/register.liquid +232 -0
- package/test-theme/templates/account/return-orders.liquid +348 -0
- package/test-theme/templates/account/store-credit.liquid +464 -0
- package/test-theme/templates/account/subscriptions.liquid +601 -0
- package/test-theme/templates/account/wishlist.liquid +419 -0
- package/test-theme/templates/address-book.liquid +1092 -0
- package/test-theme/templates/categories.liquid +452 -0
- package/test-theme/templates/checkout.liquid +4511 -0
- package/test-theme/templates/error.liquid +384 -0
- package/test-theme/templates/index.liquid +11 -0
- package/test-theme/templates/login.liquid +185 -0
- package/test-theme/templates/order-confirmation.liquid +720 -0
- package/test-theme/templates/page.liquid +297 -0
- package/test-theme/templates/product-detail.liquid +4363 -0
- package/test-theme/templates/products.liquid +518 -0
- package/test-theme/templates/search.liquid +922 -0
- package/test-theme/theme.json.example +19 -0
- package/test-theme/widgets/brand-carousel.liquid +676 -0
- package/test-theme/widgets/brand.liquid +245 -0
- package/test-theme/widgets/carousel.liquid +843 -0
- package/test-theme/widgets/category-list-carousel.liquid +656 -0
- package/test-theme/widgets/category-list.liquid +340 -0
- package/test-theme/widgets/category.liquid +475 -0
- package/test-theme/widgets/discount-time.liquid +176 -0
- package/test-theme/widgets/footer-menu.liquid +695 -0
- package/test-theme/widgets/footer.liquid +179 -0
- package/test-theme/widgets/gallery.liquid +271 -0
- package/test-theme/widgets/header-menu.liquid +932 -0
- package/test-theme/widgets/header.liquid +159 -0
- package/test-theme/widgets/html.liquid +214 -0
- package/test-theme/widgets/news.liquid +217 -0
- package/test-theme/widgets/product-canvas.liquid +235 -0
- package/test-theme/widgets/product-carousel.liquid +502 -0
- package/test-theme/widgets/product.liquid +45 -0
- package/test-theme/widgets/recently-viewed.liquid +26 -0
- package/test-theme/widgets/shared/product-grid.liquid +339 -0
- package/test-theme/widgets/simple-product.liquid +42 -0
- package/test-theme/widgets/single-product.liquid +610 -0
- package/test-theme/widgets/spacebar-carousel.liquid +663 -0
- package/test-theme/widgets/spacebar.liquid +279 -0
- package/test-theme/widgets/splash.liquid +378 -0
- package/test-theme/widgets/testimonial-carousel.liquid +709 -0
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
{% liquid
|
|
2
|
+
assign widget_settings = widget.settings
|
|
3
|
+
assign widget_data = widget.data
|
|
4
|
+
|
|
5
|
+
comment
|
|
6
|
+
Dynamic Widget: Products come from widget_data.products (enriched by WidgetService)
|
|
7
|
+
Settings come from widget_settings
|
|
8
|
+
endcomment
|
|
9
|
+
assign products = widget_data.products
|
|
10
|
+
assign heading = widget_settings.title
|
|
11
|
+
assign description = widget_settings.subtitle
|
|
12
|
+
assign show_widget_title_raw = widget_settings.showWidgetTitle | default: 'Yes'
|
|
13
|
+
if show_widget_title_raw == null or show_widget_title_raw == blank or show_widget_title_raw == 'null'
|
|
14
|
+
assign show_widget_title = true
|
|
15
|
+
else
|
|
16
|
+
if show_widget_title_raw == 'Yes'
|
|
17
|
+
assign show_widget_title = true
|
|
18
|
+
elsif show_widget_title_raw == true
|
|
19
|
+
assign show_widget_title = true
|
|
20
|
+
else
|
|
21
|
+
assign show_widget_title = false
|
|
22
|
+
endif
|
|
23
|
+
endif
|
|
24
|
+
assign widget_title_alignment_raw = widget_settings.widgetTitleAlignment | default: 'center'
|
|
25
|
+
assign widget_title_alignment = widget_title_alignment_raw | downcase
|
|
26
|
+
if widget_title_alignment != 'left' and widget_title_alignment != 'right' and widget_title_alignment != 'center'
|
|
27
|
+
assign widget_title_alignment = 'center'
|
|
28
|
+
endif
|
|
29
|
+
assign background_color = widget_settings.backgroundColor
|
|
30
|
+
%}
|
|
31
|
+
|
|
32
|
+
<section class="widget widget-product-carousel" data-widget-id="{{ widget.id }}" data-carousel-widget{% if background_color and background_color != blank and background_color != 'null' %} style="background-color: {{ background_color }};"{% endif %}>
|
|
33
|
+
<div class="carousel-container">
|
|
34
|
+
{% if show_widget_title %}
|
|
35
|
+
{% if heading or description %}
|
|
36
|
+
<header class="carousel-header">
|
|
37
|
+
<div class="carousel-header__text" style="text-align: {{ widget_title_alignment }};">
|
|
38
|
+
{% if heading %}
|
|
39
|
+
<h2 class="carousel-title">{{ heading }}</h2>
|
|
40
|
+
{% endif %}
|
|
41
|
+
{% if description %}
|
|
42
|
+
<p class="carousel-subtitle">{{ description }}</p>
|
|
43
|
+
{% endif %}
|
|
44
|
+
</div>
|
|
45
|
+
{% if products and products.size > 0 %}
|
|
46
|
+
<div class="carousel-nav">
|
|
47
|
+
<button type="button" class="carousel-arrow carousel-arrow--prev" data-carousel-prev aria-label="Previous">
|
|
48
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
49
|
+
<polyline points="15 18 9 12 15 6"></polyline>
|
|
50
|
+
</svg>
|
|
51
|
+
</button>
|
|
52
|
+
<button type="button" class="carousel-arrow carousel-arrow--next" data-carousel-next aria-label="Next">
|
|
53
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
54
|
+
<polyline points="9 18 15 12 9 6"></polyline>
|
|
55
|
+
</svg>
|
|
56
|
+
</button>
|
|
57
|
+
</div>
|
|
58
|
+
{% endif %}
|
|
59
|
+
</header>
|
|
60
|
+
{% endif %}
|
|
61
|
+
{% endif %}
|
|
62
|
+
|
|
63
|
+
{% if products and products.size > 0 %}
|
|
64
|
+
<div class="carousel-wrapper">
|
|
65
|
+
<!-- Side Navigation Arrows (Desktop) -->
|
|
66
|
+
<button type="button" class="carousel-side-arrow carousel-side-arrow--prev" data-carousel-prev aria-label="Previous">
|
|
67
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
68
|
+
<polyline points="15 18 9 12 15 6"></polyline>
|
|
69
|
+
</svg>
|
|
70
|
+
</button>
|
|
71
|
+
|
|
72
|
+
<div class="carousel-track-wrapper" data-carousel-viewport>
|
|
73
|
+
<div class="carousel-track" data-carousel-track>
|
|
74
|
+
{% for product in products %}
|
|
75
|
+
<div class="carousel-slide" data-slide-index="{{ forloop.index0 }}">
|
|
76
|
+
{% render 'snippets/product-card', product: product, widget: widget %}
|
|
77
|
+
</div>
|
|
78
|
+
{% endfor %}
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<button type="button" class="carousel-side-arrow carousel-side-arrow--next" data-carousel-next aria-label="Next">
|
|
83
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
84
|
+
<polyline points="9 18 15 12 9 6"></polyline>
|
|
85
|
+
</svg>
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<!-- Dot Indicators -->
|
|
90
|
+
<div class="carousel-dots" data-carousel-dots></div>
|
|
91
|
+
{% else %}
|
|
92
|
+
<div class="widget-empty">
|
|
93
|
+
<p>{{ widget_settings.empty_state | default: 'No products found.' }}</p>
|
|
94
|
+
</div>
|
|
95
|
+
{% endif %}
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<style>
|
|
99
|
+
/* Product Carousel - True Carousel Behavior */
|
|
100
|
+
.widget-product-carousel {
|
|
101
|
+
padding: 48px 0;
|
|
102
|
+
overflow: hidden;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.widget-product-carousel .carousel-container {
|
|
106
|
+
max-width: 1400px;
|
|
107
|
+
margin: 0 auto;
|
|
108
|
+
padding: 0 24px;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* Header */
|
|
112
|
+
.widget-product-carousel .carousel-header {
|
|
113
|
+
display: flex;
|
|
114
|
+
align-items: center;
|
|
115
|
+
justify-content: space-between;
|
|
116
|
+
margin-bottom: 28px;
|
|
117
|
+
gap: 16px;
|
|
118
|
+
position: static;
|
|
119
|
+
z-index: auto;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.widget-product-carousel .carousel-header__text {
|
|
123
|
+
flex: 1;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.widget-product-carousel .carousel-title {
|
|
127
|
+
font-size: 1.4rem;
|
|
128
|
+
font-weight: 500;
|
|
129
|
+
line-height: 1.3;
|
|
130
|
+
color: #111;
|
|
131
|
+
margin: 0;
|
|
132
|
+
letter-spacing: -0.01em;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.widget-product-carousel .carousel-subtitle {
|
|
136
|
+
font-size: 14px;
|
|
137
|
+
color: #6b7280;
|
|
138
|
+
margin: 6px 0 0 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* Header Nav Arrows */
|
|
142
|
+
.widget-product-carousel .carousel-nav {
|
|
143
|
+
display: flex;
|
|
144
|
+
gap: 8px;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.widget-product-carousel .carousel-arrow {
|
|
148
|
+
width: 40px;
|
|
149
|
+
height: 40px;
|
|
150
|
+
border-radius: 50%;
|
|
151
|
+
border: 1px solid #e0e0e0;
|
|
152
|
+
background: #fff;
|
|
153
|
+
display: flex;
|
|
154
|
+
align-items: center;
|
|
155
|
+
justify-content: center;
|
|
156
|
+
cursor: pointer;
|
|
157
|
+
color: #333;
|
|
158
|
+
transition: all 0.2s ease;
|
|
159
|
+
flex-shrink: 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.widget-product-carousel .carousel-arrow:hover:not(:disabled) {
|
|
163
|
+
background: #111;
|
|
164
|
+
color: #fff;
|
|
165
|
+
border-color: #111;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.widget-product-carousel .carousel-arrow:disabled {
|
|
169
|
+
opacity: 0.3;
|
|
170
|
+
cursor: not-allowed;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/* Carousel Wrapper with Side Arrows */
|
|
174
|
+
.widget-product-carousel .carousel-wrapper {
|
|
175
|
+
position: relative;
|
|
176
|
+
display: flex;
|
|
177
|
+
align-items: center;
|
|
178
|
+
gap: 12px;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* Side Navigation Arrows */
|
|
182
|
+
.widget-product-carousel .carousel-side-arrow {
|
|
183
|
+
display: none; /* Hidden by default, shown on desktop */
|
|
184
|
+
width: 30px;
|
|
185
|
+
height: 30px;
|
|
186
|
+
border-radius: 50%;
|
|
187
|
+
border: none;
|
|
188
|
+
background: #fff;
|
|
189
|
+
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
|
|
190
|
+
align-items: center;
|
|
191
|
+
justify-content: center;
|
|
192
|
+
cursor: pointer;
|
|
193
|
+
color: #333;
|
|
194
|
+
transition: all 0.2s ease;
|
|
195
|
+
flex-shrink: 0;
|
|
196
|
+
z-index: 10;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.widget-product-carousel .carousel-side-arrow:hover:not(:disabled) {
|
|
200
|
+
background: #111;
|
|
201
|
+
color: #fff;
|
|
202
|
+
transform: scale(1.05);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.widget-product-carousel .carousel-side-arrow:disabled {
|
|
206
|
+
opacity: 0;
|
|
207
|
+
pointer-events: none;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/* Track Wrapper */
|
|
211
|
+
.widget-product-carousel .carousel-track-wrapper {
|
|
212
|
+
flex: 1;
|
|
213
|
+
overflow: hidden;
|
|
214
|
+
position: relative;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/* The Scrolling Track */
|
|
218
|
+
.widget-product-carousel .carousel-track {
|
|
219
|
+
display: flex !important;
|
|
220
|
+
gap: 20px;
|
|
221
|
+
overflow-x: auto !important;
|
|
222
|
+
overflow-y: hidden !important;
|
|
223
|
+
scroll-snap-type: x mandatory;
|
|
224
|
+
scroll-behavior: smooth;
|
|
225
|
+
-webkit-overflow-scrolling: touch;
|
|
226
|
+
scrollbar-width: none;
|
|
227
|
+
-ms-overflow-style: none;
|
|
228
|
+
padding: 4px 0;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.widget-product-carousel .carousel-track::-webkit-scrollbar {
|
|
232
|
+
display: none;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/* Individual Slides - Desktop: 4 items */
|
|
236
|
+
.widget-product-carousel .carousel-slide {
|
|
237
|
+
flex: 0 0 calc(25% - 15px) !important;
|
|
238
|
+
min-width: 0 !important;
|
|
239
|
+
max-width: calc(25% - 15px) !important;
|
|
240
|
+
scroll-snap-align: start;
|
|
241
|
+
scroll-snap-stop: normal;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/* Override any product-card grid styles */
|
|
245
|
+
.widget-product-carousel .carousel-slide .product-card {
|
|
246
|
+
width: 100% !important;
|
|
247
|
+
max-width: 100% !important;
|
|
248
|
+
grid-column: unset !important;
|
|
249
|
+
grid-row: unset !important;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/* Dot Indicators */
|
|
253
|
+
.widget-product-carousel .carousel-dots {
|
|
254
|
+
display: flex;
|
|
255
|
+
justify-content: center;
|
|
256
|
+
gap: 8px;
|
|
257
|
+
margin-top: 20px;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.widget-product-carousel .carousel-dot {
|
|
261
|
+
width: 8px;
|
|
262
|
+
height: 8px;
|
|
263
|
+
border-radius: 50%;
|
|
264
|
+
background: #d1d5db;
|
|
265
|
+
border: none;
|
|
266
|
+
padding: 0;
|
|
267
|
+
cursor: pointer;
|
|
268
|
+
transition: all 0.2s ease;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.widget-product-carousel .carousel-dot:hover {
|
|
272
|
+
background: #9ca3af;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.widget-product-carousel .carousel-dot.active {
|
|
276
|
+
background: #111;
|
|
277
|
+
width: 24px;
|
|
278
|
+
border-radius: 4px;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/* Desktop: Show side arrows, hide header arrows */
|
|
282
|
+
@media (min-width: 1025px) {
|
|
283
|
+
.widget-product-carousel .carousel-side-arrow {
|
|
284
|
+
display: flex;
|
|
285
|
+
}
|
|
286
|
+
.widget-product-carousel .carousel-nav {
|
|
287
|
+
display: none;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/* Tablet: 3 items */
|
|
292
|
+
@media (max-width: 1024px) {
|
|
293
|
+
.widget-product-carousel .carousel-slide {
|
|
294
|
+
flex: 0 0 calc(33.333% - 14px) !important;
|
|
295
|
+
max-width: calc(33.333% - 14px) !important;
|
|
296
|
+
}
|
|
297
|
+
.widget-product-carousel .carousel-track {
|
|
298
|
+
gap: 20px;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/* Mobile: 2 items */
|
|
303
|
+
@media (max-width: 768px) {
|
|
304
|
+
.widget-product-carousel {
|
|
305
|
+
padding: 36px 0;
|
|
306
|
+
}
|
|
307
|
+
.widget-product-carousel .carousel-container {
|
|
308
|
+
padding: 0 16px;
|
|
309
|
+
}
|
|
310
|
+
.widget-product-carousel .carousel-title {
|
|
311
|
+
font-size: 1.3rem;
|
|
312
|
+
}
|
|
313
|
+
.widget-product-carousel .carousel-track {
|
|
314
|
+
gap: 12px;
|
|
315
|
+
padding-left: 0;
|
|
316
|
+
padding-right: 0;
|
|
317
|
+
}
|
|
318
|
+
.widget-product-carousel .carousel-slide {
|
|
319
|
+
flex: 0 0 calc(50% - 6px) !important;
|
|
320
|
+
max-width: calc(50% - 6px) !important;
|
|
321
|
+
}
|
|
322
|
+
.widget-product-carousel .carousel-arrow {
|
|
323
|
+
width: 36px;
|
|
324
|
+
height: 36px;
|
|
325
|
+
}
|
|
326
|
+
.widget-product-carousel .carousel-dots {
|
|
327
|
+
margin-top: 16px;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/* Small Mobile */
|
|
332
|
+
@media (max-width: 480px) {
|
|
333
|
+
.widget-product-carousel .carousel-container {
|
|
334
|
+
padding: 0 12px;
|
|
335
|
+
}
|
|
336
|
+
.widget-product-carousel .carousel-track {
|
|
337
|
+
gap: 10px;
|
|
338
|
+
}
|
|
339
|
+
.widget-product-carousel .carousel-slide {
|
|
340
|
+
flex: 0 0 calc(50% - 5px) !important;
|
|
341
|
+
max-width: calc(50% - 5px) !important;
|
|
342
|
+
}
|
|
343
|
+
.widget-product-carousel .carousel-title {
|
|
344
|
+
font-size: 1.2rem;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
</style>
|
|
348
|
+
|
|
349
|
+
<script>
|
|
350
|
+
(function() {
|
|
351
|
+
// Wait for DOM
|
|
352
|
+
if (document.readyState === 'loading') {
|
|
353
|
+
document.addEventListener('DOMContentLoaded', initCarousel);
|
|
354
|
+
} else {
|
|
355
|
+
initCarousel();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function initCarousel() {
|
|
359
|
+
const widget = document.querySelector('[data-widget-id="{{ widget.id }}"]');
|
|
360
|
+
if (!widget) return;
|
|
361
|
+
|
|
362
|
+
const track = widget.querySelector('[data-carousel-track]');
|
|
363
|
+
const viewport = widget.querySelector('[data-carousel-viewport]');
|
|
364
|
+
const prevBtns = widget.querySelectorAll('[data-carousel-prev]');
|
|
365
|
+
const nextBtns = widget.querySelectorAll('[data-carousel-next]');
|
|
366
|
+
const dotsContainer = widget.querySelector('[data-carousel-dots]');
|
|
367
|
+
const slides = widget.querySelectorAll('.carousel-slide');
|
|
368
|
+
|
|
369
|
+
if (!track || slides.length === 0) return;
|
|
370
|
+
|
|
371
|
+
// Calculate items per view based on screen width
|
|
372
|
+
function getItemsPerView() {
|
|
373
|
+
const width = window.innerWidth;
|
|
374
|
+
if (width <= 480) return 2;
|
|
375
|
+
if (width <= 768) return 2;
|
|
376
|
+
if (width <= 1024) return 3;
|
|
377
|
+
return 4;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Calculate number of pages
|
|
381
|
+
function getPageCount() {
|
|
382
|
+
const itemsPerView = getItemsPerView();
|
|
383
|
+
return Math.ceil(slides.length / itemsPerView);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Get current page based on scroll position
|
|
387
|
+
function getCurrentPage() {
|
|
388
|
+
if (!track.scrollWidth || track.scrollWidth <= viewport.clientWidth) return 0;
|
|
389
|
+
const scrollRatio = track.scrollLeft / (track.scrollWidth - viewport.clientWidth);
|
|
390
|
+
return Math.round(scrollRatio * (getPageCount() - 1));
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Create dot indicators
|
|
394
|
+
function createDots() {
|
|
395
|
+
if (!dotsContainer) return;
|
|
396
|
+
dotsContainer.innerHTML = '';
|
|
397
|
+
const pageCount = getPageCount();
|
|
398
|
+
if (pageCount <= 1) return;
|
|
399
|
+
|
|
400
|
+
for (let i = 0; i < pageCount; i++) {
|
|
401
|
+
const dot = document.createElement('button');
|
|
402
|
+
dot.className = 'carousel-dot' + (i === 0 ? ' active' : '');
|
|
403
|
+
dot.setAttribute('aria-label', 'Go to page ' + (i + 1));
|
|
404
|
+
dot.addEventListener('click', () => goToPage(i));
|
|
405
|
+
dotsContainer.appendChild(dot);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Update active dot
|
|
410
|
+
function updateDots() {
|
|
411
|
+
if (!dotsContainer) return;
|
|
412
|
+
const dots = dotsContainer.querySelectorAll('.carousel-dot');
|
|
413
|
+
const currentPage = getCurrentPage();
|
|
414
|
+
dots.forEach((dot, index) => {
|
|
415
|
+
dot.classList.toggle('active', index === currentPage);
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Update arrow states
|
|
420
|
+
function updateArrows() {
|
|
421
|
+
const atStart = track.scrollLeft <= 5;
|
|
422
|
+
const atEnd = track.scrollLeft >= track.scrollWidth - viewport.clientWidth - 5;
|
|
423
|
+
|
|
424
|
+
prevBtns.forEach(btn => {
|
|
425
|
+
btn.disabled = atStart;
|
|
426
|
+
});
|
|
427
|
+
nextBtns.forEach(btn => {
|
|
428
|
+
btn.disabled = atEnd;
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Navigate to specific page
|
|
433
|
+
function goToPage(pageIndex) {
|
|
434
|
+
const itemsPerView = getItemsPerView();
|
|
435
|
+
const slide = slides[0];
|
|
436
|
+
if (!slide) return;
|
|
437
|
+
|
|
438
|
+
const gap = parseFloat(getComputedStyle(track).gap) || 20;
|
|
439
|
+
const slideWidth = slide.offsetWidth + gap;
|
|
440
|
+
const scrollTo = pageIndex * itemsPerView * slideWidth;
|
|
441
|
+
|
|
442
|
+
track.scrollTo({ left: scrollTo, behavior: 'smooth' });
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Scroll by one page
|
|
446
|
+
function scrollByPage(direction) {
|
|
447
|
+
const itemsPerView = getItemsPerView();
|
|
448
|
+
const slide = slides[0];
|
|
449
|
+
if (!slide) return;
|
|
450
|
+
|
|
451
|
+
const gap = parseFloat(getComputedStyle(track).gap) || 20;
|
|
452
|
+
const scrollAmount = (slide.offsetWidth + gap) * Math.max(1, itemsPerView - 1);
|
|
453
|
+
|
|
454
|
+
track.scrollBy({ left: direction * scrollAmount, behavior: 'smooth' });
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Event listeners
|
|
458
|
+
prevBtns.forEach(btn => {
|
|
459
|
+
btn.addEventListener('click', (e) => {
|
|
460
|
+
e.preventDefault();
|
|
461
|
+
scrollByPage(-1);
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
nextBtns.forEach(btn => {
|
|
466
|
+
btn.addEventListener('click', (e) => {
|
|
467
|
+
e.preventDefault();
|
|
468
|
+
scrollByPage(1);
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
// Track scroll events
|
|
473
|
+
let scrollTimeout;
|
|
474
|
+
track.addEventListener('scroll', () => {
|
|
475
|
+
clearTimeout(scrollTimeout);
|
|
476
|
+
scrollTimeout = setTimeout(() => {
|
|
477
|
+
updateDots();
|
|
478
|
+
updateArrows();
|
|
479
|
+
}, 50);
|
|
480
|
+
}, { passive: true });
|
|
481
|
+
|
|
482
|
+
// Touch/swipe support is built-in via CSS scroll-snap
|
|
483
|
+
|
|
484
|
+
// Handle resize
|
|
485
|
+
let resizeTimeout;
|
|
486
|
+
window.addEventListener('resize', () => {
|
|
487
|
+
clearTimeout(resizeTimeout);
|
|
488
|
+
resizeTimeout = setTimeout(() => {
|
|
489
|
+
createDots();
|
|
490
|
+
updateArrows();
|
|
491
|
+
}, 200);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
// Initialize
|
|
495
|
+
createDots();
|
|
496
|
+
updateArrows();
|
|
497
|
+
}
|
|
498
|
+
})();
|
|
499
|
+
</script>
|
|
500
|
+
</section>
|
|
501
|
+
|
|
502
|
+
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{% liquid
|
|
2
|
+
assign widget_settings = widget.settings
|
|
3
|
+
assign widget_content = widget.content
|
|
4
|
+
assign widget_data = widget.data
|
|
5
|
+
|
|
6
|
+
comment
|
|
7
|
+
Dynamic Widget: Products come from widget_data.products (enriched by WidgetService)
|
|
8
|
+
Settings come from widget_settings
|
|
9
|
+
This widget supports filtering by CategoryId, BrandId, OrderBy, IsFeatured, NumberOfProducts
|
|
10
|
+
endcomment
|
|
11
|
+
|
|
12
|
+
assign products = widget_data.products
|
|
13
|
+
assign heading = widget_settings.title | default: widget.Title | default: widget.title
|
|
14
|
+
assign description = widget_settings.subtitle
|
|
15
|
+
assign columns = widget_settings.columns | default: 4
|
|
16
|
+
assign show_add_to_cart = widget_settings.show_add_to_cart | default: true
|
|
17
|
+
assign show_widget_title_raw = widget_settings.showWidgetTitle | default: 'Yes'
|
|
18
|
+
if show_widget_title_raw == null or show_widget_title_raw == blank or show_widget_title_raw == 'null'
|
|
19
|
+
assign show_widget_title = true
|
|
20
|
+
else
|
|
21
|
+
if show_widget_title_raw == 'Yes'
|
|
22
|
+
assign show_widget_title = true
|
|
23
|
+
elsif show_widget_title_raw == true
|
|
24
|
+
assign show_widget_title = true
|
|
25
|
+
else
|
|
26
|
+
assign show_widget_title = false
|
|
27
|
+
endif
|
|
28
|
+
endif
|
|
29
|
+
assign widget_title_alignment_raw = widget_settings.widgetTitleAlignment | default: 'center'
|
|
30
|
+
assign widget_title_alignment = widget_title_alignment_raw | downcase
|
|
31
|
+
if widget_title_alignment != 'left' and widget_title_alignment != 'right' and widget_title_alignment != 'center'
|
|
32
|
+
assign widget_title_alignment = 'center'
|
|
33
|
+
endif
|
|
34
|
+
%}
|
|
35
|
+
|
|
36
|
+
{% render 'widgets/shared/product-grid',
|
|
37
|
+
widget: widget,
|
|
38
|
+
heading: heading,
|
|
39
|
+
description: description,
|
|
40
|
+
products: products,
|
|
41
|
+
columns: columns,
|
|
42
|
+
show_add_to_cart: show_add_to_cart,
|
|
43
|
+
show_widget_title: show_widget_title,
|
|
44
|
+
widget_title_alignment: widget_title_alignment %}
|
|
45
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{% liquid
|
|
2
|
+
assign widget_settings = widget.settings
|
|
3
|
+
assign widget_content = widget.content
|
|
4
|
+
assign widget_data = widget.data
|
|
5
|
+
|
|
6
|
+
assign products = widget_data.products | default: widget_content.products | default: widget_settings.products | default: cart.recentlyViewed
|
|
7
|
+
assign heading = widget_settings.title | default: widget_content.title | default: 'Recently viewed'
|
|
8
|
+
assign description = widget_settings.subtitle | default: widget_content.subtitle
|
|
9
|
+
assign columns = widget_settings.columns | default: widget_content.columns | default: 4
|
|
10
|
+
assign show_add_to_cart = widget_settings.show_add_to_cart
|
|
11
|
+
if show_add_to_cart == nil and widget_content and widget_content.show_add_to_cart != nil
|
|
12
|
+
assign show_add_to_cart = widget_content.show_add_to_cart
|
|
13
|
+
endif
|
|
14
|
+
if show_add_to_cart == nil
|
|
15
|
+
assign show_add_to_cart = false
|
|
16
|
+
endif
|
|
17
|
+
%}
|
|
18
|
+
|
|
19
|
+
{% render 'widgets/shared/product-grid',
|
|
20
|
+
widget: widget,
|
|
21
|
+
heading: heading,
|
|
22
|
+
description: description,
|
|
23
|
+
products: products,
|
|
24
|
+
columns: columns,
|
|
25
|
+
show_add_to_cart: show_add_to_cart %}
|
|
26
|
+
|