working with UI library now

This commit is contained in:
Priec
2026-05-16 14:59:17 +02:00
parent 6006e1e7b1
commit 231b11b8b3
10 changed files with 153 additions and 24 deletions

View File

@@ -30,7 +30,8 @@ edit-booking = Edit Booking
date = Date
hour = Hour
booking-color = Colour
booking-name = Name
booking-name = Name (private)
booking-title = Title (public)
booking-contact = Contact
booking-note = Note
save = Save
@@ -48,3 +49,6 @@ court-delete-error = Court name did not match — nothing was removed.
theme-system = System
theme-light = Light
theme-dark = Dark
settings = Settings
settings-language = Language
settings-theme = Theme

View File

@@ -30,7 +30,8 @@ edit-booking = Upraviť rezerváciu
date = Dátum
hour = Hodina
booking-color = Farba
booking-name = Meno
booking-name = Meno (súkromné)
booking-title = Názov (verejný)
booking-contact = Kontakt
booking-note = Poznámka
save = Uložiť
@@ -48,3 +49,6 @@ court-delete-error = Názov kurtu sa nezhodoval — nič sa neodstránilo.
theme-system = Systém
theme-light = Svetlý
theme-dark = Tmavý
settings = Nastavenia
settings-language = Jazyk
settings-theme = Téma

View File

@@ -41,6 +41,10 @@
<label class="label"><span class="label-text">{{ t(key="booking-name", lang=lang) }}</span></label>
<input name="name" value="{{ name }}" required class="input input-bordered w-full">
</div>
<div class="form-control">
<label class="label"><span class="label-text">{{ t(key="booking-title", lang=lang) }}</span></label>
<input name="title" value="{{ title }}" class="input input-bordered w-full">
</div>
<div class="form-control">
<label class="label"><span class="label-text">{{ t(key="booking-contact", lang=lang) }}</span></label>
<input name="contact" value="{{ contact }}" class="input input-bordered w-full">

View File

