diff --git a/frontend/src/index.css b/frontend/src/index.css index 704a8da..f5b7997 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -530,6 +530,16 @@ kbd { background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); } +.archive-head-btn { + all: unset; + cursor: pointer; + font-weight: 800; + color: var(--subtle); + display: inline-flex; + align-items: center; + gap: 4px; +} +.archive-head-btn:hover { color: var(--text); } .archive-row { background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.05); diff --git a/frontend/src/pages/ArchivePage.tsx b/frontend/src/pages/ArchivePage.tsx index 4ccdcf9..79c8732 100644 --- a/frontend/src/pages/ArchivePage.tsx +++ b/frontend/src/pages/ArchivePage.tsx @@ -5,6 +5,9 @@ import { useAuth } from "../auth/AuthProvider"; import { toastError } 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"; +type SortDir = "asc" | "desc" | null; + function formatTimestamp(ts: string) { if (!ts) return ""; const d = new Date(ts); @@ -17,6 +20,7 @@ export default function ArchivePage() { 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 load = React.useCallback(async () => { if (!idToken) { @@ -46,6 +50,51 @@ export default function ArchivePage() { } }, [enabled, isAdmin, idToken, load]); + const sortedItems = React.useMemo(() => { + const { key, dir } = sort; + if (!key || !dir) return items; + const next = [...items]; + next.sort((a, b) => { + let av: string | number = ""; + let bv: string | number = ""; + switch (key) { + case "id": + case "ep_num": + av = a[key]; + bv = b[key]; + break; + case "date_created": + case "date_archived": + av = new Date(a[key]).getTime(); + bv = new Date(b[key]).getTime(); + break; + default: + av = (a as Record)[key] as string | number | undefined ?? ""; + bv = (b as Record)[key] as string | number | undefined ?? ""; + } + if (av === bv) return 0; + const asc = av > bv ? 1 : -1; + return dir === "asc" ? asc : -asc; + }); + return next; + }, [items, sort]); + + function toggleSort(key: SortKey) { + setSort((prev) => { + if (prev.key !== key) return { key, dir: "asc" }; + if (prev.dir === "asc") return { key, dir: "desc" }; + if (prev.dir === "desc") return { key: null, dir: null }; + return { key, dir: "asc" }; + }); + } + + const sortIndicator = (key: SortKey) => { + if (sort.key !== key) return ""; + if (sort.dir === "asc") return "▲"; + if (sort.dir === "desc") return "▼"; + return ""; + }; + if (!enabled) { return
認証が無効です。
; } @@ -77,16 +126,32 @@ export default function ArchivePage() {
- ID - 話数 - タイトル - シーズン - 開始時刻 - 再生時間 - 登録 - アーカイブ + + + + + + + +
- {items.map((it) => ( + {sortedItems.map((it) => (
#{it.id} 第{it.ep_num}話