265 lines
15 KiB
HTML
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>
|