diff --git a/frontend/src/pages/ShowsPage.tsx b/frontend/src/pages/ShowsPage.tsx index 83e5c39..f551934 100644 --- a/frontend/src/pages/ShowsPage.tsx +++ b/frontend/src/pages/ShowsPage.tsx @@ -1,4 +1,5 @@ -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useState, useRef } from "react"; +import { useNavigate } from "react-router-dom"; import { API_ENDPOINT } from "../api/endpoint"; type Show = { @@ -13,6 +14,7 @@ type Show = { const GET_URL = API_ENDPOINT.v1.SHOWS; const POST_URL = API_ENDPOINT.v1.CURRENT; +const REDIRECT_DELAY_S = 3; const HHMM = /^(\d{1,2}):([0-5]\d)$/; @@ -37,11 +39,34 @@ export default function ShowsPage() { const [loading, setLoading] = useState(true); const [posting, setPosting] = useState(false); const [error, setError] = useState(null); + const [redirectIn, setRedirectIn] = useState(null); + const redirectTid = useRef(null); + const isRedirecting = redirectIn !== null; + const navigate = useNavigate(); // フォーム状態 const [selectedId, setSelectedId] = useState(null); const [startTime, setStartTime] = useState(""); + function formatPlaybackLen(v: string): string { + const parts = v.split(":").map(Number); + if (parts.length === 2) { + // already MM:SS + const [mm, ss] = parts; + return `${String(mm).padStart(2, "0")}:${String(ss).padStart(2, "0")}`; + } + if (parts.length === 3) { + const [hh, mm, ss] = parts; + if (hh <= 0) { + // show MM:SS when hours are zero + return `${String(mm).padStart(2, "0")}:${String(ss).padStart(2, "0")}`; + } + // show HH:MM:SS when hours exist + return `${String(hh).padStart(2, "0")}:${String(mm).padStart(2, "0")}:${String(ss).padStart(2, "0")}`; + } + return v; // fallback (unexpected format) + } + // 一覧取得 useEffect(() => { let cancelled = false; @@ -51,7 +76,8 @@ export default function ShowsPage() { const res = await fetch(GET_URL, { cache: "no-store" }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = (await res.json()) as Show[]; - if (!cancelled) setShows(data); + data.sort((a, b) => a.id - b.id); // ASC + setShows(data); } catch (e: any) { if (!cancelled) setError(e.message || "番組一覧の取得に失敗しました。"); } finally { @@ -60,6 +86,13 @@ export default function ShowsPage() { })(); return () => { cancelled = true; }; }, []); + useEffect(() => { + return () => { + if (redirectTid.current) { + window.clearInterval(redirectTid.current); + } + }; + }, []); const current = useMemo(() => shows.find(s => s.id === selectedId) || null, [shows, selectedId]); @@ -67,12 +100,17 @@ export default function ShowsPage() { setError(null); if (!selectedId) { setError("エピソードを選択してください。"); return; } - let payload: any = { id: selectedId }; + const selected = shows.find(s => s.id === selectedId); + if (!selected) { setError("選択中のエピソードが見つかりません。"); return; } - if (startTime) { + const payload: any = { id: selectedId }; + + if (startTime.trim()) { const normalized = toHHMMSS(startTime); if (!normalized) { setError("開始時刻は HH:MM の形式で入力してください。"); return; } - payload.start_time = normalized; // API expects HH:MM:SS + payload.start_time = normalized; + } else { + payload.start_time = selected.start_time; } try { @@ -83,7 +121,13 @@ export default function ShowsPage() { body: JSON.stringify(payload), }); if (!res.ok) throw new Error(`POST 失敗 (${res.status})`); - setStartTime(""); + setRedirectIn(REDIRECT_DELAY_S); + const start = Date.now(); + redirectTid.current = window.setInterval(() => { + const left = REDIRECT_DELAY_S - Math.floor((Date.now() - start) / 1000); + if (left <= 0) { window.clearInterval(redirectTid.current!); navigate("/", { replace: true }); } + else setRedirectIn(left); + }, 250); } catch (e: any) { setError(e.message || "現在のエピソード設定に失敗しました。"); } finally { @@ -109,12 +153,15 @@ export default function ShowsPage() { ))} @@ -139,11 +186,12 @@ export default function ShowsPage() { disabled={ posting || !selectedId || + isRedirecting || (!!startTime && !toHHMMSS(startTime)) } onClick={submit} > - {posting ? "設定中…" : "現在のエピソードに設定"} + {redirectIn !== null ? `設定しました(${redirectIn})` : posting ? "設定中…" : "現在のエピソードに設定"}