diff options
| author | Tyler Hoang <tyler@tylerhoang.xyz> | 2026-05-12 03:15:17 -0700 |
|---|---|---|
| committer | Tyler Hoang <tyler@tylerhoang.xyz> | 2026-05-12 03:15:17 -0700 |
| commit | 4279408876268f4960c98492d3814f5475e36e38 (patch) | |
| tree | 9fc4828768534368a575c2e60d39d02de0973b79 /static | |
| parent | 61d68b339fee628c258e15c8664b6bcad2e70ab1 (diff) | |
Add stats totals, runtime summary, and duplicate detection on add form
- Stats page now shows total films watched and total runtime (formatted
as Xd Yh) in an overview panel above the world map
- /stats/data endpoint includes total_runtime_minutes in payload
- New GET /films/find endpoint returns all shelf matches for a tmdb_id
- Add film form shows an inline notice when the selected TMDB film is
already logged, with shelf name, date, and a link to the entry
- Update CLAUDE.md and README to reflect current auth, OMDb, and
router/service structure
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'static')
| -rw-r--r-- | static/app.js | 25 | ||||
| -rw-r--r-- | static/styles.css | 11 |
2 files changed, 32 insertions, 4 deletions
diff --git a/static/app.js b/static/app.js index 938cf70..04997a0 100644 --- a/static/app.js +++ b/static/app.js @@ -111,15 +111,17 @@ const applyResult = async (film) => { setValue("#tmdb_id", film.tmdb_id || ""); setPoster(film.poster_url); - // Check if this is the add form (not edit) + const duplicateNotice = document.getElementById("duplicate-notice"); + if (duplicateNotice) duplicateNotice.hidden = true; + const isAddForm = window.location.pathname.endsWith("/films/new"); if (film.tmdb_id) { try { - // Fetch full detail for genre and check for rewatches in parallel - const [detailResponse, rewatchResponse] = await Promise.all([ + const [detailResponse, rewatchResponse, findResponse] = await Promise.all([ fetch(`/tmdb/detail/${film.tmdb_id}`), isAddForm ? fetch(`/films/check-rewatch?tmdb_id=${film.tmdb_id}`) : Promise.resolve(null), + isAddForm ? fetch(`/films/find?tmdb_id=${film.tmdb_id}`) : Promise.resolve(null), ]); if (detailResponse.ok) { @@ -137,8 +139,23 @@ const applyResult = async (film) => { } } } + + if (findResponse && findResponse.ok && duplicateNotice) { + const found = await findResponse.json(); + if (found.matches && found.matches.length > 0) { + const shelfLabel = { diary: "Diary", queue: "Queue", abandoned: "Abandoned" }; + const parts = found.matches.map((m) => { + const label = shelfLabel[m.shelf] || m.shelf; + const date = m.date_watched + ? ` · ${new Date(m.date_watched + "T00:00:00").toLocaleDateString(undefined, { month: "short", year: "numeric" })}` + : ""; + return `<a href="/films/${m.id}" class="inline-link">${label}${date}</a>`; + }); + duplicateNotice.innerHTML = `Already logged — ${parts.join(", ")}`; + duplicateNotice.hidden = false; + } + } } catch (error) { - // Fail silently if detail/rewatch fetch fails console.error("Failed to fetch details", error); } } diff --git a/static/styles.css b/static/styles.css index 9f8619b..2a6cd04 100644 --- a/static/styles.css +++ b/static/styles.css @@ -687,6 +687,11 @@ textarea:focus { margin-top: 12px; } +#duplicate-notice { + margin-top: 12px; + margin-bottom: 0; +} + .tmdb-result { display: grid; grid-template-columns: 38px minmax(0, 1fr); @@ -927,6 +932,12 @@ textarea:focus { background: linear-gradient(90deg, rgba(240, 184, 77, 0.4), var(--accent)); } +.stats-overview-row { + display: flex; + gap: 40px; + flex-wrap: wrap; +} + .stats-metric { display: grid; gap: 8px; |
