Enhance user and media management with sorting and statistics: add sorting options to user and media listing endpoints, implement media statistics retrieval, and update frontend components for sorting and displaying media processing times. Refactor related query handlers and API types to support new features.
build / backend (push) Successful in 1m35s
build / frontend (push) Successful in 31s
tests / backend-tests (push) Successful in 1m48s

This commit is contained in:
Leonid Pershin
2026-07-26 00:28:22 +03:00
parent d6ad8e11a4
commit 64a2dade74
27 changed files with 1634 additions and 39 deletions
+73
View File
@@ -0,0 +1,73 @@
import { useState } from 'react'
import { ArrowDown, ArrowUp, ChevronsUpDown } from 'lucide-react'
import { cn } from '@/shared/lib/cn'
export type SortState = { key: string; desc: boolean }
/**
* Состояние сортировки таблицы. Клик по новому столбцу — по возрастанию; повторный клик по тому же —
* переключает направление. Для серверных списков `sort`/`desc` передаются в API (и в queryKey), для
* клиентских — в {@link sortRows}.
*/
export function useTableSort(defaultKey: string, defaultDesc = false) {
const [sort, setSort] = useState<SortState>({ key: defaultKey, desc: defaultDesc })
const toggle = (key: string) =>
setSort((s) => (s.key === key ? { key, desc: !s.desc } : { key, desc: false }))
return { sort, toggle }
}
/** Заголовок-кнопка столбца со стрелкой сортировки. */
export function SortHeader({
label,
sortKey,
sort,
onToggle,
className,
}: {
label: string
sortKey: string
sort: SortState
onToggle: (key: string) => void
className?: string
}) {
const active = sort.key === sortKey
const Icon = !active ? ChevronsUpDown : sort.desc ? ArrowDown : ArrowUp
return (
<th className={cn('px-4 py-2 font-medium', className)}>
<button
type="button"
onClick={() => onToggle(sortKey)}
aria-sort={active ? (sort.desc ? 'descending' : 'ascending') : 'none'}
className={cn(
'inline-flex items-center gap-1 hover:text-foreground',
active && 'text-foreground',
)}
>
{label}
<Icon className={cn('h-3.5 w-3.5', !active && 'opacity-40')} />
</button>
</th>
)
}
type Comparable = string | number | boolean | null | undefined
/** Клиентская сортировка строк по выбранному ключу (для непагинированных списков). nulls — в конец. */
export function sortRows<T>(
rows: T[],
sort: SortState,
accessors: Record<string, (row: T) => Comparable>,
): T[] {
const accessor = accessors[sort.key]
if (!accessor) return rows
const dir = sort.desc ? -1 : 1
return [...rows].sort((a, b) => {
const av = accessor(a)
const bv = accessor(b)
if (av == null && bv == null) return 0
if (av == null) return 1
if (bv == null) return -1
if (typeof av === 'string' && typeof bv === 'string') return av.localeCompare(bv) * dir
return (av < bv ? -1 : av > bv ? 1 : 0) * dir
})
}