Files
resolutionflow/frontend/src/components/admin/DataTable.tsx
chihlasm 2dbb8b6abf refactor: design critique fixes for account pages
- Admin accounts: replace dense card grid with compact DataTable
- Account settings: remove redundant hero card, stat grid, header pills
- Fix bg-accent (orange) misuse on decorative elements across 7 files
- Add ConfirmButton for destructive actions (deactivate, remove member)
- Replace single-field modals with inline editing (plan, trial)
- Add contextual help: display code tooltip, improved empty states
- Non-owner aside explanation for hidden owner-only sections
- Admin sidebar: group 11 items into 5 labeled sections
- Rename UsersPage.tsx → AccountsPage.tsx to match route
- Fix border radius consistency, hide zero-count badges

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 04:54:26 +00:00

127 lines
3.8 KiB
TypeScript

import { useState, type ReactNode } from 'react'
import { ChevronUp, ChevronDown, ChevronsUpDown } from 'lucide-react'
import { cn } from '@/lib/utils'
export interface Column<T> {
key: string
header: string
render: (item: T) => ReactNode
sortable?: boolean
className?: string
}
interface DataTableProps<T> {
columns: Column<T>[]
data: T[]
keyExtractor: (item: T) => string
isLoading?: boolean
skeletonRows?: number
onSort?: (key: string, direction: 'asc' | 'desc') => void
sortKey?: string
sortDirection?: 'asc' | 'desc'
emptyState?: ReactNode
}
export function DataTable<T>({
columns,
data,
keyExtractor,
isLoading = false,
skeletonRows = 5,
onSort,
sortKey,
sortDirection,
emptyState,
}: DataTableProps<T>) {
const [localSortKey, setLocalSortKey] = useState<string | null>(null)
const [localSortDir, setLocalSortDir] = useState<'asc' | 'desc'>('asc')
const activeSortKey = sortKey ?? localSortKey
const activeSortDir = sortDirection ?? localSortDir
const handleSort = (key: string) => {
const newDir = activeSortKey === key && activeSortDir === 'asc' ? 'desc' : 'asc'
if (onSort) {
onSort(key, newDir)
} else {
setLocalSortKey(key)
setLocalSortDir(newDir)
}
}
return (
<div className="overflow-x-auto rounded-lg border border-border">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border bg-elevated">
{columns.map((col) => (
<th
key={col.key}
className={cn(
'px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-muted-foreground',
col.sortable && 'cursor-pointer select-none hover:text-foreground',
col.className
)}
onClick={col.sortable ? () => handleSort(col.key) : undefined}
>
<div className="flex items-center gap-1">
{col.header}
{col.sortable && (
<span className="inline-flex">
{activeSortKey === col.key ? (
activeSortDir === 'asc' ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)
) : (
<ChevronsUpDown className="h-3.5 w-3.5 opacity-40" />
)}
</span>
)}
</div>
</th>
))}
</tr>
</thead>
<tbody>
{isLoading ? (
Array.from({ length: skeletonRows }).map((_, i) => (
<tr key={i} className="border-b border-border last:border-0">
{columns.map((col) => (
<td key={col.key} className="px-4 py-3">
<div className="h-4 w-3/4 animate-pulse rounded bg-muted" />
</td>
))}
</tr>
))
) : data.length === 0 ? (
<tr>
<td colSpan={columns.length} className="px-4 py-12 text-center">
{emptyState || (
<span className="text-muted-foreground">No data found</span>
)}
</td>
</tr>
) : (
data.map((item) => (
<tr
key={keyExtractor(item)}
className="border-b border-border last:border-0 hover:bg-elevated transition-colors"
>
{columns.map((col) => (
<td key={col.key} className={cn('px-4 py-3', col.className)}>
{col.render(item)}
</td>
))}
</tr>
))
)}
</tbody>
</table>
</div>
)
}
export default DataTable