feat(archive): implement sortable columns in archive table with toggle functionality
This commit is contained in:
parent
2889a38ab6
commit
5b02eb1226
@ -530,6 +530,16 @@ kbd {
|
|||||||
background: rgba(255,255,255,0.04);
|
background: rgba(255,255,255,0.04);
|
||||||
border: 1px solid rgba(255,255,255,0.08);
|
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 {
|
.archive-row {
|
||||||
background: rgba(255,255,255,0.03);
|
background: rgba(255,255,255,0.03);
|
||||||
border: 1px solid rgba(255,255,255,0.05);
|
border: 1px solid rgba(255,255,255,0.05);
|
||||||
|
|||||||
@ -5,6 +5,9 @@ import { useAuth } from "../auth/AuthProvider";
|
|||||||
import { toastError } from "../utils/toastBus";
|
import { toastError } from "../utils/toastBus";
|
||||||
import { logApiError } from "../utils/logger";
|
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) {
|
function formatTimestamp(ts: string) {
|
||||||
if (!ts) return "";
|
if (!ts) return "";
|
||||||
const d = new Date(ts);
|
const d = new Date(ts);
|
||||||
@ -17,6 +20,7 @@ export default function ArchivePage() {
|
|||||||
const [items, setItems] = React.useState<ArchiveItem[]>([]);
|
const [items, setItems] = React.useState<ArchiveItem[]>([]);
|
||||||
const [loading, setLoading] = React.useState(true);
|
const [loading, setLoading] = React.useState(true);
|
||||||
const [error, setError] = React.useState<string | null>(null);
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
|
const [sort, setSort] = React.useState<{ key: SortKey | null; dir: SortDir }>({ key: null, dir: null });
|
||||||
|
|
||||||
const load = React.useCallback(async () => {
|
const load = React.useCallback(async () => {
|
||||||
if (!idToken) {
|
if (!idToken) {
|
||||||
@ -46,6 +50,51 @@ export default function ArchivePage() {
|
|||||||
}
|
}
|
||||||
}, [enabled, isAdmin, idToken, load]);
|
}, [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<string, unknown>)[key] as string | number | undefined ?? "";
|
||||||
|
bv = (b as Record<string, unknown>)[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) {
|
if (!enabled) {
|
||||||
return <div className="subtle">認証が無効です。</div>;
|
return <div className="subtle">認証が無効です。</div>;
|
||||||
}
|
}
|
||||||
@ -77,16 +126,32 @@ export default function ArchivePage() {
|
|||||||
<div className="archive-scroll">
|
<div className="archive-scroll">
|
||||||
<div className="archive-table">
|
<div className="archive-table">
|
||||||
<div className="archive-header">
|
<div className="archive-header">
|
||||||
<span>ID</span>
|
<button className="archive-head-btn" onClick={() => toggleSort("id")}>
|
||||||
<span>話数</span>
|
ID {sortIndicator("id")}
|
||||||
<span>タイトル</span>
|
</button>
|
||||||
<span>シーズン</span>
|
<button className="archive-head-btn" onClick={() => toggleSort("ep_num")}>
|
||||||
<span>開始時刻</span>
|
話数 {sortIndicator("ep_num")}
|
||||||
<span>再生時間</span>
|
</button>
|
||||||
<span>登録</span>
|
<button className="archive-head-btn" onClick={() => toggleSort("ep_title")}>
|
||||||
<span>アーカイブ</span>
|
タイトル {sortIndicator("ep_title")}
|
||||||
|
</button>
|
||||||
|
<button className="archive-head-btn" onClick={() => toggleSort("season_name")}>
|
||||||
|
シーズン {sortIndicator("season_name")}
|
||||||
|
</button>
|
||||||
|
<button className="archive-head-btn" onClick={() => toggleSort("start_time")}>
|
||||||
|
開始時刻 {sortIndicator("start_time")}
|
||||||
|
</button>
|
||||||
|
<button className="archive-head-btn" onClick={() => toggleSort("playback_length")}>
|
||||||
|
再生時間 {sortIndicator("playback_length")}
|
||||||
|
</button>
|
||||||
|
<button className="archive-head-btn" onClick={() => toggleSort("date_created")}>
|
||||||
|
登録 {sortIndicator("date_created")}
|
||||||
|
</button>
|
||||||
|
<button className="archive-head-btn" onClick={() => toggleSort("date_archived")}>
|
||||||
|
アーカイブ {sortIndicator("date_archived")}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{items.map((it) => (
|
{sortedItems.map((it) => (
|
||||||
<div key={it.id} className="archive-row">
|
<div key={it.id} className="archive-row">
|
||||||
<span>#{it.id}</span>
|
<span>#{it.id}</span>
|
||||||
<span>第{it.ep_num}話</span>
|
<span>第{it.ep_num}話</span>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user