first commit

This commit is contained in:
Axel
2026-04-30 03:13:31 +08:00
commit b9f58a20af
6 changed files with 2279 additions and 0 deletions
+306
View File
@@ -0,0 +1,306 @@
<!-- sections/predictive-search.liquid -->
<!-- yagor! -->
{%- liquid
comment
Get each resource's result count
endcomment
assign query_count = predictive_search.resources.queries.size
assign product_count = predictive_search.resources.products.size
assign collection_count = predictive_search.resources.collections.size
assign article_count = predictive_search.resources.articles.size
assign page_count = predictive_search.resources.pages.size
assign collection_counter = 0
assign total_results_counter = 0
assign terms = predictive_search.terms
-%}
{%- if collection_count > 0 -%}
{%- liquid
assign exclude_collections_strict = settings.exclude_collections_strict | split: ','
assign exclude_collections_contain = settings.exclude_collections_contain | split: ','
assign collections_markup = ''
-%}
{%- for collection in predictive_search.resources.collections -%}
{%- liquid
assign skip_current_collection = false
for exclude_collection_strict in exclude_collections_strict
assign exclude_collection_strict_stripped = exclude_collection_strict | strip
if exclude_collection_strict_stripped == collection.handle
assign skip_current_collection = true
break
endif
endfor
for exclude_collection_contain in exclude_collections_contain
assign exclude_collection_contain_stripped = exclude_collection_contain | strip
if collection.handle contains exclude_collection_contain_stripped
assign skip_current_collection = true
break
endif
endfor
if skip_current_collection
continue
endif
assign collection_counter = collection_counter | plus: 1
assign collection_counter_animate = collection_counter | plus: product_count | plus: query_count
-%}
{%- capture collections_markup -%}
{{ collections_markup }}
<div class="other__inline animates" style="animation-delay: {{ collection_counter_animate | times: 90 | plus: 10 }}ms;" id="predictive-search-option-collection-{{ collection_counter }}" role="option" aria-selected="false">
<p class="other__inline__title">
<a href="{{ collection.url }}">{{ collection.title }}</a>
</p>
</div>
{%- endcapture -%}
{%- endfor -%}
{%- endif -%}
{%- assign total_results = product_count | plus: query_count | plus: collection_counter | plus: page_count | plus: article_count -%}
{%- if predictive_search.performed -%}
<div class="wrapper">
<span class="predictive-search-status visually-hidden" data-predictive-search-live-region-count-value role="status">
{%- liquid
if total_results == 0
echo 'general.search.no_results' | t: terms: terms
else
echo 'general.search.results_with_count' | t: count: total_results | replace: '**', '' | append: ': '
if query_count > 0
assign count = query_count | plus: collection_counter
echo 'general.search.results_suggestions_with_count' | t: count: count | append: ', '
endif
if page_count > 0
assign count = page_count | plus: article_count
echo 'general.search.results_pages_with_count' | t: count: count | append: ', '
endif
if product_count > 0
echo 'general.search.results_products_with_count' | t: count: product_count
endif
endif
-%}
</span>
<div class="search__results__wrapper" id="predictive-search-results" role="listbox" aria-label="{{ 'general.search.search_for' | t: terms: terms }}">
{%- if total_results > 0 -%}
<div class="search__results__outer">
<div class="search__results__other">
{%- if query_count > 0 -%}
<div class="search__results__other__list">
<div class="search__results__heading">
<p class="search__results__title">{{ 'general.search.suggestions' | t }}</p>
<span class="badge">{{ query_count }}</span>
</div>
<ul aria-label="{{ 'general.search.suggestions' | t }}" role="group">
{%- for query in predictive_search.resources.queries -%}
{%- assign total_results_counter = total_results_counter | plus: 1 -%}
<li class="other__inline animates" id="predictive-search-option-query-{{ forloop.index }}" role="option" aria-selected="false" style="animation-delay: {{ total_results_counter | times: 90 | plus: 10 }}ms;">
<p class="other__inline__title">
<a href="{{ query.url }}">
<span>{{ query.styled_text }}</span>
</a>
</p>
</li>
{%- endfor -%}
</ul>
</div>
{%- endif -%}
{%- if product_count > 0 -%}
<div class="search__results__products">
<div class="search__results__products__title">
<div id="search_product_results_heading" class="search__results__heading">
<p class="search__results__title">{{ 'products.general.products' | t }}</p>
<span class="badge">{{ product_count }}</span>
</div>
</div>
<ul aria-label="{{ 'products.general.products' | t }}" class="search__results__products__list">
{%- for product in predictive_search.resources.products -%}
{%- liquid
assign total_results_counter = total_results_counter | plus: 1
assign on_sale = false
assign product_price = product.price
assign product_compare_at_price = product.compare_at_price
comment
start Yagi app code
endcomment
assign public_or_tags_matched = true
if product.metafields.app--168074346497.segment_tags.value.size > 0
assign public_or_tags_matched = false
endif
for etag in product.metafields.app--168074346497.segment_tags.value
if customer.tags contains etag
assign public_or_tags_matched = true
break
endif
endfor
if public_or_tags_matched
assign product_price = product.metafields.app--168074346497.min_auto_discounted_price.value | default: product.price
assign product_compare_at_price = product.compare_at_price
if shop.metafields.app--168074346497.discount_percentage.value > 0.005
assign discount_percentage = shop.metafields.app--168074346497.discount_percentage.value | times: 1.0
assign deducted_percentage = 1.0 | minus: discount_percentage
assign product_price = product.price | divided_by: 100.0 | times: deducted_percentage | times: 100.0 | ceil
endif
if product_price < product.price and product_compare_at_price == 0 or product_compare_at_price == blank
assign product_compare_at_price = product.price
endif
endif
comment
end Yagi app code
endcomment
if product_compare_at_price > product_price
assign on_sale = true
endif
assign sold_out = true
if product.available
assign sold_out = false
endif
-%}
<div class="product__inline animates" style="animation-delay: {{ total_results_counter | times: 90 | plus: 10 }}ms;" role="option" aria-selected="false">
<a href="{{ product.url }}" class="product__inline__link">
{%- if product.featured_image != blank -%}
<div role="presentation" class="product__inline__image">
{% assign alt = product.title | strip_html %}
{{ product.featured_image | image_url: width: product.featured_image.width | image_tag: class: 'img-aspect-ratio', alt: alt }}
</div>
{%- endif -%}
<div>
<p class="product__inline__title">
{{ product.title | strip_html }}
</p>
<p class="product__inline__price">
<span class="price{% if on_sale %} on-sale{% endif %}{% if sold_out %} sold-out{% endif %}">
{%- if settings.currency_code_enable -%}
{{ product_price | money_with_currency }}
{%- else -%}
{{ product_price | money }}
{%- endif -%}
</span>
{%- if sold_out -%}
<br>
<em>{{ 'products.product.sold_out' | t }}</em>
{%- endif -%}
{% if on_sale %}
<span class="compare-at">
{%- if settings.currency_code_enable -%}
{{ product_compare_at_price | money_with_currency }}
{%- else -%}
{{ product_compare_at_price | money }}
{%- endif -%}
</span>
{% endif %}
</p>
</div>
</a>
</div>
{%- endfor -%}
</ul>
</div>
{%- endif -%}
{%- if collection_counter > 0 -%}
{%- assign total_results_counter = total_results_counter | plus: collection_counter -%}
<div class="search__results__other__list">
<div id="search_collection_results_heading" class="search__results__heading">
<p class="search__results__title">{{ 'collections.sidebar.collections' | t }}</p>
<span class="badge">{{ collection_counter }}</span>
</div>
<ul aria-label="{{ 'collections.sidebar.collections' | t }}" role="group" class="search__results__collections__list">
{{ collections_markup }}
</ul>
</div>
{%- endif -%}
{%- if article_count > 0 -%}
<div class="search__results__other__list">
<div id="search_article_results_heading" class="search__results__heading">
<p class="search__results__title">{{ 'blogs.article.articles' | t }}</p>
<span class="badge">{{ article_count }}</span>
</div>
<ul aria-label="{{ 'blogs.article.articles' | t }}" role="group" class="search__results__articles__list">
{%- for article in predictive_search.resources.articles -%}
{%- assign total_results_counter = total_results_counter | plus: 1 -%}
<li class="other__inline animates" style="animation-delay: {{ total_results_counter | times: 90 | plus: 10 }}ms;" id="predictive-search-option-article-{{ forloop.index }}" role="option" aria-selected="false">
<p class="other__inline__title">
<a href="{{ article.url }}">{{ article.title }}</a>
</p>
</li>
{%- endfor -%}
</ul>
</div>
{%- endif -%}
{%- if page_count > 0 -%}
<div class="search__results__other__list">
<div id="search_page_results_heading" class="search__results__heading">
<p class="search__results__title">{{ 'general.page.pages' | t }}</p>
<span class="badge">{{ page_count }}</span>
</div>
<ul aria-label="{{ 'general.page.pages' | t }}" role="group" class="search__results__articles__list">
{%- for page in predictive_search.resources.pages -%}
{%- assign total_results_counter = total_results_counter | plus: 1 -%}
<li class="other__inline animates" style="animation-delay: {{ total_results_counter | times: 90 | plus: 10 }}ms;" id="predictive-search-option-page-{{ forloop.index }}" role="option" aria-selected="false">
<p class="other__inline__title">
<a href="{{ page.url }}">{{ page.title }}</a>
</p>
</li>
{%- endfor -%}
</ul>
</div>
{%- endif -%}
</div>
</div>
{%- else -%}
<div class="search__results__empty">
<div aria-live="polite">
<p>
{{ 'general.search.no_results_for' | t }}
<em>{{ terms }}</em>
</p>
</div>
</div>
{%- endif -%}
<div class="search__results__actions">
<button
class="btn btn--outline search__results__btn !flex items-center flex-nowrap gap-r2 whitespace-normal max-w-full"
tabindex="-1"
role="option"
aria-selected="false"
>
<span class="line-clamp-1 text-ellipsis" data-predictive-search-search-for-text>{{ 'general.search.search_for' | t: terms: terms }}</span>
<span class="accent-size-5 [&>svg]:inline-block rtl:-scale-x-100">{%- render 'icon-core-chevron-right' -%}</span>
</button>
</div>
</div>
</div>
{%- endif -%}
+384
View File
@@ -0,0 +1,384 @@
<!-- /sections/search.liquid -->
{%- liquid
assign has_filters = false
if search.filters != empty and section.settings.show_filters
assign has_filters = true
endif
assign types = search.types | join: ',' | url_encode
assign types = 'type=' | append: types
assign accordion_initial_state = 'accordion-is-open'
assign accordion_open_boolean = true
if section.settings.collapse_filters
# no class means the accordion is closed
assign accordion_initial_state = ''
# accordion_open_boolean is used to set aria-expanded and
# match js logic for 'display: none' on accordion body elements
# which are non-adjacent siblings that cannot easily be styled with CSS
assign accordion_open_boolean = false
endif
-%}
<section class="page-search {{ section.settings.width }} section-padding"
data-section-type="search-page"
data-section-id="{{ section.id }}"
style="--PT: {{ section.settings.padding_top }}px; --PB: {{ section.settings.padding_bottom }}px;">
<div class="search__page__heading">
{% render 'search-bar' %}
{%- if search.terms.size > 0 -%}
<div class="search__page__query note">
<p>
{%- if search.results_count > 0 -%}
{{ 'general.search.results_for' | t }} <span class="strong">{{ search.terms | escape }}</span>
{%- else -%}
{{ 'general.search.no_results_for' | t }} <span class="strong">{{ search.terms | escape }}</span>
{%- endif -%}
</p>
</div>
{%- endif -%}
</div>
{% if search.performed %}
{%- if has_filters -%}
{%- assign filter_count = 0 -%}
{%- capture filter_clears -%}
{%- for filter in search.filters -%}
{%- if filter.type == "price_range" -%}
{%- if filter.min_value.value != nil or filter.max_value.value != nil -%}
{%- assign filter_count = filter_count | plus: 1 -%}
<a class="active__filters__remove" href="{{ filter.url_to_remove }}" data-filter-update-url>
{%- assign min_value = filter.min_value.value | default: 0 -%}
{%- assign max_value = filter.max_value.value | default: filter.range_max -%}
{{ min_value | money_without_trailing_zeros }} - {{ max_value | money_without_trailing_zeros }} X
</a>
{%- endif -%}
{%- else -%}
{%- for filter_value in filter.active_values -%}
{%- assign filter_count = filter_count | plus: 1 -%}
<a class="active__filters__remove" href="{{ filter_value.url_to_remove }}" data-filter-update-url>
{{ filter_value.label }} <span class="filter__x">X</span>
</a>
{%- endfor -%}
{%- endif -%}
{%- endfor -%}
{%- endcapture -%}
<nav class="collection__nav" data-collection-tools>
<div class="collection__nav__buttons">
{% if filter_count > 0 %}
{% capture current_filters_count %}
<div class="badge">{{ filter_count }}</div>
{% endcapture %}
{% endif %}
<button class="collection__filters__toggle filters--default-visible drawer--visible" data-filters-toggle="filters">
<span class="hide-filters">{{ 'collections.sidebar.hide_filters' | t }}</span>
<span class="show-filters">{{ 'collections.sidebar.filter' | t }}</span>
<span data-active-filters-count>{{ current_filters_count }}</span>
{% render 'icon-core-filter' %}
</button>
</div>
</nav>
{%- endif -%}
<div class="collection__active__filters__wrapper{% unless filter_count > 0 %} is-hidden{% endunless %}">
<div class="collection__active__filters" data-active-filters>
{%- if filter_count > 1 -%}
<a href="{{ routes.search_url }}?{{ types }}&q={{ search.terms | url_encode }}" class="active__filters__clear" data-filter-update-url>{{ 'collections.sidebar.all_tags' | t }}</a>
{%- endif -%}
{{- filter_clears -}}
</div>
{%- if section.settings.show_products_count -%}
<div class="collection__count" data-products-count>
{%- if search.results_count -%}
{{ 'general.search.results_with_count' | t: count: search.results_count | replace_first: '**', '<strong>' | replace_first: '**', '</strong>' | replace_first: '**', '<strong>' | replace_first: '**', '</strong>' }}
{%- endif -%}
</div>
{%- endif -%}
</div>
<div class="collection__content">
{%- if has_filters -%}
<div class="collection__filters__wrapper filters--default-visible" data-collection-sidebar data-filters="filters" data-default-hide="false">
<div class="collection__filters__outer">
<div class="drawer__top">
<div class="drawer__top__left">
<p class="cart__drawer__title">{{ 'collections.sidebar.filter' | t }}</p>
</div>
<button class="drawer__button drawer__close"
data-first-focus
data-filters-toggle="filters"
aria-label="{{ 'general.accessibility.close' | t }}">
{% render 'icon-core-x' %}
</button>
</div>
<div class="collection__filters__inner">
<form data-sidebar-filter-form>
<input type="hidden" name="q" value="{{ search.terms }}">
{%- for filter in search.filters -%}
{% render 'filters',
filter: filter,
forloop: forloop,
accordion_open_boolean: accordion_open_boolean,
accordion_initial_state: accordion_initial_state %}
{%- endfor -%}
</form>
</div>
</div>
<span class="drawer__underlay" data-filters-underlay>
<span class="drawer__underlay__fill"></span>
<span class="drawer__underlay__blur"></span>
</span>
</div>
{%- endif -%}
<div class="collection__products" data-products-grid>
{% paginate search.results by section.settings.pagination_count %}
{% for item in search.results %}
<div class="search__item__generic">
{% if item.image or item.featured_media.preview_image %}
<div class="search__item__generic__image">
<a href="{{ item.url }}" title="{{ item.title | escape }}">
{% assign image = item.featured_media.preview_image | default: item.image %}
{% assign image_width = 70 | at_most: image.width %}
{% assign image_width_2x = image_width | times: 2 | at_most: image.width %}
{% assign alt = image.alt %}
{% capture srcset %}
{{ image | image_url: width: image_width_2x }} 2x,
{{ image | image_url: width: image_width }}
{% endcapture %}
{%- render 'image',
img_object: image,
wh_ratio: image.aspect_ratio,
width: image_width,
srcset: srcset,
alt: alt
-%}
</a>
</div>
{% endif %}
<div class="search__item__generic__text">
<p class="product__inline__title">
{{ item.title | link_to: item.url }}
</p>
{% if item.object_type == 'product' %}
<p class="product__inline__price">
{%- assign product = item -%}
{%- assign on_sale = false -%}
{%- assign sold_out = true -%}
{%- assign current_variant = product.first_available_variant -%}
{% liquid
assign product_price = product.price
assign product_compare_at_price = product.compare_at_price
comment
start Yagi app code
endcomment
assign public_or_tags_matched = true
if product.metafields.app--168074346497.segment_tags.value.size > 0
assign public_or_tags_matched = false
endif
for etag in product.metafields.app--168074346497.segment_tags.value
if customer.tags contains etag
assign public_or_tags_matched = true
break
endif
endfor
if public_or_tags_matched
assign product_price = product.metafields.app--168074346497.min_auto_discounted_price.value | default: product.price
assign product_compare_at_price = product.compare_at_price
if shop.metafields.app--168074346497.discount_percentage.value > 0.005
assign discount_percentage = shop.metafields.app--168074346497.discount_percentage.value | times: 1.0
assign deducted_percentage = 1.0 | minus: discount_percentage
assign product_price = product.price | divided_by: 100.0 | times: deducted_percentage | times: 100.0 | ceil
endif
if product_price < product.price and product_compare_at_price == 0 or product_compare_at_price == blank
assign product_compare_at_price = product.price
endif
endif
comment
end Yagi app code
endcomment
%}
{%- if product_compare_at_price > product_price -%}
{%- assign on_sale = true -%}
{%- endif -%}
{%- if product.available -%}
{%- assign sold_out = false -%}
{%- endif -%}
<span class="price{% if on_sale %} on-sale{% endif %}">
{% if product.price_varies %}{{ 'products.general.from' | t }} {% endif %}
{% if settings.currency_code_enable %}
{{ product_price | money_with_currency }}
{% else %}
{{ product_price | money }}
{% endif %}
</span>
{% if on_sale %}
<span class="compare-at">
{% if settings.currency_code_enable %}
{{ product_compare_at_price | money_with_currency }}
{% else %}
{{ product_compare_at_price | money }}
{% endif %}
</span>
{% endif %}
{% if current_variant.unit_price %}
{% capture unit_price_separator %}
<span aria-hidden="true">/</span><span class="visually-hidden">{{ 'general.accessibility.unit_price_separator' | t }}&nbsp;</span>
{% endcapture %}
{% capture unit_price_base_unit %}
{% if current_variant.unit_price_measurement.reference_value != 1 %}
{{ current_variant.unit_price_measurement.reference_value }}
{% endif %}
{{ current_variant.unit_price_measurement.reference_unit }}
{% endcapture %}
<br />
<span class="visually-hidden">{{ 'products.product.unit_price_label' | t }}</span>
<span class="price-per-unit">{{ current_variant.unit_price | money }}{{ unit_price_separator }}{{ unit_price_base_unit }}</span>
{% endif %}
{% if sold_out %}
<br /><em>{{ 'products.product.sold_out' | t }}</em>
{% endif %}
</p>
{%- if settings.product_grid_show_rating and product.metafields.reviews.rating.value != blank -%}
<div class="rating__wrapper__search">
{% render 'product-rating', product: product, show_rating_count: settings.product_grid_show_rating_count %}
</div>
{%- endif -%}
{% endif %}
</div>
</div>
{% unless forloop.last %}<hr>{% endunless %}
{% endfor %}
{% if paginate.pages > 1 %}
<div class="text-center pt-r11">
{% render 'pagination-custom', paginate: paginate %}
</div>
{% endif %}
{% endpaginate %}
</div>
</div>
{% endif %}
</section>
{% schema %}
{
"name": "Search",
"settings": [
{
"type": "header",
"content": "Search"
},
{
"type": "checkbox",
"id": "show_filters",
"default": true,
"label": "Show product filters"
},
{
"type": "checkbox",
"id": "collapse_filters",
"label": "Collapse filter accordions",
"info": "Active filters will remain open",
"default": false
},
{
"type": "checkbox",
"id": "show_products_count",
"label": "Show products count",
"info": "Product count will be shown when filters are active",
"default": true
},
{
"type": "range",
"id": "pagination_count",
"min": 3,
"max": 50,
"step": 1,
"label": "Results per page",
"default": 24
},
{
"type": "header",
"content": "Section spacing"
},
{
"type": "select",
"id": "width",
"label": "Width",
"default": "wrapper",
"options": [
{
"value": "wrapper--full",
"label": "Full width padded"
},
{
"value": "wrapper",
"label": "Page width"
},
{
"value": "wrapper--narrow",
"label": "Page width narrow"
},
{
"value": "wrapper--tiny",
"label": "Page width extra narrow"
}
]
},
{
"type": "range",
"id": "padding_top",
"min": 0,
"max": 180,
"step": 2,
"unit": "px",
"label": "Padding top",
"default": 36
},
{
"type": "range",
"id": "padding_bottom",
"min": 0,
"max": 180,
"step": 2,
"unit": "px",
"label": "Padding bottom",
"default": 36
}
]
}
{% endschema %}
+173
View File
@@ -0,0 +1,173 @@
<!-- /snippets/product-buttons.liquid -->
{%- liquid
assign buybutton_setting = false
assign gift_card_recipient_feature_active = false
assign current_variant = product.selected_or_first_available_variant
if block.settings.enable_gift_card_recipient and product.gift_card?
assign gift_card_recipient_feature_active = true
endif
comment
Quick buy buttons are incompatable with gift card products and subscription products
endcomment
if block.settings.enable_payment_button and gift_card_recipient_feature_active == false
assign buybutton_setting = true
endif
if product.selling_plan_groups.size > 0
assign buybutton_setting = false
endif
-%}
<div class="product__block__buttons" style="--PB: {{ block.settings.padding_bottom }}px;" {{ block.shopify_attributes }} >
<div data-product-form-outer>
{% comment %} The [data-product-form] tag distinguishes the product form from upsell instant-add-buttons. {% endcomment %}
{%- form 'product', product, id: uniq_id, data-product-form: '', data-product-handle: product.handle -%}
{%- unless hidden -%}
<div class="product__form__inner" data-form-inner>
{% comment %}
Note: the gift card recipient form is controlled in Checkout and must contain these undocumented line item propeties:
* properties[__shopify_send_gift_card_to_recipient] - Hidden - Toggles the feature on/off in checkout, following properties have no effect without it
* properties[Recipient email] - The email of the gift card recipient
* properties[Recipient name] - The name of the gift card recipient
* properties[Message] - Shopify chose a highly generic name for the gift card message, this could easily conflict with an app or custom code
{% endcomment %}
{%- if gift_card_recipient_feature_active -%}
{%- render 'gift-card-recipient-form', product: product, form: form, section: section -%}
{%- endif -%}
<div class="product__submit" data-buttons-wrapper data-add-action-wrapper data-error-boundary>
<div data-error-display role='alert' class="add-action-errors"></div>
{%- assign button_text = 'products.product.add_to_cart' | t -%}
{%- if product.metafields.theme.preorder.value == true -%}
{% comment %} Add a line item property with 'Sale type: Pre-order' and make the button say 'Pre-order'{% endcomment %}
<input type="hidden" data-product-preorder name="properties[{{ 'products.product.sale_type' | t }}]" value="{{ 'products.product.pre_order' | t }}">
{%- assign button_text = 'products.product.pre_order' | t -%}
{%- endif -%}
{%- render 'sibling-color-as-line-prop' product: product -%}
{%- if current_variant == null -%}
{%- assign button_text = 'products.product.unavailable' | t -%}
{%- endif -%}
{%- if current_variant.available == false -%}
{%- assign button_text = 'products.product.sold_out' | t -%}
{%- endif -%}
<div class="product__submit__buttons{% if product.has_only_default_variant %} product__submit__buttons--clear{% endif %}">
<span class="sr-only" aria-live="polite" x-show="isLoading">
{{ 'products.product.adding_to_cart' | t }}
</span>
<span class="sr-only" aria-live="polite" x-show="isSuccess">
{{ 'products.product.added_to_cart' | t }}
</span>
<button
type="submit"
name="add"
class="btn--outline btn--full btn--primary btn--add-to-cart"
data-add-to-cart
{% comment %} Explicitly add aria label so that button text isn't read out in screen readers in uppercase {% endcomment %}
aria-label="{{ button_text }}"
:class="{
'has-success': isSuccess,
'loading': isLoading
}"
{% unless current_variant.available %} disabled="disabled" {% endunless %}
>
<span class="btn-state-ready flex justify-center">
<span data-add-to-cart-text>
{{ button_text }}
</span>
{% if current_variant %}
{% liquid
comment
start Yagi app code
endcomment
assign current_variant_price = current_variant.price
assign public_or_tags_matched = true
if product.metafields.app--168074346497.segment_tags.value.size > 0
assign public_or_tags_matched = false
endif
for etag in product.metafields.app--168074346497.segment_tags.value
if customer.tags contains etag
assign public_or_tags_matched = true
break
endif
endfor
if public_or_tags_matched
assign current_variant_price = current_variant.metafields.app--168074346497.auto_discounted_price.value | default: current_variant.price
if current_variant.metafields.app--168074346497.discount_type.value != nil and current_variant.metafields.app--168074346497.discount_type.value != "fixed" and product.metafields.app--168074346497.discount_percentage.value > 0.01
assign deducted_percentage = 1.0 | minus: product.metafields.app--168074346497.discount_percentage.value
if current_variant.metafields.app--168074346497.discount_percentage.value > 0.01
assign deducted_percentage = 1.0 | minus: current_variant.metafields.app--168074346497.discount_percentage.value
endif
assign current_variant_price = current_variant.price | divided_by: 100.0 | times: deducted_percentage | times: 100.0 | ceil
endif
if shop.metafields.app--168074346497.discount_percentage.value > 0.005
assign discount_percentage = shop.metafields.app--168074346497.discount_percentage.value | times: 1.0
assign deducted_percentage = 1.0 | minus: discount_percentage
assign current_variant_price = current_variant.price | divided_by: 100.0 | times: deducted_percentage | times: 100.0 | ceil
endif
endif
comment
end Yagi app code
endcomment
%}
<span class="cta__dot opacity-50">•</span>
<span
data-button-price
{% if public_or_tags_matched %}data-yagi-matched{% endif %}
data-yagi-price="{{ current_variant_price }}"
x-text="$formatCurrency({{- current_variant_price -}} * Math.max(quantity, 1), { form: 'short', currency: '{{ cart.currency.iso_code }}' })"
>
{%- if settings.currency_code_enable -%}
{{ current_variant_price | money_with_currency }}
{%- else -%}
{{ current_variant_price | money }}
{%- endif -%}
</span>
{% endif %}
</span>
<span class="btn-state-loading">
<svg height="18" width="18" class="svg-loader">
<circle r="7" cx="9" cy="9" />
<circle stroke-dasharray="87.96459430051421 87.96459430051421" r="7" cx="9" cy="9" />
</svg>
</span>
<span class="btn-state-complete">&nbsp;</span>
</button>
{%- if buybutton_setting -%}
<div class="product__submit__quick">
{{ form | payment_button }}
</div>
{%- endif -%}
</div>
</div>
</div>
{% comment %} Shop pay split payment terms {% endcomment %}
<div class="shop-pay-terms">{{- form | payment_terms -}}</div>
{%- endunless -%}
{% comment %} The input with name="id" indicates the variant to add to cart {% endcomment %}
<input type="hidden" name="id" x-model.fill="variantId" value="{{ current_variant.id }}">
{%- endform -%}
</div>
</div>
+578
View File
@@ -0,0 +1,578 @@
<!-- /snippets/product-grid-item-variant.liquid -->
{% comment %}
Inner content for a grid item
{% endcomment %}
{%- liquid
assign on_sale = false
assign product_price = product.price
assign product_compare_at_price = product.compare_at_price
comment
start Yagi app code
endcomment
assign public_or_tags_matched = true
if product.metafields.app--168074346497.segment_tags.value.size > 0
assign public_or_tags_matched = false
endif
for etag in product.metafields.app--168074346497.segment_tags.value
if customer.tags contains etag
assign public_or_tags_matched = true
break
endif
endfor
if public_or_tags_matched
assign product_price = product.metafields.app--168074346497.min_auto_discounted_price.value | default: product.price
assign product_compare_at_price = product.compare_at_price
if shop.metafields.app--168074346497.discount_percentage.value > 0.005
assign discount_percentage = shop.metafields.app--168074346497.discount_percentage.value | times: 1.0
assign deducted_percentage = 1.0 | minus: discount_percentage
assign product_price = product.price | divided_by: 100.0 | times: deducted_percentage | times: 100.0 | ceil
endif
if product_price < product.price and product_compare_at_price == 0 or product_compare_at_price == blank
assign product_compare_at_price = product.price
endif
endif
comment
end Yagi app code
endcomment
if product_compare_at_price > product_price
assign on_sale = true
endif
assign sold_out = true
if product.available
assign sold_out = false
endif
assign sellout_badge = false
if sold_out and settings.badge_sellout
assign sellout_badge = true
endif
assign sale_badge = false
if on_sale and settings.badge_sale
assign sale_badge = true
assign sale_badge_content = 'products.product.sale' | t
if settings.badge_sale_discount
if settings.badge_sale_type == 'dollar'
if settings.currency_code_enable
assign sale_badge_content = product_compare_at_price | minus: product_price | money_with_currency
else
assign sale_badge_content = product_compare_at_price | minus: product_price | money_without_trailing_zeros
endif
else
assign difference = product_compare_at_price | minus: product_price
assign percent_off = difference | times: 1.0 | divided_by: product_compare_at_price | times: 100
assign sale_badge_content = percent_off | floor | append: '%'
endif
assign save_word = 'products.product.save' | t | append: ' '
assign sale_badge_content = sale_badge_content | prepend: save_word
endif
endif
assign custom_badge = false
if settings.badge_custom
if product.metafields.theme.badge != blank and product.metafields.theme.badge.type == 'single_line_text_field'
assign custom_badge = true
assign custom_badge_content = product.metafields.theme.badge.value
endif
for tag in product.tags
if tag contains "_badge_"
assign tag_content = tag | remove: '_badge_' | replace: '_', ' '
if tag_content != ''
assign custom_badge = true
assign custom_badge_content = tag_content
endif
break
endif
endfor
endif
if badge_string and badge_string != ''
assign custom_badge = true
assign custom_badge_content = badge_string
endif
assign tagged = false
if sellout_badge or sale_badge or custom_badge
assign tagged = true
endif
comment
Disqualify options that have more than 15 variants or are a combined length of > 90 characters
endcomment
if inline_variant_buttons.values.size > 15
assign inline_variant_buttons = nil
endif
if inline_variant_buttons
assign all_characters = inline_variant_buttons.values | join: ""
if all_characters.size >= 90
assign inline_variant_buttons = nil
endif
endif
# Sellign plans can be added along with inline or instand buttons if
# the product has exactly 1 selling plan with subscriptions required
assign simple_selling_plan = nil
if inline_variant_buttons or instant_add_button
if product.requires_selling_plan and product.selling_plan_groups.size == 1 and product.selling_plan_groups[0].selling_plans.size == 1
# one variant, one required subscription, no choices to make
assign simple_selling_plan = product.selected_or_first_available_selling_plan_allocation.selling_plan
elsif product.selling_plan_groups.size > 0
# Abort instant and inline add buttons, subs choices must be made
assign inline_variant_buttons = nil
assign instant_add_button = nil
endif
endif
# Catch case where first sibling has inline variants and subsequent do not
if product.has_only_default_variant and inline_variant_buttons
assign inline_variant_buttons = nil
assign instant_add_button = true
endif
# Allow configuration of image sizing for different numbers of grid columns
# Note: desktop/tablet are set to a default of 3 just in case the grid sizes are not set to prevent accidental gigantic full-width images from being loaded
assign columns_desktop = columns_desktop | default: section.settings.grid_large | default: 3
assign columns_tablet = columns_tablet | default: section.settings.grid_medium | default: columns_desktop | default: 3
assign columns_mobile = columns_mobile | default: section.settings.grid_mobile | default: 1
assign section_width = section_width | default: section.settings.width | default: null
-%}
{%- capture badge -%}
{%- if tagged %}
{%- if custom_badge -%}
<div class="product__badge product__badge--custom product__badge--{{ custom_badge_content | strip_html | handle }}">{{ custom_badge_content }}</div>
{%- elsif sellout_badge -%}
<div class="product__badge product__badge--sold">{{ 'products.product.sold_out' | t }}</div>
{%- elsif sale_badge -%}
<div class="product__badge product__badge--sale">{{ sale_badge_content }}</div>
{%- endif -%}
{%- endif -%}
{%- endcapture -%}
{%- liquid
assign first_image = product.media[0].preview_image
assign container_wh_ratio = settings.product_card_wh_ratio
assign image_cover = true
case settings.product_grid_image
when 'crop'
assign image_wh_ratio = container_wh_ratio
when 'uneven'
assign container_wh_ratio = first_image.aspect_ratio | default: settings.product_card_wh_ratio
when 'scale'
assign image_cover = false
assign image_wh_ratio = first_image.aspect_ratio | default: settings.product_card_wh_ratio
endcase
# Behavior is inferred based on setting and passed into JS
assign image_hover = 'disabled'
assign images_limit = settings.cycle_images_limit
case images_limit
when 1
assign image_hover = 'disabled'
when 2
assign image_hover = 'second_immediately'
else
assign image_hover = 'cycle_images'
endcase
-%}
{%- capture sizes -%}
{%- render 'image-grid-sizes',
columns_desktop: columns_desktop,
columns_tablet: columns_tablet,
columns_mobile: columns_mobile,
section_width: section_width
%}
{%- endcapture -%}
<product-grid-item-variant
class="
product-grid-item__content{% if on_sale %} on-sale{% endif %}
{% if sold_out %} sold-out{% endif %}
{% if tagged %} tagged{% endif %}
{% comment %} only used to hide badge on hover {% endcomment %}
{% if image_hover == 'cycle_images' %} is-slideshow{% endif %}
"
style="
--enter-animation-duration: 225ms;
--exit-animation-duration: 400ms;
"
data-grid-item="{{ product.id }}"
data-slideshow-style="{{ image_hover }}"
data-grid-item-variant="{{ variant.id }}"
{% if visible != true %} hidden {% endif %}
aria-label="{{ variant.title }}"
>
<div class="product-grid-item__container" data-error-boundary>
<div data-error-display class="product-grid-item__error-display">&nbsp;</div>
<a href="{{ product.url }}" data-grid-link aria-label="{{ product.title | strip_html | escape }}">
<div
class="product-grid-item__images aspect-[--wh-ratio]"
data-grid-images data-grid-slide
style="
--wh-ratio: {{ container_wh_ratio }};
"
>
{%- if product.media.size > 0 -%}
{% comment %}
Manually store and increment this variable since we start skipping images below when we exceed the allowed images which would
make using e.g. forloop.index0 not work since the index of the variant image could be greater than the number of images allowed
{% endcomment %}
{%- assign image_index = 0 -%}
{%- for media in product.media -%}
{%- liquid
# If we've already exceeded the number of allowed images, and this is not the variant featured media, skip it
if image_index > images_limit
if product.selected_variant and product.selected_variant.featured_media.id != media.id
continue
elsif variant.featured_media.id != media.id
continue
endif
endif
assign img_object = media.preview_image
assign class = "product-grid-item__image"
assign loading = 'lazy'
assign fetchpriority = "low"
assign visible = false
assign active_class = 'is-active'
assign is_variant_featured_media = false
assign is_selected_variant = false
assign preload_image = false
assign loading_image = 'lazy'
if variant.featured_media and media.id == variant.featured_media.id
# Variant image is not necessarily first image or default image
assign is_variant_featured_media = true
endif
if product.selected_variant and product.selected_variant.featured_image
if product.selected_variant.featured_media.id == media.id
assign is_selected_variant = true
endif
# Show variant image is there is a collection filter applied
if is_variant_featured_media and is_selected_variant
assign visible = true
endif
else
# If no filters are active show the first image first
if forloop.first
assign visible = true
endif
endif
if visible
assign fetchpriority = "high"
if preload
assign loading = 'eager'
assign preload_image = true
endif
if eagerload
assign loading_image = 'eager'
endif
endif
%}
{%- capture srcset -%}
{%- render 'image-grid-srcset',
image: img_object,
columns_desktop: columns_desktop,
columns_tablet: columns_tablet,
columns_mobile: columns_mobile,
section_width: section_width,
wh_ratio: image_wh_ratio,
crop: 'center'
%}
{%- endcapture -%}
{% comment %} Use a template to prevent hidden images from loading until user begins slideshow{% endcomment %}
<product-grid-item-image
class="
product-grid-item__image-wrapper
{% if visible %}{{ active_class }}{% endif %}
"
data-grid-image="{{ image_index }}"
data-grid-image-target="{{ media.id }}"
data-variant-id="{{ }}"
loading="{{ loading }}"
{% if visible %}data-grid-current-image{% endif %}
{% if is_selected_variant and visible %}
data-slide-for-filter-selected-variant
{% endif %}
{% if is_variant_featured_media %}
data-slide-for-variant-media
{% endif %}
>
{% unless visible %}<template>{% endunless %}
{% render 'image',
cover: image_cover,
img_object: img_object,
class: class,
sizes: sizes,
srcset: srcset,
preload: preload_image,
loading: loading_image,
fetchpriority: fetchpriority,
wh_ratio: image_wh_ratio,
placeholder: placeholder
%}
{% unless visible %}</template>{% endunless %}
</product-grid-item-image>
{%- assign image_index = image_index | plus: 1 -%}
{%- endfor -%}
{% else %}
<div class="product-grid-item__image-wrapper is-active">
{% render 'image',
cover: image_cover,
img_object: null,
class: class,
placeholder: placeholder,
wh_ratio: image_wh_ratio
%}
</div>
{%- endif -%}
</div>
{{ badge }}
</a>
{% capture quick_action_toolbar_classes %}
group/quick-actions-toolbar
absolute
flex flex-col justify-end items-end overflow-hidden
top-[calc(var(--inner)/2)]
right-[calc(var(--inner)/2)]
bottom-[calc(var(--inner)/2)]
left-[calc(var(--inner)/2)]
transition duration-[--exit-animation-duration]
md:items-normal
md:opacity-0
md:translate-y-r4
md:group-hover/product-grid-item:opacity-100
md:group-hover/product-grid-item:translate-y-0
md:group-focus-within/product-grid-item:opacity-100
md:group-focus-within/product-grid-item:translate-y-0
{% comment %}
Prevent pointer events on the outer <inline-add-product> element since it covers the whole card
and we only want the options menu to show when either the button wrapper or options menu itself
are still being hovered
{% endcomment %}
pointer-events-none
{% endcapture %}
{% capture quick_action_button_classes %}
{{ settings.quick_add_button_color }}
group/quick-action-button
bg-button
flex items-center justify-center
type-accent font-bold text-r3
transition-opacity duration-[--enter-animation-duration]
pointer-events-auto
w-r12 aspect-square
min-w-[40px]
min-h-[40px]
md:min-h-[48px]
md:px-r8 md:py-r5 md:w-full md:aspect-auto
{% if sold_out %}opacity-50 !cursor-not-allowed{% endif %}
{% endcapture %}
{%- if instant_add_button %}
{% comment %} Allow for shorter default text on longer translations {% endcomment %}
{% liquid
if product.metafields.theme.preorder.value == true
assign button_text = 'products.general.instant_add_pre_order' | t
else
assign button_text = 'products.general.instant_add' | t
endif
%}
{% capture button %}
<button
data-add-to-cart
type="submit"
name="add"
class="{{ quick_action_button_classes }}"
:class="{
'has-success': isSuccess,
'loading': isLoading
}"
title="{% if sold_out %}{{ 'products.product.sold_out' | t }}{% else %}{{ button_text }}{% endif %}"
:disabled="{{sold_out}} || isDisabled"
aria-label="{{ button_text }}"
>
<span class="btn-state-ready text-button-contrast group-hover/quick-action-button:text-button-contrast/50 whitespace-nowrap">
<span class="hidden md:block">
{{ button_text }}
</span>
<span aria-hidden class="block md:hidden">
{% render 'icon-set-classic-cart' %}
</span>
</span>
<span class="btn-state-loading">
<svg height="18" width="18" class="svg-loader" style="--border: rgb(var(--rgb-button-contrast) / 50%); --text: rgb(var(--rgb-button-contrast));">
<circle r="7" cx="9" cy="9" />
<circle stroke-dasharray="87.96459430051421 87.96459430051421" r="7" cx="9" cy="9" />
</svg>
</span>
<span class="btn-state-complete" style="--primary: rgb(var(--rgb-button-contrast));">&nbsp;</span>
</button>
{% endcapture %}
<div class="{{quick_action_toolbar_classes}}">
{% render 'product-add-button-form', variant: variant, selling_plan: simple_selling_plan, button: button, class: "md:w-full" %}
</div>
{%- elsif inline_variant_buttons %}
<div class="{{quick_action_toolbar_classes}}" x-data="productGridItemQuickAddMenu()">
<button
class="
{{ quick_action_button_classes }}
transition-opacity
"
title="{{ 'products.general.inline_add' | t }}"
aria-haspopup="true"
:aria-expanded="isOpen"
:id="$id('quick-add-menu-button')"
:aria-controls="$id('quick-add-menu-slideover')"
@click.stop="open()"
@mouseover="open()"
x-ref="button"
:class="
isOpen ?
'duration-[--enter-animation-duration] delay-0 opacity-0 md:opacity-100' :
'duration-[--exit-animation-duration] delay-[--exit-animation-duration] opacity-100'
"
aria-label="{{ 'products.general.inline_add' | t }}"
>
<span
class="
whitespace-nowrap
text-button-contrast
transition translate-y-0 transform
"
:class="
isOpen ?
'duration-[--enter-animation-duration] delay-0 translate-y-full opacity-0' :
'duration-[--exit-animation-duration] delay-[calc(var(--exit-animation-duration))] translate-y-0 opacity-100'
"
>
<span class="hidden md:block">
{{ 'products.general.inline_add' | t }}
</span>
<span aria-hidden class="block md:hidden">
{% render 'icon-set-classic-cart' %}
</span>
</span>
</button>
<div
class="absolute top-0 right-0 bottom-0 left-0 overflow-hidden flex flex-col justify-end"
role="popover"
x-show="isOpen"
x-cloak
:aria-labelledby="$id('quick-add-menu-button')"
>
<div
class="
{{ settings.quick_add_button_color }}
top-0 right-0 bottom-0 left-0 top-auto max-h-full overflow-scroll scrollbar-hide
pointer-events-auto
transition transform
absolute
origin-bottom
bg-button
md:opacity-100
"
x-show="isOpen"
{% comment %}
Animate outer slider up or down
{% endcomment %}
x-transition:enter="duration-[calc(var(--enter-animation-duration)*2)] delay-[calc(var(--enter-animation-duration)/2)]"
x-transition:enter-start="invisible translate-y-full opacity-0 md:opacity-100"
x-transition:enter-end="visible translate-y-0 opacity-100 md:opacity-100"
x-transition:leave="duration-[calc(var(--exit-animation-duration)*2)] delay-[calc(var(--exit-animation-duration)/4)]"
x-transition:leave-start="visible translate-y-0 opacity-100 md:opacity-100"
x-transition:leave-end="invisible translate-y-full opacity-0 md:opacity-100"
>
<div
class="transition transform"
x-show="isOpen"
{% comment %}
Stagger inner slider up/down animation so it animates _after_ outer slider
{% endcomment %}
x-transition:enter="duration-[calc(var(--enter-animation-duration)*2)] delay-[calc(var(--enter-animation-duration)/2)]"
x-transition:enter-start="translate-y-1/2 opacity-0"
x-transition:enter-end="translate-y-0 opacity-full"
x-transition:leave="duration-[calc(var(--exit-animation-duration))] delay-0"
x-transition:leave-start="translate-y-0 opacity-full"
x-transition:leave-end="translate-y-1/2 opacity-0"
>
{% render 'product-grid-item-quick-add-toolbar',
inline_variants: inline_variants,
inline_variant_buttons: inline_variant_buttons,
simple_selling_plan: simple_selling_plan
%}
</div>
</div>
</div>
</div>
{%- elsif settings.quickview_enable -%}
<div class="{{quick_action_toolbar_classes}}" x-data="productQuickViewButton({{ product.id }}, '{{ product.handle }}')">
<div class="quickview md:w-full" data-quickview-holder="{{ product.id }}" data-add-action-wrapper>
<button
type="button"
class="{{ quick_action_button_classes }}"
@click.prevent="clickQuickviewButton"
:class="{
'loading': isLoading
}"
title="{% if sold_out %}{{ 'products.product.sold_out' | t }}{% else %}{{ 'products.general.quick_view' | t }}{% endif %}"
:disabled="{{sold_out}} || isDisabled"
aria-label="{{ 'products.general.quick_view' | t }}"
>
<span class="btn-state-ready text-button-contrast group-hover/quick-action-button:text-button-contrast/50 whitespace-nowrap">
<span class="hidden md:block">
{{ 'products.general.quick_view' | t }}
</span>
<span aria-hidden class="block md:hidden">
{% render 'icon-set-classic-cart' %}
</span>
</span>
<span class="btn-state-loading">
<svg height="18" width="18" class="svg-loader" style="--border: rgb(var(--rgb-button-contrast) / 50%); --text: rgb(var(--rgb-button-contrast));">
<circle r="7" cx="9" cy="9" />
<circle stroke-dasharray="87.96459430051421 87.96459430051421" r="7" cx="9" cy="9" />
</svg>
</span>
</button>
<script data-quickview-modal-template type="text/x-template">
<div class="drawer drawer--right quickview__modal" data-quickview-modal data-form-holder id="{{ product.id }}" aria-hidden="true">
<div class="drawer__content" data-product-quickview-ajax x-section-api='api-product-quickview'></div>
<span class="drawer__underlay" data-micromodal-close tabindex="-1">
<span class="drawer__underlay__fill"></span>
<span class="drawer__underlay__blur"></span>
</span>
</div>
</script>
</div>
</div>
{%- endif -%}
</div>
</product-grid-item-variant>
+670
View File
@@ -0,0 +1,670 @@
<!-- /snippets/product-grid-item-variant.liquid -->
{% comment %}
A grid item for products used in collection grid view
* product {object} - The current prodcut
{% render 'product-grid-item', product: product %}
{% endcomment %}
{%- liquid
assign on_sale = false
assign product_price = product.price
assign product_compare_at_price = product.compare_at_price
comment
start Yagi app
endcomment
assign public_or_tags_matched = true
if product.metafields.app--168074346497.segment_tags.value.size > 0
assign public_or_tags_matched = false
endif
for etag in product.metafields.app--168074346497.segment_tags.value
if customer.tags contains etag
assign public_or_tags_matched = true
break
endif
endfor
if public_or_tags_matched
assign product_price = product.metafields.app--168074346497.min_auto_discounted_price.value | default: product.price
assign product_compare_at_price = product.compare_at_price
if shop.metafields.app--168074346497.discount_percentage.value > 0.005
assign discount_percentage = shop.metafields.app--168074346497.discount_percentage.value | times: 1.0
assign deducted_percentage = 1.0 | minus: discount_percentage
assign product_price = product.price | divided_by: 100.0 | times: deducted_percentage | times: 100.0 | ceil
endif
if product_price < product.price and product_compare_at_price == 0 or product_compare_at_price == blank
assign product_compare_at_price = product.price
endif
endif
comment
end Yagi app
endcomment
if product_compare_at_price > product_price
assign on_sale = true
endif
assign sold_out = true
if product.available
assign sold_out = false
endif
# Siblings are a collection metafield called theme.siblings
assign sibs_collection = nil
assign has_siblings = false
if settings.show_siblings and product.metafields.theme.siblings.value != blank and product.metafields.theme.siblings.type == 'single_line_text_field'
assign sibs_collection = collections[product.metafields.theme.siblings.value]
# Ensure the collection was set up to contain this product
for sib_product in sibs_collection.products
if sib_product == product
assign has_siblings = true
endif
endfor
endif
assign swatch_option = nil
assign has_swatches = false
assign swatch_limit = settings.card_swatch_limit
if settings.swatches_enable and settings.swatches_collection_enable
# Detect new swatch system
for option in product.options_with_values
if option.values.first.swatch != null
assign has_swatches = true
assign swatch_option = option
break
endif
endfor
if has_swatches == false
# Detect old swatch system
assign swatch_translation = 'general.swatches.color' | t
assign swatch_labels = swatch_translation | append: ',' | split: ','
for label in swatch_labels
assign sanitized_label = label | lstrip | rstrip
if product.options_by_name[sanitized_label].values.size > 0
assign has_swatches = true
assign swatch_option = product.options_by_name[sanitized_label]
break
endif
endfor
endif
endif
# A button that will add to cart immediately: product with only one default variant or a product with color swatches and no other variants
assign instant_add_button = nil
if settings.instant_add_enable
if product.variants.size == 1
assign instant_add_button = true
elsif has_swatches and product.options.size == 1
assign instant_add_button = true
endif
endif
# A quick add button that will show short variants like 'size' inside the button.
assign inline_variant_buttons = nil
# Get the non color option
# swatch_option.position -> 1 is off by one, it means product.options[0]
# In case of siblings, the inline option buttons always use the first option
if settings.instant_add_enable
if product.options.size == 1
assign inline_variant_buttons = product.options_with_values[0]
elsif has_swatches and product.options.size == 2
if swatch_option.position == 1
assign inline_variant_buttons = product.options_with_values[1]
else
assign inline_variant_buttons = product.options_with_values[0]
endif
endif
endif
# Capture the swatch link markup so we only have filter product.variants by color one time
assign swatch_link_markup = ''
# Pass pre-filtered color variants into content, because the logic needs to know about
# swatch option positions and swatches need to use the where filter to get the list
assign inline_variants = nil
# Placeholder to be displayed when no product image
assign placeholder = placeholder | default: false
-%}
<product-grid-item
aria-label="{{ product.title | strip_html | escape }}"
class="product-grid-item group/product-grid-item"
data-item-id="{{ product.id }}"
{{ attributes }}
>
{% if has_siblings and sibs_collection.products.size > 0 %}
{%- liquid
# Initialize an empty array to hold visible siblings (i.e. ones under swatch limit)
assign visible_siblings = "" | split: ","
# Initial visible sibling count is 1 to account for the current product
assign visible_sibling_count = 1
# Loop through siblings collection and add the current product, plus other siblings until we hit the swatch_limit
for sibling in sibs_collection.products
if sibling.id == product.id or visible_sibling_count < swatch_limit
# Create a new array with the current value, then concat it into the main array
assign new_array = sibling | sort
assign visible_siblings = visible_siblings | concat: new_array
unless sibling.id == product.id
assign visible_sibling_count = visible_sibling_count | plus: 1
endunless
endif
endfor
-%}
{%- for sib_product in visible_siblings -%}
{% liquid
assign visible = false
assign first_sibling_variant = sib_product.variants[0]
if sib_product.id == product.id
assign visible = true
endif
if visible and preload
assign preload_variant = true
endif
if visible and eagerload
assign eagerload_variant = true
endif
assign inline_variant_buttons = nil
if settings.instant_add_enable and sib_product.options.size == 1
assign inline_variant_buttons = sib_product.options_with_values[0]
endif
if inline_variant_buttons
assign inline_variants = sib_product.variants
endif
-%}
{% render 'product-grid-item-variant',
product: sib_product,
variant: first_sibling_variant,
badge_string: badge_string,
visible: visible,
instant_add_button: instant_add_button,
inline_variant_buttons: inline_variant_buttons,
inline_variants: inline_variants,
section_width: section_width,
eagerload: eagerload_variant,
preload: preload_variant,
placeholder: placeholder,
columns_desktop: columns_desktop,
columns_tablet: columns_tablet,
columns_mobile: columns_mobile
%}
{% endfor %}
{% elsif has_swatches %}
{%- liquid
# Note: this is a hack based on options having a property named option1, option2, or option3 -- which we rely on below to filter the variants array
assign swatch_position_key = 'option' | append: swatch_option.position
assign selected_swatch_value = swatch_option.selected_value
# Initialize an empty array to hold visible siblings (i.e. ones under swatch limit)
assign visible_swatches = "" | split: ","
# If there is a selected swatch, start the visible swatch count at 1 to account for it
if selected_swatch_value != blank
assign visible_swatch_count = 1
else
assign visible_swatch_count = 0
endif
# Loop through siblings collection and add the current product, plus other swatches until we hit the swatch limit
for swatch_value in swatch_option.values
if swatch_value == selected_swatch_value or visible_swatch_count < swatch_limit
# Create a new array with the current value, then concat it into the main array
assign new_array = swatch_value | sort
assign visible_swatches = visible_swatches | concat: new_array
unless swatch_value == selected_swatch_value
assign visible_swatch_count = visible_swatch_count | plus: 1
endunless
endif
endfor
-%}
{%- for swatch_value in visible_swatches -%}
{% liquid
# Limit S|M|L to low-variant products to ensure collection page performance
if product.variants.size < 200
assign this_color_variants = product.variants | where: swatch_position_key, swatch_value
assign in_stock_options = this_color_variants | where: 'available'
if inline_variant_buttons
assign inline_variants = this_color_variants
endif
else
# Fall back to quickview with section-rendering API variant selection if there's too many variants
assign inline_variant_buttons = false
assign inline_variants = nil
endif
# Use the default variant for the swatch value. If that variant has media attached the image will change.
# If not, the variant image will not change when swatch is selected.
assign current_variant = swatch_value.variant
# If there is a variant selected from filters, show that variant first
assign visible = false
if product.selected_variant.id == current_variant
assign visible = true
elsif forloop.first
assign visible = true
endif
if visible and preload
assign preload_variant = true
endif
if visible and eagerload
assign eagerload_variant = true
endif
-%}
{% render 'product-grid-item-variant',
product: product,
variant: current_variant,
badge_string: badge_string,
visible: visible,
instant_add_button: instant_add_button,
inline_variant_buttons: inline_variant_buttons,
inline_variants: inline_variants,
section_width: section_width,
swatch_option: swatch_option,
eagerload: eagerload_variant,
preload: preload_variant,
placeholder: placeholder,
columns_desktop: columns_desktop,
columns_tablet: columns_tablet,
columns_mobile: columns_mobile
%}
{% capture swatch_link_markup %}
{{ swatch_link_markup }}
<radio-swatch title="{{ swatch_value.name }}" class="swatch__button{% if in_stock_options.size == 0 %} sold-out{% endif %}{% if settings.swatches_squares %} swatch__button--square{% endif %}">
<a
href="{{ current_variant.url }}"
class="swatch__label"
{% if swatch_value.swatch != null %}
data-swatch
style="--swatch: {{ swatch_value.swatch.color }}"
{% else %}
data-swatch="{{ swatch_value | escape_once }}"
{% endif %}
data-grid-item-variant="{{ current_variant.id }}"
data-swatch-index="{{ forloop.index0 }}"
{% if visible %}aria-current="true"{%- endif -%}
{% if current_variant.featured_media %}
data-swatch-image="{{ current_variant.featured_media.preview_image.src }}"
{% endif %}
{% if current_variant.featured_media %}
data-swatch-image-id="{{ current_variant.featured_media.id }}"
{% endif %}
>
{% if swatch_value.swatch.image %}
{% render 'image', img_object: swatch_value.swatch.image, width: 34, wh_ratio: 1 %}
{% endif %}
<span class="visually-hidden">{{ swatch_value }}</span>
</a>
</radio-swatch>
{% endcapture %}
{% endfor %}
{% else %}
{%- liquid
# Limit S|M|L to low-variant products to ensure collection page performance
if inline_variant_buttons and product.variants.size < 200
assign inline_variants = product.variants
else
assign inline_variant_buttons = false
assign inline_variants = nil
endif
assign selected_variant = product.selected_variant | default: product.variants[0]
if preload
assign preload_variant = true
endif
if eagerload
assign eagerload_variant = true
endif
-%}
{% render 'product-grid-item-variant',
product: product,
variant: selected_variant,
badge_string: badge_string,
visible: true,
instant_add_button: instant_add_button,
inline_variant_buttons: inline_variant_buttons,
inline_variants: inline_variants,
section_width: section_width,
preload: preload_variant,
eagerload: eagerload_variant,
placeholder: placeholder,
columns_desktop: columns_desktop,
columns_tablet: columns_tablet,
columns_mobile: columns_mobile
%}
{% endif %}
<div class="product__grid__info {{ text_align | default: settings.collection_text_alignment | default: 'text-center' }}">
<a
href="{{ product.url }}" data-grid-link aria-label="{{ product.title | strip_html | escape }}"
{% comment %} The link wrapping the product variant image is already focusable, so this does not need to be {% endcomment %}
tabindex="-1"
>
<p class="visually-hidden">{{ product.title | strip_html | escape }}</p>
<div class="product__grid__title__wrapper">
<p id="product-{{ product.id }}-title" class="product__grid__title">
{{ product.title | strip_html | escape }}
</p>
{%- if settings.product_grid_show_rating and product.metafields.reviews.rating.value != blank -%}
<div class="rating__wrapper__grid">
{% render 'product-rating', product: product, show_rating_count: settings.product_grid_show_rating_count %}
</div>
{%- endif -%}
</div>
<div class="product__grid__price {% if settings.show_cutline %} product__grid__price--nowrap{% endif %}">
{%- if settings.show_cutline -%}
<span class="product__grid__cutline">{{ product.metafields.theme.cutline.value }}</span>
{%- endif -%}
<span class="price{% if on_sale %} on-sale{% endif %}">
{% if product.price_varies %}{{ 'products.general.from' | t }} {% endif %}
{%- if settings.currency_code_enable -%}
{{ product_price | money_with_currency }}
{%- else -%}
{{ product_price | money }}
{%- endif -%}
</span>
{% if on_sale %}
<span class="compare-at">
{%- if settings.currency_code_enable -%}
{{ product_compare_at_price | money_with_currency }}
{%- else -%}
{{ product_compare_at_price | money }}
{%- endif -%}
</span>
{% endif %}
</div>
{% if product.selected_or_first_available_variant.unit_price %}
{% capture unit_price_separator %}
<span aria-hidden="true">/</span><span class="visually-hidden">{{ 'general.accessibility.unit_price_separator' | t }}&nbsp;</span>
{% endcapture %}
{% capture unit_price_base_unit %}
{% if product.selected_or_first_available_variant.unit_price_measurement.reference_value != 1 %}
{{ product.selected_or_first_available_variant.unit_price_measurement.reference_value }}
{% endif %}
{{ product.selected_or_first_available_variant.unit_price_measurement.reference_unit }}
{% endcapture %}
<p class="product__grid__price__unit">
<span class="visually-hidden">{{ 'products.product.unit_price_label' | t }}</span>
<span class="price-per-unit">
{%- if settings.currency_code_enable -%}
{{ product.selected_or_first_available_variant.unit_price | money_with_currency }}
{%- else -%}
{{ product.selected_or_first_available_variant.unit_price | money }}
{%- endif -%}
{{ unit_price_separator }}{{ unit_price_base_unit }}
</span>
</p>
{% endif %}
{% comment %} {% if sold_out %}
<p class="product__grid__price__sold">
<em>{{ 'products.product.sold_out' | t }}</em>
</p>
{% endif %} {% endcomment %}
</a>
{% assign active_variant = product.selected_or_first_available_variant %}
<div class="product-card__actions mt-2">
{%- form 'product', product, class: 'product-card__atc-form', novalidate: 'novalidate', data-type: 'add-to-cart-form', is: 'product-form' -%}
<input
type="hidden"
name="id"
value="{{ active_variant.id }}"
{% if active_variant.available == false %}disabled{% endif %}
>
<button
type="submit"
name="add"
class="product-card__atc"
{% if active_variant.available == false %}disabled{% endif %}
>
{%- if active_variant.available -%}
{{ 'products.product.add_to_cart' | t }}
{%- else -%}
{{ 'products.product.sold_out' | t }}
{%- endif -%}
</button>
{%- endform -%}
</div>
{%- if has_siblings -%}
{%- assign sibs_collection = collections[product.metafields.theme.siblings.value] -%}
{% if sibs_collection != blank %}
{%- assign excess_swatches = sibs_collection.products_count | minus: swatch_limit -%}
<div class="product__grid__sibs">
<div class="grid__swatch__container">
<p class="grid__swatch__placeholder">{{ 'collections.general.swatches_with_count' | t: count: sibs_collection.products_count }}</p>
<div class="grid__swatch__hover">
<div class="sibs__slider">
<div class="sibs__inner">
{%- for sib_product in visible_siblings -%}
{%- assign title_safe = sib_product.title | strip_html | escape -%}
{%- assign color_name = sib_product.metafields.theme.cutline.value | default: title_safe -%}
<div class="siblings__link__holder {% if sib_product.available == false %} sold-out{% endif %}">
<a
href="{{ sib_product.url }}"
class="siblings__link"
data-sibling-swatch-link
data-grid-item-variant="{{ sib_product.variants[0].id }}"
data-grid-item-swatch-image="0"
title="{{ color_name }}"
{% if sib_product.handle == product.handle %}aria-current="true"{%- endif -%}
>
<div class="siblings__swatch">
<div class="sibling__image{% if settings.swatches_squares %} sibling__image--square{% endif %}">
{% assign image = sib_product.featured_media.preview_image %}
{% assign image_width = 26 %}
{% assign image_width_2x = image_width | times: 2 | at_most: image.width %}
{% assign alt = image.alt | default: color_name %}
{% capture srcset %}
{{ image | image_url: width: image_width_2x }} 2x,
{{ image | image_url: width: image_width }}
{% endcapture %}
{%- render 'image',
img_object: image,
wh_ratio: 1.0,
srcset: srcset,
fetchpriority: 'low',
width: image_width,
placeholder: placeholder,
alt: alt
-%}
</div>
</div>
</a>
</div>
{%- endfor -%}
{% if excess_swatches > 0 %} <a class="siblings__more-link" href="{{product.url}}">+{{ excess_swatches }}</a>{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endif %}
{%- elsif has_swatches and swatch_link_markup != '' -%}
{%- assign excess_swatches = swatch_option.values.size | minus: swatch_limit -%}
<div class="grid__swatch__container">
<p class="grid__swatch__placeholder">
{{ 'collections.general.swatches_with_count' | t: count: swatch_option.values.size }}
</p>
<div class="grid__swatch__hover" aria-label="Options">
{{ swatch_link_markup }} {% if excess_swatches > 0 %}
<a class="grid__swatch__more-link" href="{{product.url}}">+{{ excess_swatches }}</a>
{% endif %}
</div>
</div>
{%- endif -%}
</div>
</product-grid-item>
<style>
.product-card__actions {
position: static !important;
margin-top: 1rem;
opacity: 1 !important;
transform: none !important;
width: 100%;
}
@media (max-width: 767px) {
.product-card__actions {
margin-top: 0.75rem;
}
.product-card__atc {
font-size: 0.9rem;
padding: 0.8rem 1rem;
}
}
.product-card__actions .product-card__atc {
width: 100% !important;
justify-content: center;
align-items: center;
background-color: #000 !important;
color: #fff !important;
border: none !important;
border-radius: 8px;
padding: 0.5rem 1.25rem;
text-transform: uppercase;
letter-spacing: 0.03em;
transition: all 0.25s ease;
box-sizing: border-box;
font-size: 0.85rem;
min-height: 0 !important;
height: auto !important;
font-family: var(--FONT-STACK-BODY);
font-style: var(--FONT-STYLE-BODY);
font-weight: var(--FONT-WEIGHT-BODY);
}
@media (max-width: 767px) {
.product-card__actions .product-card__atc {
padding: 0.45rem 1rem;
font-size: 0.9rem;
}
}
.product-card__actions .product-card__atc:hover,
.product-card__actions .product-card__atc:focus {
background-color: #111 !important;
transform: translateY(-2px);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
}
.product-card__actions .product-card__atc[disabled] {
background-color: #555 !important;
color: #ccc !important;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.product-card:not(.product-card--list) .product-card__actions .btn {
opacity: 1 !important;
visibility: visible !important;
transform: none !important;
pointer-events: auto !important;
font-size: 13px;
}
.product-card__main-actions {
display: block;
}
/* === Fix product card squishing on small screens === */
@media (max-width: 767px) {
.collection-products .product-grid,
.collection-products .grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
.collection-products .product-card {
height: auto !important;
}
}
@media (max-width: 480px) {
.collection-products .product-grid,
.collection-products .grid {
grid-template-columns: 1fr;
}
}
.product-card,
.product-card__wrapper {
height: 100%;
display: flex;
flex-direction: column;
}
.product-card__image-wrapper { flex: 0 0 auto; }
.product-card__info { flex: 1 1 auto; display: flex; flex-direction: column; }
.product-card__actions { margin-top: auto; }
.product__grid__title {
line-height: 1.3;
overflow: hidden;
min-height: calc(1.3em * 2); /* ensures same vertical space even if short */
display: -webkit-box;
-webkit-line-clamp: 2; /* clamp to 2 lines */
-webkit-box-orient: vertical;
}
@supports not (-webkit-line-clamp: 2) {
.product__grid__title { max-height: calc(1.3em * 2); }
}
.product-card__type,
.f-price { white-space: normal; word-break: break-word; }
</style>
+168
View File
@@ -0,0 +1,168 @@
<!-- /snippets/product-price.liquid -->
{%- liquid
assign current_variant = product.selected_or_first_available_variant
assign price = current_variant.price
assign compare_at_price = current_variant.compare_at_price
comment
start Yagi app code
endcomment
assign public_or_tags_matched = true
if product.metafields.app--168074346497.segment_tags.value.size > 0
assign public_or_tags_matched = false
endif
for etag in product.metafields.app--168074346497.segment_tags.value
if customer.tags contains etag
assign public_or_tags_matched = true
break
endif
endfor
if public_or_tags_matched
assign price = current_variant.metafields.app--168074346497.auto_discounted_price.value | default: current_variant.price
assign compare_at_price = current_variant.compare_at_price
if shop.metafields.app--168074346497.discount_percentage.value > 0.005
assign discount_percentage = shop.metafields.app--168074346497.discount_percentage.value | times: 1.0
assign deducted_percentage = 1.0 | minus: discount_percentage
assign price = current_variant.price | divided_by: 100.0 | times: deducted_percentage | times: 100.0 | ceil
endif
if price < current_variant.price and compare_at_price == 0 or compare_at_price == blank
assign compare_at_price = current_variant.price
endif
endif
comment
end Yagi app code
endcomment
assign sale_type = settings.badge_sale_type
if block.settings.price_size
assign price_size_class = block.settings.price_size | prepend: 'accent-size-'
assign price_small_class = block.settings.price_size | minus: 1 | prepend: 'accent-size-'
endif
# Select current selling plan if it's specified in the URL
assign selected_selling_plan = product.selected_selling_plan
if product.requires_selling_plan
# Fallback to the first available selling plan
assign selected_selling_plan = product.selected_or_first_available_selling_plan_allocation.selling_plan | default: product.selling_plan_groups[0].selling_plans[0]
endif
assign sale_text = 'products.product.sale' | t
# Subscription price
if selected_selling_plan
assign sale_text = 'products.product.subscription' | t
# Make sure the variant exists(it isn't 'unavailable') and showing a price makes sense
if current_variant
assign price_adjustment = selected_selling_plan.price_adjustments[0]
case price_adjustment.value_type
when 'percentage'
assign sale_type = 'percentage'
assign plan_discount_amount = price | times: price_adjustment.value | divided_by: 100
assign compare_at_price = price
assign price = compare_at_price | minus: plan_discount_amount
when 'fixed_amount'
assign sale_type = 'dollar'
assign compare_at_price = price
assign price = compare_at_price | minus: price_adjustment.value
when 'price'
assign sale_type = 'dollar'
assign compare_at_price = price
assign price = price_adjustment.value
endcase
endif
endif
-%}
<div class="product__block__price" style="--PB: {{ block.settings.padding_bottom }}px;" {{ block.shopify_attributes }}>
<div class="product__price__wrap">
<div class="product__price__main" data-price-wrapper>
<span class="product__price {{ price_size_class }}">
<span data-product-price {% if compare_at_price > price %} class="product__price--sale"{% endif %}>
{%- if settings.currency_code_enable -%}
{{ price | money_with_currency }}
{%- else -%}
{{ price | money }}
{%- endif -%}
</span>
</span>
{% if compare_at_price > price or product.selected_selling_plan %}
{% case sale_type %}
{% when 'strike' %}
<span data-compare-price class="product__price--compare {{ price_size_class }}">
{%- if settings.currency_code_enable -%}
{{ compare_at_price | money_with_currency }}
{%- else -%}
{{ compare_at_price | money }}
{%- endif -%}
</span>
{% when 'percentage' %}
{% assign difference = compare_at_price | minus: price %}
{% assign percent_off = difference | times: 100 | divided_by: compare_at_price %}
<span class="product__price--off">
<span>{{ sale_text }}</span>
{% if percent_off > 0 %}
<em>•</em>
{{ 'products.product.save' | t }}
<span>{{ percent_off | floor | append: '%' }}</span>
{% endif %}
</span>
{% when 'dollar' %}
{% assign amount_off = compare_at_price | minus: price %}
<span class="product__price--off">
<span>{{ sale_text }}</span>
<em>•</em>
{{ 'products.product.save' | t }}
<span>{{ amount_off | money }}</span>
</span>
{% endcase %}
{% endif %}
{% if current_variant.unit_price != blank %}
{% capture show_units %}
{%- unless current_variant.unit_price -%}style="display: none;"{%- endunless -%}
{% endcapture %}
{% capture unit_price_separator %}
<span aria-hidden="true">/</span><span class="visually-hidden">{{ 'general.accessibility.unit_price_separator' | t }}&nbsp;</span>
{% endcapture %}
{% capture unit_price_base_unit %}
<span>
{% if current_variant.unit_price_measurement %}
{% if current_variant.unit_price_measurement.reference_value != 1 %}
{{ current_variant.unit_price_measurement.reference_value }}
{%- endif -%}
{{ current_variant.unit_price_measurement.reference_unit }}
{% endif %}
</span>
{% endcapture %}
<div class="product__price--unit {{ price_small_class }}">
<span data-product-unit {{ show_units }}>
<span class="visually-hidden visually-hidden--inline">{{ 'products.product.unit_price_label' | t }}</span>
<span data-product-unit-price id="unit-price-{{ block.id }}">
{%- if settings.currency_code_enable -%}
{{ current_variant.unit_price | money_with_currency }}
{%- else -%}
{{ current_variant.unit_price | money }}
{%- endif -%}
</span>
{{ unit_price_separator }}
<span data-product-base id="unit-price-base-{{ block.id }}">{{ unit_price_base_unit }}</span>
</span>
<span class="hide">
{{ 'products.product.each' | t }}
<span></span>
</span>
</div>
{% endif %}
</div>
</div>
</div>