summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTyler Hoang <tyler@tylerhoang.xyz>2026-05-14 01:31:34 -0700
committerTyler Hoang <tyler@tylerhoang.xyz>2026-05-14 01:31:34 -0700
commitcdd28b6c0a3a6444b9ae79b123fe4fa7401de5ab (patch)
tree6e04f0ca2d05825d98a1c78b56eb7c30752b4657
parent4279408876268f4960c98492d3814f5475e36e38 (diff)
Refine Lumi layouts and public profile
-rw-r--r--main.py2
-rw-r--r--routers/profile.py25
-rw-r--r--static/app.js5
-rw-r--r--static/fonts/EBGaramond-Italic-VariableFont_wght.ttfbin0 -> 811012 bytes
-rw-r--r--static/fonts/EBGaramond-VariableFont_wght.ttfbin0 -> 934420 bytes
-rw-r--r--static/fonts/IBMPlexMono-Medium.ttfbin0 -> 134956 bytes
-rw-r--r--static/fonts/IBMPlexMono-Regular.ttfbin0 -> 133796 bytes
-rw-r--r--static/fonts/IBMPlexMono-SemiBold.ttfbin0 -> 138448 bytes
-rw-r--r--static/fonts/IBMPlexSans-Italic-VariableFont_wdth,wght.ttfbin0 -> 594116 bytes
-rw-r--r--static/fonts/IBMPlexSans-VariableFont_wdth,wght.ttfbin0 -> 532740 bytes
-rw-r--r--static/styles.css1258
-rw-r--r--templates/_feed_partial.html12
-rw-r--r--templates/_film_card.html112
-rw-r--r--templates/_public_feed_partial.html71
-rw-r--r--templates/base.html66
-rw-r--r--templates/detail.html455
-rw-r--r--templates/form.html226
-rw-r--r--templates/index.html65
-rw-r--r--templates/profile.html292
19 files changed, 2002 insertions, 587 deletions
diff --git a/main.py b/main.py
index 6b1a06b..16be52f 100644
--- a/main.py
+++ b/main.py
@@ -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
new file mode 100644
index 0000000..9cb1376
--- /dev/null
+++ b/static/fonts/EBGaramond-Italic-VariableFont_wght.ttf
Binary files differ
diff --git a/static/fonts/EBGaramond-VariableFont_wght.ttf b/static/fonts/EBGaramond-VariableFont_wght.ttf
new file mode 100644
index 0000000..baf64b2
--- /dev/null
+++ b/static/fonts/EBGaramond-VariableFont_wght.ttf
Binary files differ
diff --git a/static/fonts/IBMPlexMono-Medium.ttf b/static/fonts/IBMPlexMono-Medium.ttf
new file mode 100644
index 0000000..8253c5f
--- /dev/null
+++ b/static/fonts/IBMPlexMono-Medium.ttf
Binary files differ
diff --git a/static/fonts/IBMPlexMono-Regular.ttf b/static/fonts/IBMPlexMono-Regular.ttf
new file mode 100644
index 0000000..601ae94
--- /dev/null
+++ b/static/fonts/IBMPlexMono-Regular.ttf
Binary files differ
diff --git a/static/fonts/IBMPlexMono-SemiBold.ttf b/static/fonts/IBMPlexMono-SemiBold.ttf
new file mode 100644
index 0000000..5e0b41d
--- /dev/null
+++ b/static/fonts/IBMPlexMono-SemiBold.ttf
Binary files differ
diff --git a/static/fonts/IBMPlexSans-Italic-VariableFont_wdth,wght.ttf b/static/fonts/IBMPlexSans-Italic-VariableFont_wdth,wght.ttf
new file mode 100644
index 0000000..6232aaa
--- /dev/null
+++ b/static/fonts/IBMPlexSans-Italic-VariableFont_wdth,wght.ttf
Binary files differ
diff --git a/static/fonts/IBMPlexSans-VariableFont_wdth,wght.ttf b/static/fonts/IBMPlexSans-VariableFont_wdth,wght.ttf
new file mode 100644
index 0000000..9add875
--- /dev/null
+++ b/static/fonts/IBMPlexSans-VariableFont_wdth,wght.ttf
Binary files differ
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>