diff --git a/frontend/src/api/watchparty.ts b/frontend/src/api/watchparty.ts index a3de934..e00e53f 100644 --- a/frontend/src/api/watchparty.ts +++ b/frontend/src/api/watchparty.ts @@ -164,6 +164,22 @@ export async function createShow(payload: { }); } +export async function deleteArchiveShow(id: number, idToken: string) { + if (!idToken) { + throw new ApiError("Missing auth token for delete"); + } + const url = `${API_ENDPOINT.v1.SHOWS}?id=${encodeURIComponent(id)}`; + await apiFetch(url, { + method: "DELETE", + headers: { + "Authorization": `Bearer ${idToken}`, + }, + timeoutMs: 10_000, + expect: "void", + logLabel: "delete archived show", + }); +} + export async function archiveShow(id: number, idToken: string) { if (!idToken) { throw new ApiError("Missing auth token for archive"); diff --git a/frontend/src/pages/ArchivePage.tsx b/frontend/src/pages/ArchivePage.tsx index 79c8732..b445125 100644 --- a/frontend/src/pages/ArchivePage.tsx +++ b/frontend/src/pages/ArchivePage.tsx @@ -1,8 +1,8 @@ import React from "react"; -import { fetchArchive } from "../api/watchparty"; +import { deleteArchiveShow, fetchArchive } from "../api/watchparty"; import type { ArchiveItem } from "../api/types"; import { useAuth } from "../auth/AuthProvider"; -import { toastError } from "../utils/toastBus"; +import { toastError, toastInfo } from "../utils/toastBus"; import { logApiError } from "../utils/logger"; type SortKey = "id" | "ep_num" | "ep_title" | "season_name" | "start_time" | "playback_length" | "date_created" | "date_archived"; @@ -16,11 +16,13 @@ function formatTimestamp(ts: string) { } export default function ArchivePage() { - const { enabled, idToken, isAdmin } = useAuth(); + const { enabled, idToken, isAdmin, verifying } = useAuth(); const [items, setItems] = React.useState([]); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); const [sort, setSort] = React.useState<{ key: SortKey | null; dir: SortDir }>({ key: null, dir: null }); + const [selectedId, setSelectedId] = React.useState(null); + const [deletingId, setDeletingId] = React.useState(null); const load = React.useCallback(async () => { if (!idToken) { @@ -32,6 +34,7 @@ export default function ArchivePage() { setLoading(true); const data = await fetchArchive(idToken); setItems(data); + setSelectedId((prev) => (prev != null && data.some((it) => it.id === prev) ? prev : null)); } catch (e: unknown) { const msg = e instanceof Error ? e.message : "アーカイブを取得できませんでした。"; setError(msg); @@ -95,6 +98,40 @@ export default function ArchivePage() { return ""; }; + const selectedItem = React.useMemo( + () => items.find((it) => it.id === selectedId) ?? null, + [items, selectedId], + ); + + const handleSelect = React.useCallback((id: number) => { + setSelectedId((prev) => (prev === id ? null : id)); + }, []); + + async function handleDeleteSelected() { + if (!selectedItem) return; + if (!enabled) { + toastError("認証が無効です", "管理者に確認してください"); + return; + } + if (!idToken) { + toastError("サインインしてください", "削除には管理者サインインが必要です"); + return; + } + try { + setDeletingId(selectedItem.id); + await deleteArchiveShow(selectedItem.id, idToken); + toastInfo("アーカイブを削除しました"); + setItems((prev) => prev.filter((it) => it.id !== selectedItem.id)); + setSelectedId(null); + } catch (e: unknown) { + const msg = e instanceof Error ? e.message : "削除に失敗しました。"; + toastError("削除に失敗しました", msg); + logApiError("delete archived show", e); + } finally { + setDeletingId(null); + } + } + if (!enabled) { return
認証が無効です。
; } @@ -152,7 +189,20 @@ export default function ArchivePage() { {sortedItems.map((it) => ( -
+
handleSelect(it.id)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + handleSelect(it.id); + } + }} + > #{it.id} 第{it.ep_num}話 {it.ep_title} @@ -166,6 +216,40 @@ export default function ArchivePage() {
)} + + {selectedItem && ( +
+
+
1件選択中
+
+ 第{selectedItem.ep_num}話:{selectedItem.ep_title} +
+
+ シーズン {selectedItem.season_name} ・ 開始 {selectedItem.start_time.slice(0, 5)} ・ 再生 {selectedItem.playback_length.slice(0, 5)} ・ ID #{selectedItem.id} +
+
+ アーカイブ日時 {formatTimestamp(selectedItem.date_archived)} +
+
+ {idToken + ? (verifying ? "トークン確認中…" : "サインイン済み") + : "削除には管理者サインインが必要です"} +
+
+
+ + +
+
+ )} ); }