using penguiui
Some checks failed
CI / Check Style (push) Has been cancelled
CI / Run Clippy (push) Has been cancelled
CI / Run Tests (push) Has been cancelled

This commit is contained in:
Priec
2026-06-16 12:53:11 +02:00
parent 635cb34810
commit e3b99b0fd8
7 changed files with 334 additions and 215 deletions

4
.gitignore vendored
View File

@@ -22,3 +22,7 @@ target/
uploads/ uploads/
*.report.html *.report.html
favicon_io.zip favicon_io.zip
# Tailwind standalone binary (downloaded via `make tailwind`)
bin/tailwindcss
node_modules/

View File

@@ -1,6 +1,33 @@
COMPOSE = docker compose -f docker-compose.prod.yml --env-file .env.production COMPOSE = docker compose -f docker-compose.prod.yml --env-file .env.production
.PHONY: up down restart logs build ps # --- Frontend (Tailwind v4 + PenguinUI) -----------------------------
# Uses the Tailwind v4 standalone binary (no Node required at runtime).
# The compiled assets/static/css/app.css is committed and served by loco.
TW = bin/tailwindcss
CSS_IN = assets/css/app.css
CSS_OUT = assets/static/css/app.css
UNAME_M := $(shell uname -m)
ifeq ($(UNAME_M),aarch64)
TW_TARGET = tailwindcss-linux-arm64
else
TW_TARGET = tailwindcss-linux-x64
endif
.PHONY: up down restart logs build ps css css-watch tailwind
$(TW):
@mkdir -p bin
curl -fsSL -o $(TW) \
"https://github.com/tailwindlabs/tailwindcss/releases/latest/download/$(TW_TARGET)"
chmod +x $(TW)
tailwind: $(TW)
css: $(TW)
$(TW) -i $(CSS_IN) -o $(CSS_OUT) --minify
css-watch: $(TW)
$(TW) -i $(CSS_IN) -o $(CSS_OUT) --watch
up: up:
$(COMPOSE) up -d --build $(COMPOSE) up -d --build

73
assets/css/app.css Normal file
View File

