Records page: sortable headers and short date formatting
- Click any column header to sort asc/desc (⇅ / ▲ / ▼ indicators) - Sort is client-side within the current page, numeric-aware - Dates matching ISO format are displayed as e.g. "Apr 5, 26" - Sort resets on source change Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6b7f1c1334
commit
b311092987
@ -1,16 +1,30 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import { api } from '../api'
|
||||
|
||||
const DATE_RE = /^\d{4}-\d{2}-\d{2}(T[\d:.Z+-]+)?$/
|
||||
|
||||
function formatVal(val) {
|
||||
if (val === null || val === undefined) return null
|
||||
const s = String(val)
|
||||
if (DATE_RE.test(s)) {
|
||||
const d = new Date(s)
|
||||
if (!isNaN(d)) return d.toLocaleDateString(undefined, { year: '2-digit', month: 'short', day: 'numeric' })
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
export default function Records({ source }) {
|
||||
const [rows, setRows] = useState([])
|
||||
const [exists, setExists] = useState(null)
|
||||
const [offset, setOffset] = useState(0)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [sort, setSort] = useState({ col: null, dir: 'asc' })
|
||||
const LIMIT = 100
|
||||
|
||||
useEffect(() => {
|
||||
if (!source) return
|
||||
setOffset(0)
|
||||
setSort({ col: null, dir: 'asc' })
|
||||
load(0)
|
||||
}, [source])
|
||||
|
||||
@ -27,11 +41,30 @@ export default function Records({ source }) {
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSort(col) {
|
||||
setSort(s => s.col === col
|
||||
? { col, dir: s.dir === 'asc' ? 'desc' : 'asc' }
|
||||
: { col, dir: 'asc' }
|
||||
)
|
||||
}
|
||||
|
||||
const sorted = useMemo(() => {
|
||||
if (!sort.col) return rows
|
||||
return [...rows].sort((a, b) => {
|
||||
const av = a[sort.col] ?? ''
|
||||
const bv = b[sort.col] ?? ''
|
||||
const cmp = String(av).localeCompare(String(bv), undefined, { numeric: true })
|
||||
return sort.dir === 'asc' ? cmp : -cmp
|
||||
})
|
||||
}, [rows, sort])
|
||||
|
||||
function prev() { const o = Math.max(0, offset - LIMIT); setOffset(o); load(o) }
|
||||
function next() { const o = offset + LIMIT; setOffset(o); load(o) }
|
||||
|
||||
if (!source) return <div className="p-6 text-sm text-gray-400">Select a source first.</div>
|
||||
|
||||
const cols = rows.length > 0 ? Object.keys(rows[0]) : []
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
@ -59,19 +92,34 @@ export default function Records({ source }) {
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="text-left text-xs text-gray-400 border-b border-gray-100 bg-gray-50">
|
||||
{Object.keys(rows[0]).map(col => (
|
||||
<th key={col} className="px-3 py-2 font-medium whitespace-nowrap">{col}</th>
|
||||
))}
|
||||
{cols.map(col => {
|
||||
const active = sort.col === col
|
||||
return (
|
||||
<th
|
||||
key={col}
|
||||
onClick={() => toggleSort(col)}
|
||||
className="px-3 py-2 font-medium whitespace-nowrap cursor-pointer select-none hover:text-gray-600"
|
||||
>
|
||||
{col}
|
||||
<span className="ml-1 text-gray-300">
|
||||
{active ? (sort.dir === 'asc' ? '▲' : '▼') : '⇅'}
|
||||
</span>
|
||||
</th>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((row, i) => (
|
||||
{sorted.map((row, i) => (
|
||||
<tr key={i} className="border-t border-gray-50 hover:bg-gray-50">
|
||||
{Object.values(row).map((val, j) => (
|
||||
{cols.map((col, j) => {
|
||||
const formatted = formatVal(row[col])
|
||||
return (
|
||||
<td key={j} className="px-3 py-2 text-xs text-gray-600 whitespace-nowrap max-w-48 truncate">
|
||||
{val === null ? <span className="text-gray-300">—</span> : String(val)}
|
||||
{formatted === null ? <span className="text-gray-300">—</span> : formatted}
|
||||
</td>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user