diff options
| -rw-r--r-- | main.py | 2 | ||||
| -rw-r--r-- | routers/profile.py | 25 | ||||
| -rw-r--r-- | static/app.js | 5 | ||||
| -rw-r--r-- | static/fonts/EBGaramond-Italic-VariableFont_wght.ttf | bin | 0 -> 811012 bytes | |||
| -rw-r--r-- | static/fonts/EBGaramond-VariableFont_wght.ttf | bin | 0 -> 934420 bytes | |||
| -rw-r--r-- | static/fonts/IBMPlexMono-Medium.ttf | bin | 0 -> 134956 bytes | |||
| -rw-r--r-- | static/fonts/IBMPlexMono-Regular.ttf | bin | 0 -> 133796 bytes | |||
| -rw-r--r-- | static/fonts/IBMPlexMono-SemiBold.ttf | bin | 0 -> 138448 bytes | |||
| -rw-r--r-- | static/fonts/IBMPlexSans-Italic-VariableFont_wdth,wght.ttf | bin | 0 -> 594116 bytes | |||
| -rw-r--r-- | static/fonts/IBMPlexSans-VariableFont_wdth,wght.ttf | bin | 0 -> 532740 bytes | |||
| -rw-r--r-- | static/styles.css | 1258 | ||||
| -rw-r--r-- | templates/_feed_partial.html | 12 | ||||
| -rw-r--r-- | templates/_film_card.html | 112 | ||||
| -rw-r--r-- | templates/_public_feed_partial.html | 71 | ||||
| -rw-r--r-- | templates/base.html | 66 | ||||
| -rw-r--r-- | templates/detail.html | 455 | ||||
| -rw-r--r-- | templates/form.html | 226 | ||||
| -rw-r--r-- | templates/index.html | 65 | ||||
| -rw-r--r-- | templates/profile.html | 292 |
19 files changed, 2002 insertions, 587 deletions
@@ -16,7 +16,7 @@ load_dotenv() class AuthMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next): - public_paths = {"/about", "/login", "/logout", "/tyler", "/films/partial"} + public_paths = {"/about", "/login", "/logout", "/tyler", "/films/partial", "/tyler/films/partial"} path = request.url.path if path.startswith("/static") or path in public_paths: diff --git a/routers/profile.py b/routers/profile.py index 1986257..8e4a081 100644 --- a/routers/profile.py +++ b/routers/profile.py @@ -1,12 +1,15 @@ from fastapi import APIRouter, Depends, Request +from fastapi.responses import HTMLResponse from fastapi.templating import Jinja2Templates from sqlalchemy.orm import Session from database import get_db from models import Film +from services.film_people import split_credit_names router = APIRouter(tags=["profile"]) templates = Jinja2Templates(directory="templates") +templates.env.globals.update(split_credit_names=split_credit_names) def _diary_films(db: Session) -> list[Film]: @@ -72,6 +75,8 @@ def _build_profile_payload(films: list[Film]) -> dict: "id": film.id, "title": film.title, "poster_url": film.poster_url, + "year": film.year, + "director": split_credit_names(film.director)[0] if film.director else None, "date_watched": film.date_watched.isoformat() if film.date_watched else None, "stars": film.stars, } @@ -89,3 +94,23 @@ async def public_profile(request: Request, db: Session = Depends(get_db)): name="profile.html", context={"request": request, **payload}, ) + + +@router.get("/tyler/films/partial") +async def public_profile_films_partial( + request: Request, + offset: int = 0, + limit: int = 20, + db: Session = Depends(get_db), +): + films = _diary_films(db) + page = films[offset : offset + limit] + has_more = (offset + limit) < len(films) + + html = templates.get_template("_public_feed_partial.html").render( + request=request, + films=page, + ) + response = HTMLResponse(html) + response.headers["X-Has-More"] = "true" if has_more else "false" + return response diff --git a/static/app.js b/static/app.js index 04997a0..39f8276 100644 --- a/static/app.js +++ b/static/app.js @@ -365,7 +365,10 @@ document.querySelectorAll(".star-control[data-form-stars]").forEach((control) => const cur = groups[i]; const prev = groups[i - 1]; if (cur.dataset.month === prev.dataset.month) { - cur.querySelectorAll(".film-card").forEach((card) => prev.appendChild(card)); + const prevStack = prev.querySelector(".month-stack"); + const curStack = cur.querySelector(".month-stack"); + if (!prevStack || !curStack) continue; + curStack.querySelectorAll(".film-card").forEach((card) => prevStack.appendChild(card)); cur.remove(); } } diff --git a/static/fonts/EBGaramond-Italic-VariableFont_wght.ttf b/static/fonts/EBGaramond-Italic-VariableFont_wght.ttf Binary files differnew file mode 100644 index 0000000..9cb1376 --- /dev/null +++ b/static/fonts/EBGaramond-Italic-VariableFont_wght.ttf diff --git a/static/fonts/EBGaramond-VariableFont_wght.ttf b/static/fonts/EBGaramond-VariableFont_wght.ttf Binary files differnew file mode 100644 index 0000000..baf64b2 --- /dev/null +++ b/static/fonts/EBGaramond-VariableFont_wght.ttf diff --git a/static/fonts/IBMPlexMono-Medium.ttf b/static/fonts/IBMPlexMono-Medium.ttf Binary files differnew file mode 100644 index 0000000..8253c5f --- /dev/null +++ b/static/fonts/IBMPlexMono-Medium.ttf diff --git a/static/fonts/IBMPlexMono-Regular.ttf b/static/fonts/IBMPlexMono-Regular.ttf Binary files differnew file mode 100644 index 0000000..601ae94 --- /dev/null +++ b/static/fonts/IBMPlexMono-Regular.ttf diff --git a/static/fonts/IBMPlexMono-SemiBold.ttf b/static/fonts/IBMPlexMono-SemiBold.ttf Binary files differnew file mode 100644 index 0000000..5e0b41d --- /dev/null +++ b/static/fonts/IBMPlexMono-SemiBold.ttf diff --git a/static/fonts/IBMPlexSans-Italic-VariableFont_wdth,wght.ttf b/static/fonts/IBMPlexSans-Italic-VariableFont_wdth,wght.ttf Binary files differnew file mode 100644 index 0000000..6232aaa --- /dev/null +++ b/static/fonts/IBMPlexSans-Italic-VariableFont_wdth,wght.ttf diff --git a/static/fonts/IBMPlexSans-VariableFont_wdth,wght.ttf b/static/fonts/IBMPlexSans-VariableFont_wdth,wght.ttf Binary files differnew file mode 100644 index 0000000..9add875 --- /dev/null +++ b/static/fonts/IBMPlexSans-VariableFont_wdth,wght.ttf diff --git a/static/styles.css b/static/styles.css index 2a6cd04..637348f 100644 --- a/static/styles.css +++ b/static/styles.css @@ -1,18 +1,94 @@ -@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;1,400;1,500&display=swap'); +@font-face { + font-family: "EB Garamond"; + src: url("/static/fonts/EBGaramond-VariableFont_wght.ttf") format("truetype-variations"), + url("/static/fonts/EBGaramond-VariableFont_wght.ttf") format("truetype"); + font-weight: 400 800; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "EB Garamond"; + src: url("/static/fonts/EBGaramond-Italic-VariableFont_wght.ttf") format("truetype-variations"), + url("/static/fonts/EBGaramond-Italic-VariableFont_wght.ttf") format("truetype"); + font-weight: 400 800; + font-style: italic; + font-display: swap; +} + +@font-face { + font-family: "IBM Plex Sans"; + src: url("/static/fonts/IBMPlexSans-VariableFont_wdth,wght.ttf") format("truetype-variations"), + url("/static/fonts/IBMPlexSans-VariableFont_wdth,wght.ttf") format("truetype"); + font-weight: 100 700; + font-stretch: 85% 100%; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "IBM Plex Sans"; + src: url("/static/fonts/IBMPlexSans-Italic-VariableFont_wdth,wght.ttf") format("truetype-variations"), + url("/static/fonts/IBMPlexSans-Italic-VariableFont_wdth,wght.ttf") format("truetype"); + font-weight: 100 700; + font-stretch: 85% 100%; + font-style: italic; + font-display: swap; +} + +@font-face { + font-family: "IBM Plex Mono"; + src: url("/static/fonts/IBMPlexMono-Regular.ttf") format("truetype"); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "IBM Plex Mono"; + src: url("/static/fonts/IBMPlexMono-Medium.ttf") format("truetype"); + font-weight: 500; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "IBM Plex Mono"; + src: url("/static/fonts/IBMPlexMono-SemiBold.ttf") format("truetype"); + font-weight: 600; + font-style: normal; + font-display: swap; +} + :root { color-scheme: dark; - --bg: #0b0b0a; - --panel: #171613; - --panel-soft: #201f1b; - --line: #312f28; - --text: #f4efe6; - --muted: #a9a197; - --subtle: #756f66; - --accent: #f0b84d; - --accent-strong: #ffcf73; - --green: #79a889; - --danger: #df6e62; - --shadow: 0 20px 70px rgba(0, 0, 0, 0.35); + --bg: #0b0e13; + --panel: #11151c; + --panel-soft: #181d26; + --panel-raised: #222934; + --line: #232934; + --line-strong: #2e3645; + --text: #f2ecdc; + --muted: #c7c0ae; + --subtle: #8e8676; + --faint: #5e5849; + --accent: #c2aa7a; + --accent-strong: #dcc79e; + --accent-deep: #8f7a50; + --accent-ink: #17120a; + --green: #4f8c5e; + --green-bg: #15241a; + --danger: #b5494b; + --danger-bg: #2a1517; + --warning: #c49545; + --focus-ring: rgba(194, 170, 122, 0.55); + --selection-bg: rgba(194, 170, 122, 0.25); + --font-display: "EB Garamond", "Source Serif Pro", Georgia, serif; + --font-sans: "IBM Plex Sans", "Helvetica Neue", system-ui, sans-serif; + --font-mono: "IBM Plex Mono", "SF Mono", Menlo, monospace; + --shadow: 0 1px 0 rgba(0, 0, 0, 0.4), 0 12px 32px rgba(0, 0, 0, 0.55); + --shadow-soft: 0 1px 0 rgba(0, 0, 0, 0.4), 0 4px 12px rgba(0, 0, 0, 0.45); + --shadow-inset: inset 0 1px 0 rgba(255, 255, 255, 0.04); } * { @@ -22,10 +98,25 @@ body { margin: 0; min-height: 100vh; - background: var(--bg); + background: + radial-gradient(circle at 15% -10%, rgba(194, 170, 122, 0.09), transparent 34rem), + radial-gradient(circle at 88% 10%, rgba(31, 61, 92, 0.2), transparent 30rem), + linear-gradient(180deg, #0b0e13 0%, #080a0e 100%); color: var(--text); - font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + font-family: var(--font-sans); line-height: 1.5; + font-weight: 400; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; +} + +body.modal-open { + overflow: hidden; +} + +::selection { + background: var(--selection-bg); + color: var(--text); } a { @@ -33,6 +124,15 @@ a { text-decoration: none; } +a:focus-visible, +button:focus-visible, +input:focus-visible, +select:focus-visible, +textarea:focus-visible { + outline: 2px solid var(--focus-ring); + outline-offset: 3px; +} + img { display: block; max-width: 100%; @@ -61,8 +161,11 @@ textarea { } .brand { - font-family: 'Cormorant Garamond', Georgia, serif; - font-size: 1.8rem; + font-family: var(--font-display); + font-size: 2rem; + font-weight: 500; + font-style: italic; + letter-spacing: -0.02em; color: var(--accent); } @@ -84,23 +187,32 @@ textarea { color: var(--muted); } +.nav-actions a { + font-size: 0.84rem; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; +} + .nav-actions a.is-active { - color: var(--text); + color: var(--accent-strong); } .nav-actions a.button-link { - color: #0e0a04; + color: var(--accent-ink); } .button-link, button { border: 1px solid var(--accent); - border-radius: 6px; + border-radius: 4px; background: var(--accent); - color: #0e0a04; + color: var(--accent-ink); padding: 10px 14px; cursor: pointer; - font-weight: 800; + font-weight: 700; + box-shadow: var(--shadow-inset); + transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease; } .button-link:hover, @@ -124,8 +236,8 @@ button:hover { align-items: center; justify-content: center; border: 1px solid var(--line); - border-radius: 6px; - background: transparent; + border-radius: 4px; + background: rgba(24, 29, 38, 0.58); color: var(--muted); font-weight: 700; } @@ -177,24 +289,29 @@ button:hover { .eyebrow { margin: 0 0 8px; - color: var(--green); + color: var(--subtle); font-size: 0.78rem; - font-weight: 800; + font-weight: 700; + letter-spacing: 0.12em; text-transform: uppercase; } h1 { margin-bottom: 0; - font-family: 'Cormorant Garamond', Georgia, serif; + font-family: var(--font-display); font-size: clamp(2.2rem, 5vw, 4rem); - font-weight: 500; + font-weight: 400; line-height: 1.05; + letter-spacing: -0.02em; } h2 { margin-bottom: 4px; + color: var(--text); + font-family: var(--font-display); font-size: 1.1rem; + font-weight: 500; line-height: 1.2; } @@ -206,7 +323,7 @@ h2 { .inline-link { color: var(--text); text-decoration: underline; - text-decoration-color: rgba(240, 184, 77, 0.45); + text-decoration-color: rgba(194, 170, 122, 0.45); text-underline-offset: 0.14em; } @@ -217,23 +334,23 @@ h2 { .original-title { margin-bottom: 8px; color: var(--subtle); - font-family: Georgia, "Times New Roman", serif; + font-family: var(--font-display); font-style: italic; } .notice { margin-bottom: 20px; - border: 1px solid rgba(121, 168, 137, 0.4); - border-radius: 6px; - background: rgba(121, 168, 137, 0.1); - color: #d5f0dd; + border: 1px solid rgba(79, 140, 94, 0.42); + border-radius: 4px; + background: var(--green-bg); + color: #d7ecd9; padding: 12px 14px; } .notice.error { - border-color: rgba(223, 110, 98, 0.4); - background: rgba(223, 110, 98, 0.1); - color: #ffc7c0; + border-color: rgba(181, 73, 75, 0.44); + background: var(--danger-bg); + color: #f5c3bd; } .diary-feed { @@ -245,8 +362,9 @@ h2 { .month-label { margin: 24px 0 14px; color: var(--subtle); + font-family: var(--font-mono); font-size: 0.78rem; - font-weight: 700; + font-weight: 500; letter-spacing: 0.1em; text-transform: uppercase; } @@ -256,7 +374,7 @@ h2 { grid-template-columns: 92px minmax(0, 1fr); gap: 18px; border-bottom: 1px solid var(--line); - padding: 0 0 18px; + padding: 0 0 20px; } .poster-frame { @@ -265,12 +383,12 @@ h2 { aspect-ratio: 2 / 3; overflow: hidden; border: 1px solid var(--line); - border-radius: 6px; + border-radius: 4px; background: var(--panel); color: var(--accent); - font-family: Georgia, "Times New Roman", serif; + font-family: var(--font-display); font-size: 2rem; - box-shadow: var(--shadow); + box-shadow: var(--shadow-soft); } .poster-frame img { @@ -294,6 +412,7 @@ h2 { .rating { flex: 0 0 auto; color: var(--accent); + font-family: var(--font-mono); font-weight: 800; } @@ -354,6 +473,7 @@ h2 { flex-wrap: wrap; gap: 8px; color: var(--muted); + font-family: var(--font-mono); font-size: 0.92rem; } @@ -375,10 +495,68 @@ h2 { border-radius: 999px; background: var(--panel-soft); color: var(--muted); + font-family: var(--font-mono); padding: 4px 9px; font-size: 0.84rem; } +.ledger-strip { + display: flex; + flex-wrap: wrap; + gap: 0; + margin-top: 8px; + color: var(--muted); + font-family: var(--font-mono); + font-size: 0.92rem; + line-height: 1.65; +} + +.ledger-strip span:not(:last-child)::after { + content: " / "; + color: var(--faint); +} + +.fact-list { + display: grid; + gap: 8px; + margin-top: 10px; +} + +.fact-list-compact { + gap: 6px; +} + +.fact-row { + display: grid; + grid-template-columns: 56px minmax(0, 1fr); + gap: 10px; + align-items: start; +} + +.fact-label { + color: var(--faint); + font-family: var(--font-mono); + font-size: 0.72rem; + font-weight: 500; + letter-spacing: 0.12em; + text-transform: uppercase; + white-space: nowrap; +} + +.fact-value { + color: var(--muted); + font-size: 0.95rem; + line-height: 1.6; +} + +.film-card .fact-value { + font-family: var(--font-sans); +} + +.detail-panel .fact-value { + font-family: var(--font-sans); +} + .inline-actions { display: flex; flex-wrap: wrap; @@ -392,15 +570,16 @@ h2 { .notes-preview { margin: 14px 0 0; - color: #d0c7ba; + color: var(--muted); } .empty-state { margin: 34px 0 64px; border: 1px solid var(--line); - border-radius: 8px; - background: var(--panel); + border-radius: 6px; + background: linear-gradient(180deg, rgba(17, 21, 28, 0.96), rgba(24, 29, 38, 0.76)); padding: 30px; + box-shadow: var(--shadow-soft); } .empty-state p { @@ -417,9 +596,10 @@ h2 { .summary-card, .stats-panel { border: 1px solid var(--line); - border-radius: 8px; - background: var(--panel); + border-radius: 6px; + background: linear-gradient(180deg, rgba(17, 21, 28, 0.98), rgba(17, 21, 28, 0.82)); padding: 18px; + box-shadow: var(--shadow-inset); } .summary-card strong, @@ -427,15 +607,17 @@ h2 { display: block; margin-top: 6px; font-size: clamp(1.8rem, 4vw, 2.6rem); - font-family: 'Cormorant Garamond', Georgia, serif; - font-weight: 600; + font-family: var(--font-display); + font-weight: 500; color: var(--accent); } .summary-label { color: var(--muted); + font-family: var(--font-mono); font-size: 0.82rem; - font-weight: 700; + font-weight: 500; + letter-spacing: 0.1em; text-transform: uppercase; } @@ -460,9 +642,10 @@ h2 { display: grid; gap: 12px; border: 1px solid var(--line); - border-radius: 8px; + border-radius: 6px; background: var(--panel); padding: 16px; + box-shadow: var(--shadow-inset); } .detail-aside-meta strong { @@ -492,10 +675,11 @@ h2 { .detail-panel { border: 1px solid var(--line); - border-radius: 8px; - background: var(--panel); + border-radius: 6px; + background: linear-gradient(180deg, rgba(17, 21, 28, 0.96), rgba(17, 21, 28, 0.78)); padding: 16px; margin-bottom: 20px; + box-shadow: var(--shadow-inset); } .detail-panel .eyebrow { @@ -579,13 +763,14 @@ h2 { .detail-tagline { margin: 0 0 12px; color: var(--accent-strong); - font-family: Georgia, "Times New Roman", serif; + font-family: var(--font-display); + font-style: italic; font-size: 1.08rem; } .detail-overview { margin: 0; - color: #e3dacd; + color: var(--muted); } .detail-cast { @@ -600,9 +785,9 @@ h2 { .notes-body { margin-top: 28px; max-width: 68ch; - color: #e3dacd; + color: var(--muted); white-space: pre-wrap; - font-family: Georgia, "Times New Roman", serif; + font-family: var(--font-display); font-size: 1.14rem; } @@ -642,17 +827,20 @@ h2 { .tmdb-panel { margin-bottom: 24px; border: 1px solid var(--line); - border-radius: 8px; + border-radius: 6px; background: var(--panel); padding: 18px; + box-shadow: var(--shadow-inset); } label { display: block; margin-bottom: 8px; color: var(--muted); + font-family: var(--font-mono); font-size: 0.86rem; - font-weight: 700; + font-weight: 500; + letter-spacing: 0.04em; } input, @@ -660,11 +848,12 @@ select, textarea { width: 100%; border: 1px solid var(--line); - border-radius: 6px; - background: #11100e; + border-radius: 4px; + background: var(--panel); color: var(--text); padding: 11px 12px; outline: none; + box-shadow: var(--shadow-inset); } textarea { @@ -675,6 +864,7 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent); + box-shadow: 0 0 0 3px var(--focus-ring); } .search-row input { @@ -699,7 +889,7 @@ textarea:focus { align-items: center; width: 100%; border-color: var(--line); - background: #11100e; + background: var(--panel); color: var(--text); padding: 8px; text-align: left; @@ -835,8 +1025,8 @@ textarea:focus { gap: 2px; min-width: 120px; border: 1px solid var(--line); - border-radius: 6px; - background: rgba(11, 11, 10, 0.96); + border-radius: 4px; + background: rgba(11, 14, 19, 0.96); color: var(--text); padding: 10px 12px; pointer-events: none; @@ -929,7 +1119,7 @@ textarea:focus { height: 100%; min-width: 8px; border-radius: inherit; - background: linear-gradient(90deg, rgba(240, 184, 77, 0.4), var(--accent)); + background: linear-gradient(90deg, rgba(194, 170, 122, 0.35), var(--accent)); } .stats-overview-row { @@ -985,9 +1175,10 @@ textarea:focus { .review-panel { border: 1px solid var(--line); - border-radius: 8px; - background: var(--panel); + border-radius: 6px; + background: linear-gradient(180deg, rgba(17, 21, 28, 0.96), rgba(17, 21, 28, 0.78)); padding: 18px; + box-shadow: var(--shadow-inset); } .review-panel-wide { @@ -1021,7 +1212,7 @@ textarea:focus { .year-bar-fill { height: 100%; border-radius: inherit; - background: linear-gradient(90deg, rgba(240, 184, 77, 0.35), var(--accent)); + background: linear-gradient(90deg, rgba(194, 170, 122, 0.35), var(--accent)); } .highlight-grid { @@ -1046,14 +1237,894 @@ textarea:focus { color: var(--subtle); } +.public-profile-page { + background: + radial-gradient(circle at 12% 0%, rgba(194, 170, 122, 0.08), transparent 28rem), + linear-gradient(180deg, #0b0e13 0%, #080a0e 100%); +} + +.public-shell { + width: min(1240px, calc(100% - 40px)); + margin: 0 auto; + padding: 28px 0 48px; +} + +.public-topbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; + padding-bottom: 22px; + border-bottom: 1px solid var(--line); +} + +.public-nav { + display: flex; + flex-wrap: wrap; + gap: 18px; +} + +.public-nav a { + color: var(--muted); + font-size: 0.84rem; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.public-nav a.is-active, +.public-nav a:hover { + color: var(--accent-strong); +} + +.public-main { + padding-top: 28px; +} + +.public-hero { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 24px; + align-items: end; + border: 1px solid var(--line); + border-radius: 10px; + background: + linear-gradient(180deg, rgba(17, 21, 28, 0.96), rgba(11, 14, 19, 0.84)), + radial-gradient(circle at top left, rgba(194, 170, 122, 0.08), transparent 20rem); + padding: 32px; + box-shadow: var(--shadow); +} + +.public-hero-copy { + max-width: 50rem; +} + +.public-hero-text { + margin: 14px 0 0; + color: var(--muted); + font-size: 1rem; + line-height: 1.7; +} + +.public-hero-metrics { + display: grid; + grid-template-columns: repeat(2, minmax(120px, 1fr)); + gap: 16px; +} + +.public-metric-card { + display: grid; + gap: 6px; + padding: 14px 16px; + border: 1px solid var(--line); + border-radius: 8px; + background: rgba(24, 29, 38, 0.72); +} + +.public-metric-card strong { + color: var(--accent); + font-family: var(--font-display); + font-size: 2rem; + font-weight: 500; +} + +.public-tabs { + display: flex; + gap: 22px; + margin: 24px 0 28px; + border-bottom: 1px solid var(--line); +} + +.public-tab { + border: 0; + border-bottom: 2px solid transparent; + border-radius: 0; + background: none; + box-shadow: none; + color: var(--subtle); + font-size: 0.9rem; + font-weight: 700; + letter-spacing: 0.04em; + padding: 0 0 14px; + text-transform: uppercase; +} + +.public-tab:hover, +.public-tab.is-active { + background: none; + border-bottom-color: var(--accent); + color: var(--text); +} + +.public-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 24px; +} + +.public-section { + display: grid; + gap: 18px; + border: 1px solid var(--line); + border-radius: 8px; + background: linear-gradient(180deg, rgba(17, 21, 28, 0.96), rgba(17, 21, 28, 0.82)); + padding: 24px; + box-shadow: var(--shadow-inset); +} + +.public-section-wide { + grid-column: 1 / -1; +} + +.public-section-head { + display: grid; + gap: 4px; +} + +.public-section-head h2 { + margin: 0; +} + +.public-list, +.public-bars { + display: grid; + gap: 12px; +} + +.public-list-row, +.public-bar-row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 12px; + align-items: center; +} + +.public-list-row { + padding-bottom: 10px; + border-bottom: 1px solid var(--line); +} + +.public-list-row:last-child { + border-bottom: 0; + padding-bottom: 0; +} + +.public-list-row strong, +.public-bar-row strong, +.public-country-card strong { + color: var(--accent); + font-family: var(--font-display); + font-weight: 500; +} + +.public-bar-row { + grid-template-columns: 92px minmax(0, 1fr) auto; +} + +.public-bar-track { + height: 10px; + overflow: hidden; + border-radius: 999px; + background: var(--panel-soft); +} + +.public-bar-fill { + height: 100%; + min-width: 8px; + border-radius: inherit; + background: linear-gradient(90deg, rgba(194, 170, 122, 0.35), var(--accent)); +} + +.public-country-grid, +.public-poster-grid { + display: grid; + gap: 16px; +} + +.public-country-grid { + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); +} + +.public-country-card { + display: grid; + gap: 6px; + padding: 16px; + border: 1px solid var(--line); + border-radius: 6px; + background: rgba(17, 21, 28, 0.68); +} + +.public-country-card span, +.public-poster-card p, +.public-poster-meta { + color: var(--muted); +} + +.public-poster-grid { + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); +} + +.public-poster-card { + display: grid; + gap: 8px; +} + +.public-poster-frame { + box-shadow: none; +} + +.public-poster-card h3, +.public-film-head h2 { + margin: 0; +} + +.public-poster-card p, +.public-film-head p { + margin: 0; +} + +.public-poster-meta { + font-family: var(--font-mono); + font-size: 0.84rem; +} + +.public-feed { + display: grid; + gap: 0; +} + +.public-film-row { + display: grid; + grid-template-columns: 84px minmax(0, 1fr); + gap: 18px; + padding: 20px 0; + border-top: 1px solid var(--line); +} + +.public-film-row:first-child { + border-top: 0; +} + +.public-film-poster { + box-shadow: none; +} + +.public-film-body { + display: grid; + gap: 8px; +} + +.public-film-head { + display: flex; + align-items: start; + justify-content: space-between; + gap: 16px; +} + +.public-film-head p { + color: var(--muted); +} + +.public-feed-sentinel { + height: 1px; + margin: 20px 0; +} + +.public-footer { + margin-top: 48px; + padding-top: 20px; + border-top: 1px solid var(--line); + color: var(--muted); + font-size: 0.84rem; + text-align: center; +} + +.public-footer p { + margin: 0; +} + +.public-footer a { + color: var(--accent); + border-bottom: 0; +} + +.shell { + width: min(1400px, calc(100% - 40px)); + display: grid; + grid-template-columns: 280px minmax(0, 1fr); + gap: 32px; + padding: 24px 0 40px; +} + +.app-sidebar { + display: block; +} + +.app-sidebar-inner { + position: sticky; + top: 24px; + display: grid; + gap: 24px; + min-height: calc(100vh - 48px); + border-right: 1px solid var(--line); + background: transparent; + padding: 28px 24px; +} + +.sidebar-intro { + margin: -8px 0 0; + max-width: 20ch; + color: var(--subtle); + font-family: var(--font-display); + font-size: 1.15rem; + font-style: italic; + line-height: 1.35; +} + +.sidebar-cta { + display: inline-flex; + align-items: center; + justify-content: center; + width: 100%; + align-self: start; + padding: 12px 14px; +} + +.sidebar-nav { + display: grid; + gap: 24px; +} + +.sidebar-group { + display: grid; + gap: 6px; +} + +.sidebar-label { + margin: 0 0 8px; + color: var(--faint); + font-family: var(--font-mono); + font-size: 0.72rem; + letter-spacing: 0.14em; + text-transform: uppercase; +} + +.sidebar-nav a { + border-left: 1px solid transparent; + color: var(--muted); + font-size: 0.9rem; + font-weight: 600; + letter-spacing: 0.02em; + padding: 11px 0 11px 12px; + transition: border-color 0.15s ease, color 0.15s ease; +} + +.sidebar-nav a:hover, +.sidebar-nav a.is-active { + border-left-color: var(--accent); + color: var(--text); +} + +.sidebar-logout { + margin-top: auto; +} + +.sidebar-logout-button, +.nav-logout-button { + border: 0; + background: none; + box-shadow: none; + color: var(--subtle); + padding: 0; +} + +.sidebar-logout-button:hover, +.nav-logout-button:hover { + background: none; + color: var(--text); +} + +.app-main { + min-width: 0; +} + +.app-content { + min-width: 0; +} + +.topbar { + display: none; +} + +.nav-logout { + display: contents; +} + +.shelf-hero, +.form-hero { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 24px; + align-items: end; + margin-bottom: 20px; + border: 1px solid var(--line); + border-radius: 10px; + background: + linear-gradient(180deg, rgba(17, 21, 28, 0.96), rgba(11, 14, 19, 0.84)), + radial-gradient(circle at top left, rgba(194, 170, 122, 0.08), transparent 22rem); + padding: 32px; + box-shadow: var(--shadow); +} + +.shelf-hero-copy, +.form-heading { + max-width: 46rem; +} + +.form-hero .form-heading, +.shelf-hero-copy { + padding: 0; +} + +.form-hero .form-heading-row { + align-items: flex-start; +} + +.shelf-hero-text, +.form-intro { + margin: 14px 0 0; + color: var(--muted); + font-size: 1rem; + line-height: 1.7; +} + +.shelf-hero-meta { + display: grid; + gap: 16px; + justify-items: end; +} + +.shelf-stat { + display: grid; + gap: 6px; + min-width: 140px; + padding: 14px 16px; + border: 1px solid var(--line); + border-radius: 8px; + background: rgba(24, 29, 38, 0.72); +} + +.shelf-stat strong { + color: var(--accent); + font-family: var(--font-display); + font-size: 2rem; + font-weight: 500; +} + +.feed-toolbar { + position: sticky; + top: 18px; + z-index: 20; + margin-bottom: 28px; + border: 1px solid var(--line); + border-radius: 8px; + background: rgba(11, 14, 19, 0.88); + backdrop-filter: blur(16px); + padding: 14px; +} + +.search-row { + gap: 10px; +} + +.search-row input { + flex: 1 1 360px; +} + +.search-row select { + flex: 0 0 260px; +} + +.diary-feed { + gap: 24px; +} + +.month-group { + display: grid; + grid-template-columns: 132px minmax(0, 1fr); + gap: 18px; + align-items: start; +} + +.month-rail { + position: sticky; + top: 106px; +} + +.month-label { + margin: 0; + padding-top: 10px; +} + +.month-stack { + display: grid; + gap: 0; +} + +.film-card { + grid-template-columns: 72px minmax(0, 1fr); + gap: 16px; + border: 0; + border-top: 1px solid var(--line); + border-radius: 0; + background: transparent; + padding: 18px 0 20px; + box-shadow: none; +} + +.film-card-body { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 18px; + align-items: start; + padding-top: 0; +} + +.film-card-main { + min-width: 0; +} + +.film-card-header { + display: grid; + gap: 4px; +} + +.film-card-header h2 { + margin: 0; + font-size: 1.45rem; +} + +.film-card-header .muted { + margin: 0; +} + +.film-card-side { + display: grid; + justify-items: end; + align-content: start; + gap: 16px; + min-width: 150px; +} + +.star-control { + margin-left: 0; +} + +.inline-actions { + justify-content: end; + margin-top: 0; + opacity: 0; + visibility: hidden; + pointer-events: none; + transition: opacity 0.15s ease; +} + +.film-card:hover .inline-actions, +.film-card:focus-within .inline-actions { + opacity: 1; + visibility: visible; + pointer-events: auto; +} + +.notes-preview { + max-width: 62ch; + line-height: 1.65; +} + +.detail-layout { + grid-template-columns: 280px minmax(0, 1fr); + gap: 32px; +} + +.detail-poster { + position: sticky; + top: 24px; +} + +.detail-poster-button { + width: 100%; +} + +.poster-picker-modal { + position: fixed; + inset: 0; + z-index: 120; + display: grid; + place-items: center; + padding: 32px; +} + +.poster-picker-modal[hidden] { + display: none; +} + +.poster-picker-backdrop { + position: absolute; + inset: 0; + background: rgba(8, 10, 14, 0.82); + backdrop-filter: blur(10px); +} + +.poster-picker-dialog { + position: relative; + z-index: 1; + display: grid; + gap: 18px; + width: min(980px, 100%); + max-height: min(88vh, 900px); + overflow: auto; + border: 1px solid var(--line); + border-radius: 8px; + background: linear-gradient(180deg, rgba(17, 21, 28, 0.98), rgba(11, 14, 19, 0.96)); + padding: 24px; + box-shadow: var(--shadow); +} + +.poster-picker-head { + display: flex; + align-items: start; + justify-content: space-between; + gap: 16px; +} + +.poster-picker-head h2 { + margin: 4px 0 0; +} + +.poster-picker-close { + flex: 0 0 auto; +} + +.poster-picker-status { + margin: 0; + color: var(--muted); + font-size: 0.86rem; +} + +.poster-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(132px, 1fr)); + gap: 12px; +} + +.poster-option { + position: relative; + padding: 0; + border: 2px solid transparent; + border-radius: 4px; + overflow: hidden; + cursor: pointer; + background: none; + box-shadow: none; +} + +.poster-option.is-current, +.poster-option:hover, +.poster-option:focus-visible { + border-color: var(--accent); + background: none; +} + +.poster-option-badge { + position: absolute; + top: 8px; + right: 8px; + padding: 4px 7px; + border: 1px solid rgba(194, 170, 122, 0.5); + border-radius: 999px; + background: rgba(11, 14, 19, 0.86); + color: var(--accent-strong); + font-family: var(--font-mono); + font-size: 0.68rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.poster-option img { + width: 100%; + display: block; + aspect-ratio: 2 / 3; + object-fit: cover; +} + +.detail-body { + display: grid; + gap: 24px; +} + +.detail-hero { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 28px; + align-items: start; + border: 1px solid var(--line); + border-radius: 10px; + background: + linear-gradient(180deg, rgba(17, 21, 28, 0.96), rgba(11, 14, 19, 0.84)), + radial-gradient(circle at top left, rgba(194, 170, 122, 0.08), transparent 20rem); + padding: 28px 30px; + box-shadow: var(--shadow); +} + +.detail-hero-copy h1 { + margin-bottom: 8px; +} + +.detail-subtitle { + margin-bottom: 0; +} + +.detail-columns { + display: grid; + grid-template-columns: 280px minmax(0, 1fr); + gap: 24px; + align-items: start; +} + +.detail-sidebar, +.detail-main { + display: grid; + gap: 18px; +} + +.detail-panel { + margin-bottom: 0; +} + +.detail-panel-feature { + padding: 22px; +} + +.detail-actions { + display: flex; + flex-wrap: wrap; + justify-content: end; + align-items: center; + gap: 10px; + margin-top: 0; +} + +.form-shell { + width: min(1080px, 100%); +} + +.form-panel { + display: grid; + gap: 18px; + border: 1px solid var(--line); + border-radius: 8px; + background: linear-gradient(180deg, rgba(17, 21, 28, 0.96), rgba(17, 21, 28, 0.82)); + padding: 24px; + box-shadow: var(--shadow-inset); +} + +.form-panel-head { + display: grid; + gap: 4px; +} + +.form-panel-head h2 { + margin: 0; +} + +.form-grid { + gap: 20px; +} + +.form-grid-archive { + align-items: start; +} + +.poster-preview-field { + grid-column: 1; +} + +.poster-preview { + width: min(220px, 100%); +} + +.notes-field { + grid-column: 2; +} + @media (max-width: 760px) { + .public-shell { + width: min(100% - 24px, 1240px); + padding-top: 20px; + } + + .public-topbar, + .public-hero, + .public-grid, + .public-hero-metrics, + .public-film-row { + grid-template-columns: 1fr; + } + + .public-topbar { + align-items: flex-start; + flex-direction: column; + } + + .public-nav { + gap: 12px; + } + + .public-hero { + padding: 24px 20px; + } + + .public-tabs { + gap: 16px; + overflow-x: auto; + } + + .public-bar-row { + grid-template-columns: 74px minmax(0, 1fr) auto; + } + + .public-section { + padding: 20px; + } + + .public-film-row { + gap: 14px; + } + + .public-film-poster { + width: min(96px, 100%); + } + .shell { width: min(100% - 24px, 1120px); + grid-template-columns: 1fr; + gap: 0; + padding-top: 0; + } + + .app-sidebar { + display: none; + } + + .app-main { + min-width: 0; } .topbar { + display: flex; align-items: center; flex-direction: row; + padding: 20px 0; } .menu-toggle { @@ -1095,7 +2166,7 @@ textarea:focus { flex-direction: column; gap: 0; border-bottom: 1px solid var(--line); - background: var(--bg); + background: rgba(11, 14, 19, 0.98); padding: 12px 0; z-index: 100; visibility: hidden; @@ -1126,7 +2197,7 @@ textarea:focus { } .nav-actions .button-link { - color: #0e0a04; + color: var(--accent-ink); background: var(--accent); margin: 8px 16px; border: none; @@ -1137,6 +2208,38 @@ textarea:focus { padding-top: 34px; } + .shelf-hero, + .form-hero, + .detail-hero, + .detail-columns, + .film-card-body, + .month-group { + grid-template-columns: 1fr; + } + + .shelf-hero, + .form-hero, + .detail-hero { + padding: 22px 20px; + } + + .shelf-hero-meta, + .detail-actions, + .film-card-side { + justify-items: start; + justify-content: start; + } + + .feed-toolbar { + top: 10px; + margin-bottom: 20px; + padding: 12px; + } + + .month-rail { + position: static; + } + .film-card, .detail-layout, .form-grid, @@ -1147,6 +2250,7 @@ textarea:focus { .film-card { grid-template-columns: 72px minmax(0, 1fr); + padding: 16px 0 18px; } .detail-layout { @@ -1154,16 +2258,28 @@ textarea:focus { } .detail-poster { + position: static; + } + + .detail-poster { width: min(220px, 70%); } + .film-card-side, + .inline-actions { + opacity: 1; + visibility: visible; + pointer-events: auto; + } + .poster-preview-field, .notes-field { grid-column: 1; } .search-row, - .form-actions { + .form-actions, + .detail-actions { align-items: stretch; flex-direction: column; } diff --git a/templates/_feed_partial.html b/templates/_feed_partial.html index e22cdde..47800be 100644 --- a/templates/_feed_partial.html +++ b/templates/_feed_partial.html @@ -1,10 +1,14 @@ {% if active_shelf == 'diary' and grouped_films %} {% for group in grouped_films %} <div class="month-group" data-month="{{ group.month }}"> - <p class="month-label">{{ group.month }}</p> - {% for film in group.films %} - {% include "_film_card.html" %} - {% endfor %} + <div class="month-rail"> + <p class="month-label">{{ group.month }}</p> + </div> + <div class="month-stack"> + {% for film in group.films %} + {% include "_film_card.html" %} + {% endfor %} + </div> </div> {% endfor %} {% else %} diff --git a/templates/_film_card.html b/templates/_film_card.html index a4e206f..6723821 100644 --- a/templates/_film_card.html +++ b/templates/_film_card.html @@ -7,8 +7,8 @@ {% endif %} </a> <div class="film-card-body"> - <div class="film-card-header"> - <div> + <div class="film-card-main"> + <div class="film-card-header"> <h2><a href="/films/{{ film.id }}">{{ film.title }}</a></h2> <p class="muted"> {% if film.year %}{{ film.year }}{% endif %} @@ -21,6 +21,55 @@ {% endif %} </p> </div> + + <div class="ledger-strip"> + <span>{{ film.shelf|title }}</span> + {% if film.date_watched %}<span>{{ film.date_watched }}</span>{% endif %} + {% if film.runtime %}<span>{{ film.runtime }} min</span>{% endif %} + {% if film.rewatch %}<span>Rewatch{% if film.rewatch_count %} #{{ film.rewatch_count }}{% endif %}</span>{% endif %} + </div> + + {% if film.language or film.genre or film.context or film.how_found or film.watched_with %} + <div class="fact-list fact-list-compact"> + {% if film.language %} + <div class="fact-row"> + <span class="fact-label">Lang</span> + <span class="fact-value">{{ film.language }}</span> + </div> + {% endif %} + {% if film.genre %} + <div class="fact-row"> + <span class="fact-label">Genre</span> + <span class="fact-value">{{ film.genre }}</span> + </div> + {% endif %} + {% if film.context %} + <div class="fact-row"> + <span class="fact-label">Context</span> + <span class="fact-value">{{ film.context }}</span> + </div> + {% endif %} + {% if film.how_found %} + <div class="fact-row"> + <span class="fact-label">Found</span> + <span class="fact-value">{{ film.how_found }}</span> + </div> + {% endif %} + {% if film.watched_with %} + <div class="fact-row"> + <span class="fact-label">With</span> + <span class="fact-value">{{ film.watched_with }}</span> + </div> + {% endif %} + </div> + {% endif %} + + {% if film.notes %} + <p class="notes-preview">{{ film.notes[:220] }}{% if film.notes|length > 220 %}...{% endif %}</p> + {% endif %} + </div> + + <div class="film-card-side"> {% if film.shelf == 'diary' %} <div class="star-control" role="group" aria-label="Rate film" data-film-id="{{ film.id }}" data-current-stars="{{ film.stars }}"> {% for value in range(1, 4) %} @@ -36,48 +85,27 @@ {% elif film.stars %} <span class="rating">{% for _ in range(film.stars) %}✦{% endfor %}</span> {% endif %} - </div> - - <div class="meta-row"> - <span>{{ film.shelf|title }}</span> - {% if film.date_watched %}<span>{{ film.date_watched }}</span>{% endif %} - {% if film.runtime %}<span>{{ film.runtime }} min</span>{% endif %} - {% if film.language %}<span>{{ film.language }}</span>{% endif %} - {% if film.rewatch %}<span>Rewatch{% if film.rewatch_count %} #{{ film.rewatch_count }}{% endif %}</span>{% endif %} - </div> - {% if film.genre or film.context or film.how_found or film.watched_with %} - <div class="tag-row"> - {% if film.genre %}<span>{{ film.genre }}</span>{% endif %} - {% if film.context %}<span>{{ film.context }}</span>{% endif %} - {% if film.how_found %}<span>{{ film.how_found }}</span>{% endif %} - {% if film.watched_with %}<span>With {{ film.watched_with }}</span>{% endif %} + <div class="inline-actions"> + {% if film.shelf == 'queue' %} + <a class="small-button" href="/films/{{ film.id }}/edit?shelf=diary">Mark watched</a> + <form method="post" action="/films/{{ film.id }}/shelf/abandoned"> + <button class="secondary-button" type="submit">Abandon</button> + </form> + {% elif film.shelf == 'diary' %} + <form method="post" action="/films/{{ film.id }}/shelf/queue"> + <button class="secondary-button" type="submit">Move to queue</button> + </form> + <form method="post" action="/films/{{ film.id }}/shelf/abandoned"> + <button class="secondary-button" type="submit">Mark abandoned</button> + </form> + {% else %} + <a class="small-button" href="/films/{{ film.id }}/edit?shelf=diary">Mark watched</a> + <form method="post" action="/films/{{ film.id }}/shelf/queue"> + <button class="secondary-button" type="submit">Move to queue</button> + </form> + {% endif %} </div> - {% endif %} - - {% if film.notes %} - <p class="notes-preview">{{ film.notes[:220] }}{% if film.notes|length > 220 %}...{% endif %}</p> - {% endif %} - - <div class="inline-actions"> - {% if film.shelf == 'queue' %} - <a class="small-button" href="/films/{{ film.id }}/edit?shelf=diary">Mark watched</a> - <form method="post" action="/films/{{ film.id }}/shelf/abandoned"> - <button class="secondary-button" type="submit">Abandon</button> - </form> - {% elif film.shelf == 'diary' %} - <form method="post" action="/films/{{ film.id }}/shelf/queue"> - <button class="secondary-button" type="submit">Move to queue</button> - </form> - <form method="post" action="/films/{{ film.id }}/shelf/abandoned"> - <button class="secondary-button" type="submit">Mark abandoned</button> - </form> - {% else %} - <a class="small-button" href="/films/{{ film.id }}/edit?shelf=diary">Mark watched</a> - <form method="post" action="/films/{{ film.id }}/shelf/queue"> - <button class="secondary-button" type="submit">Move to queue</button> - </form> - {% endif %} </div> </div> </article> diff --git a/templates/_public_feed_partial.html b/templates/_public_feed_partial.html new file mode 100644 index 0000000..ba84c61 --- /dev/null +++ b/templates/_public_feed_partial.html @@ -0,0 +1,71 @@ +{% for film in films %} +<article class="public-film-row"> + <div class="poster-frame public-film-poster"> + {% if film.poster_url %} + <img src="{{ film.poster_url }}" alt="{{ film.title }} poster" loading="lazy"> + {% else %} + <span>{{ film.title[:1] }}</span> + {% endif %} + </div> + <div class="public-film-body"> + <div class="public-film-head"> + <div> + <h2>{{ film.title }}</h2> + <p> + {% if film.year %}{{ film.year }}{% endif %} + {% set directors = split_credit_names(film.director) %} + {% if directors %} + {% if film.year %} · {% endif %} + {{ directors|join(", ") }} + {% endif %} + </p> + </div> + {% if film.stars %} + <span class="rating">{% for _ in range(film.stars) %}✦{% endfor %}</span> + {% endif %} + </div> + + <div class="ledger-strip"> + <span>Diary</span> + {% if film.date_watched %}<span>{{ film.date_watched }}</span>{% endif %} + {% if film.runtime %}<span>{{ film.runtime }} min</span>{% endif %} + {% if film.rewatch %}<span>Rewatch{% if film.rewatch_count %} #{{ film.rewatch_count }}{% endif %}</span>{% endif %} + </div> + + {% if film.language or film.genre or film.context or film.how_found or film.watched_with %} + <div class="fact-list fact-list-compact"> + {% if film.language %} + <div class="fact-row"> + <span class="fact-label">Lang</span> + <span class="fact-value">{{ film.language }}</span> + </div> + {% endif %} + {% if film.genre %} + <div class="fact-row"> + <span class="fact-label">Genre</span> + <span class="fact-value">{{ film.genre }}</span> + </div> + {% endif %} + {% if film.context %} + <div class="fact-row"> + <span class="fact-label">Context</span> + <span class="fact-value">{{ film.context }}</span> + </div> + {% endif %} + {% if film.how_found %} + <div class="fact-row"> + <span class="fact-label">Found</span> + <span class="fact-value">{{ film.how_found }}</span> + </div> + {% endif %} + {% if film.watched_with %} + <div class="fact-row"> + <span class="fact-label">With</span> + <span class="fact-value">{{ film.watched_with }}</span> + </div> + {% endif %} + </div> + {% endif %} + </div> +</article> +{% endfor %} diff --git a/templates/base.html b/templates/base.html index 4e814ad..1b48f06 100644 --- a/templates/base.html +++ b/templates/base.html @@ -10,26 +10,56 @@ </head> <body> <div class="shell"> - <header class="topbar"> - <a class="brand" href="/">Lumière</a> - <button id="menu-toggle" class="menu-toggle" aria-label="Menu" aria-expanded="false"></button> - <nav class="nav-actions" id="nav-actions" aria-label="Primary"> - <a class="{% if active_shelf == 'diary' %}is-active{% endif %}" href="/diary">Diary</a> - <a class="{% if active_shelf == 'queue' %}is-active{% endif %}" href="/queue">Queue</a> - <a class="{% if active_shelf == 'abandoned' %}is-active{% endif %}" href="/abandoned">Abandoned</a> - <a class="{% if active_page == 'stats' %}is-active{% endif %}" href="/stats">Stats</a> - <a class="{% if active_page == 'about' %}is-active{% endif %}" href="/about">About</a> - <a class="{% if active_page == 'import' %}is-active{% endif %}" href="/import">Import</a> - <a class="button-link" href="/films/new">Add Film</a> - <form method="post" action="/logout" style="display: contents;"> - <button type="submit" style="background: none; border: none; color: var(--muted); cursor: pointer; font-size: inherit; padding: 0;">Logout</button> + <aside class="app-sidebar"> + <div class="app-sidebar-inner"> + <a class="brand" href="/">Lumière</a> + <p class="sidebar-intro">A private film ledger for what stayed with you.</p> + + <a class="button-link sidebar-cta" href="/films/new">Add Film</a> + + <nav class="sidebar-nav" aria-label="Primary"> + <div class="sidebar-group"> + <p class="sidebar-label">Shelves</p> + <a class="{% if active_shelf == 'diary' %}is-active{% endif %}" href="/diary">Diary</a> + <a class="{% if active_shelf == 'queue' %}is-active{% endif %}" href="/queue">Queue</a> + <a class="{% if active_shelf == 'abandoned' %}is-active{% endif %}" href="/abandoned">Abandoned</a> + </div> + <div class="sidebar-group"> + <p class="sidebar-label">Library</p> + <a class="{% if active_page == 'stats' %}is-active{% endif %}" href="/stats">Stats</a> + <a class="{% if active_page == 'about' %}is-active{% endif %}" href="/about">About</a> + <a class="{% if active_page == 'import' %}is-active{% endif %}" href="/import">Import</a> + </div> + </nav> + + <form class="sidebar-logout" method="post" action="/logout"> + <button type="submit" class="sidebar-logout-button">Logout</button> </form> - </nav> - </header> + </div> + </aside> + + <div class="app-main"> + <header class="topbar"> + <a class="brand" href="/">Lumière</a> + <button id="menu-toggle" class="menu-toggle" aria-label="Menu" aria-expanded="false"></button> + <nav class="nav-actions" id="nav-actions" aria-label="Primary"> + <a class="{% if active_shelf == 'diary' %}is-active{% endif %}" href="/diary">Diary</a> + <a class="{% if active_shelf == 'queue' %}is-active{% endif %}" href="/queue">Queue</a> + <a class="{% if active_shelf == 'abandoned' %}is-active{% endif %}" href="/abandoned">Abandoned</a> + <a class="{% if active_page == 'stats' %}is-active{% endif %}" href="/stats">Stats</a> + <a class="{% if active_page == 'about' %}is-active{% endif %}" href="/about">About</a> + <a class="{% if active_page == 'import' %}is-active{% endif %}" href="/import">Import</a> + <a class="button-link" href="/films/new">Add Film</a> + <form method="post" action="/logout" class="nav-logout"> + <button type="submit" class="nav-logout-button">Logout</button> + </form> + </nav> + </header> - <main> - {% block content %}{% endblock %} - </main> + <main class="app-content"> + {% block content %}{% endblock %} + </main> + </div> </div> {% block scripts %}{% endblock %} </body> diff --git a/templates/detail.html b/templates/detail.html index 9c558f7..f51372d 100644 --- a/templates/detail.html +++ b/templates/detail.html @@ -17,236 +17,335 @@ <button type="button" id="change-poster-btn" - class="secondary-button" - style="width: 100%; margin-top: 12px;" + class="secondary-button detail-poster-button" data-tmdb-id="{{ film.tmdb_id }}" data-film-id="{{ film.id }}" >Change Poster</button> - - <div id="poster-picker" style="display: none; margin-top: 12px;"> - <p id="poster-picker-status" style="color: var(--muted); font-size: 0.86rem; margin: 0 0 10px;">Loading posters…</p> - <div id="poster-grid" style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px;"></div> - </div> {% endif %} - - <div class="detail-aside-meta"> - <div> - <span class="summary-label">Shelf</span> - <strong>{{ film.shelf|title }}</strong> - </div> - <div> - <span class="summary-label">Watched</span> - <strong>{% if film.date_watched %}{{ film.date_watched }}{% else %}Not set{% endif %}</strong> - </div> - <div> - <span class="summary-label">Stars</span> - <strong>{% if film.stars %}{% for _ in range(film.stars) %}✦{% endfor %}{% else %}Unstarred{% endif %}</strong> - </div> - </div> </aside> <section class="detail-body"> - <p class="eyebrow">{{ film.shelf|title }} Entry</p> - <h1>{{ film.title }}</h1> - {% if film.original_title %} - <p class="original-title">{{ film.original_title }}</p> - {% endif %} - <div class="detail-subtitle"> - <p class="subtitle"> - {% if film.year %}{{ film.year }}{% endif %} - {% set directors = split_credit_names(film.director) %} - {% if directors %} - {% if film.year %} · {% endif %} - {% for director in directors %} - <a class="inline-link" href="{{ director_href(director) }}">{{ director }}</a>{% if not loop.last %}, {% endif %} - {% endfor %} + <section class="detail-hero"> + <div class="detail-hero-copy"> + <p class="eyebrow">{{ film.shelf|title }} Entry</p> + <h1>{{ film.title }}</h1> + {% if film.original_title %} + <p class="original-title">{{ film.original_title }}</p> {% endif %} - </p> - {% if ratings %} - <div class="ratings-inline"> - {% if ratings.imdb %} - <span class="rating-badge"> - <img src="/static/logos/imdb.svg" alt="IMDb" height="18"> - <span>{{ ratings.imdb }}</span> - </span> - {% endif %} - {% if ratings.rt %} - <span class="rating-badge"> - <img src="/static/logos/rt.svg" alt="Rotten Tomatoes" height="18"> - <span>{{ ratings.rt }}</span> - </span> - {% endif %} - {% if ratings.metacritic %} - <span class="rating-badge"> - <img src="/static/logos/metacritic.svg" alt="Metacritic" height="18"> - <span>{{ ratings.metacritic }}</span> - </span> + <div class="detail-subtitle"> + <p class="subtitle"> + {% if film.year %}{{ film.year }}{% endif %} + {% set directors = split_credit_names(film.director) %} + {% if directors %} + {% if film.year %} · {% endif %} + {% for director in directors %} + <a class="inline-link" href="{{ director_href(director) }}">{{ director }}</a>{% if not loop.last %}, {% endif %} + {% endfor %} + {% endif %} + </p> + {% if ratings %} + <div class="ratings-inline"> + {% if ratings.imdb %} + <span class="rating-badge"> + <img src="/static/logos/imdb.svg" alt="IMDb" height="18"> + <span>{{ ratings.imdb }}</span> + </span> + {% endif %} + {% if ratings.rt %} + <span class="rating-badge"> + <img src="/static/logos/rt.svg" alt="Rotten Tomatoes" height="18"> + <span>{{ ratings.rt }}</span> + </span> + {% endif %} + {% if ratings.metacritic %} + <span class="rating-badge"> + <img src="/static/logos/metacritic.svg" alt="Metacritic" height="18"> + <span>{{ ratings.metacritic }}</span> + </span> + {% endif %} + </div> + {% endif %} + </div> + </div> + + <div class="detail-actions"> + <a class="secondary-button" href="/films/{{ film.id }}/edit">Edit</a> + {% if film.shelf == 'queue' %} + <a class="button-link" href="/films/{{ film.id }}/edit?shelf=diary">Mark watched</a> + <form method="post" action="/films/{{ film.id }}/shelf/abandoned"> + <button class="secondary-button" type="submit">Abandon</button> + </form> + {% elif film.shelf == 'diary' %} + <form method="post" action="/films/{{ film.id }}/shelf/queue"> + <button class="secondary-button" type="submit">Move to queue</button> + </form> + <form method="post" action="/films/{{ film.id }}/shelf/abandoned"> + <button class="secondary-button" type="submit">Mark abandoned</button> + </form> + {% else %} + <a class="button-link" href="/films/{{ film.id }}/edit?shelf=diary">Mark watched</a> + <form method="post" action="/films/{{ film.id }}/shelf/queue"> + <button class="secondary-button" type="submit">Move to queue</button> + </form> {% endif %} + <form method="post" action="/films/{{ film.id }}/delete"> + <button class="danger-button" type="submit">Delete</button> + </form> </div> - {% endif %} - </div> + </section> - <section class="detail-grid"> - <article class="detail-panel"> - <p class="eyebrow">Watch log</p> - <div class="detail-meta"> - {% if film.date_watched %}<span>{{ film.date_watched }}</span>{% endif %} - {% if film.runtime %}<span>{{ film.runtime }} min</span>{% endif %} - {% if film.rewatch %}<span>Rewatch{% if film.rewatch_count %} #{{ film.rewatch_count }}{% endif %}</span>{% endif %} - {% if film.watched_with %}<span>With {{ film.watched_with }}</span>{% endif %} - {% if film.context %}<span>{{ film.context }}</span>{% endif %} - {% if film.how_found %}<span>{{ film.how_found }}</span>{% endif %} + <section class="detail-columns"> + <aside class="detail-sidebar"> + <div class="detail-aside-meta"> + <div> + <span class="summary-label">Shelf</span> + <strong>{{ film.shelf|title }}</strong> + </div> + <div> + <span class="summary-label">Watched</span> + <strong>{% if film.date_watched %}{{ film.date_watched }}{% else %}Not set{% endif %}</strong> + </div> + <div> + <span class="summary-label">Stars</span> + <strong>{% if film.stars %}{% for _ in range(film.stars) %}✦{% endfor %}{% else %}Unstarred{% endif %}</strong> + </div> </div> - </article> - <article class="detail-panel"> - <p class="eyebrow">Production</p> - <div class="detail-meta"> - {% if film.genre %}<span>{{ film.genre }}</span>{% endif %} - {% if film.country %}<span>{{ film.country }}</span>{% endif %} - {% if film.language %}<span>{{ film.language }}</span>{% endif %} - {% if film.year %}<span>{{ film.year }}</span>{% endif %} - {% if film.tmdb_id %}<span>TMDB {{ film.tmdb_id }}</span>{% endif %} - </div> - </article> - </section> + <article class="detail-panel"> + <p class="eyebrow">Watch log</p> + <div class="fact-list"> + {% if film.date_watched %} + <div class="fact-row"> + <span class="fact-label">Watched</span> + <span class="fact-value">{{ film.date_watched }}</span> + </div> + {% endif %} + {% if film.runtime %} + <div class="fact-row"> + <span class="fact-label">Runtime</span> + <span class="fact-value">{{ film.runtime }} min</span> + </div> + {% endif %} + {% if film.rewatch %} + <div class="fact-row"> + <span class="fact-label">Rewatch</span> + <span class="fact-value">Yes{% if film.rewatch_count %} · #{{ film.rewatch_count }}{% endif %}</span> + </div> + {% endif %} + {% if film.watched_with %} + <div class="fact-row"> + <span class="fact-label">With</span> + <span class="fact-value">{{ film.watched_with }}</span> + </div> + {% endif %} + {% if film.context %} + <div class="fact-row"> + <span class="fact-label">Context</span> + <span class="fact-value">{{ film.context }}</span> + </div> + {% endif %} + {% if film.how_found %} + <div class="fact-row"> + <span class="fact-label">Found</span> + <span class="fact-value">{{ film.how_found }}</span> + </div> + {% endif %} + </div> + </article> - {% if rewatch_history|length > 1 %} - <section class="detail-panel"> - <p class="eyebrow">Rewatches</p> - <div class="rewatch-list"> - {% for entry in rewatch_history %} - <div class="rewatch-row"> - <div> - <p class="rewatch-meta">{{ entry.date_watched }}</p> - <p class="rewatch-rating"> - <span class="rewatch-stars">{% for _ in range(entry.stars) %}✦{% endfor %}</span> - {% if entry.watched_with %}<span class="rewatch-companion">with {{ entry.watched_with }}</span>{% endif %} - </p> + <article class="detail-panel"> + <p class="eyebrow">Production</p> + <div class="fact-list"> + {% if film.genre %} + <div class="fact-row"> + <span class="fact-label">Genre</span> + <span class="fact-value">{{ film.genre }}</span> + </div> + {% endif %} + {% if film.country %} + <div class="fact-row"> + <span class="fact-label">Country</span> + <span class="fact-value">{{ film.country }}</span> + </div> + {% endif %} + {% if film.language %} + <div class="fact-row"> + <span class="fact-label">Lang</span> + <span class="fact-value">{{ film.language }}</span> + </div> + {% endif %} + {% if film.year %} + <div class="fact-row"> + <span class="fact-label">Year</span> + <span class="fact-value">{{ film.year }}</span> + </div> + {% endif %} + {% if film.tmdb_id %} + <div class="fact-row"> + <span class="fact-label">TMDB</span> + <span class="fact-value">{{ film.tmdb_id }}</span> + </div> + {% endif %} </div> - {% if not loop.first %} - <span class="rewatch-delta"> - {% set prev_entry = rewatch_history[loop.index - 2] %} - {% if prev_entry.date_watched %} - {% set days = (entry.date_watched - prev_entry.date_watched).days %} - {{ days }}d - {% if entry.stars != prev_entry.stars %} - <span class="rewatch-delta-rating">({{ prev_entry.stars }}→{{ entry.stars }})</span> + </article> + </aside> + + <div class="detail-main"> + {% if tmdb_context %} + <section class="detail-panel detail-panel-feature"> + <p class="eyebrow">Summary</p> + {% if tmdb_context.tagline %} + <p class="detail-tagline">{{ tmdb_context.tagline }}</p> + {% endif %} + {% if tmdb_context.overview %} + <p class="detail-overview">{{ tmdb_context.overview }}</p> {% endif %} + {% if tmdb_context.cast %} + <div class="detail-cast"> + <span class="summary-label">Cast</span> + <p>{{ tmdb_context.cast|join(", ") }}</p> + </div> {% endif %} - </span> + </section> + {% endif %} + + <section class="detail-panel detail-panel-feature"> + <p class="eyebrow">Notes</p> + {% if film.notes %} + <div class="notes-body">{{ film.notes }}</div> + {% else %} + <p class="muted">No notes saved.</p> {% endif %} - </div> - {% endfor %} - </div> - </section> - {% endif %} + </section> - {% if tmdb_context %} - <section class="detail-panel"> - <p class="eyebrow">Summary</p> - {% if tmdb_context.tagline %} - <p class="detail-tagline">{{ tmdb_context.tagline }}</p> - {% endif %} - {% if tmdb_context.overview %} - <p class="detail-overview">{{ tmdb_context.overview }}</p> - {% endif %} - {% if tmdb_context.cast %} - <div class="detail-cast"> - <span class="summary-label">Cast</span> - <p>{{ tmdb_context.cast|join(", ") }}</p> + {% if rewatch_history|length > 1 %} + <section class="detail-panel"> + <p class="eyebrow">Rewatches</p> + <div class="rewatch-list"> + {% for entry in rewatch_history %} + <div class="rewatch-row"> + <div> + <p class="rewatch-meta">{{ entry.date_watched }}</p> + <p class="rewatch-rating"> + <span class="rewatch-stars">{% for _ in range(entry.stars) %}✦{% endfor %}</span> + {% if entry.watched_with %}<span class="rewatch-companion">with {{ entry.watched_with }}</span>{% endif %} + </p> + </div> + {% if not loop.first %} + <span class="rewatch-delta"> + {% set prev_entry = rewatch_history[loop.index - 2] %} + {% if prev_entry.date_watched %} + {% set days = (entry.date_watched - prev_entry.date_watched).days %} + {{ days }}d + {% if entry.stars != prev_entry.stars %} + <span class="rewatch-delta-rating">({{ prev_entry.stars }}→{{ entry.stars }})</span> + {% endif %} + {% endif %} + </span> + {% endif %} + </div> + {% endfor %} </div> + </section> {% endif %} - </section> - {% endif %} - - <section class="detail-panel"> - <p class="eyebrow">Notes</p> - {% if film.notes %} - <div class="notes-body">{{ film.notes }}</div> - {% else %} - <p class="muted">No notes saved.</p> - {% endif %} + </div> </section> - - <div class="detail-actions"> - <a class="secondary-button" href="/films/{{ film.id }}/edit">Edit</a> - {% if film.shelf == 'queue' %} - <a class="button-link" href="/films/{{ film.id }}/edit?shelf=diary">Mark watched</a> - <form method="post" action="/films/{{ film.id }}/shelf/abandoned"> - <button class="secondary-button" type="submit">Abandon</button> - </form> - {% elif film.shelf == 'diary' %} - <form method="post" action="/films/{{ film.id }}/shelf/queue"> - <button class="secondary-button" type="submit">Move to queue</button> - </form> - <form method="post" action="/films/{{ film.id }}/shelf/abandoned"> - <button class="secondary-button" type="submit">Mark abandoned</button> - </form> - {% else %} - <a class="button-link" href="/films/{{ film.id }}/edit?shelf=diary">Mark watched</a> - <form method="post" action="/films/{{ film.id }}/shelf/queue"> - <button class="secondary-button" type="submit">Move to queue</button> - </form> - {% endif %} - <form method="post" action="/films/{{ film.id }}/delete"> - <button class="danger-button" type="submit">Delete</button> - </form> - </div> </section> </article> + {% if film.tmdb_id %} + <div id="poster-picker-modal" class="poster-picker-modal" hidden> + <div class="poster-picker-backdrop" data-close-poster-picker></div> + <div class="poster-picker-dialog" role="dialog" aria-modal="true" aria-labelledby="poster-picker-title"> + <div class="poster-picker-head"> + <div> + <p class="eyebrow">Poster Archive</p> + <h2 id="poster-picker-title">Choose a poster for {{ film.title }}</h2> + </div> + <button type="button" class="secondary-button poster-picker-close" data-close-poster-picker>Close</button> + </div> + <p id="poster-picker-status" class="poster-picker-status">Loading posters…</p> + <div id="poster-grid" class="poster-grid"></div> + </div> + </div> + {% endif %} + {% if film.tmdb_id %} <script> (function () { const btn = document.getElementById("change-poster-btn"); - const picker = document.getElementById("poster-picker"); + const modal = document.getElementById("poster-picker-modal"); const grid = document.getElementById("poster-grid"); const status = document.getElementById("poster-picker-status"); const filmId = btn.dataset.filmId; const tmdbId = btn.dataset.tmdbId; + const closeControls = document.querySelectorAll("[data-close-poster-picker]"); + const posterImg = document.querySelector(".poster-large img"); let loaded = false; + let lastFocused = null; + let currentPosterUrl = posterImg ? posterImg.src : ""; - btn.addEventListener("click", async () => { - if (picker.style.display === "none") { - picker.style.display = "block"; - btn.textContent = "Close"; + function syncCurrentPosterOption(url) { + grid.querySelectorAll(".poster-option").forEach((button) => { + const isCurrent = button.dataset.url === url; + button.classList.toggle("is-current", isCurrent); + const badge = button.querySelector(".poster-option-badge"); + if (badge) { + badge.hidden = !isCurrent; + } + }); + } + + function openPicker() { + lastFocused = document.activeElement; + modal.hidden = false; + document.body.classList.add("modal-open"); + } + + function closePicker() { + modal.hidden = true; + document.body.classList.remove("modal-open"); + btn.textContent = "Change Poster"; + if (lastFocused instanceof HTMLElement) { + lastFocused.focus(); } else { - picker.style.display = "none"; - btn.textContent = "Change Poster"; - return; + btn.focus(); } + } + + btn.addEventListener("click", async () => { + openPicker(); if (loaded) return; status.textContent = "Loading posters…"; - status.style.display = "block"; + status.hidden = false; try { const resp = await fetch(`/tmdb/posters?tmdb_id=${tmdbId}`); if (!resp.ok) throw new Error("fetch failed"); const data = await resp.json(); - status.style.display = "none"; + status.hidden = true; if (!data.posters.length) { status.textContent = "No posters found."; - status.style.display = "block"; + status.hidden = false; return; } grid.innerHTML = data.posters.map((url) => ` - <button type="button" class="poster-option" data-url="${url}" style="padding: 0; border: 2px solid transparent; border-radius: 6px; overflow: hidden; cursor: pointer; background: none;"> - <img src="${url}" alt="Poster option" loading="lazy" style="width: 100%; display: block; aspect-ratio: 2/3; object-fit: cover;"> + <button type="button" class="poster-option" data-url="${url}"> + <img src="${url}" alt="Poster option" loading="lazy"> + <span class="poster-option-badge" ${currentPosterUrl === url ? "" : "hidden"}>Current</span> </button> `).join(""); + syncCurrentPosterOption(currentPosterUrl); + grid.querySelectorAll(".poster-option").forEach((optBtn) => { optBtn.addEventListener("click", async () => { const url = optBtn.dataset.url; - grid.querySelectorAll(".poster-option").forEach((b) => b.style.borderColor = "transparent"); - optBtn.style.borderColor = "var(--accent)"; - try { const saveResp = await fetch(`/films/${filmId}/poster`, { method: "POST", @@ -255,10 +354,10 @@ }); if (!saveResp.ok) return; - const posterImg = document.querySelector(".poster-large img"); if (posterImg) posterImg.src = url; - picker.style.display = "none"; - btn.textContent = "Change Poster"; + currentPosterUrl = url; + syncCurrentPosterOption(url); + closePicker(); } catch (err) { console.error("Failed to save poster", err); } @@ -271,6 +370,16 @@ console.error(err); } }); + + closeControls.forEach((control) => { + control.addEventListener("click", closePicker); + }); + + document.addEventListener("keydown", (event) => { + if (event.key === "Escape" && !modal.hidden) { + closePicker(); + } + }); })(); </script> {% endif %} diff --git a/templates/form.html b/templates/form.html index cd29e67..f7bbbf1 100644 --- a/templates/form.html +++ b/templates/form.html @@ -13,138 +13,168 @@ {% set current_stars = film.stars if film and film.stars else 0 %} <input type="hidden" id="stars" name="stars" value="{{ current_stars }}"> - <div class="form-heading"> - <p class="eyebrow">Diary Entry</p> - <div class="form-heading-row"> - <h1>{{ page_title }}</h1> - <div class="star-control" role="group" aria-label="Rate film" data-form-stars data-current-stars="{{ current_stars }}"> - {% for value in range(1, 4) %} - <button - type="button" - class="star-button {% if current_stars >= value %}is-active{% endif %}" - data-stars="{{ value }}" - aria-label="{{ value }} star{% if value > 1 %}s{% endif %}" - aria-pressed="{% if current_stars >= value %}true{% else %}false{% endif %}" - >✦</button> - {% endfor %} + <section class="form-hero"> + <div class="form-heading"> + <p class="eyebrow">Diary Entry</p> + <div class="form-heading-row"> + <div> + <h1>{{ page_title }}</h1> + <p class="form-intro">Search TMDB first, then tune the entry like a ledger instead of filling a blank spreadsheet.</p> + </div> + <div class="star-control" role="group" aria-label="Rate film" data-form-stars data-current-stars="{{ current_stars }}"> + {% for value in range(1, 4) %} + <button + type="button" + class="star-button {% if current_stars >= value %}is-active{% endif %}" + data-stars="{{ value }}" + aria-label="{{ value }} star{% if value > 1 %}s{% endif %}" + aria-pressed="{% if current_stars >= value %}true{% else %}false{% endif %}" + >✦</button> + {% endfor %} + </div> </div> </div> - </div> + </section> - <div class="tmdb-panel"> - <label for="tmdb-query">TMDB title search</label> + <section class="form-panel tmdb-panel"> + <div class="form-panel-head"> + <p class="eyebrow">Lookup</p> + <h2>TMDB title search</h2> + </div> <div class="search-row"> <input id="tmdb-query" type="search" autocomplete="off" placeholder="Search by title"> <button id="tmdb-search" type="button">Search</button> </div> <div id="tmdb-results" class="tmdb-results" aria-live="polite"></div> <div id="duplicate-notice" class="notice" hidden></div> - </div> + </section> - <div class="form-grid"> - <div class="field span-2"> - <label for="title">Display title</label> - <input id="title" name="title" required value="{{ film.title if film else '' }}"> + <section class="form-panel"> + <div class="form-panel-head"> + <p class="eyebrow">Identity</p> + <h2>Core film record</h2> </div> + <div class="form-grid"> + <div class="field span-2"> + <label for="title">Display title</label> + <input id="title" name="title" required value="{{ film.title if film else '' }}"> + </div> - <div class="field span-2"> - <label for="original_title">Original title</label> - <input id="original_title" name="original_title" value="{{ film.original_title if film and film.original_title else '' }}"> - </div> + <div class="field span-2"> + <label for="original_title">Original title</label> + <input id="original_title" name="original_title" value="{{ film.original_title if film and film.original_title else '' }}"> + </div> - <div class="field"> - <label for="director">Director</label> - <input id="director" name="director" value="{{ film.director if film and film.director else '' }}"> - </div> + <div class="field"> + <label for="director">Director</label> + <input id="director" name="director" value="{{ film.director if film and film.director else '' }}"> + </div> - <div class="field"> - <label for="year">Year</label> - <input id="year" name="year" inputmode="numeric" value="{{ film.year if film and film.year else '' }}"> - </div> + <div class="field"> + <label for="year">Year</label> + <input id="year" name="year" inputmode="numeric" value="{{ film.year if film and film.year else '' }}"> + </div> - <div class="field"> - <label for="country">Country</label> - <input id="country" name="country" value="{{ film.country if film and film.country else '' }}"> - </div> + <div class="field"> + <label for="country">Country</label> + <input id="country" name="country" value="{{ film.country if film and film.country else '' }}"> + </div> - <div class="field"> - <label for="genre">Genre</label> - <input id="genre" name="genre" value="{{ film.genre if film and film.genre else '' }}"> - </div> + <div class="field"> + <label for="genre">Genre</label> + <input id="genre" name="genre" value="{{ film.genre if film and film.genre else '' }}"> + </div> - <div class="field"> - <label for="language">Language</label> - <input id="language" name="language" value="{{ film.language if film and film.language else '' }}"> - </div> + <div class="field"> + <label for="language">Language</label> + <input id="language" name="language" value="{{ film.language if film and film.language else '' }}"> + </div> - <div class="field"> - <label for="runtime">Runtime</label> - <input id="runtime" name="runtime" type="number" min="0" inputmode="numeric" value="{{ film.runtime if film and film.runtime is not none else '' }}"> + <div class="field"> + <label for="runtime">Runtime</label> + <input id="runtime" name="runtime" type="number" min="0" inputmode="numeric" value="{{ film.runtime if film and film.runtime is not none else '' }}"> + </div> </div> + </section> - <div class="field"> - <label for="date_watched">Watched</label> - <input id="date_watched" name="date_watched" type="date" value="{{ film.date_watched if film and film.date_watched else '' }}" - {% if not (film and film.date_watched) %}data-default-today{% endif %}> + <section class="form-panel"> + <div class="form-panel-head"> + <p class="eyebrow">Log</p> + <h2>Watch context</h2> </div> + <div class="form-grid"> + <div class="field"> + <label for="date_watched">Watched</label> + <input id="date_watched" name="date_watched" type="date" value="{{ film.date_watched if film and film.date_watched else '' }}" + {% if not (film and film.date_watched) %}data-default-today{% endif %}> + </div> - <div class="field"> - <label for="shelf">Shelf</label> - {% set current_shelf = shelf_override if shelf_override else (film.shelf if film and film.shelf else 'diary') %} - <select id="shelf" name="shelf"> - <option value="diary" {% if current_shelf == 'diary' %}selected{% endif %}>Diary</option> - <option value="queue" {% if current_shelf == 'queue' %}selected{% endif %}>Queue</option> - <option value="abandoned" {% if current_shelf == 'abandoned' %}selected{% endif %}>Abandoned</option> - </select> - </div> + <div class="field"> + <label for="shelf">Shelf</label> + {% set current_shelf = shelf_override if shelf_override else (film.shelf if film and film.shelf else 'diary') %} + <select id="shelf" name="shelf"> + <option value="diary" {% if current_shelf == 'diary' %}selected{% endif %}>Diary</option> + <option value="queue" {% if current_shelf == 'queue' %}selected{% endif %}>Queue</option> + <option value="abandoned" {% if current_shelf == 'abandoned' %}selected{% endif %}>Abandoned</option> + </select> + </div> - <div class="field checkbox-field"> - <label class="check-label" for="rewatch"> - <input id="rewatch" name="rewatch" type="checkbox" value="on" {% if film and film.rewatch %}checked{% endif %}> - Rewatch - </label> - </div> + <div class="field checkbox-field"> + <label class="check-label" for="rewatch"> + <input id="rewatch" name="rewatch" type="checkbox" value="on" {% if film and film.rewatch %}checked{% endif %}> + Rewatch + </label> + </div> - <div class="field"> - <label for="rewatch_count">Rewatch count</label> - <input id="rewatch_count" name="rewatch_count" type="number" min="0" inputmode="numeric" value="{{ film.rewatch_count if film and film.rewatch_count is not none else 0 }}"> - </div> + <div class="field"> + <label for="rewatch_count">Rewatch count</label> + <input id="rewatch_count" name="rewatch_count" type="number" min="0" inputmode="numeric" value="{{ film.rewatch_count if film and film.rewatch_count is not none else 0 }}"> + </div> - <div class="field span-2"> - <label for="watched_with">Watched with</label> - <input id="watched_with" name="watched_with" placeholder="Solo, Maya, Film club" value="{{ film.watched_with if film and film.watched_with else '' }}"> - </div> + <div class="field span-2"> + <label for="watched_with">Watched with</label> + <input id="watched_with" name="watched_with" placeholder="Solo, Maya, Film club" value="{{ film.watched_with if film and film.watched_with else '' }}"> + </div> - <div class="field"> - <label for="how_found">How found</label> - <input id="how_found" name="how_found" placeholder="Recommendation, festival, streaming queue" value="{{ film.how_found if film and film.how_found else '' }}"> - </div> + <div class="field"> + <label for="how_found">How found</label> + <input id="how_found" name="how_found" placeholder="Recommendation, festival, streaming queue" value="{{ film.how_found if film and film.how_found else '' }}"> + </div> - <div class="field"> - <label for="context">Context</label> - <input id="context" name="context" placeholder="Criterion, Festival, Kiarostami deep-dive" value="{{ film.context if film and film.context else '' }}"> + <div class="field"> + <label for="context">Context</label> + <input id="context" name="context" placeholder="Criterion, Festival, Kiarostami deep-dive" value="{{ film.context if film and film.context else '' }}"> + </div> </div> + </section> - <div class="field span-2"> - <label for="poster_url">Poster URL</label> - <input id="poster_url" name="poster_url" value="{{ film.poster_url if film and film.poster_url else '' }}"> + <section class="form-panel"> + <div class="form-panel-head"> + <p class="eyebrow">Archive</p> + <h2>Poster and notes</h2> </div> + <div class="form-grid form-grid-archive"> + <div class="field span-2"> + <label for="poster_url">Poster URL</label> + <input id="poster_url" name="poster_url" value="{{ film.poster_url if film and film.poster_url else '' }}"> + </div> - <div class="poster-preview-field"> - <div class="poster-frame poster-preview"> - {% if film and film.poster_url %} - <img id="poster-preview" src="{{ film.poster_url }}" alt="Poster preview"> - {% else %} - <img id="poster-preview" alt="Poster preview"> - {% endif %} + <div class="poster-preview-field"> + <div class="poster-frame poster-preview"> + {% if film and film.poster_url %} + <img id="poster-preview" src="{{ film.poster_url }}" alt="Poster preview"> + {% else %} + <img id="poster-preview" alt="Poster preview"> + {% endif %} + </div> </div> - </div> - <div class="field notes-field"> - <label for="notes">Notes</label> - <textarea id="notes" name="notes" rows="10">{{ film.notes if film and film.notes else '' }}</textarea> + <div class="field notes-field"> + <label for="notes">Notes</label> + <textarea id="notes" name="notes" rows="10">{{ film.notes if film and film.notes else '' }}</textarea> + </div> </div> - </div> + </section> <div class="form-actions"> <a href="{{ '/films/' ~ film.id if film and film.id else '/' }}">Cancel</a> diff --git a/templates/index.html b/templates/index.html index dc76e5d..7e74c07 100644 --- a/templates/index.html +++ b/templates/index.html @@ -3,35 +3,44 @@ {% block title %}{{ shelf_meta.title }} · Lumière{% endblock %} {% block content %} - <section class="page-heading"> - <p class="eyebrow">{{ shelf_meta.eyebrow }}</p> - <div class="page-heading-row"> + <section class="shelf-hero"> + <div class="shelf-hero-copy"> + <p class="eyebrow">{{ shelf_meta.eyebrow }}</p> <h1>{{ shelf_meta.title }}</h1> + <p class="shelf-hero-text">{{ shelf_meta.empty_text }}</p> + </div> + <div class="shelf-hero-meta"> + <div class="shelf-stat"> + <span class="summary-label">Entries</span> + <strong>{{ total_films or 0 }}</strong> + </div> {% if active_shelf == 'queue' %} <a class="button-link" href="/queue/random">Surprise me</a> {% endif %} </div> </section> - <div class="search-row" style="margin-bottom: 20px;"> - <input - type="search" - id="film-search" - placeholder="Search by title or director…" - autocomplete="off" - > - <select id="film-sort"> - <option value="">Default order</option> - <option value="date_watched_desc">Date watched — newest</option> - <option value="date_watched_asc">Date watched — oldest</option> - <option value="title_asc">Title — A → Z</option> - <option value="title_desc">Title — Z → A</option> - <option value="year_desc">Year — newest</option> - <option value="year_asc">Year — oldest</option> - <option value="stars_desc">Stars — highest</option> - <option value="stars_asc">Stars — lowest</option> - </select> - </div> + <section class="feed-toolbar"> + <div class="search-row"> + <input + type="search" + id="film-search" + placeholder="Search by title or director…" + autocomplete="off" + > + <select id="film-sort"> + <option value="">Default order</option> + <option value="date_watched_desc">Date watched — newest</option> + <option value="date_watched_asc">Date watched — oldest</option> + <option value="title_asc">Title — A → Z</option> + <option value="title_desc">Title — Z → A</option> + <option value="year_desc">Year — newest</option> + <option value="year_asc">Year — oldest</option> + <option value="stars_desc">Stars — highest</option> + <option value="stars_asc">Stars — lowest</option> + </select> + </div> + </section> {% if imported is not none %} <div class="notice">{{ imported }} entries imported.</div> @@ -62,10 +71,14 @@ {% if active_shelf == 'diary' and grouped_films %} {% for group in grouped_films %} <div class="month-group" data-month="{{ group.month }}"> - <p class="month-label">{{ group.month }}</p> - {% for film in group.films %} - {% include "_film_card.html" %} - {% endfor %} + <div class="month-rail"> + <p class="month-label">{{ group.month }}</p> + </div> + <div class="month-stack"> + {% for film in group.films %} + {% include "_film_card.html" %} + {% endfor %} + </div> </div> {% endfor %} {% else %} diff --git a/templates/profile.html b/templates/profile.html index 7c847f4..bbf70a2 100644 --- a/templates/profile.html +++ b/templates/profile.html @@ -3,199 +3,175 @@ <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> - <title>Tyler's Film Diary - Lumière</title> + <title>Tyler's Film Diary · Lumière</title> <link rel="icon" href="/static/favicon.svg" type="image/svg+xml"> <link rel="stylesheet" href="/static/styles.css" /> </head> - <body> - <div class="shell"> - <header class="topbar"> - <a class="brand" href="/">Lumière</a> - <nav class="nav-actions" aria-label="Primary"> - <a href="/tyler">Profile</a> + <body class="public-profile-page"> + <div class="public-shell"> + <header class="public-topbar"> + <a class="brand" href="/tyler">Lumière</a> + <nav class="public-nav" aria-label="Primary"> + <a class="is-active" href="/tyler">Profile</a> <a href="/about">About</a> + <a href="/login">Private Log In</a> </nav> </header> - <main> - <div style="max-width: 1200px; margin: 0 auto; padding: 40px 20px;"> - <!-- Hero Section --> - <div style="margin-bottom: 40px; padding-bottom: 24px; border-bottom: 1px solid var(--line);"> - <h1 style="margin: 0 0 8px 0; font-size: 40px; color: var(--text);">Tyler's Film Diary</h1> - <p style="margin: 0 0 24px 0; color: var(--muted); font-size: 18px;">A curated collection of films watched and loved</p> - - <!-- Tabs --> - <div style="display: flex; gap: 24px; border-bottom: 1px solid var(--line); padding-bottom: 16px;"> - <button id="tab-overview" class="profile-tab is-active" data-tab="overview" style="background: none; border: none; color: var(--text); cursor: pointer; font-size: 16px; padding: 0; border-bottom: 2px solid var(--accent);">Overview</button> - <button id="tab-films" class="profile-tab" data-tab="films" style="background: none; border: none; color: var(--muted); cursor: pointer; font-size: 16px; padding: 0; border-bottom: 2px solid transparent;">All Films</button> + <main class="public-main"> + <section class="public-hero"> + <div class="public-hero-copy"> + <p class="eyebrow">Public Ledger</p> + <h1>Tyler's Film Diary</h1> + <p class="public-hero-text">A public cut of the diary: recent watches, recurring directors, and the films that keep returning.</p> + </div> + <div class="public-hero-metrics"> + <div class="public-metric-card"> + <span class="summary-label">Watched</span> + <strong>{{ total_watched }}</strong> + </div> + <div class="public-metric-card"> + <span class="summary-label">Average</span> + <strong>{{ average_stars }}</strong> </div> </div> + </section> - <!-- Overview Tab --> - <div id="panel-overview" class="tab-panel is-active" style="display: block;"> - <!-- Hero Section Stats --> - <div style="margin-bottom: 60px;"> + <section class="public-tabs"> + <button id="tab-overview" class="public-tab is-active" data-tab="overview" type="button">Overview</button> + <button id="tab-films" class="public-tab" data-tab="films" type="button">All Films</button> + </section> - - <!-- Summary Stats --> - <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 24px; margin-top: 32px;"> - <div style="padding: 20px; background: var(--panel); border: 1px solid var(--line); border-radius: 8px;"> - <div style="font-size: 32px; font-weight: bold; color: var(--text);">{{ total_watched }}</div> - <div style="color: var(--muted); margin-top: 4px;">Films Watched</div> + <section id="panel-overview" class="public-panel is-active"> + <div class="public-grid"> + <section class="public-section"> + <div class="public-section-head"> + <p class="eyebrow">Rotation</p> + <h2>Most watched directors</h2> </div> - <div style="padding: 20px; background: var(--panel); border: 1px solid var(--line); border-radius: 8px;"> - <div style="font-size: 32px; font-weight: bold; color: var(--text);"><span class="rating">{% for _ in range(average_stars|int) %}✦{% endfor %}</span> {{ average_stars }}</div> - <div style="color: var(--muted); margin-top: 4px;">Average Rating</div> + <div class="public-list"> + {% for item in most_watched_directors %} + <div class="public-list-row"> + <span>{{ item.director }}</span> + <strong>{{ item.count }}</strong> + </div> + {% endfor %} </div> - </div> - </div> + </section> - <!-- Top Directors --> - {% if most_watched_directors %} - <div style="margin-bottom: 60px;"> - <h2 style="margin: 0 0 24px 0; font-size: 24px; color: var(--text);">Top Directors</h2> - <div style="display: grid; gap: 12px;"> - {% for item in most_watched_directors %} - <div style="padding: 12px 16px; background: var(--panel); border: 1px solid var(--line); border-radius: 6px; display: flex; justify-content: space-between; align-items: center;"> - <span style="color: var(--text);">{{ item.director }}</span> - <span style="color: var(--muted); font-size: 14px;">{{ item.count }} film{{ 's' if item.count > 1 else '' }}</span> + <section class="public-section"> + <div class="public-section-head"> + <p class="eyebrow">Distribution</p> + <h2>Ratings</h2> </div> - {% endfor %} - </div> - </div> - {% endif %} - - <!-- Star Distribution --> - <div style="margin-bottom: 60px;"> - <h2 style="margin: 0 0 24px 0; font-size: 24px; color: var(--text);">Rating Distribution</h2> - <div style="display: grid; gap: 16px;"> - {% for item in star_distribution %} - <div> - <div style="display: flex; justify-content: space-between; margin-bottom: 8px;"> - <span style="color: var(--text);"> - {% if item.stars == 0 %}No rating{% elif item.stars == 1 %}<span class="rating">✦</span>{% elif item.stars == 2 %}<span class="rating">✦✦</span>{% elif item.stars == 3 %}<span class="rating">✦✦✦</span>{% endif %} + <div class="public-bars"> + {% for item in star_distribution %} + <div class="public-bar-row"> + <span> + {% if item.stars == 0 %}Unrated{% else %}{% for _ in range(item.stars) %}✦{% endfor %}{% endif %} </span> - <span style="color: var(--muted);">{{ item.count }}</span> - </div> - <div style="background: var(--panel-soft); height: 24px; border-radius: 4px; overflow: hidden;"> - {% if total_watched > 0 %} - <div style="background: var(--accent); height: 100%; width: {{ (item.count / total_watched * 100) }}%; border-radius: 4px;"></div> - {% endif %} + <div class="public-bar-track"> + {% if total_watched > 0 %} + <div class="public-bar-fill" style="width: {{ (item.count / total_watched * 100) }}%;"></div> + {% endif %} + </div> + <strong>{{ item.count }}</strong> </div> + {% endfor %} </div> - {% endfor %} - </div> - </div> + </section> - <!-- Top Countries --> - {% if films_per_country %} - <div style="margin-bottom: 60px;"> - <h2 style="margin: 0 0 24px 0; font-size: 24px; color: var(--text);">Top Countries</h2> - <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 16px;"> - {% for item in films_per_country %} - <div style="padding: 16px; background: var(--panel); border: 1px solid var(--line); border-radius: 6px; text-align: center;"> - <div style="font-size: 20px; font-weight: bold; color: var(--text);">{{ item.count }}</div> - <div style="color: var(--muted); margin-top: 4px; font-size: 14px;">{{ item.country }}</div> + {% if films_per_country %} + <section class="public-section public-section-wide"> + <div class="public-section-head"> + <p class="eyebrow">Geography</p> + <h2>Most watched countries</h2> </div> - {% endfor %} - </div> - </div> - {% endif %} - - <!-- Recent Films --> - {% if recent_films %} - <div> - <h2 style="margin: 0 0 24px 0; font-size: 24px; color: var(--text);">Recently Watched</h2> - <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 24px;"> - {% for film in recent_films %} - <div style="text-align: center;"> - {% if film.poster_url %} - <img src="{{ film.poster_url }}" alt="{{ film.title }}" loading="lazy" style="width: 100%; border-radius: 8px; margin-bottom: 12px; aspect-ratio: 2/3; object-fit: cover;" /> - {% else %} - <div style="width: 100%; aspect-ratio: 2/3; background: var(--panel); border: 1px solid var(--line); border-radius: 8px; margin-bottom: 12px; display: flex; align-items: center; justify-content: center; color: var(--muted);">No poster</div> - {% endif %} - <h3 style="margin: 0; font-size: 14px; line-height: 1.4; color: var(--text);">{{ film.title }}</h3> - {% if film.stars %} - <div style="color: var(--accent); font-size: 12px; margin-top: 4px; font-weight: 800;"> - <span class="rating">{% for i in range(film.stars) %}✦{% endfor %}</span> + <div class="public-country-grid"> + {% for item in films_per_country %} + <div class="public-country-card"> + <strong>{{ item.count }}</strong> + <span>{{ item.country }}</span> </div> - {% endif %} - {% if film.date_watched %} - <div style="color: var(--muted); font-size: 12px; margin-top: 4px;">{{ film.date_watched[:10] }}</div> - {% endif %} + {% endfor %} </div> - {% endfor %} - </div> - </div> - {% endif %} - </div> - <!-- End Overview Tab --> + </section> + {% endif %} - <!-- All Films Tab --> - <div id="panel-films" class="tab-panel" style="display: none;"> - <div id="films-feed" style="display: grid; gap: 20px;"> - <!-- Films will be loaded here via infinite scroll --> - </div> - <div id="films-sentinel" data-shelf="diary" data-offset="0" data-total="{{ total_watched }}" style="height: 1px; margin: 20px 0;"></div> + {% if recent_films %} + <section class="public-section public-section-wide"> + <div class="public-section-head"> + <p class="eyebrow">Recent</p> + <h2>Recently watched</h2> + </div> + <div class="public-poster-grid"> + {% for film in recent_films %} + <article class="public-poster-card"> + <div class="poster-frame public-poster-frame"> + {% if film.poster_url %} + <img src="{{ film.poster_url }}" alt="{{ film.title }} poster" loading="lazy"> + {% else %} + <span>{{ film.title[:1] }}</span> + {% endif %} + </div> + <h3>{{ film.title }}</h3> + <p>{{ film.year }}{% if film.director %} · {{ film.director }}{% endif %}</p> + {% if film.date_watched %} + <span class="public-poster-meta">{{ film.date_watched[:10] }}</span> + {% endif %} + </article> + {% endfor %} + </div> + </section> + {% endif %} </div> - <!-- End All Films Tab --> - </div> + </section> + + <section id="panel-films" class="public-panel" hidden> + <div id="public-films-feed" class="public-feed"></div> + <div id="public-films-sentinel" data-offset="0" data-total="{{ total_watched }}" class="public-feed-sentinel"></div> + </section> </main> - <footer style="text-align: center; padding: 40px 20px; color: var(--muted); font-size: 12px; border-top: 1px solid var(--line); margin-top: 60px;"> - <p style="margin: 0;">Made with <a href="https://git.tylerhoang.xyz/lumi.git" style="color: var(--accent); text-decoration: none;">Lumière</a></p> + <footer class="public-footer"> + <p>Made with <a href="https://git.tylerhoang.xyz/lumi.git">Lumière</a></p> </footer> </div> <script> - // Tab switching - document.querySelectorAll(".profile-tab").forEach((tab) => { - tab.addEventListener("click", () => { - const tabName = tab.dataset.tab; - - // Update tab buttons - document.querySelectorAll(".profile-tab").forEach((t) => { - t.classList.remove("is-active"); - t.style.color = "var(--muted)"; - t.style.borderBottomColor = "transparent"; - }); - tab.classList.add("is-active"); - tab.style.color = "var(--text)"; - tab.style.borderBottomColor = "var(--accent)"; + const tabs = document.querySelectorAll(".public-tab"); + const panels = document.querySelectorAll(".public-panel"); + let filmsLoaded = false; - // Update panels - document.querySelectorAll(".tab-panel").forEach((panel) => { - panel.style.display = "none"; - }); - document.getElementById(`panel-${tabName}`).style.display = "block"; - - // Load films if switching to all films tab - if (tabName === "films" && !tab.dataset.loaded) { - loadInitialFilms(); - tab.dataset.loaded = "true"; - } + function setActiveTab(tabName) { + tabs.forEach((tab) => { + const active = tab.dataset.tab === tabName; + tab.classList.toggle("is-active", active); }); - }); + panels.forEach((panel) => { + const active = panel.id === `panel-${tabName}`; + panel.hidden = !active; + }); + } - // Load all films with infinite scroll - function loadInitialFilms() { - const sentinel = document.querySelector("#films-sentinel"); - const feed = document.querySelector("#films-feed"); + async function loadPublicFilms() { + if (filmsLoaded) return; + filmsLoaded = true; + + const sentinel = document.querySelector("#public-films-sentinel"); + const feed = document.querySelector("#public-films-feed"); let loading = false; const loadMore = async () => { if (loading) return; loading = true; - - const offset = Number(sentinel.dataset.offset); - const total = Number(sentinel.dataset.total); + const offset = Number(sentinel.dataset.offset || 0); try { - const response = await fetch(`/films/partial?shelf=diary&offset=${offset}&limit=20`); + const response = await fetch(`/tyler/films/partial?offset=${offset}&limit=20`); if (!response.ok) return; - - const hasMore = response.headers.get("X-Has-More") === "true"; const html = await response.text(); + const hasMore = response.headers.get("X-Has-More") === "true"; if (html.trim()) { feed.insertAdjacentHTML("beforeend", html); @@ -208,7 +184,7 @@ sentinel.dataset.offset = String(offset + 20); } } catch (error) { - console.error("Failed to load films", error); + console.error("Failed to load public films", error); } finally { loading = false; } @@ -223,8 +199,18 @@ }, { rootMargin: "100px" }); observer.observe(sentinel); - loadMore(); // Load first batch immediately + loadMore(); } + + tabs.forEach((tab) => { + tab.addEventListener("click", () => { + const tabName = tab.dataset.tab; + setActiveTab(tabName); + if (tabName === "films") { + loadPublicFilms(); + } + }); + }); </script> </body> </html> |
