summaryrefslogtreecommitdiff
path: root/static
diff options
context:
space:
mode:
Diffstat (limited to 'static')
-rw-r--r--static/app.js78
-rw-r--r--static/styles.css221
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;
}