summaryrefslogtreecommitdiff
path: root/templates
diff options
context:
space:
mode:
authorTyler Hoang <tyler@tylerhoang.xyz>2026-05-15 01:50:15 -0700
committerTyler Hoang <tyler@tylerhoang.xyz>2026-05-15 01:50:15 -0700
commit20c1d02b40bcb9abb5882d0503e596c82e9819bb (patch)
treea2e8918b7f742b23a580daff7f844c576f31c33d /templates
parent19f2fc05387ed0d7ad5f5fcb2fa92573ece1eae0 (diff)
Refine Lumi stats and detail UX
Diffstat (limited to 'templates')
-rw-r--r--templates/detail.html82
-rw-r--r--templates/index.html10
-rw-r--r--templates/stats.html223
3 files changed, 206 insertions, 109 deletions
diff --git a/templates/detail.html b/templates/detail.html
index f51372d..94612d7 100644
--- a/templates/detail.html
+++ b/templates/detail.html
@@ -27,7 +27,7 @@
<section class="detail-body">
<section class="detail-hero">
<div class="detail-hero-copy">
- <p class="eyebrow">{{ film.shelf|title }} Entry</p>
+ <p class="eyebrow"><a class="detail-backlink" href="{{ shelf_meta.path }}">Back to {{ film.shelf|title }}</a></p>
<h1>{{ film.title }}</h1>
{% if film.original_title %}
<p class="original-title">{{ film.original_title }}</p>
@@ -66,31 +66,51 @@
</div>
{% endif %}
</div>
+ <div class="ledger-strip detail-ledger">
+ <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 %}
+ {% if film.stars %}<span class="rating">{% for _ in range(film.stars) %}✦{% endfor %}</span>{% 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>
+ <div class="detail-actions-primary">
+ {% if film.shelf == 'queue' %}
+ <a class="button-link" href="/films/{{ film.id }}/edit?shelf=diary">Mark watched</a>
+ {% elif film.shelf == 'abandoned' %}
+ <a class="button-link" href="/films/{{ film.id }}/edit?shelf=diary">Mark watched</a>
+ {% else %}
+ <a class="button-link" href="/films/{{ film.id }}/edit">Edit entry</a>
+ {% endif %}
+ </div>
+ <div class="detail-actions-secondary">
+ {% if film.shelf != 'diary' %}
+ <a class="secondary-button" href="/films/{{ film.id }}/edit">Edit</a>
+ {% endif %}
+ {% if film.shelf == 'queue' %}
+ <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 %}
+ <form method="post" action="/films/{{ film.id }}/shelf/queue">
+ <button class="secondary-button" type="submit">Move to queue</button>
+ </form>
+ {% endif %}
+ </div>
+ <div class="detail-actions-danger">
+ <form method="post" action="/films/{{ film.id }}/delete">
+ <button class="danger-button" type="submit">Delete</button>
</form>
- {% endif %}
- <form method="post" action="/films/{{ film.id }}/delete">
- <button class="danger-button" type="submit">Delete</button>
- </form>
+ </div>
</div>
</section>
@@ -191,6 +211,15 @@
</aside>
<div class="detail-main">
+ <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 %}
+ </section>
+
{% if tmdb_context %}
<section class="detail-panel detail-panel-feature">
<p class="eyebrow">Summary</p>
@@ -209,15 +238,6 @@
</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 %}
- </section>
-
{% if rewatch_history|length > 1 %}
<section class="detail-panel">
<p class="eyebrow">Rewatches</p>
diff --git a/templates/index.html b/templates/index.html
index 7e74c07..5820cf4 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -10,9 +10,13 @@
<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 class="shelf-hero-stats">
+ {% for stat in shelf_snapshot %}
+ <div class="shelf-stat">
+ <span class="summary-label">{{ stat.label }}</span>
+ <strong>{{ stat.value }}</strong>
+ </div>
+ {% endfor %}
</div>
{% if active_shelf == 'queue' %}
<a class="button-link" href="/queue/random">Surprise me</a>
diff --git a/templates/stats.html b/templates/stats.html
index 6e3a987..efb952c 100644
--- a/templates/stats.html
+++ b/templates/stats.html
@@ -3,113 +3,146 @@
{% block title %}Stats · Lumière{% endblock %}
{% block content %}
- <section class="page-heading">
- <div class="page-heading-row">
- <div>
- <p class="eyebrow">Stats</p>
- <h1>Watching patterns</h1>
- </div>
+ <section class="stats-hero">
+ <div class="stats-hero-copy">
+ <p class="eyebrow">Stats</p>
+ <h1>Watching patterns</h1>
+ <p class="stats-hero-text">A research spread for your diary: the geography of what you watch, the people and genres that recur, and the rhythms that define a year of logging.</p>
+ </div>
+ <div class="stats-hero-actions">
<a class="button-link" href="/stats/year-in-review">Year in review</a>
</div>
</section>
- <section class="stats-layout">
- <section class="stats-panel stats-panel-wide">
- <div class="stats-overview-row">
- <div class="stats-metric">
- <span class="eyebrow">Films watched</span>
- <strong id="stats-total-films">—</strong>
- </div>
- <div class="stats-metric">
- <span class="eyebrow">Total runtime</span>
- <strong id="stats-total-runtime">—</strong>
- </div>
- </div>
- </section>
+ <section class="stats-summary">
+ <article class="stats-summary-card">
+ <span class="summary-label">Films watched</span>
+ <strong id="stats-total-films">—</strong>
+ </article>
+ <article class="stats-summary-card">
+ <span class="summary-label">Total runtime</span>
+ <strong id="stats-total-runtime">—</strong>
+ </article>
+ <article class="stats-summary-card">
+ <span class="summary-label">Average stars</span>
+ <strong><span id="stats-average-stars">—</span><span class="stats-average-mark" aria-hidden="true">✦</span></strong>
+ </article>
+ <article class="stats-summary-card">
+ <span class="summary-label">Rewatch rate</span>
+ <strong id="stats-rewatch-rate">—</strong>
+ </article>
+ </section>
+
+ <section class="stats-context-strip">
+ <article class="stats-context-card">
+ <span class="summary-label">Top director</span>
+ <strong id="stats-top-director">—</strong>
+ <p id="stats-top-director-meta" class="stats-context-meta">No repeat viewings yet.</p>
+ </article>
+ <article class="stats-context-card">
+ <span class="summary-label">Most active month</span>
+ <strong id="stats-top-month">—</strong>
+ <p id="stats-top-month-meta" class="stats-context-meta">No monthly cluster yet.</p>
+ </article>
+ <article class="stats-context-card">
+ <span class="summary-label">Countries logged</span>
+ <strong id="stats-country-count">—</strong>
+ <p class="stats-context-meta">Based on films with country metadata.</p>
+ </article>
+ </section>
- <section class="stats-panel stats-panel-wide">
+ <section class="stats-research">
+ <section class="stats-panel stats-panel-feature">
<div class="stats-panel-header">
<div>
- <p class="eyebrow">World Map</p>
- <h2>Films per country</h2>
+ <p class="eyebrow">World map</p>
+ <h2>Films by country</h2>
</div>
+ <p class="stats-panel-note">The map functions as a reading aid. Hover to inspect where your diary clusters geographically.</p>
</div>
- <div id="world-map" class="stats-map" aria-label="World map of watched films"></div>
- <div id="map-readout" class="map-readout" aria-live="polite">
- <strong>Hover a country</strong>
- <span>Film count will appear here.</span>
+ <div class="stats-map-shell">
+ <div id="world-map" class="stats-map" aria-label="World map of watched films"></div>
+ <div id="map-readout" class="map-readout" aria-live="polite">
+ <strong>Hover a country</strong>
+ <span>Film count will appear here.</span>
+ </div>
</div>
<div id="map-tooltip" class="stats-tooltip" hidden></div>
</section>
- <section class="stats-panel stats-panel-wide">
+ <section class="stats-panel stats-panel-feature">
<div class="stats-panel-header">
<div>
- <p class="eyebrow">Heatmap</p>
+ <p class="eyebrow">Calendar</p>
<h2>Past 365 days</h2>
</div>
+ <p class="stats-panel-note">A density view of your diary cadence over the trailing year.</p>
</div>
<div id="watch-heatmap" class="heatmap-shell" aria-label="Daily watch heatmap"></div>
</section>
- <section class="stats-panel">
- <div class="stats-panel-header">
- <div>
- <p class="eyebrow">Directors</p>
- <h2>Most watched</h2>
+ <section class="stats-column">
+ <section class="stats-panel">
+ <div class="stats-panel-header">
+ <div>
+ <p class="eyebrow">Directors</p>
+ <h2>Most watched</h2>
+ </div>
</div>
- </div>
- <ol id="top-directors" class="stats-list"></ol>
- </section>
+ <ol id="top-directors" class="stats-list"></ol>
+ </section>
- <section class="stats-panel">
- <div class="stats-panel-header">
- <div>
- <p class="eyebrow">Stars</p>
- <h2>Distribution</h2>
+ <section class="stats-panel">
+ <div class="stats-panel-header">
+ <div>
+ <p class="eyebrow">Genres</p>
+ <h2>Most watched</h2>
+ </div>
</div>
- </div>
- <div id="star-distribution" class="stats-bars"></div>
- </section>
+ <ol id="top-genres" class="stats-list"></ol>
+ </section>
- <section class="stats-panel">
- <div class="stats-panel-header">
- <div>
- <p class="eyebrow">Films</p>
- <h2>Rewatches</h2>
+ <section class="stats-panel">
+ <div class="stats-panel-header">
+ <div>
+ <p class="eyebrow">Companions</p>
+ <h2>Watched with</h2>
+ </div>
</div>
- </div>
- <div id="rewatch-patterns-list" class="stats-list"></div>
+ <ol id="watched-with" class="stats-list"></ol>
+ </section>
</section>
- <section class="stats-panel">
- <div class="stats-panel-header">
- <div>
- <p class="eyebrow">Companions</p>
- <h2>Watched with</h2>
+ <section class="stats-column">
+ <section class="stats-panel">
+ <div class="stats-panel-header">
+ <div>
+ <p class="eyebrow">Stars</p>
+ <h2>Distribution</h2>
+ </div>
</div>
- </div>
- <ol id="watched-with" class="stats-list"></ol>
- </section>
+ <div id="star-distribution" class="stats-bars"></div>
+ </section>
- <section class="stats-panel">
- <div class="stats-panel-header">
- <div>
- <p class="eyebrow">Genres</p>
- <h2>Most watched</h2>
+ <section class="stats-panel">
+ <div class="stats-panel-header">
+ <div>
+ <p class="eyebrow">Films</p>
+ <h2>Rewatches</h2>
+ </div>
</div>
- </div>
- <ol id="top-genres" class="stats-list"></ol>
- </section>
+ <div id="rewatch-patterns-list" class="stats-list"></div>
+ </section>
- <section class="stats-panel">
- <div class="stats-panel-header">
- <div>
- <p class="eyebrow">Decades</p>
- <h2>By era</h2>
+ <section class="stats-panel">
+ <div class="stats-panel-header">
+ <div>
+ <p class="eyebrow">Decades</p>
+ <h2>By era</h2>
+ </div>
</div>
- </div>
- <ol id="film-decades" class="stats-list"></ol>
+ <ol id="film-decades" class="stats-list"></ol>
+ </section>
</section>
</section>
{% endblock %}
@@ -133,6 +166,13 @@
return new Date(`${dateString}T00:00:00`).toLocaleDateString(undefined, { month: "short" });
}
+ function formatStatsMonth(monthKey) {
+ if (!monthKey) return "—";
+ const [year, month] = monthKey.split("-");
+ const date = new Date(Number(year), Number(month) - 1, 1);
+ return date.toLocaleDateString(undefined, { month: "long", year: "numeric" });
+ }
+
function formatCountryCode(value) {
return String(value).padStart(3, "0");
}
@@ -156,6 +196,25 @@
const totalRuntime = document.getElementById("stats-total-runtime");
if (totalRuntime) totalRuntime.textContent = formatRuntime(data.total_runtime_minutes);
+ const averageStars = document.getElementById("stats-average-stars");
+ if (averageStars) averageStars.textContent = data.average_stars ? data.average_stars.toFixed(1) : "—";
+
+ const rewatchRate = document.getElementById("stats-rewatch-rate");
+ if (rewatchRate) rewatchRate.textContent = data.rewatch_rate ? `${Math.round(data.rewatch_rate.rate * 100)}%` : "—";
+
+ const topDirector = document.getElementById("stats-top-director");
+ const topDirectorMeta = document.getElementById("stats-top-director-meta");
+ if (topDirector) topDirector.textContent = data.top_director ? data.top_director.director : "—";
+ if (topDirectorMeta) topDirectorMeta.textContent = data.top_director ? `${data.top_director.count} films logged` : "No repeat viewings yet.";
+
+ const topMonth = document.getElementById("stats-top-month");
+ const topMonthMeta = document.getElementById("stats-top-month-meta");
+ if (topMonth) topMonth.textContent = data.top_month ? formatStatsMonth(data.top_month.month) : "—";
+ if (topMonthMeta) topMonthMeta.textContent = data.top_month ? `${data.top_month.count} diary entries` : "No monthly cluster yet.";
+
+ const countryCount = document.getElementById("stats-country-count");
+ if (countryCount) countryCount.textContent = data.films_per_country.length;
+
const topDirectors = document.getElementById("top-directors");
topDirectors.innerHTML = data.most_watched_directors.slice(0, 8).map((item) => `
<li><span>${item.director}</span><strong>${item.count}</strong></li>
@@ -200,7 +259,7 @@
const gap = item.days_between != null
? `<span style="color:var(--subtle);font-size:13px">${item.days_between}d apart</span>`
: "";
- return `<div class="stats-list-row">
+ return `<div class="stats-list-row stats-list-row-rewatch">
<span>${item.title}</span>
<span style="display:flex;gap:12px;align-items:center">${gap}${drift}<strong>${item.watches}×</strong></span>
</div>`;
@@ -218,6 +277,19 @@
return { ...item, dateObj: date };
});
+ const containerWidth = container.clientWidth || 720;
+ const maxCell = 13;
+ const minCell = 8;
+ const weekdayWidth = containerWidth < 640 ? 20 : 34;
+ const gap = containerWidth < 640 ? 2 : 4;
+ const availableWidth = Math.max(53 * minCell, containerWidth - weekdayWidth - 8);
+ const computedCell = Math.floor((availableWidth - (52 * gap)) / 53);
+ const cellSize = Math.max(minCell, Math.min(maxCell, computedCell));
+
+ container.style.setProperty("--heatmap-cell", `${cellSize}px`);
+ container.style.setProperty("--heatmap-gap", `${gap}px`);
+ container.style.setProperty("--heatmap-weekday-width", `${weekdayWidth}px`);
+
const firstWeekStart = new Date(parsedDays[0].dateObj);
const mondayOffset = (firstWeekStart.getDay() + 6) % 7;
firstWeekStart.setDate(firstWeekStart.getDate() - mondayOffset);
@@ -343,6 +415,7 @@
renderLists(data);
renderHeatmap(data.films_per_day_365);
await renderMap(data);
+ window.addEventListener("resize", () => renderHeatmap(data.films_per_day_365));
}
bootStats();