@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,187 @@
|
|
|
1
|
+
{% comment %}
|
|
2
|
+
News Thumbnail Snippet
|
|
3
|
+
|
|
4
|
+
Parameters:
|
|
5
|
+
- news_item: News item object (required)
|
|
6
|
+
- loading: Image loading attribute (optional, default: "lazy")
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
{% render 'snippets/news-thumbnail', news_item: news_item %}
|
|
10
|
+
{% endcomment %}
|
|
11
|
+
|
|
12
|
+
{% assign image_loading = loading | default: "lazy" %}
|
|
13
|
+
{% comment %} Get title - API client normalizes to camelCase, so check camelCase first, then PascalCase {% endcomment %}
|
|
14
|
+
{% assign news_title = news_item.title | default: news_item.Title | default: news_item.name | default: news_item.Name | default: news_item.slug | default: news_item.Slug | default: news_item.id | default: news_item.Id %}
|
|
15
|
+
{% comment %} Get slug - check camelCase first {% endcomment %}
|
|
16
|
+
{% assign news_slug = news_item.slug | default: news_item.Slug | default: news_item.id | default: news_item.Id %}
|
|
17
|
+
{% comment %} Get excerpt - check camelCase first {% endcomment %}
|
|
18
|
+
{% assign news_excerpt = news_item.excerpt | default: news_item.Excerpt | default: news_item.content | default: news_item.Content | truncate: 150 %}
|
|
19
|
+
{% comment %} Handle ThumbnailImage object - API client normalizes to camelCase (thumbnailImage) {% endcomment %}
|
|
20
|
+
{% assign thumbnail_image = news_item.thumbnailImage | default: news_item.ThumbnailImage %}
|
|
21
|
+
{% if thumbnail_image %}
|
|
22
|
+
{% comment %} Check camelCase first (url, altText), then PascalCase (Url, AltText) {% endcomment %}
|
|
23
|
+
{% assign news_image = thumbnail_image.url | default: thumbnail_image.Url %}
|
|
24
|
+
{% assign news_image_alt = thumbnail_image.altText | default: thumbnail_image.AltText | default: news_title %}
|
|
25
|
+
{% else %}
|
|
26
|
+
{% comment %} Fallback to Image field if ThumbnailImage is not available {% endcomment %}
|
|
27
|
+
{% assign news_image = news_item.image | default: news_item.Image %}
|
|
28
|
+
{% assign news_image_alt = news_title %}
|
|
29
|
+
{% endif %}
|
|
30
|
+
{% comment %} Get published date - check camelCase first {% endcomment %}
|
|
31
|
+
{% assign news_published = news_item.publishedAt | default: news_item.PublishedAt | default: news_item.published_at %}
|
|
32
|
+
{% comment %} Get category - check camelCase first {% endcomment %}
|
|
33
|
+
{% assign news_category = news_item.category | default: news_item.Category %}
|
|
34
|
+
|
|
35
|
+
<div class="news-thumbnail">
|
|
36
|
+
<a href="/news/{{ news_slug }}" class="news-thumbnail__link">
|
|
37
|
+
{% if news_image and news_image != blank %}
|
|
38
|
+
<div class="news-thumbnail__image">
|
|
39
|
+
{% if news_image contains 'http://' or news_image contains 'https://' %}
|
|
40
|
+
{% assign image_src = news_image %}
|
|
41
|
+
{% else %}
|
|
42
|
+
{% assign image_src = news_image | asset_url %}
|
|
43
|
+
{% endif %}
|
|
44
|
+
<img src="{{ image_src }}" alt="{{ news_image_alt }}" width="400" height="220" loading="{{ image_loading }}">
|
|
45
|
+
</div>
|
|
46
|
+
{% else %}
|
|
47
|
+
<div class="news-thumbnail__placeholder">
|
|
48
|
+
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
|
|
49
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
|
50
|
+
<path d="M8 8h8M8 12h8M8 16h5"></path>
|
|
51
|
+
</svg>
|
|
52
|
+
</div>
|
|
53
|
+
{% endif %}
|
|
54
|
+
|
|
55
|
+
<div class="news-thumbnail__content">
|
|
56
|
+
{% if news_category %}
|
|
57
|
+
{% assign category_name = news_category.Name | default: news_category.name %}
|
|
58
|
+
{% if category_name and category_name != blank %}
|
|
59
|
+
<div class="news-thumbnail__category">{{ category_name }}</div>
|
|
60
|
+
{% endif %}
|
|
61
|
+
{% endif %}
|
|
62
|
+
|
|
63
|
+
<h3 class="news-thumbnail__title">{{ news_title }}</h3>
|
|
64
|
+
|
|
65
|
+
{% if news_excerpt and news_excerpt != blank %}
|
|
66
|
+
<p class="news-thumbnail__excerpt">{{ news_excerpt }}</p>
|
|
67
|
+
{% endif %}
|
|
68
|
+
|
|
69
|
+
{% if news_published %}
|
|
70
|
+
<div class="news-thumbnail__date">
|
|
71
|
+
{{ news_published | date: '%b %d, %Y' }}
|
|
72
|
+
</div>
|
|
73
|
+
{% endif %}
|
|
74
|
+
</div>
|
|
75
|
+
</a>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<style>
|
|
79
|
+
.news-thumbnail {
|
|
80
|
+
width: 100%;
|
|
81
|
+
height: 100%;
|
|
82
|
+
background: #fff;
|
|
83
|
+
border-radius: 8px;
|
|
84
|
+
overflow: hidden;
|
|
85
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
86
|
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.news-thumbnail:hover {
|
|
90
|
+
transform: translateY(-4px);
|
|
91
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.news-thumbnail__link {
|
|
95
|
+
display: flex;
|
|
96
|
+
flex-direction: column;
|
|
97
|
+
height: 100%;
|
|
98
|
+
text-decoration: none;
|
|
99
|
+
color: inherit;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.news-thumbnail__image {
|
|
103
|
+
width: 100%;
|
|
104
|
+
aspect-ratio: 16 / 9;
|
|
105
|
+
overflow: hidden;
|
|
106
|
+
background: #f5f5f5;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.news-thumbnail__image img {
|
|
110
|
+
width: 100%;
|
|
111
|
+
height: 100%;
|
|
112
|
+
object-fit: cover;
|
|
113
|
+
display: block;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.news-thumbnail__placeholder {
|
|
117
|
+
width: 100%;
|
|
118
|
+
aspect-ratio: 16 / 9;
|
|
119
|
+
display: flex;
|
|
120
|
+
align-items: center;
|
|
121
|
+
justify-content: center;
|
|
122
|
+
background: #f5f5f5;
|
|
123
|
+
color: #999;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.news-thumbnail__content {
|
|
127
|
+
padding: 16px;
|
|
128
|
+
flex: 1;
|
|
129
|
+
display: flex;
|
|
130
|
+
flex-direction: column;
|
|
131
|
+
gap: 8px;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.news-thumbnail__category {
|
|
135
|
+
font-size: 12px;
|
|
136
|
+
font-weight: 600;
|
|
137
|
+
text-transform: uppercase;
|
|
138
|
+
letter-spacing: 0.05em;
|
|
139
|
+
color: #666;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.news-thumbnail__title {
|
|
143
|
+
font-size: 16px;
|
|
144
|
+
font-weight: 600;
|
|
145
|
+
line-height: 1.4;
|
|
146
|
+
margin: 0;
|
|
147
|
+
color: #111;
|
|
148
|
+
display: -webkit-box;
|
|
149
|
+
-webkit-line-clamp: 2;
|
|
150
|
+
-webkit-box-orient: vertical;
|
|
151
|
+
overflow: hidden;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.news-thumbnail__excerpt {
|
|
155
|
+
font-size: 14px;
|
|
156
|
+
line-height: 1.6;
|
|
157
|
+
color: #666;
|
|
158
|
+
margin: 0;
|
|
159
|
+
display: -webkit-box;
|
|
160
|
+
-webkit-line-clamp: 3;
|
|
161
|
+
-webkit-box-orient: vertical;
|
|
162
|
+
overflow: hidden;
|
|
163
|
+
flex: 1;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.news-thumbnail__date {
|
|
167
|
+
font-size: 12px;
|
|
168
|
+
color: #999;
|
|
169
|
+
margin-top: auto;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
@media (max-width: 768px) {
|
|
173
|
+
.news-thumbnail__content {
|
|
174
|
+
padding: 12px;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.news-thumbnail__title {
|
|
178
|
+
font-size: 14px;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.news-thumbnail__excerpt {
|
|
182
|
+
font-size: 13px;
|
|
183
|
+
-webkit-line-clamp: 2;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
</style>
|
|
187
|
+
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
{% comment %}
|
|
2
|
+
Pagination Component
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
{% include 'snippets/pagination', pagination: pagination %}
|
|
6
|
+
|
|
7
|
+
Requires:
|
|
8
|
+
- pagination.currentPage: Current page number
|
|
9
|
+
- pagination.totalPages: Total number of pages
|
|
10
|
+
- pagination.hasNext: Boolean for next page availability
|
|
11
|
+
- pagination.hasPrevious: Boolean for previous page availability
|
|
12
|
+
{% endcomment %}
|
|
13
|
+
|
|
14
|
+
{% if pagination.totalPages > 1 %}
|
|
15
|
+
<div class="pagination" id="pagination-container">
|
|
16
|
+
<!-- Previous Button -->
|
|
17
|
+
{% if pagination.hasPrevious %}
|
|
18
|
+
<a href="?page={{ pagination.currentPage | minus: 1 }}" class="pagination-item pagination-prev" data-page="{{ pagination.currentPage | minus: 1 }}">
|
|
19
|
+
{% raw %}<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
20
|
+
<polyline points="15,18 9,12 15,6"></polyline>
|
|
21
|
+
</svg>{% endraw %}
|
|
22
|
+
Previous
|
|
23
|
+
</a>
|
|
24
|
+
{% else %}
|
|
25
|
+
<span class="pagination-item disabled">
|
|
26
|
+
{% raw %}<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
27
|
+
<polyline points="15,18 9,12 15,6"></polyline>
|
|
28
|
+
</svg>{% endraw %}
|
|
29
|
+
Previous
|
|
30
|
+
</span>
|
|
31
|
+
{% endif %}
|
|
32
|
+
|
|
33
|
+
<!-- Page Numbers -->
|
|
34
|
+
<div class="pagination-pages">
|
|
35
|
+
{% assign startPage = pagination.currentPage | minus: 2 %}
|
|
36
|
+
{% if startPage < 1 %}
|
|
37
|
+
{% assign startPage = 1 %}
|
|
38
|
+
{% endif %}
|
|
39
|
+
|
|
40
|
+
{% assign endPage = pagination.currentPage | plus: 2 %}
|
|
41
|
+
{% if endPage > pagination.totalPages %}
|
|
42
|
+
{% assign endPage = pagination.totalPages %}
|
|
43
|
+
{% endif %}
|
|
44
|
+
|
|
45
|
+
<!-- First page -->
|
|
46
|
+
{% if startPage > 1 %}
|
|
47
|
+
<a href="?page=1" class="pagination-item pagination-link" data-page="1">1</a>
|
|
48
|
+
{% if startPage > 2 %}
|
|
49
|
+
<span class="pagination-ellipsis">...</span>
|
|
50
|
+
{% endif %}
|
|
51
|
+
{% endif %}
|
|
52
|
+
|
|
53
|
+
<!-- Page range -->
|
|
54
|
+
{% for i in (startPage..endPage) %}
|
|
55
|
+
{% if i == pagination.currentPage %}
|
|
56
|
+
<span class="pagination-item active">{{ i }}</span>
|
|
57
|
+
{% else %}
|
|
58
|
+
<a href="?page={{ i }}" class="pagination-item pagination-link" data-page="{{ i }}">{{ i }}</a>
|
|
59
|
+
{% endif %}
|
|
60
|
+
{% endfor %}
|
|
61
|
+
|
|
62
|
+
<!-- Last page -->
|
|
63
|
+
{% if endPage < pagination.totalPages %}
|
|
64
|
+
{% if endPage < pagination.totalPages | minus: 1 %}
|
|
65
|
+
<span class="pagination-ellipsis">...</span>
|
|
66
|
+
{% endif %}
|
|
67
|
+
<a href="?page={{ pagination.totalPages }}" class="pagination-item pagination-link" data-page="{{ pagination.totalPages }}">{{ pagination.totalPages }}</a>
|
|
68
|
+
{% endif %}
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<!-- Next Button -->
|
|
72
|
+
{% if pagination.hasNext %}
|
|
73
|
+
<a href="?page={{ pagination.currentPage | plus: 1 }}" class="pagination-item pagination-next" data-page="{{ pagination.currentPage | plus: 1 }}">
|
|
74
|
+
Next
|
|
75
|
+
{% raw %}<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
76
|
+
<polyline points="9,18 15,12 9,6"></polyline>
|
|
77
|
+
</svg>{% endraw %}
|
|
78
|
+
</a>
|
|
79
|
+
{% else %}
|
|
80
|
+
<span class="pagination-item disabled">
|
|
81
|
+
Next
|
|
82
|
+
{% raw %}<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
83
|
+
<polyline points="9,18 15,12 9,6"></polyline>
|
|
84
|
+
</svg>{% endraw %}
|
|
85
|
+
</span>
|
|
86
|
+
{% endif %}
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<script>
|
|
90
|
+
// Preserve URL parameters when navigating pagination
|
|
91
|
+
(function() {
|
|
92
|
+
'use strict';
|
|
93
|
+
|
|
94
|
+
const paginationContainer = document.getElementById('pagination-container');
|
|
95
|
+
if (!paginationContainer) return;
|
|
96
|
+
|
|
97
|
+
// Get all pagination links
|
|
98
|
+
const paginationLinks = paginationContainer.querySelectorAll('.pagination-link, .pagination-prev, .pagination-next');
|
|
99
|
+
|
|
100
|
+
// Get current URL parameters
|
|
101
|
+
const currentParams = new URLSearchParams(window.location.search);
|
|
102
|
+
|
|
103
|
+
// Update each link to preserve all parameters except page
|
|
104
|
+
paginationLinks.forEach(link => {
|
|
105
|
+
const page = link.getAttribute('data-page');
|
|
106
|
+
if (!page) return;
|
|
107
|
+
|
|
108
|
+
// Clone current parameters
|
|
109
|
+
const newParams = new URLSearchParams(currentParams);
|
|
110
|
+
|
|
111
|
+
// Update page parameter
|
|
112
|
+
newParams.set('page', page);
|
|
113
|
+
|
|
114
|
+
// Update link href
|
|
115
|
+
link.href = `${window.location.pathname}?${newParams.toString()}`;
|
|
116
|
+
});
|
|
117
|
+
})();
|
|
118
|
+
</script>
|
|
119
|
+
{% endif %}
|
|
120
|
+
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
{% comment %}
|
|
2
|
+
Price Display Snippet
|
|
3
|
+
|
|
4
|
+
Displays product price with currency formatting and sale price support.
|
|
5
|
+
Usage: {% render 'snippets/price', product: product %}
|
|
6
|
+
{% endcomment %}
|
|
7
|
+
|
|
8
|
+
{% liquid
|
|
9
|
+
assign product = product | default: null
|
|
10
|
+
assign price_string = price | default: product.prices.priceString
|
|
11
|
+
assign price_value = price | default: product.prices.price
|
|
12
|
+
assign compare_at_price_string = compare_at_price | default: product.compareAtPrice | default: product.prices.mrpString
|
|
13
|
+
assign compare_at_price_value = compare_at_price | default: product.compareAtPrice | default: product.prices.mrp
|
|
14
|
+
assign show_compare = show_compare | default: true
|
|
15
|
+
assign currency_symbol = currency_symbol | default: shop.settings.currencySymbol | default: shop.currency | default: '$'
|
|
16
|
+
assign currency_code = currency_code | default: shop.currency | default: 'USD'
|
|
17
|
+
|
|
18
|
+
if price_string == null or price_string == empty
|
|
19
|
+
if price_value == null or price_value == empty
|
|
20
|
+
assign price_string = currency_symbol | append: '0.00'
|
|
21
|
+
assign price_value = 0
|
|
22
|
+
else
|
|
23
|
+
assign price_formatted = price_value | round: 2
|
|
24
|
+
assign price_string = currency_symbol | append: price_formatted
|
|
25
|
+
endif
|
|
26
|
+
endif
|
|
27
|
+
|
|
28
|
+
if compare_at_price_string == null or compare_at_price_string == empty
|
|
29
|
+
if compare_at_price_value == null or compare_at_price_value == empty
|
|
30
|
+
assign compare_at_price_string = ''
|
|
31
|
+
assign compare_at_price_value = 0
|
|
32
|
+
else
|
|
33
|
+
assign compare_at_price_formatted = compare_at_price_value | round: 2
|
|
34
|
+
assign compare_at_price_string = currency_symbol | append: compare_at_price_formatted
|
|
35
|
+
endif
|
|
36
|
+
endif
|
|
37
|
+
|
|
38
|
+
assign is_on_sale = compare_at_price_value > price_value and compare_at_price_value > 0
|
|
39
|
+
%}
|
|
40
|
+
|
|
41
|
+
<div class="price" data-price>
|
|
42
|
+
{% if is_on_sale and show_compare %}
|
|
43
|
+
<span class="price-compare" data-price-compare>
|
|
44
|
+
{{ compare_at_price_string }}
|
|
45
|
+
</span>
|
|
46
|
+
<span class="price-sale" data-price-sale>
|
|
47
|
+
{{ price_string }}
|
|
48
|
+
</span>
|
|
49
|
+
{% else %}
|
|
50
|
+
<span class="price-regular" data-price-regular>
|
|
51
|
+
{{ price_string }}
|
|
52
|
+
</span>
|
|
53
|
+
{% endif %}
|
|
54
|
+
|
|
55
|
+
{% if currency_code != 'USD' %}
|
|
56
|
+
<span class="price-currency" data-price-currency>{{ currency_code }}</span>
|
|
57
|
+
{% endif %}
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<style>
|
|
61
|
+
.price {
|
|
62
|
+
display: flex;
|
|
63
|
+
align-items: baseline;
|
|
64
|
+
gap: 0.5rem;
|
|
65
|
+
flex-wrap: wrap;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.price-compare {
|
|
69
|
+
text-decoration: line-through;
|
|
70
|
+
color: {{ settings.color_text_light }};
|
|
71
|
+
font-size: 0.875rem;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.price-sale {
|
|
75
|
+
color: #dc2626;
|
|
76
|
+
font-weight: 600;
|
|
77
|
+
font-size: 1.125rem;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.price-regular {
|
|
81
|
+
color: {{ settings.color_text }};
|
|
82
|
+
font-weight: 600;
|
|
83
|
+
font-size: 1.125rem;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.price-currency {
|
|
87
|
+
font-size: 0.75rem;
|
|
88
|
+
color: {{ settings.color_text_light }};
|
|
89
|
+
text-transform: uppercase;
|
|
90
|
+
}
|
|
91
|
+
</style>
|
|
92
|
+
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{% comment %}
|
|
2
|
+
Related Product Card Snippet (for Product Recommendations)
|
|
3
|
+
|
|
4
|
+
Parameters:
|
|
5
|
+
- product: Product object (required)
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
{% include 'snippets/product-card-related', product: related %}
|
|
9
|
+
{% endcomment %}
|
|
10
|
+
|
|
11
|
+
<div class="product-card">
|
|
12
|
+
<div class="product-card-image">
|
|
13
|
+
<a href="/{{ product.slug | default: product.id }}">
|
|
14
|
+
{% if product.thumbnailImage %}
|
|
15
|
+
<img src="{{ product.thumbnailImage }}" alt="{{ product.name | default: product.title }}" loading="lazy">
|
|
16
|
+
{% elsif product.images and product.images.first %}
|
|
17
|
+
<img src="{{ product.images.first }}" alt="{{ product.name | default: product.title }}" loading="lazy">
|
|
18
|
+
{% else %}
|
|
19
|
+
<div class="product-placeholder">
|
|
20
|
+
{% raw %}<svg width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
|
|
21
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
|
22
|
+
<circle cx="8.5" cy="8.5" r="1.5"></circle>
|
|
23
|
+
<polyline points="21,15 16,10 5,21"></polyline>
|
|
24
|
+
</svg>{% endraw %}
|
|
25
|
+
</div>
|
|
26
|
+
{% endif %}
|
|
27
|
+
</a>
|
|
28
|
+
|
|
29
|
+
<div class="product-card-badges">
|
|
30
|
+
{% assign showSale = false %}
|
|
31
|
+
{% if product.onSale %}
|
|
32
|
+
{% assign showSale = true %}
|
|
33
|
+
{% elsif product.prices.mrp and product.prices.price %}
|
|
34
|
+
{% if product.prices.mrp > product.prices.price %}
|
|
35
|
+
{% assign showSale = true %}
|
|
36
|
+
{% endif %}
|
|
37
|
+
{% endif %}
|
|
38
|
+
{% if showSale %}
|
|
39
|
+
<span class="badge badge-sale">Sale</span>
|
|
40
|
+
{% endif %}
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div class="product-card-actions">
|
|
44
|
+
<button class="action-btn wishlist-btn" data-product-id="{{ product.productId }}" aria-label="Add to wishlist">
|
|
45
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
46
|
+
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path>
|
|
47
|
+
</svg>
|
|
48
|
+
</button>
|
|
49
|
+
<button class="action-btn quick-view-btn" data-product-id="{{ product.productId }}" aria-label="Quick view">
|
|
50
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
51
|
+
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
|
|
52
|
+
<circle cx="12" cy="12" r="3"></circle>
|
|
53
|
+
</svg>
|
|
54
|
+
</button>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div class="product-card-content">
|
|
59
|
+
{% if product.vendor or product.brand %}
|
|
60
|
+
<div class="product-card-vendor">{{ product.vendor | default: product.brand }}</div>
|
|
61
|
+
{% endif %}
|
|
62
|
+
<h3 class="product-card-title">
|
|
63
|
+
<a href="/{{ product.slug | default: product.id }}">
|
|
64
|
+
{{ product.name | default: product.title }}
|
|
65
|
+
</a>
|
|
66
|
+
</h3>
|
|
67
|
+
<div class="product-card-price">
|
|
68
|
+
<span class="price-current">{{ product.prices.priceString | default: product.prices.price | money_with_settings: shop.settings }}</span>
|
|
69
|
+
{% if product.prices.mrp and product.prices.mrp > product.prices.price %}
|
|
70
|
+
<span class="price-compare">{{ product.prices.mrpString | default: product.prices.mrp | money_with_settings: shop.settings }}</span>
|
|
71
|
+
{% endif %}
|
|
72
|
+
</div>
|
|
73
|
+
<button class="btn btn-primary btn-sm add-to-cart-quick" data-product-id="{{ product.productId }}">
|
|
74
|
+
Quick Add
|
|
75
|
+
</button>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{% comment %}
|
|
2
|
+
Simple Product Card Snippet (for Search Results)
|
|
3
|
+
|
|
4
|
+
Parameters:
|
|
5
|
+
- product: Product object (required)
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
{% include 'snippets/product-card-simple', product: product %}
|
|
9
|
+
{% endcomment %}
|
|
10
|
+
|
|
11
|
+
<div class="product-card">
|
|
12
|
+
<a href="/{{ product.slug }}" class="product-link">
|
|
13
|
+
<div class="product-image">
|
|
14
|
+
{% if product.images and product.images[0] %}
|
|
15
|
+
<img src="{{ product.images[0] }}"
|
|
16
|
+
alt="{{ product.title }}"
|
|
17
|
+
loading="lazy">
|
|
18
|
+
{% else %}
|
|
19
|
+
<img src="/assets/placeholder-product.jpg"
|
|
20
|
+
alt="{{ product.title }}"
|
|
21
|
+
loading="lazy">
|
|
22
|
+
{% endif %}
|
|
23
|
+
</div>
|
|
24
|
+
<div class="product-info">
|
|
25
|
+
<h3 class="product-title">{{ product.title }}</h3>
|
|
26
|
+
<p class="product-description">{{ product.description | truncate: 100 }}</p>
|
|
27
|
+
<div class="product-price">
|
|
28
|
+
<span class="price">{{ product.prices.priceString | default: product.prices.price | money }}</span>
|
|
29
|
+
<span class="currency">{{ shop.currency }}</span>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</a>
|
|
33
|
+
<div class="product-actions">
|
|
34
|
+
<button class="btn btn-secondary add-to-cart"
|
|
35
|
+
data-product-id="{{ product.productId }}"
|
|
36
|
+
data-variant-id="{{ product.variants[0].id }}">
|
|
37
|
+
Add to Cart
|
|
38
|
+
</button>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
|