/**
* DataTable Component
* Toggleable table showing seats by province and party
*/
'use client';
import React, { useCallback, useRef, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { useTranslations, useLocale } from 'next-intl';
import { useVisualizerStore } from '@/hooks/useVisualizerStore';
import { provinces, provinceCodes } from '@/lib/visualizer/provinceData';
import { useSeatDataContext } from '@/contexts/SeatDataContext';
import { partyColors, partyAbbreviations, getPartyColor } from '@/lib/visualizer/partyColors';
export function DataTable() {
const { showDataTable, selectedProvince, selectionSource, setSelectedProvince } = useVisualizerStore();
const { seatData, nationalTotals, totalSeats } = useSeatDataContext();
const t = useTranslations('visualizer');
const locale = useLocale() as 'en' | 'fr';
const rowRefs = useRef<Record<string, HTMLTableRowElement | null>>({});
const grandTotal = totalSeats;
// Scroll to selected row only when selection comes from the map
useEffect(() => {
if (selectedProvince && selectionSource === 'map' && showDataTable && rowRefs.current[selectedProvince]) {
rowRefs.current[selectedProvince]?.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
}
}, [selectedProvince, selectionSource, showDataTable]);
// Handle keyboard navigation
const handleKeyDown = useCallback((code: string) => (e: React.KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setSelectedProvince(selectedProvince === code ? null : code, 'table');
}
}, [selectedProvince, setSelectedProvince]);
// Get ordered list of parties (by national seat count)
const partyOrder = Object.keys(partyColors)
.filter(p => p !== 'Independent')
.sort((a, b) => (nationalTotals[b] || 0) - (nationalTotals[a] || 0));
return (
<AnimatePresence>
{showDataTable && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3 }}
className="overflow-hidden"
>
<div className="mt-6 border border-border-subtle rounded-lg overflow-hidden">
<div className="overflow-x-auto">
<table
className="w-full text-sm"
role="grid"
aria-label={t('dataTableAriaLabel') || 'Federal seat distribution by province and party'}
>
<thead className="bg-bg-secondary">
<tr role="row">
<th
scope="col"
className="px-4 py-3 text-left font-semibold text-text-primary border-b border-border-subtle sticky left-0 bg-bg-secondary z-10"
>
{t('province')}
</th>
{partyOrder.map(party => (
<th
key={party}
scope="col"
className="px-3 py-3 text-center font-semibold text-text-primary border-b border-border-subtle whitespace-nowrap"
>
<div className="flex items-center justify-center gap-1">
<div
className="w-3 h-3 rounded-sm"
style={{ backgroundColor: getPartyColor(party) }}
aria-hidden="true"
/>
<span className="hidden sm:inline">{partyAbbreviations[party] || party}</span>
<span className="sm:hidden">{partyAbbreviations[party]?.slice(0, 2) || party.slice(0, 2)}</span>
</div>
</th>
))}
<th scope="col" className="px-4 py-3 text-center font-semibold text-text-primary border-b border-border-subtle">
{t('total')}
</th>
</tr>
</thead>
<tbody>
{provinceCodes.map((code, index) => {
const province = provinces[code];
const data = seatData[code];
const isSelected = selectedProvince === code;
return (
<tr
key={code}
ref={(el) => { rowRefs.current[code] = el; }}
role="row"
tabIndex={0}
aria-selected={isSelected}
onClick={() => setSelectedProvince(isSelected ? null : code, 'table')}
onKeyDown={handleKeyDown(code)}
className={`
cursor-pointer transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-accent-red focus-visible:ring-inset
${isSelected ? 'bg-accent-red/10' : index % 2 === 0 ? 'bg-bg-primary' : 'bg-bg-secondary/50'}
hover:bg-bg-elevated
`}
>
<th
scope="row"
className={`
px-4 py-2 font-medium text-text-primary border-b border-border-subtle sticky left-0 z-10 text-left
${isSelected ? 'bg-accent-red/10' : index % 2 === 0 ? 'bg-bg-primary' : 'bg-bg-secondary/50'}
hover:bg-bg-elevated
`}
>
<span className="hidden sm:inline">{province.name[locale]}</span>
<span className="sm:hidden">{code}</span>
</th>
{partyOrder.map(party => {
const seats = data?.seats[party] || 0;
return (
<td
key={party}
className={`px-3 py-2 text-center border-b border-border-subtle ${
seats > 0 ? 'text-text-primary' : 'text-text-tertiary'
}`}
>
{seats > 0 ? seats : '-'}
</td>
);
})}
<td className="px-4 py-2 text-center font-semibold text-text-primary border-b border-border-subtle">
{data?.totalSeats || 0}
</td>
</tr>
);
})}
{/* National totals row */}
<tr role="row" className="bg-bg-secondary font-semibold">
<th
scope="row"
className="px-4 py-3 text-text-primary border-t-2 border-border-subtle sticky left-0 bg-bg-secondary z-10 text-left"
>
{t('nationalTotal')}
</th>
{partyOrder.map(party => (
<td
key={party}
className="px-3 py-3 text-center text-text-primary border-t-2 border-border-subtle"
>
{nationalTotals[party] || 0}
</td>
))}
<td className="px-4 py-3 text-center text-text-primary border-t-2 border-border-subtle">
{grandTotal}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
);
}