Added delete button on frontend

This commit is contained in:
Nik Afiq 2025-12-19 22:25:28 +09:00
parent e7aa62358c
commit 14474ca024
2 changed files with 104 additions and 4 deletions

View File

@ -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<void>(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");

View File

@ -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<ArchiveItem[]>([]);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState<string | null>(null);
const [sort, setSort] = React.useState<{ key: SortKey | null; dir: SortDir }>({ key: null, dir: null });
const [selectedId, setSelectedId] = React.useState<number | null>(null);
const [deletingId, setDeletingId] = React.useState<number | null>(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 <div className="subtle"></div>;
}
@ -152,7 +189,20 @@ export default function ArchivePage() {
</button>
</div>
{sortedItems.map((it) => (
<div key={it.id} className="archive-row">
<div
key={it.id}
className={`archive-row ${selectedId === it.id ? "selected" : ""}`}
role="button"
tabIndex={0}
aria-pressed={selectedId === it.id}
onClick={() => handleSelect(it.id)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleSelect(it.id);
}
}}
>
<span>#{it.id}</span>
<span>{it.ep_num}</span>
<span>{it.ep_title}</span>
@ -166,6 +216,40 @@ export default function ArchivePage() {
</div>
</div>
)}
{selectedItem && (
<div className="selection-bar archive-detail-bar">
<div className="selection-meta">
<div className="selection-count">1</div>
<div className="selection-title">
{selectedItem.ep_num}{selectedItem.ep_title}
</div>
<div className="selection-note">
{selectedItem.season_name} {selectedItem.start_time.slice(0, 5)} {selectedItem.playback_length.slice(0, 5)} ID #{selectedItem.id}
</div>
<div className="selection-note">
{formatTimestamp(selectedItem.date_archived)}
</div>
<div className="selection-note">
{idToken
? (verifying ? "トークン確認中…" : "サインイン済み")
: "削除には管理者サインインが必要です"}
</div>
</div>
<div className="archive-detail-actions">
<button className="link-btn" type="button" onClick={() => setSelectedId(null)}>
</button>
<button
className="danger-btn"
disabled={deletingId === selectedItem.id || verifying || !idToken}
onClick={() => handleDeleteSelected().catch(() => { })}
>
{deletingId === selectedItem.id ? "削除中…" : "完全に削除"}
</button>
</div>
</div>
)}
</div>
);
}