210 lines
17 KiB
HTML
210 lines
17 KiB
HTML
<!doctype html>
|
|
<html lang="{{ lang | default(value='sk') }}" data-theme="dark">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>{% block title %}{{ t(key="brand", lang=lang | default(value='sk')) }}{% endblock title %}</title>
|
|
<meta name="description" content="{% block meta_description %}{{ t(key="meta-description", lang=lang | default(value='sk')) }}{% endblock meta_description %}">
|
|
<link rel="icon" type="image/x-icon" href="/static/favicon/favicon.ico">
|
|
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon/favicon-32x32.png">
|
|
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon/favicon-16x16.png">
|
|
<link rel="apple-touch-icon" sizes="180x180" href="/static/favicon/apple-touch-icon.png">
|
|
<link rel="manifest" href="/static/favicon/site.webmanifest">
|
|
<script>
|
|
// Apply the saved theme before first paint to avoid a flash.
|
|
function applyTheme(t) {
|
|
var dark = t === 'dark'
|
|
|| (t === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light');
|
|
}
|
|
function setTheme(t) {
|
|
localStorage.setItem('theme', t);
|
|
applyTheme(t);
|
|
document.dispatchEvent(new CustomEvent('theme:changed', { detail: t }));
|
|
}
|
|
function currentTheme() { return localStorage.getItem('theme') || 'dark'; }
|
|
applyTheme(currentTheme());
|
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function () {
|
|
if (currentTheme() === 'system') applyTheme('system');
|
|
});
|
|
// Mark the active top-nav link via aria-current (styled with Tailwind).
|
|
function markActiveNav() {
|
|
var path = location.pathname;
|
|
document.querySelectorAll('a[data-nav]').forEach(function (a) {
|
|
var h = a.getAttribute('data-nav');
|
|
var on = h === path || (h !== '/' && path.indexOf(h) === 0);
|
|
if (on) a.setAttribute('aria-current', 'page');
|
|
else a.removeAttribute('aria-current');
|
|
});
|
|
}
|
|
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>
|
|
<script defer src="/static/vendor/alpine/alpinejs-3.14.9.min.js"></script>
|
|
</head>
|
|
<body hx-boost="true"
|
|
x-data="{ cats: false, lg: window.matchMedia('(min-width: 1024px)').matches }"
|
|
x-init="window.matchMedia('(min-width: 1024px)').addEventListener('change', e => lg = e.matches)"
|
|
class="min-h-screen bg-surface text-on-surface antialiased dark:bg-surface-dark dark:text-on-surface-dark">
|
|
<header
|
|
class="sticky top-0 z-30 border-b border-outline bg-surface/95 backdrop-blur dark:border-outline-dark dark:bg-surface-dark/95">
|
|
<nav x-data="{ mobile: false }" class="mx-auto flex max-w-7xl items-center gap-4 px-4 py-3">
|
|
<!-- category sidebar toggle (mobile only) -->
|
|
<button type="button" @click="cats = !cats" :aria-expanded="cats"
|
|
aria-label="{{ t(key='categories', lang=lang | default(value='sk')) }}"
|
|
class="inline-flex size-9 items-center justify-center rounded-radius text-on-surface transition hover:bg-surface-alt lg:hidden dark:text-on-surface-dark dark:hover:bg-surface-dark-alt">
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
|
</svg>
|
|
</button>
|
|
<a href="/"
|
|
class="text-lg font-bold tracking-tight text-on-surface-strong dark:text-on-surface-dark-strong">
|
|
{{ t(key="brand", lang=lang | default(value='sk')) }}
|
|
</a>
|
|
|
|
<!-- desktop links -->
|
|
<ul class="ml-2 hidden items-center gap-1 md:flex">
|
|
<li><a href="/" data-nav="/" class="rounded-radius px-3 py-1.5 text-sm font-medium text-on-surface transition hover:bg-surface-alt hover:text-primary aria-[current=page]:text-primary aria-[current=page]:font-semibold dark:text-on-surface-dark dark:hover:bg-surface-dark-alt dark:hover:text-primary-dark dark:aria-[current=page]:text-primary-dark">{{ t(key="nav-home", lang=lang | default(value='sk')) }}</a></li>
|
|
<li><a href="/shop" data-nav="/shop" class="rounded-radius px-3 py-1.5 text-sm font-medium text-on-surface transition hover:bg-surface-alt hover:text-primary aria-[current=page]:text-primary aria-[current=page]:font-semibold dark:text-on-surface-dark dark:hover:bg-surface-dark-alt dark:hover:text-primary-dark dark:aria-[current=page]:text-primary-dark">{{ t(key="nav-shop", lang=lang | default(value='sk')) }}</a></li>
|
|
{% if logged_in_admin %}
|
|
<li><a href="/admin/dashboard" hx-boost="false" data-nav="/admin" class="rounded-radius px-3 py-1.5 text-sm font-medium text-warning transition hover:bg-surface-alt dark:hover:bg-surface-dark-alt">{{ t(key="admin-title", lang=lang | default(value='sk')) }}</a></li>
|
|
<li>
|
|
<form method="post" action="/admin/logout" hx-boost="false">
|
|
<button type="submit" class="rounded-radius px-3 py-1.5 text-sm font-medium text-danger transition hover:bg-surface-alt dark:hover:bg-surface-dark-alt">{{ t(key="logout", lang=lang | default(value='sk')) }}</button>
|
|
</form>
|
|
</li>
|
|
{% else %}
|
|
<li><a href="/admin/login" data-nav="/admin/login" class="rounded-radius px-3 py-1.5 text-sm font-medium text-on-surface transition hover:bg-surface-alt hover:text-primary aria-[current=page]:text-primary aria-[current=page]:font-semibold dark:text-on-surface-dark dark:hover:bg-surface-dark-alt dark:hover:text-primary-dark dark:aria-[current=page]:text-primary-dark">{{ t(key="nav-admin", lang=lang | default(value='sk')) }}</a></li>
|
|
{% endif %}
|
|
</ul>
|
|
|
|
<!-- right side: cart + settings + mobile toggle -->
|
|
<div class="ml-auto flex items-center gap-1">
|
|
<!-- cart with live item-count badge read from the `cart` cookie -->
|
|
<a href="/cart" data-nav="/cart"
|
|
x-data="{ count: 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">
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 0 0-3 3h15.75m-12.75-3h11.218c1.121-2.3 2.1-4.684 2.924-7.138a60.114 60.114 0 0 0-16.536-1.84M7.5 14.25 5.106 5.272M6 20.25a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Zm12.75 0a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z" />
|
|
</svg>
|
|
<span x-show="count > 0" x-cloak x-text="count"
|
|
class="absolute -right-1 -top-1 inline-flex min-w-4 items-center justify-center rounded-full bg-primary px-1 text-[10px] font-semibold leading-4 text-on-primary dark:bg-primary-dark dark:text-on-primary-dark"></span>
|
|
</a>
|
|
<!-- settings (language + theme) dropdown -->
|
|
<div x-data="{ open: false }" @keydown.escape="open = false" class="relative">
|
|
<button type="button" @click="open = !open" :aria-expanded="open"
|
|
aria-label="{{ t(key='settings', lang=lang | default(value='sk')) }}"
|
|
title="{{ t(key='settings', lang=lang | default(value='sk')) }}"
|
|
class="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">
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
|
stroke="currentColor" class="size-5">
|
|
<path stroke-linecap="round" stroke-linejoin="round"
|
|
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" />
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
|
</svg>
|
|
</button>
|
|
<div x-show="open" x-cloak @click.outside="open = false"
|
|
x-transition.origin.top.right
|
|
class="absolute right-0 mt-2 w-56 rounded-radius border border-outline bg-surface p-2 shadow-lg dark:border-outline-dark dark:bg-surface-dark-alt">
|
|
<form method="post" action="/lang" hx-boost="false">
|
|
<p class="px-2 py-1 text-xs font-semibold uppercase tracking-wide text-on-surface/60 dark:text-on-surface-dark/60">
|
|
{{ t(key="settings-language", lang=lang | default(value='sk')) }}
|
|
</p>
|
|
<button type="submit" name="lang" value="en"
|
|
class="flex w-full items-center justify-between rounded-radius px-2 py-1.5 text-sm text-on-surface transition hover:bg-surface-alt dark:text-on-surface-dark dark:hover:bg-surface-dark">
|
|
<span>English</span>
|
|
{% if lang | default(value='sk') == "en" %}<span class="text-primary dark:text-primary-dark">✓</span>{% endif %}
|
|
</button>
|
|
<button type="submit" name="lang" value="sk"
|
|
class="flex w-full items-center justify-between rounded-radius px-2 py-1.5 text-sm text-on-surface transition hover:bg-surface-alt dark:text-on-surface-dark dark:hover:bg-surface-dark">
|
|
<span>Slovenčina</span>
|
|
{% if lang | default(value='sk') == "sk" %}<span class="text-primary dark:text-primary-dark">✓</span>{% endif %}
|
|
</button>
|
|
</form>
|
|
<p class="mt-1 px-2 py-1 text-xs font-semibold uppercase tracking-wide text-on-surface/60 dark:text-on-surface-dark/60">
|
|
{{ t(key="settings-theme", lang=lang | default(value='sk')) }}
|
|
</p>
|
|
<div x-data="{ theme: currentTheme() }" @theme:changed.document="theme = $event.detail">
|
|
<button type="button" @click="setTheme('system')"
|
|
class="flex w-full items-center justify-between rounded-radius px-2 py-1.5 text-sm text-on-surface transition hover:bg-surface-alt dark:text-on-surface-dark dark:hover:bg-surface-dark">
|
|
<span>{{ t(key="theme-system", lang=lang | default(value='sk')) }}</span>
|
|
<span x-show="theme === 'system'" class="text-primary dark:text-primary-dark">✓</span>
|
|
</button>
|
|
<button type="button" @click="setTheme('light')"
|
|
class="flex w-full items-center justify-between rounded-radius px-2 py-1.5 text-sm text-on-surface transition hover:bg-surface-alt dark:text-on-surface-dark dark:hover:bg-surface-dark">
|
|
<span>{{ t(key="theme-light", lang=lang | default(value='sk')) }}</span>
|
|
<span x-show="theme === 'light'" class="text-primary dark:text-primary-dark">✓</span>
|
|
</button>
|
|
<button type="button" @click="setTheme('dark')"
|
|
class="flex w-full items-center justify-between rounded-radius px-2 py-1.5 text-sm text-on-surface transition hover:bg-surface-alt dark:text-on-surface-dark dark:hover:bg-surface-dark">
|
|
<span>{{ t(key="theme-dark", lang=lang | default(value='sk')) }}</span>
|
|
<span x-show="theme === 'dark'" class="text-primary dark:text-primary-dark">✓</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- mobile hamburger -->
|
|
<button type="button" @click="mobile = !mobile" :aria-expanded="mobile"
|
|
aria-label="{{ t(key='menu', lang=lang | default(value='sk')) }}"
|
|
class="inline-flex size-9 items-center justify-center rounded-radius text-on-surface transition hover:bg-surface-alt md:hidden dark:text-on-surface-dark dark:hover:bg-surface-dark-alt">
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
|
stroke="currentColor" class="size-6">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- mobile menu panel -->
|
|
<ul x-show="mobile" x-cloak @click.outside="mobile = false" x-transition
|
|
class="absolute inset-x-0 top-full mx-4 mt-2 flex flex-col gap-1 rounded-radius border border-outline bg-surface p-2 shadow-lg md:hidden dark:border-outline-dark dark:bg-surface-dark-alt">
|
|
<li><a href="/" class="block rounded-radius px-3 py-2 text-sm font-medium text-on-surface transition hover:bg-surface-alt hover:text-primary dark:text-on-surface-dark dark:hover:bg-surface-dark dark:hover:text-primary-dark">{{ t(key="nav-home", lang=lang | default(value='sk')) }}</a></li>
|
|
<li><a href="/shop" class="block rounded-radius px-3 py-2 text-sm font-medium text-on-surface transition hover:bg-surface-alt hover:text-primary dark:text-on-surface-dark dark:hover:bg-surface-dark dark:hover:text-primary-dark">{{ t(key="nav-shop", lang=lang | default(value='sk')) }}</a></li>
|
|
{% if logged_in_admin %}
|
|
<li><a href="/admin/dashboard" hx-boost="false" class="block rounded-radius px-3 py-2 text-sm font-medium text-warning hover:bg-surface-alt dark:hover:bg-surface-dark">{{ t(key="admin-title", lang=lang | default(value='sk')) }}</a></li>
|
|
<li>
|
|
<form method="post" action="/admin/logout" hx-boost="false">
|
|
<button type="submit" class="block w-full rounded-radius px-3 py-2 text-left text-sm font-medium text-danger hover:bg-surface-alt dark:hover:bg-surface-dark">{{ t(key="logout", lang=lang | default(value='sk')) }}</button>
|
|
</form>
|
|
</li>
|
|
{% else %}
|
|
<li><a href="/admin/login" class="block rounded-radius px-3 py-2 text-sm font-medium text-on-surface transition hover:bg-surface-alt hover:text-primary dark:text-on-surface-dark dark:hover:bg-surface-dark dark:hover:text-primary-dark">{{ t(key="nav-admin", lang=lang | default(value='sk')) }}</a></li>
|
|
{% endif %}
|
|
</ul>
|
|
</nav>
|
|
</header>
|
|
|
|
<!-- dark overlay behind the category drawer on small screens -->
|
|
<div x-cloak x-show="cats" x-transition.opacity @click="cats = false" aria-hidden="true"
|
|
class="fixed inset-0 z-30 bg-black/50 lg:hidden"></div>
|
|
|
|
<div class="mx-auto flex w-full max-w-7xl gap-8 px-4 py-8">
|
|
<!-- persistent category sidebar (off-canvas drawer on mobile).
|
|
hx-preserve keeps this node across boosted page swaps, so it is
|
|
fetched once (hx-trigger=load) and never reloaded on navigation. -->
|
|
<aside id="category-sidebar" hx-preserve="true"
|
|
x-cloak x-show="cats || lg" aria-label="{{ t(key='categories', lang=lang | default(value='sk')) }}"
|
|
hx-get="/partials/categories" hx-trigger="load"
|
|
class="fixed inset-y-0 left-0 z-40 w-64 overflow-y-auto border-r border-outline bg-surface-alt p-4 lg:static lg:z-auto lg:w-64 lg:shrink-0 lg:self-start lg:overflow-visible lg:rounded-radius lg:border lg:p-3 dark:border-outline-dark dark:bg-surface-dark-alt">
|
|
</aside>
|
|
|
|
<main class="min-w-0 flex-1">
|
|
{% block content %}{% endblock content %}
|
|
</main>
|
|
</div>
|
|
</body>
|
|
</html>
|