first commit

This commit is contained in:
Axel Kee
2025-08-29 15:28:11 +08:00
commit 60dfabd747

291
product-card.liquid Normal file
View File

@@ -0,0 +1,291 @@
{%- comment -%}
----------------------------------------------------------------------------------------------------------------------
PRODUCT CARD COMPONENT
----------------------------------------------------------------------------------------------------------------------
This component is used in collection and featured collection to render a single product as a card
********************************************
Supported variables
********************************************
* product: the product to render
* stacked: define if the product is stacked or not on mobile
* reveal: if set to true, the card will be revealed on scroll through animation
* show_badges: show or not the badges (default to true if nothing is set).
* show_rating: show or not the rating. If nothing is set, the theme uses the default product card setting
* show_vendor: show or not the vendor. If nothing is set, the theme uses the default product card setting
* show_quick_buy: show or not the quick buy (which open a modal if the product contains more than 1 choice)
* show_secondary_image: show or not the secondary media on hover. If nothing is set, the theme uses the default product card setting
* show_swatches: show or not the swatches. The swatch type is inferred from setting, and will default to true if nothing is set.
* hide_product_information: if set to true, all content (vendor, badge, title...) are not rendered, to create image-based grid
* quick_buy_context: a unique text to dissociate quick buy if the same product is embedded multiple times
{%- endcomment -%}
{%- liquid
if hide_product_information
assign show_badges = false
assign show_rating = false
assign show_vendor = false
assign show_title = false
assign show_prices = false
assign show_swatches = false
assign show_quick_buy = show_quick_buy | default: settings.show_quick_buy, allow_false: true
assign show_secondary_image = show_secondary_image | default: settings.show_secondary_image, allow_false: true
else
assign show_badges = show_badges | default: true, allow_false: true
assign show_rating = show_rating | default: settings.show_product_rating, allow_false: true
assign show_vendor = show_vendor | default: settings.show_vendor, allow_false: true
assign show_quick_buy = show_quick_buy | default: settings.show_quick_buy, allow_false: true
assign show_title = true
assign show_prices = true
assign show_secondary_image = show_secondary_image | default: settings.show_secondary_image, allow_false: true
assign show_swatches = show_swatches | default: true, allow_false: true
endif
-%}
<product-card class="product-card" {% if reveal %}reveal-on-scroll="true"{% endif %} handle="{{ product.handle | escape }}">
{%- comment -%}
--------------------------------------------------------------------------------------------------------------------
PRODUCT MEDIA
--------------------------------------------------------------------------------------------------------------------
{%- endcomment -%}
{%- if product.media.size > 0 -%}
<div class="product-card__figure">
{%- if show_badges -%}
{%- render 'product-badges', product: product, vertical: true -%}
{%- endif -%}
<a href="{{ product.url }}" class="product-card__media" data-instant>
{%- capture sizes -%}
{%- if stacked -%}
(max-width: 699px) calc(100vw / {{ section.settings.products_per_row_mobile }}), (max-width: 999px) calc(100vw / {{ 3 | at_most: section.settings.products_per_row_desktop | default: 3 }} - 64px), calc((100vw - 96px) / {{ section.settings.products_per_row_desktop | default: 3 }} - (24px / {{ section.settings.products_per_row_desktop | default: 3 }} * {{ section.settings.products_per_row_desktop | default: 3 | minus: 1 }}))
{%- else -%}
(max-width: 699px) 74vw, (max-width: 999px) 38vw, calc((100vw - 96px) / {{ section.settings.products_per_row_desktop | default: 3 }} - (24px / {{ section.settings.products_per_row_desktop | default: 3 }} * {{ section.settings.products_per_row_desktop | default: 3 | minus: 1 }}))
{%- endif -%}
{%- endcapture -%}
{%- capture main_image_classes -%}product-card__image product-card__image--primary {% if settings.product_image_aspect_ratio contains 'crop' %}object-cover{% endif %} aspect-{{ settings.product_image_aspect_ratio | split: '_' | first }}{%- endcapture -%}
{{- product.featured_media | image_url: width: product.featured_media.width | image_tag: loading: 'lazy', sizes: sizes, widths: '200,300,400,500,600,700,800,1000,1200,1400,1600,1800', class: main_image_classes -}}
{%- if show_secondary_image and product.media.size > 1 -%}
{%- assign next_media = product.media[product.featured_media.position] | default: product.media[1] -%}
{{- next_media | image_url: width: next_media.width | image_tag: class: 'product-card__image product-card__image--secondary', loading: 'lazy', fetchpriority: 'low', sizes: sizes, widths: '200,300,400,500,600,700,800,1000,1200,1400,1600,1800' -}}
{%- endif -%}
</a>
{%- if show_quick_buy and product.available -%}
{%- if product.variants.size == 1 and product.selling_plan_groups.size == 0 -%}
{%- form 'product', product, is: 'product-form' -%}
<input type="hidden" name="on_success" value="force_open_drawer">
<input type="hidden" name="quantity" value="1">
<input type="hidden" name="id" value="{{ product.selected_or_first_available_variant.id }}">
{% render 'collection-add-to-cart', product: product %}
{%- endform -%}
{%- else -%}
{%- capture quick_buy_id -%}product-quick-buy-{{ section.id }}-{{ block.id }}-{{ quick_buy_context }}-{{ product.id }}{%- endcapture -%}
{% render 'collection-add-to-cart', product: product %}
<quick-buy-modal handle="{{ product.handle }}" class="quick-buy-modal modal" id="{{ quick_buy_id }}">
</quick-buy-modal>
{%- endif -%}
{%- endif -%}
</div>
{%- endif -%}
{%- comment -%}
--------------------------------------------------------------------------------------------------------------------
PRODUCT INFO
--------------------------------------------------------------------------------------------------------------------
{%- endcomment -%}
<div class="product-card__info empty:hidden">
{%- assign text_class = '' -%}
{%- if settings.product_card_text_font == 'heading' -%}
{%- assign text_class = 'h5' -%}
{%- endif -%}
{%- if show_title or show_prices or show_vendor and product.vendor != blank -%}
<div class="v-stack justify-items-center gap-2">
{%- if show_vendor and product.vendor != blank -%}
{%- capture vendor_class -%}smallcaps {% if settings.product_card_text_font == 'heading' %}heading{% endif %}{% endcapture %}
{%- render 'vendor' with product.vendor, class: vendor_class -%}
{%- endif -%}
<div class="v-stack justify-items-center gap-1">
{%- if show_title -%}
<a href="{{ product.url }}" class="product-title {% if text_class != blank %}{{ text_class }}{% endif %} {% if settings.product_title_max_lines > 0 %}line-clamp{% endif %}" {% if settings.product_title_max_lines > 0 %}style="--line-clamp-count: {{ settings.product_title_max_lines }}"{% endif %} data-instant>
{{- product.title -}}
</a>
{%- endif -%}
{%- if show_prices and product.template_suffix != 'quote' -%}
<div class="product-price">
{{ product.selected_or_first_available_variant.price | money }}
</div>
{%- endif -%}
</div>
</div>
{%- endif -%}
{%- if show_swatches and settings.product_color_display != 'hide' -%}
{%- assign translated_color_label = 'general.label.color' | t | downcase -%}
{%- assign original_color_label = 'Color' | downcase -%}
{%- assign color_labels = translated_color_label | append: ',' | append: original_color_label | split: ',' -%}
{%- for color_label in color_labels -%}
{%- if product.options_by_name[color_label] != blank -%}
{%- assign product_option = product.options_by_name[color_label] -%}
{%- capture name -%}swatch-{{ quick_buy_context }}-{{ section.id }}-{{ product.id }}-{{ product_option.position }}{%- endcapture -%}
{%- case settings.product_color_display -%}
{%- when 'count' -%}
<p class="smallcaps text-subdued">{{- 'product.general.available_colors_count' | t: count: product_option.values.size -}}</p>
{%- when 'swatch' -%}
<fieldset class="h-stack wrap justify-center gap-1" data-option-position="{{ product_option.position }}">
{%- for option_value in product_option.values -%}
{%- if forloop.first or product.selected_or_first_available_variant.matched and option_value == product_option.selected_value -%}
{%- assign selected = true -%}
{%- else -%}
{% assign selected = false %}
{%- endif -%}
{%- unless product.template_suffix == 'quote' -%}
{%- render 'swatch', type: 'colors', value: option_value, name: name, selected: selected, size: 'sm', show_tooltip: true -%}
{%- endunless -%}
{%- endfor %}
</fieldset>
{%- endcase -%}
{%- assign type_option = product.options_with_values[1] -%}
{%- if type_option and type_option.values.size > 1 and type_option.name != 'Color' -%}
<!-- Type (Option 2) as Dropdown -->
{%- unless product.template_suffix == 'quote' -%}
<div class="product-card__option">
<label for="option-{{ type_option.name }}">{{ type_option.name }}</label>
<select name="option2" id="option-{{ type_option.name }}" class="product-card__dropdown">
{%- for value in type_option.values -%}
<option value='{{ value }}' {%- if forloop.first -%} selected {%-endif-%} >{{ value }}</option>
{%- endfor %}
</select>
</div>
{%- endunless -%}
{%- endif -%}
<!-- Option 3 -->
{% if product.options.size > 2 %}
<div class="product-card__option">
<label for="option3">{{ product.options[2] }}</label>
<select id="option3" name="Option3" class="product-card__dropdown product-card__dropdown3">
{% for value in product.options_with_values[2].values %}
<option value='{{ value }}' {%- if forloop.first -%} selected {%-endif-%} >{{ value }}</option>
{% endfor %}
</select>
</div>
{% endif %}
{%- break -%}
{%- endif -%}
{%- endfor -%}
<!-- Handle Letter-based options (e.g., 1Letter, 2Letter, 3Letter) -->
{% assign alphabet = 'A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z' | split: ',' %}
{% if product.template_suffix == "1Letter" %}
<div class="product-card__option letter-option">
<label for="letter-{{ product.id }}">{{ 'product.general.letter' | t }}</label>
<select id="letter-{{ product.id }}" name="properties[Letter-{{ product.id }}]" class="product-card__dropdown">
{% for letter in alphabet %}
<option value="{{ letter }}">{{ letter }}</option>
{% endfor %}
</select>
</div>
{% elsif product.template_suffix == "2Letter" %}
{% for i in (1..2) %}
<div class="product-card__option letter-option">
<label for="letter-{{ i }}-{{ product.id }}">{{ 'product.general.letter_' | append: i | t }}</label>
<select id="letter-{{ i }}-{{ product.id }}" name="properties[Letter-{{ i }}-{{ product.id }}]" class="product-card__dropdown">
{% for letter in alphabet %}
<option value="{{ letter }}">{{ letter }}</option>
{% endfor %}
</select>
</div>
{% endfor %}
{% elsif product.template_suffix == "3Letter" %}
{% for i in (1..3) %}
<div class="product-card__option letter-option">
<label for="letter-{{ i }}-{{ product.id }}">{{ 'product.general.letter_' | append: i | t }}</label>
<select id="letter-{{ i }}-{{ product.id }}" name="properties[Letter-{{ i }}-{{ product.id }}]" class="product-card__dropdown">
{% for letter in alphabet %}
<option value="{{ letter }}">{{ letter }}</option>
{% endfor %}
</select>
</div>
{% endfor %}
{% endif %}
<!-- Handle Date input option -->
{% if product.template_suffix == "1Date" %}
<div class="product-card__option date-option">
<label for="date-{{ product.id }}">{{ 'product.general.date' | t }}</label>
<input type="date" id="date-{{ product.id }}" name="properties[Date-{{ product.id }}]" class="product-card__dropdown product-card__input--date" placeholder="DD-MM-YYYY" required>
</div>
{% endif %}
{%- endif -%}
<!-- Rating and other optional product information -->
{%- if show_rating -%}
{%- render 'product-rating', product: product, show_empty: settings.show_product_rating_if_empty, display_mode: settings.product_rating_mode -%}
{%- endif -%}
</div>
</product-card>
<script>
$(document).on('change', 'product-card[handle="{{ product.handle }}"]', function() {
let selectedOptions = [];
// Capture selected color if available
const option1 = $(this).find('input:checked').val();
if(option1) selectedOptions.push(option1);
// Capture selected size, length, or letter
const option2 = $(this).find('select.product-card__dropdown').val();
if(option2) selectedOptions.push(option2);
// Capture selected option3
const option3 = $(this).find('select.product-card__dropdown3').val();
if(option3) selectedOptions.push(option3);
// Capture selected date if applicable
const selectedDate = $(this).find('input[type="date"]').val();
if(selectedDate) selectedOptions.push(selectedDate);
// Fetch available product variants from the product JSON
let variants = {{ product.variants | json }};
// Match selected options to find the corresponding variant
let selectedVariant = variants.find(variant => {
return variant.options.every(function(option, index) {
return option === selectedOptions[index];
});
});
if (selectedVariant) {
// Update hidden input with the correct variant ID
$('#selected-variant-id-{{ product.id }}').val(selectedVariant.id);
// Update displayed price
let locale = Shopify.locale;
let currency = Shopify.currency.active;
let country = Shopify.country;
let amount = new Intl.NumberFormat(`${locale}-${country}`, { style: "currency", currency: currency }).format(selectedVariant.price / 100 );
$(this).find('.product-price').text(amount);
} else {
console.log('No matching variant found for the selected options.');
}
});
</script>