Add clickable column sorting for admin tags list

This commit is contained in:
2026-07-24 11:59:15 -04:00
parent b1ef0d139a
commit 020381c3a0
2 changed files with 102 additions and 11 deletions
+27
View File
@@ -262,3 +262,30 @@
color: var(--danger); color: var(--danger);
} }
} }
// Clickable table sort headers
.admin-sort-th {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0;
margin: 0;
border: 0;
background: none;
color: inherit;
font: inherit;
font-weight: inherit;
letter-spacing: inherit;
text-transform: inherit;
cursor: pointer;
user-select: none;
&:hover {
color: var(--accent);
}
&:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
}
+74 -10
View File
@@ -20,6 +20,8 @@ export function AdminCategories() {
const [tagQuery, setTagQuery] = useState(""); const [tagQuery, setTagQuery] = useState("");
const [tagFilter, setTagFilter] = useState<"all" | "used" | "unused">("all"); const [tagFilter, setTagFilter] = useState<"all" | "used" | "unused">("all");
const [tagPage, setTagPage] = useState(1); const [tagPage, setTagPage] = useState(1);
const [sortKey, setSortKey] = useState<"name" | "slug" | "projects">("projects");
const [sortDir, setSortDir] = useState<"asc" | "desc">("desc");
usePageTitle("Categories & Tags · Admin"); usePageTitle("Categories & Tags · Admin");
@@ -32,13 +34,29 @@ export function AdminCategories() {
load(); load();
}, []); }, []);
// Reset page when filters change // Reset page when filters/sort change
useEffect(() => { useEffect(() => {
setTagPage(1); setTagPage(1);
}, [tagQuery, tagFilter]); }, [tagQuery, tagFilter, sortKey, sortDir]);
function toggleSort(key: "name" | "slug" | "projects") {
if (sortKey === key) {
setSortDir((d) => (d === "asc" ? "desc" : "asc"));
} else {
setSortKey(key);
// Projects default to high→low; name/slug A→Z
setSortDir(key === "projects" ? "desc" : "asc");
}
}
function sortLabel(key: "name" | "slug" | "projects", label: string) {
if (sortKey !== key) return label;
return `${label} ${sortDir === "asc" ? "↑" : "↓"}`;
}
const filteredTags = useMemo(() => { const filteredTags = useMemo(() => {
const q = tagQuery.trim().toLowerCase(); const q = tagQuery.trim().toLowerCase();
const dir = sortDir === "asc" ? 1 : -1;
return tags return tags
.filter((t) => { .filter((t) => {
if (q && !t.name.toLowerCase().includes(q) && !t.slug.toLowerCase().includes(q)) { if (q && !t.name.toLowerCase().includes(q) && !t.slug.toLowerCase().includes(q)) {
@@ -50,13 +68,22 @@ export function AdminCategories() {
return true; return true;
}) })
.sort((a, b) => { .sort((a, b) => {
// Used first by count desc, then name if (sortKey === "projects") {
const ua = tagUsage(a); const diff = tagUsage(a) - tagUsage(b);
const ub = tagUsage(b); if (diff !== 0) return diff * dir;
if (ua !== ub) return ub - ua;
return a.name.localeCompare(b.name); return a.name.localeCompare(b.name);
}
if (sortKey === "slug") {
const diff = a.slug.localeCompare(b.slug);
if (diff !== 0) return diff * dir;
return a.name.localeCompare(b.name);
}
// name
const diff = a.name.localeCompare(b.name);
if (diff !== 0) return diff * dir;
return tagUsage(b) - tagUsage(a);
}); });
}, [tags, tagQuery, tagFilter]); }, [tags, tagQuery, tagFilter, sortKey, sortDir]);
const totalPages = Math.max(1, Math.ceil(filteredTags.length / PAGE_SIZE)); const totalPages = Math.max(1, Math.ceil(filteredTags.length / PAGE_SIZE));
const page = Math.min(tagPage, totalPages); const page = Math.min(tagPage, totalPages);
@@ -232,9 +259,46 @@ export function AdminCategories() {
<table className="admin-table"> <table className="admin-table">
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>
<th>Slug</th> <button
<th>Projects</th> type="button"
className="admin-sort-th"
onClick={() => toggleSort("name")}
aria-sort={
sortKey === "name" ? (sortDir === "asc" ? "ascending" : "descending") : "none"
}
>
{sortLabel("name", "Name")}
</button>
</th>
<th>
<button
type="button"
className="admin-sort-th"
onClick={() => toggleSort("slug")}
aria-sort={
sortKey === "slug" ? (sortDir === "asc" ? "ascending" : "descending") : "none"
}
>
{sortLabel("slug", "Slug")}
</button>
</th>
<th>
<button
type="button"
className="admin-sort-th"
onClick={() => toggleSort("projects")}
aria-sort={
sortKey === "projects"
? sortDir === "asc"
? "ascending"
: "descending"
: "none"
}
>
{sortLabel("projects", "Projects")}
</button>
</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>