Files
2026-05-16 22:42:22 +02:00

265 lines
15 KiB
HTML

<!DOCTYPE html>
<html lang="{{ lang }}" data-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script>
// Resolve and 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 highlightTheme(t) {
document.querySelectorAll('[data-theme-opt]').forEach(function (b) {
var on = b.getAttribute('data-theme-opt') === t;
b.classList.toggle('active', on);
var chk = b.querySelector('.opt-check');
if (chk) chk.classList.toggle('hidden', !on);
});
}
function setTheme(t) {
localStorage.setItem('theme', t);
applyTheme(t);
highlightTheme(t);
}
applyTheme(localStorage.getItem('theme') || 'system');
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function () {
if ((localStorage.getItem('theme') || 'system') === 'system') applyTheme('system');
});
document.addEventListener('DOMContentLoaded', function () {
highlightTheme(localStorage.getItem('theme') || 'system');
});
</script>
<title>{% block title %}{{ t(key="brand", lang=lang) }}{% endblock title %}</title>
<meta name="description"
content="{% block meta_description %}{{ t(key='meta-description', lang=lang) }}{% endblock meta_description %}">
<!-- Open Graph / Twitter — how the page previews when its link is shared
(chat apps, social). Not scored by Lighthouse SEO, but cheap to have.
og:url and og:image are left out: they need the absolute production
domain, so wire them once the site has one. -->
<meta property="og:type" content="website">
<meta property="og:site_name" content="{{ t(key='brand', lang=lang) }}">
<meta property="og:title" content="{{ t(key='brand', lang=lang) }}">
<meta property="og:description" content="{{ t(key='meta-description', lang=lang) }}">
<meta property="og:locale" content="{% if lang == 'en' %}en_US{% else %}sk_SK{% endif %}">
<meta name="twitter:card" content="summary">
<!-- Tailwind + daisyUI, compiled and purged at build time — see
`npm run build:css`. Replaces the render-blocking Tailwind Play CDN. -->
<link href="/static/css/app.css" rel="stylesheet" type="text/css" />
<style>
/* Keep buttons static — disable daisyUI's press-shrink animation. */
.btn { --animation-btn: 0; --btn-focus-scale: 1; }
/* App-wide contrast. The daisyUI base palette is very low-contrast, so
panels, tables and form fields blend into the page. Tie their edges to
base-content so every region is easy to pick out by eye, on light and
dark alike. The calendar grid keeps its own stronger #cal rules. */
.border.border-base-300 { border-color: hsl(var(--bc) / 0.2); }
.navbar { border-bottom: 1px solid hsl(var(--bc) / 0.15); }
.input.input-bordered,
.select.select-bordered,
.textarea.textarea-bordered { border-color: hsl(var(--bc) / 0.3); }
.checkbox { border-color: hsl(var(--bc) / 0.3); }
.table thead th { background-color: hsl(var(--bc) / 0.08); }
.table tbody tr:not(:last-child) :where(th, td) {
border-bottom-color: hsl(var(--bc) / 0.18);
}
/* Mobile: pin navbar dropdowns to the viewport's right edge and cap their
width so they can never spill off-screen, whatever the trigger's spot. */
@media (max-width: 767px) {
.navbar .dropdown-content {
position: fixed;
top: 4rem;
right: 0.5rem;
left: auto;
margin-top: 0;
max-width: calc(100vw - 1rem);
}
}
/* Mobile: a dimming backdrop behind an open navbar dropdown, driven by
CSS alone. `:has()` shows it whenever a dropdown holds focus; a tap
outside the menu blurs the trigger, which closes the dropdown. The
delayed `visibility` transition keeps the backdrop hit-testable for a
beat after that tap, so the tap lands on the backdrop instead of
falling through to the page. It sits below the dropdown content
(z-50) so the menu items stay tappable. */
#nav-backdrop { display: none; }
@media (max-width: 767px) {
#nav-backdrop {
display: block;
position: fixed;
inset: 0;
z-index: 40;
background-color: rgba(0, 0, 0, 0.25);
opacity: 0;
visibility: hidden;
transition: opacity 0.15s ease, visibility 0s linear 0.2s;
}
.navbar:has(.dropdown:focus-within) ~ #nav-backdrop {
opacity: 1;
visibility: visible;
transition: opacity 0.15s ease, visibility 0s;
}
}
</style>
{% block head %}{% endblock head %}
</head>
<body class="min-h-screen bg-base-200 font-sans text-base-content antialiased">
<div class="navbar bg-base-100 shadow-sm">
<div class="mx-auto flex w-full max-w-6xl items-center justify-between gap-2 px-4">
<a href="/" class="min-w-0 truncate text-lg font-bold">{{ t(key="brand", lang=lang) }}</a>
<nav class="flex items-center gap-1">
<!-- Page links — inline on desktop, tucked into a menu on mobile. -->
<div class="hidden items-center gap-1 md:flex">
<a href="/" class="btn btn-ghost btn-sm">{{ t(key="nav-calendar", lang=lang) }}</a>
<a href="/about" class="btn btn-ghost btn-sm">{{ t(key="nav-about", lang=lang) }}</a>
{% if logged_in | default(value=false) %}
<a href="/admin" class="btn btn-ghost btn-sm">{{ t(key="admin-title", lang=lang) }}</a>
<a href="/admin/courts" class="btn btn-ghost btn-sm">{{ t(key="manage-courts", lang=lang) }}</a>
<form method="post" action="/admin/logout">
<button class="btn btn-ghost btn-sm">{{ t(key="logout", lang=lang) }}</button>
</form>
{% else %}
<a href="/admin/login" class="btn btn-ghost btn-sm">{{ t(key="nav-admin", lang=lang) }}</a>
{% endif %}
</div>
<div class="dropdown dropdown-end md:hidden">
<div tabindex="0" role="button" class="btn btn-ghost btn-sm btn-circle"
aria-label="{{ t(key='menu', lang=lang) }}">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="h-5 w-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
</svg>
</div>
<div tabindex="0"
class="dropdown-content z-50 mt-3 flex w-52 flex-col gap-1 rounded-box border border-base-300 bg-base-100 p-2 shadow-lg">
<a href="/" class="btn btn-ghost btn-sm justify-start">{{ t(key="nav-calendar", lang=lang) }}</a>
<a href="/about" class="btn btn-ghost btn-sm justify-start">{{ t(key="nav-about", lang=lang) }}</a>
{% if logged_in | default(value=false) %}
<a href="/admin" class="btn btn-ghost btn-sm justify-start">{{ t(key="admin-title", lang=lang) }}</a>
<a href="/admin/courts" class="btn btn-ghost btn-sm justify-start">{{ t(key="manage-courts", lang=lang) }}</a>
<form method="post" action="/admin/logout">
<button class="btn btn-ghost btn-sm w-full justify-start">{{ t(key="logout", lang=lang) }}</button>
</form>
{% else %}
<a href="/admin/login" class="btn btn-ghost btn-sm justify-start">{{ t(key="nav-admin", lang=lang) }}</a>
{% endif %}
</div>
</div>
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost btn-sm btn-circle"
aria-label="{{ t(key='settings', lang=lang) }}" title="{{ t(key='settings', lang=lang) }}">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="h-5 w-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>
</div>
<!-- The language buttons submit this form; the /lang handler sets
the `lang` cookie and redirects back. The theme buttons are
type="button" so they never submit. -->
<form method="post" action="/lang">
<ul tabindex="0"
class="menu dropdown-content z-50 mt-3 w-56 rounded-box border border-base-300 bg-base-100 p-2 shadow-lg">
<li class="menu-title">{{ t(key="settings-language", lang=lang) }}</li>
<li>
<button type="submit" name="lang" value="en" class="{% if lang == 'en' %}active{% endif %}">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="h-4 w-4">
<path stroke-linecap="round" stroke-linejoin="round"
d="M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5c-3.162 0-6.133-.815-8.716-2.247" />
</svg>
<span>English</span>
{% if lang == 'en' %}
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="3"
stroke="currentColor" class="ml-auto h-4 w-4">
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
</svg>
{% endif %}
</button>
</li>
<li>
<button type="submit" name="lang" value="sk" class="{% if lang == 'sk' %}active{% endif %}">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="h-4 w-4">
<path stroke-linecap="round" stroke-linejoin="round"
d="M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5c-3.162 0-6.133-.815-8.716-2.247" />
</svg>
<span>Slovenčina</span>
{% if lang == 'sk' %}
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="3"
stroke="currentColor" class="ml-auto h-4 w-4">
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
</svg>
{% endif %}
</button>
</li>
<li class="menu-title">{{ t(key="settings-theme", lang=lang) }}</li>
<li>
<button type="button" data-theme-opt="system" onclick="setTheme('system')">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="h-4 w-4">
<path stroke-linecap="round" stroke-linejoin="round"
d="M9 17.25v1.007a3 3 0 0 1-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0 1 15 18.257V17.25m6-12V15a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 15V5.25m18 0A2.25 2.25 0 0 0 18.75 3H5.25A2.25 2.25 0 0 0 3 5.25m18 0V12a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 12V5.25" />
</svg>
<span>{{ t(key="theme-system", lang=lang) }}</span>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="3"
stroke="currentColor" class="opt-check ml-auto hidden h-4 w-4">
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
</svg>
</button>
</li>
<li>
<button type="button" data-theme-opt="light" onclick="setTheme('light')">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="h-4 w-4">
<path stroke-linecap="round" stroke-linejoin="round"
d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z" />
</svg>
<span>{{ t(key="theme-light", lang=lang) }}</span>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="3"
stroke="currentColor" class="opt-check ml-auto hidden h-4 w-4">
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
</svg>
</button>
</li>
<li>
<button type="button" data-theme-opt="dark" onclick="setTheme('dark')">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="h-4 w-4">
<path stroke-linecap="round" stroke-linejoin="round"
d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z" />
</svg>
<span>{{ t(key="theme-dark", lang=lang) }}</span>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="3"
stroke="currentColor" class="opt-check ml-auto hidden h-4 w-4">
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
</svg>
</button>
</li>
</ul>
</form>
</div>
</nav>
</div>
</div>
<div id="nav-backdrop" aria-hidden="true"></div>
<main class="mx-auto max-w-6xl px-4 py-6">
{% block content %}{% endblock content %}
</main>
{% block js %}{% endblock js %}
</body>
</html>