diff options
Diffstat (limited to 'static')
| -rw-r--r-- | static/app.js | 78 | ||||
| -rw-r--r-- | static/styles.css | 221 |
2 files changed, 299 insertions, 0 deletions
diff --git a/static/app.js b/static/app.js index 01b1d79..e2213f3 100644 --- a/static/app.js +++ b/static/app.js @@ -2,6 +2,37 @@ const tmdbQuery = document.querySelector("#tmdb-query"); const tmdbButton = document.querySelector("#tmdb-search"); const tmdbResults = document.querySelector("#tmdb-results"); +const syncStarControl = (control, stars) => { + control.dataset.currentStars = String(stars); + control.dataset.previewStars = ""; + control.querySelectorAll(".star-button").forEach((button) => { + const value = Number(button.dataset.stars || 0); + const active = stars >= value; + button.classList.toggle("is-active", active); + button.classList.remove("is-preview"); + button.setAttribute("aria-pressed", active ? "true" : "false"); + }); +}; + +const previewStarControl = (control, stars) => { + control.dataset.previewStars = String(stars); + control.querySelectorAll(".star-button").forEach((button) => { + const value = Number(button.dataset.stars || 0); + const previewed = stars >= value; + button.classList.toggle("is-preview", previewed); + }); +}; + +const clearStarPreview = (control) => { + control.dataset.previewStars = ""; + const stars = Number(control.dataset.currentStars || 0); + control.querySelectorAll(".star-button").forEach((button) => { + const value = Number(button.dataset.stars || 0); + button.classList.toggle("is-preview", false); + button.classList.toggle("is-active", stars >= value); + }); +}; + const setValue = (selector, value) => { const element = document.querySelector(selector); if (element && value !== null && value !== undefined) { @@ -129,3 +160,50 @@ document.querySelectorAll("form[data-confirm]").forEach((form) => { } }); }); + +document.addEventListener("click", async (event) => { + const button = event.target.closest(".star-button"); + if (!button) return; + + const control = button.closest(".star-control"); + if (!control || !control.dataset.filmId) return; + + const stars = Number(button.dataset.stars || 0); + const filmId = control.dataset.filmId; + + button.disabled = true; + try { + const response = await fetch(`/films/${filmId}/stars`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ stars }), + }); + + const data = await response.json(); + if (!response.ok) { + return; + } + + syncStarControl(control, Number(data.stars || 0)); + } catch (error) { + console.error("Failed to update stars", error); + } finally { + button.disabled = false; + } +}); + +document.querySelectorAll(".star-control").forEach((control) => { + syncStarControl(control, Number(control.dataset.currentStars || 0)); + control.addEventListener("pointerleave", () => clearStarPreview(control)); + control.addEventListener("focusout", (event) => { + if (!control.contains(event.relatedTarget)) { + clearStarPreview(control); + } + }); + control.querySelectorAll(".star-button").forEach((button) => { + button.addEventListener("pointerenter", () => previewStarControl(control, Number(button.dataset.stars || 0))); + button.addEventListener("focus", () => previewStarControl(control, Number(button.dataset.stars || 0))); + }); +}); diff --git a/static/styles.css b/static/styles.css index f7c6fbc..b9ab7dd 100644 --- a/static/styles.css +++ b/static/styles.css @@ -278,6 +278,57 @@ h2 { font-weight: 800; } +.star-control { + display: inline-flex; + align-items: center; + gap: 0; + width: auto; + margin-left: auto; +} + +.star-button { + display: inline-flex; + align-items: center; + justify-content: center; + width: 1.15em; + min-height: 0; + padding: 0; + margin: 0; + border: 0 !important; + border-radius: 0; + background: none !important; + box-shadow: none !important; + appearance: none; + color: var(--subtle); + font-size: 1.1rem; + font-weight: 700; + line-height: 1; + opacity: 0.6; +} + +.star-button.is-active { + color: var(--accent); + opacity: 1; +} + +.star-button.is-preview { + color: var(--accent-strong); + opacity: 1; +} + +.star-button:hover { + color: var(--accent-strong); + opacity: 1; + background: none !important; +} + +.star-button:focus-visible { + outline: none; + color: var(--accent-strong); + opacity: 1; + background: none !important; +} + .meta-row, .detail-meta { display: flex; @@ -380,6 +431,27 @@ h2 { width: 100%; } +.detail-poster { + display: grid; + gap: 16px; + align-content: start; +} + +.detail-aside-meta { + display: grid; + gap: 12px; + border: 1px solid var(--line); + border-radius: 8px; + background: var(--panel); + padding: 16px; +} + +.detail-aside-meta strong { + display: block; + margin-top: 4px; + color: var(--text); +} + .detail-body h1 { margin-bottom: 10px; } @@ -392,6 +464,45 @@ h2 { margin-top: 18px; } +.detail-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; + margin-top: 18px; +} + +.detail-panel { + border: 1px solid var(--line); + border-radius: 8px; + background: var(--panel); + padding: 16px; +} + +.detail-panel .eyebrow { + margin-bottom: 10px; +} + +.detail-tagline { + margin: 0 0 12px; + color: var(--accent-strong); + font-family: Georgia, "Times New Roman", serif; + font-size: 1.08rem; +} + +.detail-overview { + margin: 0; + color: #e3dacd; +} + +.detail-cast { + margin-top: 16px; +} + +.detail-cast p { + margin: 6px 0 0; + color: var(--muted); +} + .notes-body { margin-top: 28px; max-width: 68ch; @@ -731,6 +842,101 @@ textarea:focus { color: var(--muted); } +.review-hero { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 18px; + padding: 48px 0 22px; +} + +.review-intro { + max-width: 60ch; + color: var(--muted); +} + +.year-picker { + display: grid; + gap: 8px; + min-width: 180px; +} + +.year-picker label { + margin: 0; +} + +.review-metrics { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 14px; + margin-bottom: 18px; +} + +.review-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 18px; +} + +.review-panel { + border: 1px solid var(--line); + border-radius: 8px; + background: var(--panel); + padding: 18px; +} + +.review-panel-wide { + grid-column: 1 / -1; +} + +.year-bars { + display: grid; + gap: 10px; + margin-top: 10px; +} + +.year-bar-row { + display: grid; + grid-template-columns: 34px minmax(0, 1fr) auto; + gap: 12px; + align-items: center; +} + +.year-bar-track { + height: 10px; + overflow: hidden; + border-radius: 999px; + background: var(--panel-soft); +} + +.year-bar-fill { + height: 100%; + border-radius: inherit; + background: linear-gradient(90deg, rgba(240, 184, 77, 0.35), var(--accent)); +} + +.highlight-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 14px; + margin-top: 12px; +} + +.highlight-card { + display: grid; + grid-template-columns: 72px minmax(0, 1fr); + gap: 14px; + align-items: start; +} + +.highlight-card h2 { + margin-top: 0; +} + +.highlight-meta { + color: var(--subtle); +} + @media (max-width: 760px) { .shell { width: min(100% - 24px, 1120px); @@ -783,6 +989,21 @@ textarea:focus { flex-direction: column; } + .review-hero { + align-items: flex-start; + flex-direction: column; + } + + .review-metrics, + .review-grid { + grid-template-columns: 1fr; + } + + .highlight-card { + grid-template-columns: 1fr; + align-items: start; + } + .heatmap-months { margin-left: 38px; } |
