Files
kompress_eshop/assets/views/shop/show.html
Priec 49d64d397e
Some checks failed
CI / Check Style (push) Has been cancelled
CI / Run Clippy (push) Has been cancelled
CI / Run Tests (push) Has been cancelled
cena on multiple choice
2026-07-29 22:54:45 +02:00

194 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% import "macros/ui.html" as ui %}
{% block title %}{{ product.name }}{% endblock title %}
{# Keeps this product's category selected and expanded in the sidebar, which
otherwise matches on the URL alone and so goes blank on /shop/{slug}. #}
{% block nav_category %}{% if category %}{{ category.slug }}{% endif %}{% endblock nav_category %}
{# Same trail as the category listing, extended with the product itself, so the
path back up the tree survives the click into a product. #}
{% block breadcrumbs %}
{% set L = lang | default(value='sk') %}
<nav aria-label="breadcrumb" class="mb-5 text-sm">
<ol class="flex flex-wrap items-center gap-1.5 text-on-surface/60 dark:text-on-surface-dark/60">
{{ ui::crumb(label=t(key="nav-home", lang=L), href="/") }}
{{ ui::crumb(label=t(key="nav-shop", lang=L), href="/shop") }}
{% for crumb in breadcrumbs %}
{{ ui::crumb(label=crumb.name, href="/category/" ~ crumb.slug) }}
{% endfor %}
{% if category %}{{ ui::crumb(label=category.name, href="/category/" ~ category.slug) }}{% endif %}
{{ ui::crumb_current(label=product.name) }}
</ol>
</nav>
{% endblock breadcrumbs %}
{% block content %}
<div class="space-y-12">
<div class="grid gap-10 lg:grid-cols-2">
<!-- gallery — prev/next arrows + opacity transitions adapted from
penguinui/carousel/default-carousel.html; kept our product thumbnail strip
(more useful than carousel dots for a product) and 0-based `active` -->
<div x-data="{ active: 0, count: {{ images | length }},
prev() { this.active = this.active > 0 ? this.active - 1 : this.count - 1 },
next() { this.active = this.active < this.count - 1 ? this.active + 1 : 0 } }"
class="space-y-4">
<div class="relative aspect-square overflow-hidden rounded-radius border border-outline bg-surface-alt dark:border-outline-dark dark:bg-surface-dark-alt">
{% if images | length > 0 %}
{% for image in images %}
<img x-show="active === {{ loop.index0 }}" x-transition.opacity.duration.300ms src="/images/{{ image }}" alt="{{ product.name }}" class="absolute inset-0 size-full object-cover">
{% endfor %}
{% endif %}
{% if images | length > 1 %}
<!-- previous slide -->
<button type="button" @click="prev()" aria-label="{{ t(key='gallery-prev', lang=lang | default(value='sk')) }}"
class="absolute left-3 top-1/2 z-10 flex -translate-y-1/2 items-center justify-center rounded-full bg-surface/40 p-2 text-on-surface transition hover:bg-surface/60 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary dark:bg-surface-dark/40 dark:text-on-surface-dark dark:hover:bg-surface-dark/60 dark:focus-visible:outline-primary-dark">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke="currentColor" fill="none" stroke-width="3" class="size-5" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5" />
</svg>
</button>
<!-- next slide -->
<button type="button" @click="next()" aria-label="{{ t(key='gallery-next', lang=lang | default(value='sk')) }}"
class="absolute right-3 top-1/2 z-10 flex -translate-y-1/2 items-center justify-center rounded-full bg-surface/40 p-2 text-on-surface transition hover:bg-surface/60 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary dark:bg-surface-dark/40 dark:text-on-surface-dark dark:hover:bg-surface-dark/60 dark:focus-visible:outline-primary-dark">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke="currentColor" fill="none" stroke-width="3" class="size-5" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
</svg>
</button>
{% endif %}
</div>
{% if images | length > 1 %}
<div class="flex flex-wrap gap-2">
{% for image in images %}
<button type="button" @click="active = {{ loop.index0 }}"
:class="active === {{ loop.index0 }} ? 'border-primary dark:border-primary-dark' : 'border-outline dark:border-outline-dark'"
class="size-16 overflow-hidden rounded-radius border">
<img src="/images/{{ image }}" alt="" class="size-full object-cover">
</button>
{% endfor %}
</div>
{% endif %}
</div>
<!-- details -->
{% set fld = "w-full rounded-radius border border-outline bg-surface-alt px-3 py-2 text-sm text-on-surface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary dark:border-outline-dark dark:bg-surface-dark-alt/50 dark:text-on-surface-dark dark:focus-visible:outline-primary-dark" %}
{% set btn = "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-radius px-5 py-2 text-sm text-center font-medium tracking-wide transition hover:opacity-75 focus-visible:outline-2 focus-visible:outline-offset-2 active:opacity-100 active:outline-offset-0 disabled:cursor-not-allowed disabled:opacity-75 border border-cta bg-cta text-on-cta focus-visible:outline-cta dark:border-cta-dark dark:bg-cta-dark dark:text-on-cta-dark dark:focus-visible:outline-cta-dark" %}
<script id="variant-data" type="application/json">{{ variants | json_encode() | safe }}</script>
<div class="space-y-6"
x-data="{
variants: JSON.parse(document.getElementById('variant-data').textContent) || [],
sel: 0,
qty: 1,
get current() { return this.variants[this.sel] || null },
/* Informational line total for the typed quantity. Mirrors the server's
format_price (minor units -> '12.34'); the currency symbol is appended
in the markup, as everywhere else. */
get lineTotal() {
const cents = (this.current ? this.current.price_cents : 0) * Math.max(1, this.qty || 1);
return Math.floor(cents / 100) + '.' + String(cents % 100).padStart(2, '0');
},
init() {
const firstInStock = this.variants.findIndex(v => v.in_stock);
this.sel = Math.max(0, firstInStock);
},
}">
{% if category %}
<a href="/category/{{ category.slug }}" class="text-sm font-medium text-primary dark:text-primary-dark">{{ category.name }}</a>
{% endif %}
<h1 class="text-3xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">{{ product.name }}</h1>
{% if product.short_description %}
<div class="rich-content rich-summary text-on-surface/80 dark:text-on-surface-dark/80">
{{ product.short_description | safe }}
{% if product.description %}
<a href="#product-description" class="product-more-link inline font-medium text-primary underline underline-offset-4 hover:opacity-75 dark:text-primary-dark">{{ t(key="product-more", lang=lang | default(value='sk')) }}</a>
{% endif %}
</div>
{% elif product.description %}
<a href="#product-description" class="inline-flex text-sm font-medium text-primary underline underline-offset-4 hover:opacity-75 dark:text-primary-dark">{{ t(key="product-more", lang=lang | default(value='sk')) }}</a>
{% endif %}
<template x-if="current">
<div class="space-y-6">
<!-- option picker (only when there's a real choice); first option is
selected by default and switching it updates the price + buy form -->
<template x-if="variants.length > 1">
<div class="max-w-sm space-y-1.5">
<label for="variant-select" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="choose-option", lang=lang | default(value='sk')) }}</label>
<select id="variant-select" x-model.number="sel" class="{{ fld }}">
<template x-for="(v, i) in variants" :key="v.id">
<option :value="i" x-text="(v.label || '—') + ' · ' + v.price + ' {{ currency_symbol }}' + (v.in_stock ? '' : ' ({{ t(key='out-of-stock', lang=lang | default(value='sk')) }})')"></option>
</template>
</select>
</div>
</template>
<div class="flex items-baseline gap-3">
<p class="text-2xl font-semibold" :class="current.on_sale ? 'text-orange-700 dark:text-orange-400' : 'text-primary dark:text-primary-dark'">
<span x-text="current.price"></span> {{ currency_symbol }}
</p>
<template x-if="current.on_sale">
<p class="text-lg text-on-surface/50 line-through dark:text-on-surface-dark/50"><span x-text="current.regular_price"></span> {{ currency_symbol }}</p>
</template>
</div>
{# Stock number of the selected option; switching the picker updates it. #}
<template x-if="current.sku">
<p class="text-sm text-on-surface/60 dark:text-on-surface-dark/60">
{{ t(key="sku", lang=lang | default(value='sk')) }}: <span x-text="current.sku"></span>
</p>
</template>
<template x-if="current.in_stock">
<div class="space-y-2">
<form method="post" action="/cart/add" hx-post="/cart/add" hx-swap="none" class="flex flex-wrap items-end gap-3"
hx-on::after-request="if (event.detail.successful) toast('{{ t(key='cart-added', lang=lang | default(value='sk')) }}')">
{{ ui::csrf_field() }}
<input type="hidden" name="variant_id" :value="current.id">
<div class="space-y-1.5">
<label for="quantity" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="quantity", lang=lang | default(value='sk')) }}</label>
<input type="number" id="quantity" name="quantity" x-model.number="qty" value="1" min="1" :max="current.stock" class="{{ fld }} w-24">
</div>
<button type="submit" class="{{ btn }}">{{ t(key="add-to-cart", lang=lang | default(value='sk')) }}</button>
</form>
{# Informational only — what the typed quantity costs in total; the
cart recomputes it server-side. Hidden at qty 1, where it would
just repeat the unit price. #}
<template x-if="qty > 1">
<p class="text-sm text-on-surface/80 dark:text-on-surface-dark/80">
{{ t(key="price-for-quantity", lang=lang | default(value='sk')) }}
<span class="font-semibold text-on-surface-strong dark:text-on-surface-dark-strong"><span x-text="lineTotal"></span> {{ currency_symbol }}</span>
<span class="text-on-surface/60 dark:text-on-surface-dark/60">(<span x-text="qty"></span> × <span x-text="current.price"></span> {{ currency_symbol }})</span>
</p>
</template>
<p class="text-sm text-on-surface/60 dark:text-on-surface-dark/60">
<template x-if="current.tracked">
<span>{{ t(key="in-stock", lang=lang | default(value='sk')) }}: <span x-text="current.stock"></span></span>
</template>
<template x-if="!current.tracked">
<span>{{ t(key="available", lang=lang | default(value='sk')) }}</span>
</template>
</p>
</div>
</template>
<template x-if="!current.in_stock">
<p class="inline-flex rounded-radius bg-orange-50 px-3 py-2 text-sm font-semibold text-orange-700 ring-1 ring-inset ring-orange-600/20 dark:bg-orange-500/10 dark:text-orange-400 dark:ring-orange-400/25">{{ t(key="out-of-stock", lang=lang | default(value='sk')) }}</p>
</template>
</div>
</template>
<template x-if="!current">
<p class="inline-flex rounded-radius bg-danger/10 px-3 py-2 text-sm font-medium text-danger">{{ t(key="out-of-stock", lang=lang | default(value='sk')) }}</p>
</template>
</div>
</div>
{% if product.description %}
<section id="product-description" class="scroll-mt-28 border-t border-outline pt-8 dark:border-outline-dark">
<h2 class="mb-4 text-2xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="description", lang=lang | default(value='sk')) }}</h2>
{# Authored as rich text (Quill) in the admin; render the stored HTML. #}
<div class="rich-content text-on-surface/80 dark:text-on-surface-dark/80">{{ product.description | safe }}</div>
</section>
{% endif %}
</div>
{% endblock content %}