83 lines
6.3 KiB
HTML
83 lines
6.3 KiB
HTML
{# Imported locally (not just inherited from base.html) so the card also renders
|
||
inside standalone htmx fragments like shop/_results.html, where Tera's import
|
||
chain from the layout isn't present. #}
|
||
{% import "macros/ui.html" as ui %}
|
||
{# Adapted from the vendored Penguin UI component
|
||
(penguinui-components/card/ecommerce-product-card.html):
|
||
wired to our product data + i18n + htmx add-to-cart + toast. The demo rating
|
||
stars, hardcoded title/price/description/image and the `max-w-sm` (which fights
|
||
the shop grid) are dropped; the whole card links to the product page. #}
|
||
{# Layout adapts to the `view` Alpine state set by _product_grid.html:
|
||
'grid' (default) → vertical card; 'list' → horizontal row. On pages without
|
||
that state (e.g. home) `view` is undefined, so the grid layout applies. #}
|
||
<article
|
||
class="group flex overflow-hidden rounded-radius border border-outline bg-surface-alt text-on-surface transition hover:border-primary dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark dark:hover:border-primary-dark"
|
||
:class="view === 'list' ? 'flex-row flex-wrap' : 'flex-col'">
|
||
<a href="/shop/{{ product.slug }}" class="flex min-w-0 flex-1"
|
||
:class="view === 'list' ? 'flex-row' : 'flex-col'">
|
||
<!-- Image -->
|
||
<div class="relative overflow-hidden bg-surface-alt dark:bg-surface-dark"
|
||
:class="view === 'list' ? 'size-28 shrink-0 sm:size-40' : 'h-44 md:h-64'">
|
||
{% if product.on_sale and product.percent_off > 0 %}
|
||
<span class="absolute left-2 top-2 z-10 rounded-full bg-danger px-2 py-0.5 text-[11px] font-bold text-on-danger shadow-sm">−{{ product.percent_off }} %</span>
|
||
{% endif %}
|
||
{% if product.image %}
|
||
<img src="/images/{{ product.image }}" alt="{{ product.name }}" class="size-full object-cover transition duration-700 ease-out group-hover:scale-105">
|
||
{% endif %}
|
||
</div>
|
||
<!-- Content -->
|
||
<div class="flex min-w-0 flex-1 flex-col gap-1"
|
||
:class="view === 'list' ? 'p-4 sm:p-5' : 'p-6 pb-2'">
|
||
<!-- Header: Title & Price (stacked so neither overflows the narrow card) -->
|
||
<h3 class="break-words text-lg font-bold text-on-surface-strong dark:text-on-surface-dark-strong">{{ product.name }}</h3>
|
||
{# Short blurb for the card; falls back to the full description (clamped)
|
||
for products without a dedicated short one. Both are authored as rich
|
||
text (Quill), so render the stored HTML — `.rich-blurb` strips block
|
||
spacing so the line-clamp stays tidy. Overflow is truncated with an
|
||
ellipsis: 2 lines in the grid, 3 in the roomier list row. #}
|
||
{% if product.short_description or product.description %}
|
||
<div class="rich-blurb line-clamp-2 break-words text-sm text-on-surface/70 dark:text-on-surface-dark/70"
|
||
:class="view === 'list' && 'line-clamp-3'">{% if product.short_description %}{{ product.short_description | safe }}{% else %}{{ product.description | safe }}{% endif %}</div>
|
||
{% endif %}
|
||
{% if product.on_sale %}
|
||
<div class="flex flex-wrap items-baseline gap-x-2 leading-tight">
|
||
<span class="text-xl font-semibold text-danger"><span class="sr-only">Price</span>{% if product.has_options %}{{ t(key="from-price", price=product.price, lang=lang | default(value='sk')) }}{% else %}{{ product.price }}{% endif %} {{ currency_symbol }}</span>
|
||
<span class="text-sm text-on-surface/50 line-through dark:text-on-surface-dark/50">{{ product.regular_price }} {{ currency_symbol }}</span>
|
||
</div>
|
||
{% else %}
|
||
<span class="break-words text-xl"><span class="sr-only">Price</span>{% if product.has_options %}{{ t(key="from-price", price=product.price, lang=lang | default(value='sk')) }}{% else %}{{ product.price }}{% endif %} {{ currency_symbol }}</span>
|
||
{% endif %}
|
||
<!-- stock pill (Kompress design): green "in stock" / red "sold out" -->
|
||
<div class="mt-0.5">
|
||
{% if product.in_stock %}
|
||
<span class="inline-flex items-center gap-1.5 rounded-full bg-success/10 px-2 py-0.5 text-xs font-semibold text-success">
|
||
<span class="size-1.5 rounded-full bg-success" aria-hidden="true"></span>{{ t(key="in-stock", lang=lang | default(value='sk')) }}
|
||
</span>
|
||
{% else %}
|
||
<span class="inline-flex items-center gap-1.5 rounded-full bg-danger/10 px-2 py-0.5 text-xs font-semibold text-danger">
|
||
<span class="size-1.5 rounded-full bg-danger" aria-hidden="true"></span>{{ t(key="out-of-stock", lang=lang | default(value='sk')) }}
|
||
</span>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</a>
|
||
<div class="flex flex-col gap-2"
|
||
:class="view === 'list' ? 'w-full justify-center p-4 sm:w-56 sm:p-5' : 'p-6 pt-0'">
|
||
{% if product.has_options %}
|
||
{# Multiple variants: customer must pick on the product page. #}
|
||
{{ ui::button(label=t(key="choose-option", lang=lang | default(value='sk')), href="/shop/" ~ product.slug, extra="w-full") }}
|
||
{% elif product.in_stock %}
|
||
<p class="text-xs text-on-surface/60 dark:text-on-surface-dark/60">{% if product.tracked %}{{ t(key="in-stock", lang=lang | default(value='sk')) }}: {{ product.stock }}{% else %}{{ t(key="available", lang=lang | default(value='sk')) }}{% endif %}</p>
|
||
<form method="post" action="/cart/add" hx-post="/cart/add" hx-swap="none"
|
||
hx-on::after-request="if (event.detail.successful) toast('{{ t(key='cart-added', lang=lang | default(value='sk')) }}')">
|
||
<input type="hidden" name="_csrf" value="{{ csrf_token() }}">
|
||
<input type="hidden" name="variant_id" value="{{ product.variant_id }}">
|
||
<input type="hidden" name="quantity" value="1">
|
||
{{ ui::button(label=t(key="add-to-cart", lang=lang | default(value='sk')), type="submit", extra="w-full", icon='<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="size-3.5"><path fill-rule="evenodd" d="M5 4a3 3 0 0 1 6 0v1h.643a1.5 1.5 0 0 1 1.492 1.35l.7 7A1.5 1.5 0 0 1 12.342 15H3.657a1.5 1.5 0 0 1-1.492-1.65l.7-7A1.5 1.5 0 0 1 4.357 5H5V4Zm4.5 0v1h-3V4a1.5 1.5 0 0 1 3 0Zm-3 3.75a.75.75 0 0 0-1.5 0v1a3 3 0 1 0 6 0v-1a.75.75 0 0 0-1.5 0v1a1.5 1.5 0 1 1-3 0v-1Z" clip-rule="evenodd" /></svg>') }}
|
||
</form>
|
||
{% else %}
|
||
<p class="inline-flex justify-center rounded-radius bg-danger/10 px-3 py-2 text-xs font-medium text-danger">{{ t(key="out-of-stock", lang=lang | default(value='sk')) }}</p>
|
||
{% endif %}
|
||
</div>
|
||
</article>
|