219 lines
17 KiB
HTML
219 lines
17 KiB
HTML
{% extends "base.html" %}
|
||
{% import "macros/ui.html" as ui %}
|
||
|
||
{% block title %}{{ t(key="checkout-title", lang=lang | default(value='sk')) }}{% endblock title %}
|
||
|
||
{% block content %}
|
||
{% if packeta_api_key %}<script src="https://widget.packeta.com/v6/www/js/library.js"></script>{% endif %}
|
||
|
||
<h1 class="text-3xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-title", lang=lang | default(value='sk')) }}</h1>
|
||
|
||
<form method="post" action="/checkout" hx-boost="false"
|
||
x-data="{
|
||
paymentMethod: '',
|
||
carrier: '',
|
||
carrierPrice: 0,
|
||
requiresPoint: false,
|
||
pointId: '',
|
||
pointName: '',
|
||
subtotal: {{ subtotal_cents }},
|
||
packetaKey: '{{ packeta_api_key }}',
|
||
fmt(c) { return (c / 100).toFixed(2) },
|
||
pickPoint() {
|
||
Packeta.Widget.pick(this.packetaKey, (point) => {
|
||
if (point) { this.pointId = String(point.id); this.pointName = point.formatedValue || point.name }
|
||
})
|
||
},
|
||
get canSubmit() {
|
||
return this.paymentMethod && this.carrier && (!this.requiresPoint || this.pointId)
|
||
}
|
||
}"
|
||
class="mt-6 grid gap-8 lg:grid-cols-3">
|
||
|
||
<div class="space-y-6 lg:col-span-2">
|
||
<!-- contact -->
|
||
<fieldset class="space-y-4 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
|
||
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-contact", lang=lang | default(value='sk')) }}</legend>
|
||
<div class="space-y-1.5">
|
||
<label for="email" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-email", lang=lang | default(value='sk')) }}</label>
|
||
<input id="email" name="email" type="email" 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">
|
||
</div>
|
||
<div class="space-y-1.5">
|
||
<label for="customer_name" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-name", lang=lang | default(value='sk')) }}</label>
|
||
<input id="customer_name" name="customer_name" type="text" 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">
|
||
</div>
|
||
<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">
|
||
<!-- 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>
|
||
</div>
|
||
</fieldset>
|
||
|
||
<!-- shipping address -->
|
||
<fieldset class="space-y-4 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
|
||
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-shipping", lang=lang | default(value='sk')) }}</legend>
|
||
<div class="space-y-1.5">
|
||
<label for="address" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-address", lang=lang | default(value='sk')) }}</label>
|
||
<input id="address" name="address" type="text" 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">
|
||
</div>
|
||
<div class="grid gap-4 sm:grid-cols-3">
|
||
<div class="space-y-1.5">
|
||
<label for="city" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-city", lang=lang | default(value='sk')) }}</label>
|
||
<input id="city" name="city" type="text" 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">
|
||
</div>
|
||
<div class="space-y-1.5">
|
||
<label for="zip" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-zip", lang=lang | default(value='sk')) }}</label>
|
||
<input id="zip" name="zip" type="text" 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">
|
||
</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>
|
||
<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>
|
||
|
||
<!-- carrier -->
|
||
<fieldset class="space-y-3 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
|
||
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-carrier", lang=lang | default(value='sk')) }}</legend>
|
||
{% for m in shipping_methods %}
|
||
<label class="flex cursor-pointer items-center justify-between gap-3 rounded-radius border border-outline px-4 py-3 transition has-[:checked]:border-primary dark:border-outline-dark dark:has-[:checked]:border-primary-dark">
|
||
<span class="flex items-center gap-3">
|
||
<input type="radio" name="carrier_code" value="{{ m.code }}" required
|
||
@change="carrier='{{ m.code }}'; carrierPrice={{ m.price_cents }}; requiresPoint={{ m.requires_pickup_point }}; pointId=''; pointName=''"
|
||
class="size-4 border-outline text-primary focus:ring-primary dark:border-outline-dark">
|
||
<span class="font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ m.name }}</span>
|
||
</span>
|
||
<span class="tabular-nums text-on-surface/80 dark:text-on-surface-dark/80">{{ m.price }} {{ currency }}</span>
|
||
</label>
|
||
{% endfor %}
|
||
|
||
<!-- pickup point (carriers that need one, e.g. Packeta) -->
|
||
<div x-show="requiresPoint" x-cloak class="space-y-2 pt-1">
|
||
<input type="hidden" name="pickup_point_id" x-model="pointId">
|
||
<input type="hidden" name="pickup_point_name" x-model="pointName">
|
||
{% if packeta_api_key %}
|
||
<button type="button" @click="pickPoint()"
|
||
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="checkout-pick-point", lang=lang | default(value='sk')) }}
|
||
</button>
|
||
<p x-show="pointName" x-cloak class="text-sm text-on-surface/80 dark:text-on-surface-dark/80">
|
||
<span class="font-medium">{{ t(key="checkout-chosen-point", lang=lang | default(value='sk')) }}:</span> <span x-text="pointName"></span>
|
||
</p>
|
||
{% else %}
|
||
<label class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-pickup-point", lang=lang | default(value='sk')) }}</label>
|
||
<input type="text" x-model="pointName" @input="pointId = pointName"
|
||
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">
|
||
{% endif %}
|
||
</div>
|
||
</fieldset>
|
||
|
||
<!-- payment -->
|
||
<fieldset class="space-y-3 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
|
||
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-payment", lang=lang | default(value='sk')) }}</legend>
|
||
<label class="flex cursor-pointer items-center gap-3 rounded-radius border border-outline px-4 py-3 transition has-[:checked]:border-primary dark:border-outline-dark dark:has-[:checked]:border-primary-dark">
|
||
<input type="radio" name="payment_method" value="cod" required x-model="paymentMethod"
|
||
class="size-4 border-outline text-primary focus:ring-primary dark:border-outline-dark">
|
||
<span class="font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="payment-cod", lang=lang | default(value='sk')) }}</span>
|
||
</label>
|
||
<label class="flex cursor-pointer items-center gap-3 rounded-radius border border-outline px-4 py-3 transition has-[:checked]:border-primary dark:border-outline-dark dark:has-[:checked]:border-primary-dark">
|
||
<input type="radio" name="payment_method" value="bank_transfer" required x-model="paymentMethod"
|
||
class="size-4 border-outline text-primary focus:ring-primary dark:border-outline-dark">
|
||
<span class="font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="payment-bank", lang=lang | default(value='sk')) }}</span>
|
||
</label>
|
||
</fieldset>
|
||
|
||
<div class="space-y-1.5">
|
||
<label for="note" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-note", lang=lang | default(value='sk')) }}</label>
|
||
<textarea id="note" name="note" rows="3"
|
||
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"></textarea>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- summary -->
|
||
<aside class="h-fit space-y-4 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
|
||
<h2 class="text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-summary", lang=lang | default(value='sk')) }}</h2>
|
||
<ul class="space-y-2 text-sm">
|
||
{% for item in items %}
|
||
<li class="flex justify-between gap-2">
|
||
<span class="text-on-surface/80 dark:text-on-surface-dark/80">{{ item.name }} × {{ item.quantity }}</span>
|
||
<span class="tabular-nums">{{ item.line_total }} {{ item.currency }}</span>
|
||
</li>
|
||
{% endfor %}
|
||
</ul>
|
||
<div class="space-y-1 border-t border-outline pt-3 text-sm dark:border-outline-dark">
|
||
<div class="flex justify-between">
|
||
<span class="text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="checkout-subtotal", lang=lang | default(value='sk')) }}</span>
|
||
<span class="tabular-nums">{{ subtotal }} {{ currency }}</span>
|
||
</div>
|
||
<div class="flex justify-between">
|
||
<span class="text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="checkout-shipping-cost", lang=lang | default(value='sk')) }}</span>
|
||
<span class="tabular-nums" x-text="fmt(carrierPrice) + ' {{ currency }}'"></span>
|
||
</div>
|
||
</div>
|
||
<div class="flex justify-between border-t border-outline pt-3 text-base font-bold dark:border-outline-dark">
|
||
<span>{{ t(key="cart-total", lang=lang | default(value='sk')) }}</span>
|
||
<span class="tabular-nums text-primary dark:text-primary-dark" x-text="fmt(subtotal + carrierPrice) + ' {{ currency }}'"></span>
|
||
</div>
|
||
{{ ui::button(label=t(key="checkout-place-order", lang=lang | default(value='sk')), type="submit", attrs=':disabled="!canSubmit"', extra="w-full", size="px-6 py-2.5 text-sm") }}
|
||
</aside>
|
||
</form>
|
||
{% endblock content %}
|