diff --git a/client/src/pages/admin/Admin.scss b/client/src/pages/admin/Admin.scss index f428cee..3c43866 100644 --- a/client/src/pages/admin/Admin.scss +++ b/client/src/pages/admin/Admin.scss @@ -262,3 +262,30 @@ 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; + } +} diff --git a/client/src/pages/admin/AdminCategories.tsx b/client/src/pages/admin/AdminCategories.tsx index 682f10b..6672895 100644 --- a/client/src/pages/admin/AdminCategories.tsx +++ b/client/src/pages/admin/AdminCategories.tsx @@ -20,6 +20,8 @@ export function AdminCategories() { const [tagQuery, setTagQuery] = useState(""); const [tagFilter, setTagFilter] = useState<"all" | "used" | "unused">("all"); const [tagPage, setTagPage] = useState(1); + const [sortKey, setSortKey] = useState<"name" | "slug" | "projects">("projects"); + const [sortDir, setSortDir] = useState<"asc" | "desc">("desc"); usePageTitle("Categories & Tags · Admin"); @@ -32,13 +34,29 @@ export function AdminCategories() { load(); }, []); - // Reset page when filters change + // Reset page when filters/sort change useEffect(() => { 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 q = tagQuery.trim().toLowerCase(); + const dir = sortDir === "asc" ? 1 : -1; return tags .filter((t) => { if (q && !t.name.toLowerCase().includes(q) && !t.slug.toLowerCase().includes(q)) { @@ -50,13 +68,22 @@ export function AdminCategories() { return true; }) .sort((a, b) => { - // Used first by count desc, then name - const ua = tagUsage(a); - const ub = tagUsage(b); - if (ua !== ub) return ub - ua; - return a.name.localeCompare(b.name); + if (sortKey === "projects") { + const diff = tagUsage(a) - tagUsage(b); + if (diff !== 0) return diff * dir; + 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 page = Math.min(tagPage, totalPages); @@ -232,9 +259,46 @@ export function AdminCategories() {
| Name | -Slug | -Projects | ++ + | ++ + | ++ + |
|---|