@@ -12,8 +12,11 @@
document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light');
}
function highlightTheme(t) {
document.querySelectorAll('#theme-switch [data-theme-opt]').forEach(function (b) {
b.classList.toggle('btn-neutral', b.getAttribute('data-theme-opt') === 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) {
@@ -54,19 +57,96 @@
{% else %}
<a href="/admin/login" class="btn btn-ghost btn-sm">{{ t(key="nav-admin", lang=lang) }}</a>
{% endif %}
<div class="join ml-1">
<button onclick="setLang('en')"
class="btn btn-xs join-item {% if lang == 'en' %}btn-neutral{% endif %}">EN</button>
<button onclick="setLang('sk')"
class="btn btn-xs join-item {% if lang == 'sk' %}btn-neutral{% endif %}">SK</button>
</div>
<div class="join" id="theme-switch">
<button type="button" data-theme-opt="system" onclick="setTheme('system')"
class="btn btn-xs join-item">{{ t(key="theme-system", lang=lang) }}</button>
<button type="button" data-theme-opt="light" onclick="setTheme('light')"
class="btn btn-xs join-item">{{ t(key="theme-light", lang=lang) }}</button>
<button type="button" data-theme-opt="dark" onclick="setTheme('dark')"
class="btn btn-xs join-item">{{ t(key="theme-dark", lang=lang) }}</button>
<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>
<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="button" onclick="setLang('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="button" onclick="setLang('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>
</div>
</nav>
</div>

View File

@@ -51,22 +51,28 @@
<tr>
<td class="border border-base-300 bg-base-200 p-2 text-center font-medium opacity-70">{{ row.hour_label }}</td>
{% for cell in row.cells %}
<td class="h-14 border border-base-300 p-1 align-top">
<td class="relative h-14 border border-base-300">
{% if cell.booked %}
{% if is_admin %}
<a href="/admin/booking/{{ cell.booking_id }}"
class="block h-full rounded px-1 py-1 text-xs font-medium text-white shadow-sm transition hover:opacity-90"
style="background-color: {{ cell.color }}">{{ cell.name }}</a>
class="absolute inset-1 flex items-center overflow-hidden rounded px-1.5 text-xs font-medium text-white shadow-sm transition hover:opacity-90"
style="background-color: {{ cell.color }}">
<span class="min-w-0 truncate">{{ cell.name }}</span>
</a>
{% else %}
<div class="h-full rounded px-1 py-1 text-xs font-medium text-white shadow-sm"
style="background-color: {{ cell.color }}" title="{{ cell.name }}">{{ t(key="booked", lang=lang) }}</div>
<div class="absolute inset-1 flex items-center overflow-hidden rounded px-1.5 text-xs font-medium text-white shadow-sm"
style="background-color: {{ cell.color }}"{% if cell.title %} title="{{ cell.title }}"{% endif %}>
<span class="min-w-0 truncate">
{%- if cell.title %}{{ cell.title }}{% else %}{{ t(key="booked", lang=lang) }}{% endif -%}
</span>
</div>
{% endif %}
{% else %}
{% if is_admin %}
<a href="/admin/booking?court={{ court_id }}&date={{ cell.date }}&hour={{ cell.hour }}"
class="flex h-full items-center justify-center rounded text-lg opacity-30 transition hover:bg-base-200 hover:opacity-100">+</a>
class="absolute inset-0 flex items-center justify-center text-lg opacity-30 transition hover:bg-base-200 hover:opacity-100">+</a>
{% else %}
<div class="px-1 py-1 text-xs opacity-30">{{ t(key="free", lang=lang) }}</div>
<div class="absolute inset-0 flex items-center justify-center text-xs opacity-30">{{ t(key="free", lang=lang) }}</div>
{% endif %}
{% endif %}
</td>

View File

@@ -5,6 +5,7 @@ mod m20220101_000001_users;
mod m20260515_162423_courts;
mod m20260515_170417_bookings;
mod m20260516_111747_add_title_to_bookings;
pub struct Migrator;
#[async_trait::async_trait]
@@ -14,6 +15,7 @@ impl MigratorTrait for Migrator {
Box::new(m20220101_000001_users::Migration),
Box::new(m20260515_162423_courts::Migration),
Box::new(m20260515_170417_bookings::Migration),
Box::new(m20260516_111747_add_title_to_bookings::Migration),
// inject-above (do not remove this comment)
]
}

View File

@@ -0,0 +1,18 @@
use loco_rs::schema::*;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {
add_column(m, "bookings", "title", ColType::StringNull).await?;
Ok(())
}
async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {
remove_column(m, "bookings", "title").await?;
Ok(())
}
}

View File

@@ -301,6 +301,7 @@ pub async fn booking_new(
"hour": q.hour.unwrap_or(FIRST_HOUR),
"color": "#3b82f6",
"name": "",
"title": "",
"contact": "",
"note": "",
"hours": hour_options(),
@@ -316,6 +317,7 @@ pub struct BookingForm {
pub hour: i32,
pub color: String,
pub name: String,
pub title: Option<String>,
pub contact: Option<String>,
pub note: Option<String>,
}
@@ -338,6 +340,7 @@ pub async fn booking_create(
hour: Set(form.hour),
color: Set(form.color),
name: Set(form.name),
title: Set(form.title.filter(|s| !s.is_empty())),
contact: Set(form.contact.filter(|s| !s.is_empty())),
note: Set(form.note.filter(|s| !s.is_empty())),
..Default::default()
@@ -381,6 +384,7 @@ pub async fn booking_edit(
"hour": booking.hour,
"color": booking.color,
"name": booking.name,
"title": booking.title.unwrap_or_default(),
"contact": booking.contact.unwrap_or_default(),
"note": booking.note.unwrap_or_default(),
"hours": hour_options(),
@@ -407,6 +411,7 @@ pub async fn booking_update(
active.hour = Set(form.hour);
active.color = Set(form.color);
active.name = Set(form.name);
active.title = Set(form.title.filter(|s| !s.is_empty()));
active.contact = Set(form.contact.filter(|s| !s.is_empty()));
active.note = Set(form.note.filter(|s| !s.is_empty()));
active.update(&ctx.db).await?;

View File

@@ -50,7 +50,10 @@ pub struct Cell {
pub booked: bool,
pub color: String,
pub booking_id: i32,
/// Private — admin only. Never rendered on the public calendar.
pub name: String,
/// Public-facing label shown on the calendar when set.
pub title: String,
}
#[derive(Debug, Serialize)]
@@ -173,6 +176,7 @@ pub async fn build_calendar(
color: b.color.clone(),
booking_id: b.id,
name: b.name.clone(),
title: b.title.clone().unwrap_or_default(),
},
None => Cell {
date: iso,
@@ -181,6 +185,7 @@ pub async fn build_calendar(
color: String::new(),
booking_id: 0,
name: String::new(),
title: String::new(),
},
}
})

View File

@@ -18,6 +18,7 @@ pub struct Model {
#[sea_orm(column_type = "Text", nullable)]
pub note: Option<String>,
pub court_id: i32,
pub title: Option<String>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]