diff --git a/fdhview/api/info.py b/fdhview/api/info.py new file mode 100644 index 0000000..01c9121 --- /dev/null +++ b/fdhview/api/info.py @@ -0,0 +1,16 @@ +from pathlib import Path + +from fastapi import APIRouter +from fastapi.responses import FileResponse + +BASE_DIR = Path(__file__).resolve().parent.parent + +router = APIRouter( + prefix="/info", + tags=["Info"] +) + + +@router.get("", response_class=FileResponse) +async def get_info(): + return FileResponse(BASE_DIR / "html" / "info.html") diff --git a/fdhview/api/screenings.py b/fdhview/api/screenings.py index 5c61cc5..6534124 100644 --- a/fdhview/api/screenings.py +++ b/fdhview/api/screenings.py @@ -27,7 +27,7 @@ async def get_infodesk_screenings( select(Screening) .where(Screening.start > now) .order_by(col(Screening.start)) - .limit(10) + .limit(6) ) screenings = await session.exec(query) return [ScreeningRead.model_validate(s) for s in screenings.all()] diff --git a/fdhview/html/info.html b/fdhview/html/info.html new file mode 100644 index 0000000..cfac768 --- /dev/null +++ b/fdhview/html/info.html @@ -0,0 +1,30 @@ + + + + + +VANDAAG — Filmhuis Den Haag + + + + + +
+
+

Vandaag

+
+
+ +
+ + + diff --git a/fdhview/main.py b/fdhview/main.py index 64e9d18..c2d9397 100644 --- a/fdhview/main.py +++ b/fdhview/main.py @@ -5,6 +5,7 @@ from fastapi import FastAPI from fastapi.staticfiles import StaticFiles from fdhview.api.admin import router as admin_router +from fdhview.api.info import router as info_router from fdhview.api.screenings import router as screenings_router from fdhview.core.database import init_db @@ -24,3 +25,4 @@ app.mount("/static", StaticFiles(directory=BASE_DIR / "static"), name="static") app.include_router(admin_router) app.include_router(screenings_router, prefix="/api") +app.include_router(info_router) diff --git a/fdhview/static/info.js b/fdhview/static/info.js new file mode 100644 index 0000000..1682d54 --- /dev/null +++ b/fdhview/static/info.js @@ -0,0 +1,63 @@ +const POLL_MS = 5 * 60 * 1000; + +// "2026-10-13T17:30:00" -> "17:30" +function hhmm(iso) { + return iso.slice(11, 16); +} + +// map kijkwijzer codes to /static/.png +function kijkwijzerIcons(codes) { + return (codes || []).map(c => + `${c}` + ).join(""); +} + +function renderFilms(films) { + document.getElementById("films").innerHTML = films.map(f => { + const isBoutique = f.screen === "6"; + return ` +
+
${hhmm(f.start)}
${hhmm(f.end)}
+
${kijkwijzerIcons(f.kijkwijzer)}
+
+
${f.title}
+
+ ${isBoutique + ? `Zaal 6 - Boutique Cinema` + : `Zaal ${f.screen}`} + ${f.en_subs ? "EN subs" : ""} +
+
+ ${f.seats_sold / f.seats_total > 0.85 ? '
Laatste kaarten
' : "
"} +
`; + }).join(""); +} +function renderPrices(prices) { + const row = p => `
${p.label}${p.price}
`; + document.getElementById("prijzen").innerHTML = prices.prijzen.map(row).join(""); + document.getElementById("kortingsprijzen").innerHTML = prices.kortingsprijzen.map(row).join(""); +} + +function renderClock() { + const d = new Date(); + document.getElementById("date").textContent = + d.toLocaleTimeString("nl-NL", { hour: "2-digit", minute: "2-digit" }); +} + +async function refresh() { + try { + const [filmsRes, pricesRes] = await Promise.all([ + fetch("/api/screenings"), + fetch("/api/screenings/prices"), + ]); + renderFilms(await filmsRes.json()); + renderPrices(await pricesRes.json()); + } catch (err) { + console.error("refresh failed, keeping current display:", err); + } +} + +renderClock(); +refresh(); // initial paint +setInterval(refresh, POLL_MS); // poll every 5 minutes +setInterval(renderClock, 30_000); // keep the clock accurate diff --git a/fdhview/static/style.css b/fdhview/static/style.css index 2088656..81b441e 100644 --- a/fdhview/static/style.css +++ b/fdhview/static/style.css @@ -24,15 +24,26 @@ body { } .film { display: grid; - grid-template-columns: 90px 60px 1fr 170px; - gap: 12px; align-items: start; + grid-template-columns: 90px 160px 1fr 170px; + gap: 14px; align-items: start; padding: 16px 0 18px; border-top: 1px solid var(--rule); } .times { line-height: 1.25; } .times .start { font-size: 20px; font-weight: 600; } .times .end { font-size: 20px; color: var(--muted); } -.icons { display: flex; gap: 6px; padding-top: 3px; } +.icons { + display: flex; + flex-wrap: nowrap; + gap: 6px; + align-items: center; + padding-top: 3px; +} +.icons .kijkwijzer { + width: 26px; height: 26px; + object-fit: contain; + display: block; +} .icons .dot { width: 26px; height: 26px; border-radius: 50%; background: var(--badge); color: #fff; @@ -49,7 +60,8 @@ body { .pill { background: #b9bfc9; border-radius: 14px; padding: 4px 14px; font-size: 12px; font-weight: 600; - letter-spacing: 1px; text-transform: uppercase; + letter-spacing: 1px; + text-transform: none; /* "Boutique Cinema" reads like the photo */ } .badge-last { justify-self: end; align-self: start; margin-top: 3px;