Point 33: Sortable Table Headers Implementation Guide¶
Overview¶
Add click-to-sort functionality to all table headers across all UIs with visual indicators (arrows) for ascending/descending sort order.
Created Components¶
1. SortableTableHeader Component¶
Location: client/src/components/SortableTableHeader.tsx
Features: - Click-to-sort functionality - Visual indicators (↑ ↓) for sort direction - Hover state showing sort availability - Blue color for active sort column - Gray hover icons for sortable but inactive columns
Props:
- label: Header text to display
- sortKey: Key to use for sorting data
- currentSortKey: Currently active sort key
- currentSortDirection: Current direction ('asc' | 'desc')
- onSort: Callback when header is clicked
- className: Additional CSS classes
- sortable: Whether header is sortable (default: true)
2. useTableSort Hook¶
Location: client/src/components/SortableTableHeader.tsx
Features:
- Manages sort state (key and direction)
- Toggle direction when clicking same column
- Switch to new column with ascending default
- sortData() function for sorting arrays
API:
sortData() Features:
- Handles null/undefined values
- Case-insensitive string comparison
- Number comparison
- Date comparison
- Locale-aware sorting
- Custom value extraction via getValueFn
Implementation Example¶
Basic Usage (DisciplinesUnified.tsx)¶
import { SortableTableHeader, useTableSort } from '../components/SortableTableHeader';
function DisciplinesUnified() {
const [disciplines, setDisciplines] = useState<Discipline[]>([]);
const { sortKey, sortDirection, handleSort, sortData } = useTableSort();
// Apply sorting to data before filtering/pagination
const sortedDisciplines = sortData(disciplines);
// Then apply search filter
const filteredDisciplines = sortedDisciplines.filter(d =>
d.name.toLowerCase().includes(searchTerm.toLowerCase())
);
const renderTableHeaders = () => (
<tr>
<SortableTableHeader
label={t('disciplines.table.name')}
sortKey="name"
currentSortKey={sortKey}
currentSortDirection={sortDirection}
onSort={handleSort}
/>
<SortableTableHeader
label={t('disciplines.table.shortName')}
sortKey="short_name"
currentSortKey={sortKey}
currentSortDirection={sortDirection}
onSort={handleSort}
/>
<SortableTableHeader
label={t('disciplines.table.sport')}
sortKey="sport_id"
currentSortKey={sortKey}
currentSortDirection={sortDirection}
onSort={handleSort}
/>
{/* Non-sortable header */}
<th className="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
);
}
Advanced Usage with Custom Value Extraction¶
// For nested properties or computed values
const sortedData = sortData(data, (item, key) => {
if (key === 'sport') {
return item.sport?.var_name || '';
}
if (key === 'age') {
return calculateAge(item.birthdate);
}
return item[key];
});
Pages to Update¶
High Priority (Core Data Management)¶
- ✅ DisciplinesUnified - Name, Short Name, Sport, Gender, Formula
- ParticipantsUnified - Name, Age, Gender, Club, Region
- ClubsUnified - Club Name, Region, Contact, Athletes Count
- EventsUnified - Event Name, Dates, Location, Participants
- CertificateLayouts - Layout Name, Comment, Fields Count, Status
Medium Priority (Event Management)¶
- EventParticipants - Name, Squad, Start Number, Age, Club
- FormulasUnified - Formula Name, Type, Code, Usage Count
- DisciplineFieldsUnified - Field Name, Discipline, Sort Order, Group
- StatusManagement - Status Name, Color, Usage
Low Priority (Supporting Data)¶
- Associations - Name, Abbreviation, Country
- Regions - Name, Abbreviation, Association
- Persons - Name, Email, City, Role
- Sports - Name, Disciplines Count
- Locations - Name, City, Address
Implementation Checklist¶
For each page:
- Import
SortableTableHeaderanduseTableSort - Add
useTableSort()hook call - Apply
sortData()to data BEFORE filtering/searching - Replace
<th>elements with<SortableTableHeader> - Keep non-sortable columns (like Actions) as regular
<th> - Test sorting with:
- Strings (case-insensitive)
- Numbers
- Dates
- Null/undefined values
- Different locales (German ü, ö, ä, ß)
Best Practices¶
-
Sort Before Filter/Search:
-
Non-Sortable Columns:
- Actions columns
- Complex computed fields
- Image previews
-
Multi-value fields
-
Default Sort:
-
Persisted Sort (Optional):
Testing Scenarios¶
- Click header once → Ascending sort
- Click same header again → Descending sort
- Click different header → New column ascending
- Hover over sortable header → Show gray arrows
- Active column → Blue arrows
- German characters → Proper locale sorting (ä, ö, ü, ß)
- Null values → Moved to end
- Numbers → Numeric sort (not string)
- Dates → Chronological order
Translation Keys¶
Add to de.json and en.json:
{
"common": {
"table": {
"sortAscending": "Sort ascending",
"sortDescending": "Sort descending",
"sortBy": "Sort by {{column}}"
}
}
}
Migration from Static Headers¶
Before:
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{t('disciplines.table.name')}
</th>
After:
<SortableTableHeader
label={t('disciplines.table.name')}
sortKey="name"
currentSortKey={sortKey}
currentSortDirection={sortDirection}
onSort={handleSort}
/>
Performance Considerations¶
- ✅ Sorting happens client-side (suitable for <1000 items)
- ✅ For larger datasets, consider server-side sorting
- ✅ Use React.memo() for table rows if performance issues
- ✅ sortData() creates new array (doesn't mutate original)
Accessibility¶
- ✅ Keyboard support (Enter/Space to activate sort)
- ✅ Screen reader announcements for sort state
- ✅ Visual indicators (icons) for sort direction
- ✅ Title attribute on hover explaining sort functionality
Status¶
- ✅ Component created:
SortableTableHeader.tsx - ✅ Hook created:
useTableSort() - ✅ Documentation created
- ⏳ Waiting to implement on pages (to be done next)
Next Steps¶
- Start with DisciplinesUnified as pilot implementation
- Test thoroughly with German locale and special characters
- Roll out to ParticipantsUnified
- Continue with remaining pages
- Add unit tests for sortData() function
- Consider server-side sorting for large datasets (>1000 items)