mobile resolution
This commit is contained in:
@@ -54,6 +54,9 @@ settings-language = Language
|
||||
settings-theme = Theme
|
||||
view-details = Details
|
||||
pick-week = Go to week
|
||||
menu = Menu
|
||||
prev-day = Previous day
|
||||
next-day = Next day
|
||||
hour-from = From
|
||||
hour-to = Until
|
||||
repeat-weeks = Repeat for (weeks)
|
||||
|
||||
@@ -54,6 +54,9 @@ settings-language = Jazyk
|
||||
settings-theme = Téma
|
||||
view-details = Detaily
|
||||
pick-week = Prejsť na týždeň
|
||||
menu = Menu
|
||||
prev-day = Predchádzajúci deň
|
||||
next-day = Nasledujúci deň
|
||||
hour-from = Od
|
||||
hour-to = Do
|
||||
repeat-weeks = Opakovať (počet týždňov)
|
||||
|
||||
@@ -53,15 +53,30 @@
|
||||
.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);
|
||||
}
|
||||
}
|
||||
</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 flex-wrap items-center justify-between gap-2 px-4">
|
||||
<a href="/" class="text-lg font-bold">{{ t(key="brand", lang=lang) }}</a>
|
||||
<nav class="flex flex-wrap items-center gap-1">
|
||||
<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>
|
||||
{% if logged_in | default(value=false) %}
|
||||
<a href="/admin" class="btn btn-ghost btn-sm">{{ t(key="admin-title", lang=lang) }}</a>
|
||||
@@ -72,6 +87,30 @@
|
||||
{% 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>
|
||||
{% 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"
|
||||
|
||||
@@ -20,6 +20,20 @@
|
||||
#cal thead .opacity-50 { opacity: 0.8; }
|
||||
#cal td .opacity-30 { opacity: 0.7; }
|
||||
#cal td a.opacity-30:hover { opacity: 1; }
|
||||
|
||||
/* Mobile: show a small window of days (2 on a narrow phone, 3 otherwise)
|
||||
instead of the full week. JS marks the visible columns with .cal-day-on
|
||||
and the day navigator slides the window; before JS runs, the first three
|
||||
days stand in. Wider screens keep the full 7-day grid. */
|
||||
@media (max-width: 767px) {
|
||||
#cal thead { display: none; }
|
||||
#cal .cal-day { display: none; }
|
||||
#cal .cal-day-0,
|
||||
#cal .cal-day-1,
|
||||
#cal .cal-day-2 { display: table-cell; }
|
||||
#cal.cal-js .cal-day { display: none; }
|
||||
#cal.cal-js .cal-day.cal-day-on { display: table-cell; }
|
||||
}
|
||||
</style>
|
||||
{% if is_admin %}
|
||||
<style>
|
||||
@@ -75,14 +89,15 @@
|
||||
<div class="join">
|
||||
{% if can_prev %}
|
||||
<a href="{{ base_path }}?court={{ court_id }}&week={{ prev_week }}"
|
||||
class="btn btn-sm join-item">« {{ t(key="prev-week", lang=lang) }}</a>
|
||||
class="btn btn-sm join-item">«<span class="ml-1 hidden sm:inline">{{ t(key="prev-week", lang=lang) }}</span></a>
|
||||
{% else %}
|
||||
<span class="btn btn-sm join-item btn-disabled" aria-disabled="true">« {{ t(key="prev-week", lang=lang) }}</span>
|
||||
<span class="btn btn-sm join-item btn-disabled" aria-disabled="true">«<span
|
||||
class="ml-1 hidden sm:inline">{{ t(key="prev-week", lang=lang) }}</span></span>
|
||||
{% endif %}
|
||||
<a href="{{ base_path }}?court={{ court_id }}&week={{ this_week }}"
|
||||
class="btn btn-sm join-item">{{ t(key="this-week", lang=lang) }}</a>
|
||||
<a href="{{ base_path }}?court={{ court_id }}&week={{ next_week }}"
|
||||
class="btn btn-sm join-item">{{ t(key="next-week", lang=lang) }} »</a>
|
||||
class="btn btn-sm join-item"><span class="mr-1 hidden sm:inline">{{ t(key="next-week", lang=lang) }}</span>»</a>
|
||||
</div>
|
||||
{% if is_admin %}
|
||||
<button type="button" id="cal-detail-toggle" class="btn btn-sm gap-1" aria-pressed="false"
|
||||
@@ -92,7 +107,7 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75V6.75Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0ZM3.75 12h.007v.008H3.75V12Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm-.375 5.25h.007v.008H3.75v-.008Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" />
|
||||
</svg>
|
||||
{{ t(key="view-details", lang=lang) }}
|
||||
<span class="hidden sm:inline">{{ t(key="view-details", lang=lang) }}</span>
|
||||
</button>
|
||||
<form method="get" action="{{ base_path }}" class="relative inline-flex">
|
||||
<input type="hidden" name="court" value="{{ court_id }}">
|
||||
@@ -103,7 +118,7 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5" />
|
||||
</svg>
|
||||
{{ t(key="pick-week", lang=lang) }}
|
||||
<span class="hidden sm:inline">{{ t(key="pick-week", lang=lang) }}</span>
|
||||
</button>
|
||||
<input type="date" name="week" id="cal-jump" value="{{ week }}"
|
||||
onchange="this.form.submit()" tabindex="-1" aria-hidden="true"
|
||||
@@ -114,13 +129,32 @@
|
||||
<div class="text-sm font-medium opacity-60">{{ court_name }} · {{ week_label }}</div>
|
||||
</div>
|
||||
|
||||
<div id="cal-daynav" class="mb-3 flex items-center gap-2 md:hidden">
|
||||
<button type="button" id="cal-day-prev" class="btn btn-square"
|
||||
aria-label="{{ t(key='prev-day', lang=lang) }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2"
|
||||
stroke="currentColor" class="h-5 w-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5" />
|
||||
</svg>
|
||||
</button>
|
||||
<div id="cal-daynav-label" class="flex-1 text-center text-base font-semibold"></div>
|
||||
<button type="button" id="cal-day-next" class="btn btn-square"
|
||||
aria-label="{{ t(key='next-day', lang=lang) }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2"
|
||||
stroke="currentColor" class="h-5 w-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto border border-base-300 bg-base-100 shadow-sm">
|
||||
<table id="cal" class="w-full border-collapse text-sm">
|
||||
<table id="cal" data-day="{{ mobile_day }}" class="w-full border-collapse text-sm">
|
||||
<thead>
|
||||
<tr class="bg-base-200">
|
||||
<th class="w-16 border border-base-300 p-2"></th>
|
||||
{% for d in days %}
|
||||
<th class="border border-base-300 p-2 text-center">
|
||||
<th class="cal-day cal-day-{{ loop.index0 }} border border-base-300 p-2 text-center"
|
||||
data-d="{{ t(key=d.key, lang=lang) }} {{ d.num }}">
|
||||
<div class="font-semibold">{{ t(key=d.key, lang=lang) }}</div>
|
||||
<div class="text-xs font-normal opacity-50">{{ d.num }}</div>
|
||||
</th>
|
||||
@@ -140,7 +174,7 @@
|
||||
<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="relative h-14 border border-base-300">
|
||||
<td class="cal-day cal-day-{{ loop.index0 }} relative h-14 border border-base-300">
|
||||
{% if cell.booked %}
|
||||
{% if is_admin %}
|
||||
<a href="/admin/booking/{{ cell.booking_id }}"
|
||||
@@ -187,6 +221,64 @@
|
||||
{% endblock content %}
|
||||
|
||||
{% block js %}
|
||||
<script>
|
||||
// Mobile: show a sliding window of days (2 on a narrow phone, 3 otherwise)
|
||||
// rather than the whole week. The navigator buttons and a horizontal swipe
|
||||
// slide the window; a resize re-fits it. Public and admin calendars alike.
|
||||
(function () {
|
||||
var cal = document.getElementById('cal');
|
||||
var nav = document.getElementById('cal-daynav');
|
||||
if (!cal || !nav) return;
|
||||
var label = document.getElementById('cal-daynav-label');
|
||||
var prevBtn = document.getElementById('cal-day-prev');
|
||||
var nextBtn = document.getElementById('cal-day-next');
|
||||
var dayLabels = [].slice.call(cal.querySelectorAll('thead th[data-d]'))
|
||||
.map(function (th) { return th.getAttribute('data-d'); });
|
||||
var start = parseInt(cal.getAttribute('data-day'), 10) || 0;
|
||||
cal.classList.add('cal-js');
|
||||
|
||||
function windowSize() {
|
||||
return window.innerWidth < 480 ? 2 : 3;
|
||||
}
|
||||
function apply() {
|
||||
var size = windowSize();
|
||||
var maxStart = 7 - size;
|
||||
if (start > maxStart) start = maxStart;
|
||||
if (start < 0) start = 0;
|
||||
for (var d = 0; d < 7; d++) {
|
||||
var on = d >= start && d < start + size;
|
||||
var cells = cal.querySelectorAll('.cal-day-' + d);
|
||||
for (var i = 0; i < cells.length; i++) {
|
||||
cells[i].classList.toggle('cal-day-on', on);
|
||||
}
|
||||
}
|
||||
var last = start + size - 1;
|
||||
label.textContent = (dayLabels[start] || '') + ' – ' + (dayLabels[last] || '');
|
||||
prevBtn.disabled = start <= 0;
|
||||
nextBtn.disabled = start >= maxStart;
|
||||
}
|
||||
function slide(delta) { start += delta; apply(); }
|
||||
|
||||
prevBtn.addEventListener('click', function () { slide(-1); });
|
||||
nextBtn.addEventListener('click', function () { slide(1); });
|
||||
window.addEventListener('resize', apply);
|
||||
|
||||
var x0 = null, y0 = null;
|
||||
cal.addEventListener('touchstart', function (e) {
|
||||
x0 = e.touches[0].clientX;
|
||||
y0 = e.touches[0].clientY;
|
||||
}, { passive: true });
|
||||
cal.addEventListener('touchend', function (e) {
|
||||
if (x0 === null) return;
|
||||
var dx = e.changedTouches[0].clientX - x0;
|
||||
var dy = e.changedTouches[0].clientY - y0;
|
||||
if (Math.abs(dx) > 50 && Math.abs(dx) > Math.abs(dy)) slide(dx < 0 ? 1 : -1);
|
||||
x0 = y0 = null;
|
||||
}, { passive: true });
|
||||
|
||||
apply();
|
||||
})();
|
||||
</script>
|
||||
{% if is_admin %}
|
||||
<script>
|
||||
// Detailed-view toggle for the admin calendar. The choice is remembered
|
||||
|
||||
@@ -89,6 +89,9 @@ pub struct CalendarPage {
|
||||
/// `false` on the public calendar once it reaches the two-week look-back
|
||||
/// limit, which disables the "previous week" button. Always `true` for admin.
|
||||
pub can_prev: bool,
|
||||
/// Index (0–6, Mon–Sun) of the day shown first in the mobile single-day
|
||||
/// view — today when the displayed week contains it, otherwise Monday.
|
||||
pub mobile_day: i64,
|
||||
pub days: Vec<DayHead>,
|
||||
pub rows: Vec<Row>,
|
||||
}
|
||||
@@ -193,6 +196,9 @@ pub async fn build_calendar(
|
||||
let min_monday = monday_of(Utc::now().date_naive()) - Duration::weeks(2);
|
||||
let monday = if is_admin { monday } else { monday.max(min_monday) };
|
||||
let can_prev = is_admin || monday > min_monday;
|
||||
// Mobile single-day view opens on today when it falls in the shown week.
|
||||
let day_offset = (Utc::now().date_naive() - monday).num_days();
|
||||
let mobile_day = if (0..7).contains(&day_offset) { day_offset } else { 0 };
|
||||
let sunday = monday + Duration::days(6);
|
||||
|
||||
let days: Vec<DayHead> = (0..7i64)
|
||||
@@ -278,6 +284,7 @@ pub async fn build_calendar(
|
||||
.to_string(),
|
||||
next_week: (monday + Duration::days(7)).format("%Y-%m-%d").to_string(),
|
||||
can_prev,
|
||||
mobile_day,
|
||||
days,
|
||||
rows,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user