From 4d939838d1a62e05c5734f324b3fdc041edeb7ee Mon Sep 17 00:00:00 2001 From: Nik Afiq Date: Wed, 5 Nov 2025 15:28:17 +0900 Subject: [PATCH] Added JP TL --- frontend/index.html | 2 +- frontend/src/index.css | 16 +++--- frontend/src/pages/ShowsPage.tsx | 88 +++++++++++++++++++++++--------- 3 files changed, 71 insertions(+), 35 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index 1eeac51..e061920 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,5 +1,5 @@ - + diff --git a/frontend/src/index.css b/frontend/src/index.css index 7cc45fd..0530a0e 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -4,6 +4,7 @@ --text: #e6eef8; --subtle: #9fb3c8; --accent: #79c0ff; + --header-h: 56px; } * { box-sizing: border-box; } @@ -145,7 +146,7 @@ kbd { } /* ====== Site chrome (outside card) ====== */ .site { - min-height: 100%; + min-height: 100dvh; position: relative; /* push-mode: the content will slide right when sidebar is open */ } @@ -153,11 +154,9 @@ kbd { .site-header { position: fixed; z-index: 50; - top: 16px; - left: 16px; - display: flex; - gap: 12px; - align-items: center; + top: 0; left: 16px; right: 16px; + display: flex; gap: 12px; align-items: center; + height: var(--header-h); } .burger { @@ -229,11 +228,10 @@ kbd { /* Main content wrapper; centers the card like before */ .site-content { - min-height: 100%; + min-height: calc(100dvh - var(--header-h)); display: grid; place-items: center; - padding: 24px; - transition: transform 180ms ease; + padding: calc(var(--header-h) + 12px) 24px 32px; } /* ====== Card stays as you had ====== */ diff --git a/frontend/src/pages/ShowsPage.tsx b/frontend/src/pages/ShowsPage.tsx index 0b086c5..79c3e2d 100644 --- a/frontend/src/pages/ShowsPage.tsx +++ b/frontend/src/pages/ShowsPage.tsx @@ -5,15 +5,31 @@ type Show = { ep_num: number; ep_title: string; season_name: string; - start_time: string; // "HH:MM:SS" - playback_length: string; // "HH:MM:SS" or "MM:SS" + start_time: string; + playback_length: string; date_created: string; }; const GET_URL = "/api/v1/shows"; const POST_URL = "/api/v1/current"; -const HHMMSS = /^([01]\d|2[0-3]):[0-5]\d:[0-5]\d$/; +const HHMM = /^(\d{1,2}):([0-5]\d)$/; + +function toHHMMSS(v: string): string | null { + const m = v.trim().match(HHMM); + if (!m) return null; + const h = Math.abs(parseInt(m[1], 10)); + if (h > 23) return null; + const hh = String(h).padStart(2, "0"); + const mm = m[2]; + return `${hh}:${mm}:00`; +} +function toHHMM(v: string): string | null { + const m = v.trim().match(/^(\d{1,2}):([0-5]\d)$/); + if (!m) return null; + const h = Number(m[1]); if (h > 23) return null; + return `${String(h).padStart(2, "0")}:${m[2]}`; +} export default function ShowsPage() { const [shows, setShows] = useState([]); @@ -21,10 +37,11 @@ export default function ShowsPage() { const [posting, setPosting] = useState(false); const [error, setError] = useState(null); - // form state + // フォーム状態 const [selectedId, setSelectedId] = useState(null); const [startTime, setStartTime] = useState(""); + // 一覧取得 useEffect(() => { let cancelled = false; (async () => { @@ -35,7 +52,7 @@ export default function ShowsPage() { const data = (await res.json()) as Show[]; if (!cancelled) setShows(data); } catch (e: any) { - if (!cancelled) setError(e.message || "Failed to load shows"); + if (!cancelled) setError(e.message || "番組一覧の取得に失敗しました。"); } finally { if (!cancelled) setLoading(false); } @@ -47,23 +64,27 @@ export default function ShowsPage() { async function submit() { setError(null); - if (!selectedId) { setError("Pick an episode first"); return; } - if (startTime && !HHMMSS.test(startTime)) { setError("Start time must be HH:MM:SS"); return; } + if (!selectedId) { setError("エピソードを選択してください。"); return; } + + let payload: any = { id: selectedId }; + + if (startTime) { + const normalized = toHHMMSS(startTime); + if (!normalized) { setError("開始時刻は HH:MM の形式で入力してください。"); return; } + payload.start_time = normalized; // API expects HH:MM:SS + } try { setPosting(true); - const body: any = { id: selectedId }; - if (startTime) body.start_time = startTime; const res = await fetch(POST_URL, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body), + body: JSON.stringify(payload), }); - if (!res.ok) throw new Error(`POST failed (${res.status})`); - // success UX + if (!res.ok) throw new Error(`POST 失敗 (${res.status})`); setStartTime(""); } catch (e: any) { - setError(e.message || "Failed to set current"); + setError(e.message || "現在のエピソード設定に失敗しました。"); } finally { setPosting(false); } @@ -71,13 +92,15 @@ export default function ShowsPage() { return (
-

Shows

-

Pick an episode, set optional start time (HH:MM:SS), then “Set current”.

+

エピソード一覧

+

+ エピソードを選択し、必要であれば開始時刻(HH:MM)を入力して「現在のエピソードに設定」を押してください。 +

- {loading &&
Loading…
} + {loading &&
読み込み中…
} {error &&
{error}
} - {!loading && shows.length === 0 &&
No shows.
} + {!loading && shows.length === 0 &&
エピソードがありません。
} {shows.length > 0 && (
@@ -87,9 +110,11 @@ export default function ShowsPage() { className={`show-card ${selectedId === s.id ? "selected" : ""}`} onClick={() => setSelectedId(s.id)} > -
Ep {s.ep_num}: {s.ep_title}
+
第{s.ep_num}話:{s.ep_title}
{s.season_name}
-
Start {s.start_time} • Length {s.playback_length}
+
+ 開始 {s.start_time.slice(0,5)}・長さ {s.playback_length.slice(0,5)} +
))}
@@ -98,19 +123,32 @@ export default function ShowsPage() {
setStartTime(e.target.value.trim())} - maxLength={8} + onChange={(e) => setStartTime(e.target.value)} + onBlur={(e) => { + const v = toHHMM(e.target.value); + if (v) setStartTime(v); + }} /> -
{current && (
- Selected: Ep {current.ep_num} — {current.ep_title} + 選択中:第{current.ep_num}話「{current.ep_title}」
)}