Merge branch 'main' into develop

This commit is contained in:
Nik Afiq 2025-11-12 22:19:21 +09:00
commit a24b72740b

View File

@ -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<string | null>(null);
const [redirectIn, setRedirectIn] = useState<number | null>(null);
const redirectTid = useRef<number | null>(null);
const isRedirecting = redirectIn !== null;
const navigate = useNavigate();
// フォーム状態
const [selectedId, setSelectedId] = useState<number | null>(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() {
<button
key={s.id}
className={`show-card ${selectedId === s.id ? "selected" : ""}`}
onClick={() => setSelectedId(s.id)}
onClick={() => {
setSelectedId(s.id);
setStartTime(s.start_time.slice(0, 5));
}}
>
<div className="title">{s.ep_num}{s.ep_title}</div>
<div className="season subtle">{s.season_name}</div>
<div className="meta subtle">
{s.start_time.slice(0,5)} {s.playback_length.slice(0,5)}
{s.start_time.slice(0, 5)} {formatPlaybackLen(s.playback_length)}
</div>
</button>
))}
@ -139,11 +186,12 @@ export default function ShowsPage() {
disabled={
posting ||
!selectedId ||
isRedirecting ||
(!!startTime && !toHHMMSS(startTime))
}
onClick={submit}
>
{posting ? "設定中…" : "現在のエピソードに設定"}
{redirectIn !== null ? `設定しました(${redirectIn}` : posting ? "設定中…" : "現在のエピソードに設定"}
</button>
</div>