@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,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
+