@@ -0,0 +1,73 @@
/* ============================================================
* Tailwind v4 source — built into assets/static/css/app.css
* ------------------------------------------------------------
* Stack: Tailwind CSS v4 + Alpine.js v3 + PenguinUI components
* (https://www.penguinui.com). PenguinUI is copy-paste: paste a
* component's markup into a template and it picks up the design
* tokens defined in the @theme block below.
*
* Build: make css (one-off, minified)
* make css-watch (rebuild on change while developing)
*
* The compiled output assets/static/css/app.css IS committed, so
* the Docker image / loco static server need no Node at runtime.
* ============================================================ */
@import "tailwindcss";
/* Scan every template so used utility classes are emitted. */
@source "../views";
/* PenguinUI toggles dark styles with a `dark:` variant. This app
* already sets <html data-theme="dark|light"> (see base.html), so
* key the variant off that attribute instead of the OS setting. */
@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
/* === PenguinUI design tokens ================================
* "Modern" starting palette. Swap any line for another Tailwind
* color (e.g. --color-primary: var(--color-emerald-600)) or grab
* a ready-made theme from https://www.penguinui.com/theme.
* Components reference these as bg-primary, text-on-surface,
* dark:bg-surface-dark, border-outline, etc.
* ============================================================ */
@theme {
/* light mode */
--color-surface: var(--color-white);
--color-surface-alt: var(--color-slate-100);
--color-on-surface: var(--color-slate-700);
--color-on-surface-strong: var(--color-slate-900);
--color-primary: var(--color-indigo-600);
--color-on-primary: var(--color-white);
--color-secondary: var(--color-slate-600);
--color-on-secondary: var(--color-white);
--color-outline: var(--color-slate-300);
--color-outline-strong: var(--color-slate-800);
/* dark mode */
--color-surface-dark: var(--color-slate-900);
--color-surface-dark-alt: var(--color-slate-800);
--color-on-surface-dark: var(--color-slate-300);
--color-on-surface-dark-strong: var(--color-white);
--color-primary-dark: var(--color-indigo-400);
--color-on-primary-dark: var(--color-slate-950);
--color-secondary-dark: var(--color-slate-300);
--color-on-secondary-dark: var(--color-slate-950);
--color-outline-dark: var(--color-slate-700);
--color-outline-dark-strong: var(--color-slate-300);
/* shared status colors (same in both modes) */
--color-info: var(--color-sky-500);
--color-on-info: var(--color-white);
--color-success: var(--color-green-600);
--color-on-success: var(--color-white);
--color-warning: var(--color-amber-500);
--color-on-warning: var(--color-white);
--color-danger: var(--color-red-600);
--color-on-danger: var(--color-white);
/* shared design tokens */
--radius-radius: 0.375rem;
}
/* Hide Alpine x-cloak elements until Alpine initializes. */
[x-cloak] { display: none !important; }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -16,136 +16,141 @@
|| (t === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches); || (t === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light'); 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) { function setTheme(t) {
localStorage.setItem('theme', t); localStorage.setItem('theme', t);
applyTheme(t); applyTheme(t);
highlightTheme(t); document.dispatchEvent(new CustomEvent('theme:changed', { detail: t }));
} }
applyTheme(localStorage.getItem('theme') || 'dark'); function currentTheme() { return localStorage.getItem('theme') || 'dark'; }
applyTheme(currentTheme());
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function () { window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function () {
if ((localStorage.getItem('theme') || 'dark') === 'system') applyTheme('system'); if (currentTheme() === 'system') applyTheme('system');
}); });
document.addEventListener('DOMContentLoaded', function () { function markActiveNav() {
highlightTheme(localStorage.getItem('theme') || 'dark');
var path = location.pathname; var path = location.pathname;
document.querySelectorAll('.term-navlinks a[data-nav]').forEach(function (a) { document.querySelectorAll('a[data-nav]').forEach(function (a) {
var h = a.getAttribute('data-nav'); var h = a.getAttribute('data-nav');
if (h === path || (h !== '/' && path.indexOf(h) === 0)) a.classList.add('is-active'); 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);
</script> </script>
<link href="/static/css/app.css?v=2026-06-16" rel="stylesheet" type="text/css">
{% block head %}{% endblock head %} {% block head %}{% endblock head %}
<script src="/static/vendor/htmx/htmx-1.9.12.min.js"></script> <script src="/static/vendor/htmx/htmx-1.9.12.min.js"></script>
<style> <script defer src="/static/vendor/alpine/alpinejs-3.14.9.min.js"></script>
@media (min-width: 768px) {
.nav-menu { flex-direction: row; }
}
#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.5);
opacity: 0;
visibility: hidden;
transition: opacity 0.15s ease, visibility 0s linear 0.2s;
}
.term-titlebar:has(.dropdown:focus-within) ~ #nav-backdrop {
opacity: 1;
visibility: visible;
transition: opacity 0.15s ease, visibility 0s;
}
}
</style>
</head> </head>
<body class="flex min-h-screen flex-col bg-base-100 text-base-content antialiased"> <body
<header class="term-titlebar"> class="flex min-h-screen flex-col bg-surface text-on-surface antialiased dark:bg-surface-dark dark:text-on-surface-dark">
<nav class="term-nav"> <header
<a href="/admin/dashboard" class="term-brand">{{ t(key="admin-title", lang=lang | default(value='sk')) }}</a> class="sticky top-0 z-30 border-b border-outline bg-surface/95 backdrop-blur dark:border-outline-dark dark:bg-surface-dark/95">
<ul class="nav-menu term-navlinks menu menu-sm hidden items-center md:flex"> <nav x-data="{ mobile: false }" class="mx-auto flex max-w-6xl items-center gap-4 px-4 py-3">
<li><a href="/admin/dashboard" data-nav="/admin/dashboard">{{ t(key="admin-dashboard", lang=lang | default(value='sk')) }}</a></li> <a href="/admin/dashboard"
<li><a href="/admin/blog/articles" data-nav="/admin/blog">{{ t(key="admin-blog", lang=lang | default(value='sk')) }}</a></li> class="text-lg font-bold tracking-tight text-on-surface-strong dark:text-on-surface-dark-strong">
<li><a href="/admin/audio/albums" data-nav="/admin/audio">{{ t(key="admin-audio", lang=lang | default(value='sk')) }}</a></li> {{ t(key="admin-title", lang=lang | default(value='sk')) }}
<li><a href="/admin/about" data-nav="/admin/about">{{ t(key="admin-about", lang=lang | default(value='sk')) }}</a></li> </a>
<li><a href="/" class="t-blue">{{ t(key="admin-exit", lang=lang | default(value='sk')) }}</a></li>
<!-- desktop links -->
<ul class="ml-2 hidden items-center gap-1 md:flex">
<li><a href="/admin/dashboard" data-nav="/admin/dashboard" 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="admin-dashboard", lang=lang | default(value='sk')) }}</a></li>
<li><a href="/admin/blog/articles" data-nav="/admin/blog" 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="admin-blog", lang=lang | default(value='sk')) }}</a></li>
<li><a href="/admin/audio/albums" data-nav="/admin/audio" 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="admin-audio", lang=lang | default(value='sk')) }}</a></li>
<li><a href="/admin/about" data-nav="/admin/about" 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="admin-about", lang=lang | default(value='sk')) }}</a></li>
<li><a href="/" class="rounded-radius px-3 py-1.5 text-sm font-medium text-info transition hover:bg-surface-alt dark:hover:bg-surface-dark-alt">{{ t(key="admin-exit", lang=lang | default(value='sk')) }}</a></li>
<li> <li>
<form method="post" action="/admin/logout"> <form method="post" action="/admin/logout">
<button type="submit" class="t-red w-full">{{ t(key="logout", lang=lang | default(value='sk')) }}</button> <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> </form>
</li> </li>
</ul> </ul>
<div class="term-nav-right">
<div class="dropdown dropdown-end md:hidden"> <div class="ml-auto flex items-center gap-1">
<div tabindex="0" role="button" class="btn btn-ghost btn-sm btn-circle" aria-label="{{ t(key='menu', lang=lang | default(value='sk')) }}"> <!-- 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" <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"> stroke="currentColor" class="size-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
</svg>
</div>
<ul tabindex="0"
class="menu dropdown-content z-50 mt-3 w-52 border border-base-300 bg-base-200 p-2 shadow-lg">
<li><a href="/admin/dashboard">{{ t(key="admin-dashboard", lang=lang | default(value='sk')) }}</a></li>
<li><a href="/admin/blog/articles">{{ t(key="admin-blog", lang=lang | default(value='sk')) }}</a></li>
<li><a href="/admin/audio/albums">{{ t(key="admin-audio", lang=lang | default(value='sk')) }}</a></li>
<li><a href="/admin/about">{{ t(key="admin-about", lang=lang | default(value='sk')) }}</a></li>
<li><a href="/" class="t-blue">{{ t(key="admin-exit", lang=lang | default(value='sk')) }}</a></li>
<li>
<form method="post" action="/admin/logout">
<button type="submit" class="t-red w-full">{{ t(key="logout", lang=lang | default(value='sk')) }}</button>
</form>
</li>
</ul>
</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 | default(value='sk')) }}" title="{{ t(key='settings', lang=lang | default(value='sk')) }}">
<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" <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" /> 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" /> <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
</svg> </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>{{ t(key="language-en", lang=lang | default(value='sk')) }}</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>{{ t(key="language-sk", lang=lang | default(value='sk')) }}</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>
<form method="post" action="/lang" hx-boost="false">
<ul tabindex="0" class="menu dropdown-content z-50 mt-3 w-56 border border-base-300 bg-base-200 p-2 shadow-lg">
<li class="menu-title">{{ t(key="settings-language", lang=lang | default(value='sk')) }}</li>
<li>
<button type="submit" name="lang" value="en" class="{% if lang | default(value='sk') == 'en' %}active{% endif %}">
{{ t(key="language-en", lang=lang | default(value='sk')) }}
{% if lang | default(value='sk') == 'en' %}
<span class="ml-auto"></span>
{% endif %}
</button>
</li>
<li>
<button type="submit" name="lang" value="sk" class="{% if lang | default(value='sk') == 'sk' %}active{% endif %}">
{{ t(key="language-sk", lang=lang | default(value='sk')) }}
{% if lang | default(value='sk') == 'sk' %}
<span class="ml-auto"></span>
{% endif %}
</button>
</li>
<li class="menu-title">{{ t(key="settings-theme", lang=lang | default(value='sk')) }}</li>
<li><button type="button" data-theme-opt="system" onclick="setTheme('system')">{{ t(key="theme-system", lang=lang | default(value='sk')) }} <span class="opt-check ml-auto hidden"></span></button></li>
<li><button type="button" data-theme-opt="light" onclick="setTheme('light')">{{ t(key="theme-light", lang=lang | default(value='sk')) }} <span class="opt-check ml-auto hidden"></span></button></li>
<li><button type="button" data-theme-opt="dark" onclick="setTheme('dark')">{{ t(key="theme-dark", lang=lang | default(value='sk')) }} <span class="opt-check ml-auto hidden"></span></button></li>
</ul>
</form>
</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> </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="/admin/dashboard" 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="admin-dashboard", lang=lang | default(value='sk')) }}</a></li>
<li><a href="/admin/blog/articles" 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="admin-blog", lang=lang | default(value='sk')) }}</a></li>
<li><a href="/admin/audio/albums" 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="admin-audio", lang=lang | default(value='sk')) }}</a></li>
<li><a href="/admin/about" 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="admin-about", lang=lang | default(value='sk')) }}</a></li>
<li><a href="/" class="block rounded-radius px-3 py-2 text-sm font-medium text-info hover:bg-surface-alt dark:hover:bg-surface-dark">{{ t(key="admin-exit", lang=lang | default(value='sk')) }}</a></li>
<li>
<form method="post" action="/admin/logout">
<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>
</ul>
</nav> </nav>
</header> </header>
<div id="nav-backdrop" aria-hidden="true"></div>
<main class="term-main"> <main class="mx-auto w-full max-w-6xl flex-1 px-4 py-8">
{% block content %}{% endblock content %} {% block content %}{% endblock content %}
</main> </main>
</body> </body>

