cart and buy page
This commit is contained in:
@@ -39,6 +39,14 @@
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', markActiveNav);
|
||||
document.addEventListener('htmx:afterSwap', markActiveNav);
|
||||
// Sum the quantities stored in the `cart` cookie for the header badge.
|
||||
function cartCount() {
|
||||
var m = document.cookie.split('; ').find(function (c) { return c.indexOf('cart=') === 0 });
|
||||
if (!m) return 0;
|
||||
var v = decodeURIComponent(m.split('=')[1] || '');
|
||||
if (!v) return 0;
|
||||
return v.split(',').reduce(function (s, e) { return s + (parseInt(e.split(':')[1]) || 0) }, 0);
|
||||
}
|
||||
</script>
|
||||
<link href="/static/css/app.css?v=2026-06-16" rel="stylesheet" type="text/css">
|
||||
<script src="/static/vendor/htmx/htmx-1.9.12.min.js"></script>
|
||||
@@ -85,7 +93,7 @@
|
||||
<!-- cart with live item-count badge read from the `cart` cookie -->
|
||||
<a href="/cart" data-nav="/cart"
|
||||
x-data="{ count: 0 }"
|
||||
x-init="count = (function(){ var m = document.cookie.split('; ').find(function(c){return c.indexOf('cart=')===0}); if(!m) return 0; var v = decodeURIComponent(m.split('=')[1]||''); if(!v) return 0; return v.split(',').reduce(function(s,e){ return s + (parseInt(e.split(':')[1])||0) }, 0) })()"
|
||||
x-init="count = cartCount(); window.addEventListener('htmx:afterSwap', function () { count = cartCount() })"
|
||||
aria-label="{{ t(key='cart-title', lang=lang | default(value='sk')) }}"
|
||||
title="{{ t(key='cart-title', lang=lang | default(value='sk')) }}"
|
||||
class="relative inline-flex size-9 items-center justify-center rounded-radius text-on-surface transition hover:bg-surface-alt dark:text-on-surface-dark dark:hover:bg-surface-dark-alt">
|
||||
|
||||
62
assets/views/shop/_cart_body.html
Normal file
62
assets/views/shop/_cart_body.html
Normal file
@@ -0,0 +1,62 @@
|
||||
{# Cart contents, swapped in via htmx on quantity change / removal so the page
|
||||
never does a full reload. Rendered inside <div id="cart-body"> in cart.html
|
||||
and returned on its own by /cart/update and /cart/remove. #}
|
||||
{% if items | length > 0 %}
|
||||
<div class="overflow-hidden rounded-radius border border-outline dark:border-outline-dark">
|
||||
<table class="w-full text-left text-sm">
|
||||
<thead class="border-b border-outline bg-surface-alt text-xs uppercase tracking-wide text-on-surface/70 dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark/70">
|
||||
<tr>
|
||||
<th class="px-4 py-3 font-semibold">{{ t(key="product", lang=lang | default(value='sk')) }}</th>
|
||||
<th class="px-4 py-3 font-semibold">{{ t(key="price", lang=lang | default(value='sk')) }}</th>
|
||||
<th class="px-4 py-3 font-semibold">{{ t(key="quantity", lang=lang | default(value='sk')) }}</th>
|
||||
<th class="px-4 py-3 text-right font-semibold">{{ t(key="cart-total", lang=lang | default(value='sk')) }}</th>
|
||||
<th class="px-4 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-outline dark:divide-outline-dark">
|
||||
{% for item in items %}
|
||||
<tr>
|
||||
<td class="px-4 py-3">
|
||||
<a href="/shop/{{ item.slug }}" class="font-medium text-on-surface-strong hover:text-primary dark:text-on-surface-dark-strong dark:hover:text-primary-dark">{{ item.name }}</a>
|
||||
</td>
|
||||
<td class="px-4 py-3 tabular-nums">{{ item.price }} {{ item.currency }}</td>
|
||||
<td class="px-4 py-3">
|
||||
{# Changing the quantity posts via htmx and swaps only #cart-body. #}
|
||||
<form method="post" action="/cart/update"
|
||||
hx-post="/cart/update" hx-trigger="change" hx-target="#cart-body" hx-swap="innerHTML">
|
||||
<input type="hidden" name="product_id" value="{{ item.id }}">
|
||||
<input type="number" name="quantity" min="0" max="{{ item.stock }}" value="{{ item.quantity }}"
|
||||
class="w-20 rounded-radius border border-outline bg-surface px-2 py-1 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
|
||||
</form>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right font-medium tabular-nums">{{ item.line_total }} {{ item.currency }}</td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
<form method="post" action="/cart/remove"
|
||||
hx-post="/cart/remove" hx-target="#cart-body" hx-swap="innerHTML">
|
||||
<input type="hidden" name="product_id" value="{{ item.id }}">
|
||||
<button type="submit" class="text-xs font-medium text-danger hover:underline">{{ t(key="cart-remove", lang=lang | default(value='sk')) }}</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot class="border-t border-outline dark:border-outline-dark">
|
||||
<tr>
|
||||
<td colspan="3" class="px-4 py-3 text-right font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="cart-total", lang=lang | default(value='sk')) }}</td>
|
||||
<td class="px-4 py-3 text-right text-lg font-bold tabular-nums text-primary dark:text-primary-dark">{{ total }} {{ currency }}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex flex-wrap justify-between gap-3">
|
||||
<a href="/shop" class="inline-flex items-center rounded-radius border border-outline px-4 py-2 text-sm font-medium text-on-surface transition hover:bg-surface-alt dark:border-outline-dark dark:text-on-surface-dark dark:hover:bg-surface-dark-alt">{{ t(key="cart-continue", lang=lang | default(value='sk')) }}</a>
|
||||
<a href="/checkout" class="inline-flex items-center justify-center rounded-radius bg-primary px-5 py-2 text-sm font-medium tracking-wide text-on-primary transition hover:opacity-75 dark:bg-primary-dark dark:text-on-primary-dark">{{ t(key="cart-checkout", lang=lang | default(value='sk')) }}</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="rounded-radius border border-outline px-6 py-16 text-center dark:border-outline-dark">
|
||||
<p class="text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="cart-empty", lang=lang | default(value='sk')) }}</p>
|
||||
<a href="/shop" class="mt-4 inline-flex items-center justify-center rounded-radius bg-primary px-4 py-2 text-sm font-medium text-on-primary transition hover:opacity-75 dark:bg-primary-dark dark:text-on-primary-dark">{{ t(key="cart-continue", lang=lang | default(value='sk')) }}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -6,62 +6,8 @@
|
||||
<div class="space-y-6">
|
||||
<h1 class="text-3xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="cart-title", lang=lang | default(value='sk')) }}</h1>
|
||||
|
||||
{% if items | length > 0 %}
|
||||
<div class="overflow-hidden rounded-radius border border-outline dark:border-outline-dark">
|
||||
<table class="w-full text-left text-sm">
|
||||
<thead class="border-b border-outline bg-surface-alt text-xs uppercase tracking-wide text-on-surface/70 dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark/70">
|
||||
<tr>
|
||||
<th class="px-4 py-3 font-semibold">{{ t(key="product", lang=lang | default(value='sk')) }}</th>
|
||||
<th class="px-4 py-3 font-semibold">{{ t(key="price", lang=lang | default(value='sk')) }}</th>
|
||||
<th class="px-4 py-3 font-semibold">{{ t(key="quantity", lang=lang | default(value='sk')) }}</th>
|
||||
<th class="px-4 py-3 text-right font-semibold">{{ t(key="cart-total", lang=lang | default(value='sk')) }}</th>
|
||||
<th class="px-4 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-outline dark:divide-outline-dark">
|
||||
{% for item in items %}
|
||||
<tr>
|
||||
<td class="px-4 py-3">
|
||||
<a href="/shop/{{ item.slug }}" class="font-medium text-on-surface-strong hover:text-primary dark:text-on-surface-dark-strong dark:hover:text-primary-dark">{{ item.name }}</a>
|
||||
</td>
|
||||
<td class="px-4 py-3 tabular-nums">{{ item.price }} {{ item.currency }}</td>
|
||||
<td class="px-4 py-3">
|
||||
<form method="post" action="/cart/update" hx-boost="false" class="flex items-center gap-2">
|
||||
<input type="hidden" name="product_id" value="{{ item.id }}">
|
||||
<input type="number" name="quantity" min="0" max="{{ item.stock }}" value="{{ item.quantity }}"
|
||||
class="w-20 rounded-radius border border-outline bg-surface px-2 py-1 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
|
||||
<button type="submit" class="rounded-radius border border-outline px-2 py-1 text-xs font-medium text-on-surface transition hover:bg-surface-alt dark:border-outline-dark dark:text-on-surface-dark dark:hover:bg-surface-dark-alt">{{ t(key="cart-update", lang=lang | default(value='sk')) }}</button>
|
||||
</form>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right font-medium tabular-nums">{{ item.line_total }} {{ item.currency }}</td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
<form method="post" action="/cart/remove" hx-boost="false">
|
||||
<input type="hidden" name="product_id" value="{{ item.id }}">
|
||||
<button type="submit" class="text-xs font-medium text-danger hover:underline">{{ t(key="cart-remove", lang=lang | default(value='sk')) }}</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot class="border-t border-outline dark:border-outline-dark">
|
||||
<tr>
|
||||
<td colspan="3" class="px-4 py-3 text-right font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="cart-total", lang=lang | default(value='sk')) }}</td>
|
||||
<td class="px-4 py-3 text-right text-lg font-bold tabular-nums text-primary dark:text-primary-dark">{{ total }} {{ currency }}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
<div id="cart-body">
|
||||
{% include "shop/_cart_body.html" %}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap justify-between gap-3">
|
||||
<a href="/shop" class="inline-flex items-center rounded-radius border border-outline px-4 py-2 text-sm font-medium text-on-surface transition hover:bg-surface-alt dark:border-outline-dark dark:text-on-surface-dark dark:hover:bg-surface-dark-alt">{{ t(key="cart-continue", lang=lang | default(value='sk')) }}</a>
|
||||
<a href="/checkout" class="inline-flex items-center justify-center rounded-radius bg-primary px-5 py-2 text-sm font-medium tracking-wide text-on-primary transition hover:opacity-75 dark:bg-primary-dark dark:text-on-primary-dark">{{ t(key="cart-checkout", lang=lang | default(value='sk')) }}</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="rounded-radius border border-outline px-6 py-16 text-center dark:border-outline-dark">
|
||||
<p class="text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="cart-empty", lang=lang | default(value='sk')) }}</p>
|
||||
<a href="/shop" class="mt-4 inline-flex items-center justify-center rounded-radius bg-primary px-4 py-2 text-sm font-medium text-on-primary transition hover:opacity-75 dark:bg-primary-dark dark:text-on-primary-dark">{{ t(key="cart-continue", lang=lang | default(value='sk')) }}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
@@ -46,18 +46,32 @@
|
||||
<div class="space-y-1.5">
|
||||
<label for="phone" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-phone", lang=lang | default(value='sk')) }}</label>
|
||||
<div class="flex gap-2">
|
||||
<select id="phone_prefix" name="phone_prefix" aria-label="{{ t(key='checkout-phone', lang=lang | default(value='sk')) }}"
|
||||
class="w-32 shrink-0 rounded-radius border border-outline bg-surface px-2 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
|
||||
<option value="+421">🇸🇰 +421</option>
|
||||
<option value="+420">🇨🇿 +420</option>
|
||||
<option value="+43">🇦🇹 +43</option>
|
||||
<option value="+49">🇩🇪 +49</option>
|
||||
<option value="+48">🇵🇱 +48</option>
|
||||
<option value="+36">🇭🇺 +36</option>
|
||||
<option value="+44">🇬🇧 +44</option>
|
||||
<option value="+39">🇮🇹 +39</option>
|
||||
<option value="+33">🇫🇷 +33</option>
|
||||
</select>
|
||||
<!-- editable combobox: type freely or pick from the dropdown -->
|
||||
<div class="relative w-28 shrink-0" @click.outside="prefixOpen = false"
|
||||
x-data="{ prefixOpen: false, prefix: '+421', opts: [
|
||||
{ v: '+421', l: '🇸🇰 +421' }, { v: '+420', l: '🇨🇿 +420' },
|
||||
{ v: '+43', l: '🇦🇹 +43' }, { v: '+49', l: '🇩🇪 +49' },
|
||||
{ v: '+48', l: '🇵🇱 +48' }, { v: '+36', l: '🇭🇺 +36' },
|
||||
{ v: '+44', l: '🇬🇧 +44' }, { v: '+39', l: '🇮🇹 +39' }, { v: '+33', l: '🇫🇷 +33' }
|
||||
], get filtered() { return this.opts.filter(o => !this.prefix || o.v.includes(this.prefix)) } }">
|
||||
<input name="phone_prefix" type="text" x-model="prefix" required @focus="prefixOpen = true" @input="prefixOpen = true"
|
||||
aria-label="{{ t(key='checkout-phone', lang=lang | default(value='sk')) }}" autocomplete="tel-country-code" inputmode="tel"
|
||||
class="w-full rounded-radius border border-outline bg-surface py-2 pl-3 pr-7 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
|
||||
<button type="button" tabindex="-1" @click="prefixOpen = !prefixOpen"
|
||||
class="absolute inset-y-0 right-0 flex w-7 items-center justify-center text-on-surface/60 dark:text-on-surface-dark/60">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"
|
||||
class="size-4 transition-transform" :class="prefixOpen && 'rotate-180'">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
|
||||
</svg>
|
||||
</button>
|
||||
<ul x-show="prefixOpen" x-cloak x-transition
|
||||
class="absolute z-20 mt-1 max-h-56 w-full overflow-auto rounded-radius border border-outline bg-surface p-1 shadow-lg dark:border-outline-dark dark:bg-surface-dark-alt">
|
||||
<template x-for="o in filtered" :key="o.v">
|
||||
<li><button type="button" @click="prefix = o.v; prefixOpen = false" x-text="o.l"
|
||||
class="block w-full rounded-radius px-3 py-1.5 text-left text-sm text-on-surface transition hover:bg-surface-alt dark:text-on-surface-dark dark:hover:bg-surface-dark"></button></li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
<input id="phone" name="phone" type="tel" required autocomplete="tel" inputmode="tel" placeholder="900 000 000"
|
||||
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
|
||||
</div>
|
||||
@@ -85,15 +99,32 @@
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<label for="country" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-country", lang=lang | default(value='sk')) }}</label>
|
||||
<select id="country" name="country" required
|
||||
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
|
||||
<option value="Slovensko" selected>🇸🇰 {{ t(key="country-sk", lang=lang | default(value='sk')) }}</option>
|
||||
<option value="Česko">🇨🇿 {{ t(key="country-cz", lang=lang | default(value='sk')) }}</option>
|
||||
<option value="Rakúsko">🇦🇹 {{ t(key="country-at", lang=lang | default(value='sk')) }}</option>
|
||||
<option value="Nemecko">🇩🇪 {{ t(key="country-de", lang=lang | default(value='sk')) }}</option>
|
||||
<option value="Poľsko">🇵🇱 {{ t(key="country-pl", lang=lang | default(value='sk')) }}</option>
|
||||
<option value="Maďarsko">🇭🇺 {{ t(key="country-hu", lang=lang | default(value='sk')) }}</option>
|
||||
</select>
|
||||
<div class="relative" @click.outside="countryOpen = false"
|
||||
x-data="{ countryOpen: false, country: '{{ t(key='country-sk', lang=lang | default(value='sk')) }}', opts: [
|
||||
{ v: '{{ t(key='country-sk', lang=lang | default(value='sk')) }}', l: '🇸🇰 {{ t(key='country-sk', lang=lang | default(value='sk')) }}' },
|
||||
{ v: '{{ t(key='country-cz', lang=lang | default(value='sk')) }}', l: '🇨🇿 {{ t(key='country-cz', lang=lang | default(value='sk')) }}' },
|
||||
{ v: '{{ t(key='country-at', lang=lang | default(value='sk')) }}', l: '🇦🇹 {{ t(key='country-at', lang=lang | default(value='sk')) }}' },
|
||||
{ v: '{{ t(key='country-de', lang=lang | default(value='sk')) }}', l: '🇩🇪 {{ t(key='country-de', lang=lang | default(value='sk')) }}' },
|
||||
{ v: '{{ t(key='country-pl', lang=lang | default(value='sk')) }}', l: '🇵🇱 {{ t(key='country-pl', lang=lang | default(value='sk')) }}' },
|
||||
{ v: '{{ t(key='country-hu', lang=lang | default(value='sk')) }}', l: '🇭🇺 {{ t(key='country-hu', lang=lang | default(value='sk')) }}' }
|
||||
], get filtered() { return this.opts.filter(o => !this.country || o.v.toLowerCase().includes(this.country.toLowerCase())) } }">
|
||||
<input id="country" name="country" type="text" x-model="country" required @focus="countryOpen = true" @input="countryOpen = true"
|
||||
class="w-full rounded-radius border border-outline bg-surface py-2 pl-3 pr-8 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
|
||||
<button type="button" tabindex="-1" @click="countryOpen = !countryOpen"
|
||||
class="absolute inset-y-0 right-0 flex w-8 items-center justify-center text-on-surface/60 dark:text-on-surface-dark/60">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"
|
||||
class="size-4 transition-transform" :class="countryOpen && 'rotate-180'">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
|
||||
</svg>
|
||||
</button>
|
||||
<ul x-show="countryOpen" x-cloak x-transition
|
||||
class="absolute z-20 mt-1 max-h-56 w-full overflow-auto rounded-radius border border-outline bg-surface p-1 shadow-lg dark:border-outline-dark dark:bg-surface-dark-alt">
|
||||
<template x-for="o in filtered" :key="o.v">
|
||||
<li><button type="button" @click="country = o.v; countryOpen = false" x-text="o.l"
|
||||
class="block w-full rounded-radius px-3 py-1.5 text-left text-sm text-on-surface transition hover:bg-surface-alt dark:text-on-surface-dark dark:hover:bg-surface-dark"></button></li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
Reference in New Issue
Block a user