MuSProt / frontend /src /components /SequenceTable.tsx
WinslowFan's picture
Claude Opus 5.5
Redesign the MuSProt website for the new database release
dd079af
Raw History Blame Contribute Delete
3.65 kB
import { useNavigate } from 'react-router-dom';
import type { SequenceSummary } from '../lib/api';
import { cleanFunction, fmtInt, methodShort, sequencePath, year } from '../lib/format';
import { MiniSplit } from './charts';
export type SortKey = 'sequence' | 'length' | 'observations' | 'states' | 'entries' | 'released';
function Th({
k,
label,
sort,
order,
onSort,
num,
}: {
k: SortKey;
label: string;
sort?: SortKey;
order?: 'asc' | 'desc';
onSort?: (k: SortKey) => void;
num?: boolean;
}) {
const active = sort === k;
if (!onSort) return <th className={num ? 'num' : ''}>{label}</th>;
return (
<th className={num ? 'num' : ''} aria-sort={active ? (order === 'asc' ? 'ascending' : 'descending') : 'none'}>
<button type="button" className={`th-sort ${active ? 'is-active' : ''}`} onClick={() => onSort(k)}>
{label}
<span className="th-sort__arrow" aria-hidden>
{active ? (order === 'asc' ? '↑' : '↓') : '↕'}
</span>
</button>
</th>
);
}
export function SequenceTable({
items,
sort,
order,
onSort,
}: {
items: SequenceSummary[];
sort?: SortKey;
order?: 'asc' | 'desc';
onSort?: (k: SortKey) => void;
}) {
const navigate = useNavigate();
const p = { sort, order, onSort };
return (
<div className="table-wrap">
<table className="table table--rows">
<thead>
<tr>
<Th k="sequence" label="Sequence" {...p} />
<th>Function</th>
<Th k="length" label="Length" num {...p} />
<Th k="observations" label="Obs." num {...p} />
<Th k="states" label="States" num {...p} />
<Th k="entries" label="Entries" num {...p} />
<th>Apo / holo</th>
<th>Methods</th>
<Th k="released" label="Latest" num {...p} />
</tr>
</thead>
<tbody>
{items.map((s) => (
<tr
key={s.sequence_id}
className="is-clickable"
onClick={(e) => {
if ((e.target as HTMLElement).closest('a')) return;
navigate(sequencePath(s.sequence_id));
}}
>
<td>
<a
href={sequencePath(s.sequence_id)}
className="mono strong"
onClick={(e) => {
e.preventDefault();
navigate(sequencePath(s.sequence_id));
}}
>
{s.sequence_id}
</a>
<div className="sub mono">{s.uniprot_id ?? 'no UniProt'}</div>
</td>
<td className="fn-cell">
<span title={s.top_function ?? undefined}>{s.top_function ? cleanFunction(s.top_function) : '—'}</span>
</td>
<td className="num tabular">{fmtInt(s.length)}</td>
<td className="num tabular">{fmtInt(s.n_obs)}</td>
<td className="num tabular strong">{fmtInt(s.n_states)}</td>
<td className="num tabular">{fmtInt(s.n_entries)}</td>
<td>
<MiniSplit
a={s.n_apo}
b={s.n_holo}
colorA="var(--apo)"
colorB="var(--holo)"
title={`${s.n_apo} apo · ${s.n_holo} holo`}
/>
</td>
<td className="methods-cell">{s.methods.slice(0, 2).map(methodShort).join(', ')}</td>
<td className="num tabular">{year(s.last_release)}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}