View File

@@ -11,155 +11,158 @@
<link rel="apple-touch-icon" sizes="180x180" href="/static/favicon/apple-touch-icon.png"> <link rel="apple-touch-icon" sizes="180x180" href="/static/favicon/apple-touch-icon.png">
<link rel="manifest" href="/static/favicon/site.webmanifest"> <link rel="manifest" href="/static/favicon/site.webmanifest">
<script> <script>
// Apply the saved theme before first paint to avoid a flash.
function applyTheme(t) { function applyTheme(t) {
var dark = t === 'dark' var dark = t === 'dark'
|| (t === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches); || (t === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light'); 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) { function setTheme(t) {
localStorage.setItem('theme', t); localStorage.setItem('theme', t);
applyTheme(t); applyTheme(t);
highlightTheme(t); document.dispatchEvent(new CustomEvent('theme:changed', { detail: t }));
} }
applyTheme(localStorage.getItem('theme') || 'dark'); function currentTheme() { return localStorage.getItem('theme') || 'dark'; }
applyTheme(currentTheme());
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function () { window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function () {
if ((localStorage.getItem('theme') || 'dark') === 'system') applyTheme('system'); if (currentTheme() === 'system') applyTheme('system');
}); });
// Mark the active top-nav link via aria-current (styled with Tailwind).
function markActiveNav() { function markActiveNav() {
var path = location.pathname; var path = location.pathname;
document.querySelectorAll('.term-navlinks a[data-nav]').forEach(function (a) { document.querySelectorAll('a[data-nav]').forEach(function (a) {
var h = a.getAttribute('data-nav'); var h = a.getAttribute('data-nav');
a.classList.toggle('is-active', h === path || (h !== '/' && path.indexOf(h) === 0)); var on = h === path || (h !== '/' && path.indexOf(h) === 0);
if (on) a.setAttribute('aria-current', 'page');
else a.removeAttribute('aria-current');
}); });
} }
function initPage() { document.addEventListener('DOMContentLoaded', markActiveNav);
highlightTheme(localStorage.getItem('theme') || 'dark'); document.addEventListener('htmx:afterSwap', markActiveNav);
markActiveNav();
}
document.addEventListener('DOMContentLoaded', initPage);
document.addEventListener('htmx:afterSwap', initPage);
</script> </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 src="/static/vendor/htmx/htmx-1.9.12.min.js"></script>
<style> <script defer src="/static/vendor/alpine/alpinejs-3.14.9.min.js"></script>
@media (min-width: 768px) {
.nav-menu { flex-direction: row; }
}
#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.5);
opacity: 0;
visibility: hidden;
transition: opacity 0.15s ease, visibility 0s linear 0.2s;
}
.term-titlebar:has(.dropdown:focus-within) ~ #nav-backdrop {
opacity: 1;
visibility: visible;
transition: opacity 0.15s ease, visibility 0s;
}
}
</style>
</head> </head>
<body hx-boost="true" class="flex min-h-screen flex-col bg-base-100 text-base-content antialiased"> <body hx-boost="true"
<header class="term-titlebar"> class="flex min-h-screen flex-col bg-surface text-on-surface antialiased dark:bg-surface-dark dark:text-on-surface-dark">
<nav class="term-nav"> <header
<a href="/" class="term-brand">{{ t(key="brand", lang=lang | default(value='sk')) }}</a> class="sticky top-0 z-30 border-b border-outline bg-surface/95 backdrop-blur dark:border-outline-dark dark:bg-surface-dark/95">
<ul class="nav-menu term-navlinks menu menu-sm hidden items-center md:flex"> <nav x-data="{ mobile: false }" class="mx-auto flex max-w-6xl items-center gap-4 px-4 py-3">
<li><a href="/" data-nav="/">{{ t(key="nav-home", lang=lang | default(value='sk')) }}</a></li> <a href="/"
<li><a href="/blog" data-nav="/blog">{{ t(key="nav-blog", lang=lang | default(value='sk')) }}</a></li> class="text-lg font-bold tracking-tight text-on-surface-strong dark:text-on-surface-dark-strong">
<li><a href="/audio/albums" data-nav="/audio/albums">{{ t(key="nav-audio", lang=lang | default(value='sk')) }}</a></li> {{ t(key="brand", lang=lang | default(value='sk')) }}
<li><a href="/audio/tracks" data-nav="/audio/tracks">{{ t(key="nav-songs", lang=lang | default(value='sk')) }}</a></li> </a>
<li><a href="/about" data-nav="/about">{{ t(key="nav-about", lang=lang | default(value='sk')) }}</a></li>
<!-- 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="/blog" data-nav="/blog" 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-blog", lang=lang | default(value='sk')) }}</a></li>
<li><a href="/audio/albums" data-nav="/audio/albums" 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-audio", lang=lang | default(value='sk')) }}</a></li>
<li><a href="/audio/tracks" data-nav="/audio/tracks" 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-songs", lang=lang | default(value='sk')) }}</a></li>
<li><a href="/about" data-nav="/about" 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-about", lang=lang | default(value='sk')) }}</a></li>
{% if logged_in_admin %} {% if logged_in_admin %}
<li><a href="/admin/dashboard" hx-boost="false" class="t-yellow" data-nav="/admin">{{ t(key="admin-title", lang=lang | default(value='sk')) }}</a></li> <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> <li>
<form method="post" action="/admin/logout" hx-boost="false"> <form method="post" action="/admin/logout" hx-boost="false">
<button type="submit" class="t-red w-full">{{ t(key="logout", lang=lang | default(value='sk')) }}</button> <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> </form>
</li> </li>
{% else %} {% else %}
<li><a href="/admin/login" data-nav="/admin/login">{{ t(key="nav-admin", lang=lang | default(value='sk')) }}</a></li> <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 %} {% endif %}
</ul> </ul>
<div class="term-nav-right">
<div class="dropdown dropdown-end md:hidden"> <!-- right side: settings + mobile toggle -->
<div tabindex="0" role="button" class="btn btn-ghost btn-sm btn-circle" aria-label="{{ t(key='menu', lang=lang | default(value='sk')) }}"> <div class="ml-auto flex items-center gap-1">
<!-- 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" <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"> stroke="currentColor" class="size-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
</svg>
</div>
<ul tabindex="0"
class="menu dropdown-content z-50 mt-3 w-52 border border-base-300 bg-base-200 p-2 shadow-lg">
<li><a href="/">{{ t(key="nav-home", lang=lang | default(value='sk')) }}</a></li>
<li><a href="/blog">{{ t(key="nav-blog", lang=lang | default(value='sk')) }}</a></li>
<li><a href="/audio/albums">{{ t(key="nav-audio", lang=lang | default(value='sk')) }}</a></li>
<li><a href="/audio/tracks">{{ t(key="nav-songs", lang=lang | default(value='sk')) }}</a></li>
<li><a href="/about">{{ t(key="nav-about", lang=lang | default(value='sk')) }}</a></li>
{% if logged_in_admin %}
<li><a href="/admin/dashboard" hx-boost="false" class="t-yellow">{{ t(key="admin-title", lang=lang | default(value='sk')) }}</a></li>
<li>
<form method="post" action="/admin/logout">
<button type="submit" class="t-red w-full">{{ t(key="logout", lang=lang | default(value='sk')) }}</button>
</form>
</li>
{% else %}
<li><a href="/admin/login">{{ t(key="nav-admin", lang=lang | default(value='sk')) }}</a></li>
{% endif %}
</ul>
</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 | default(value='sk')) }}" title="{{ t(key='settings', lang=lang | default(value='sk')) }}">
<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" <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" /> 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" /> <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
</svg> </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>
<form method="post" action="/lang" hx-boost="false">
<ul tabindex="0" class="menu dropdown-content z-50 mt-3 w-56 border border-base-300 bg-base-200 p-2 shadow-lg">
<li class="menu-title">{{ t(key="settings-language", lang=lang | default(value='sk')) }}</li>
<li>
<button type="submit" name="lang" value="en" class="{% if lang | default(value='sk') == 'en' %}active{% endif %}">
English
{% if lang | default(value='sk') == 'en' %}
<span class="ml-auto"></span>
{% endif %}
</button>
</li>
<li>
<button type="submit" name="lang" value="sk" class="{% if lang | default(value='sk') == 'sk' %}active{% endif %}">
Slovenčina
{% if lang | default(value='sk') == 'sk' %}
<span class="ml-auto"></span>
{% endif %}
</button>
</li>
<li class="menu-title">{{ t(key="settings-theme", lang=lang | default(value='sk')) }}</li>
<li><button type="button" data-theme-opt="system" onclick="setTheme('system')">{{ t(key="theme-system", lang=lang | default(value='sk')) }} <span class="opt-check ml-auto hidden"></span></button></li>
<li><button type="button" data-theme-opt="light" onclick="setTheme('light')">{{ t(key="theme-light", lang=lang | default(value='sk')) }} <span class="opt-check ml-auto hidden"></span></button></li>
<li><button type="button" data-theme-opt="dark" onclick="setTheme('dark')">{{ t(key="theme-dark", lang=lang | default(value='sk')) }} <span class="opt-check ml-auto hidden"></span></button></li>
</ul>
</form>
</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> </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="/blog" 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-blog", lang=lang | default(value='sk')) }}</a></li>
<li><a href="/audio/albums" 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-audio", lang=lang | default(value='sk')) }}</a></li>
<li><a href="/audio/tracks" 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-songs", lang=lang | default(value='sk')) }}</a></li>
<li><a href="/about" 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-about", 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> </nav>
</header> </header>
<div id="nav-backdrop" aria-hidden="true"></div>
<main class="term-main"> <main class="mx-auto w-full max-w-6xl flex-1 px-4 py-8">
{% block content %}{% endblock content %} {% block content %}{% endblock content %}
</main> </main>
</body> </body>