Files
school_compare/nextjs-app/components/RankingsView.tsx

264 lines
9.3 KiB
TypeScript
Raw Normal View History

/**
* RankingsView Component
* Client-side rankings interface with filters
*/
'use client';
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
import { useComparison } from '@/hooks/useComparison';
import type { RankingEntry, Filters, MetricDefinition } from '@/lib/types';
import { formatPercentage, formatProgress } from '@/lib/utils';
import styles from './RankingsView.module.css';
interface RankingsViewProps {
rankings: RankingEntry[];
filters: Filters;
metrics: MetricDefinition[];
selectedMetric: string;
selectedArea?: string;
selectedYear?: number;
}
export function RankingsView({
rankings,
filters,
metrics,
selectedMetric,
selectedArea,
selectedYear,
}: RankingsViewProps) {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const { addSchool, isSelected } = useComparison();
const updateFilters = (updates: Record<string, string | undefined>) => {
const params = new URLSearchParams(searchParams);
Object.entries(updates).forEach(([key, value]) => {
if (value) {
params.set(key, value);
} else {
params.delete(key);
}
});
router.push(`${pathname}?${params.toString()}`);
};
const handleMetricChange = (metric: string) => {
updateFilters({ metric });
};
const handleAreaChange = (area: string) => {
updateFilters({ local_authority: area || undefined });
};
const handleYearChange = (year: string) => {
updateFilters({ year: year || undefined });
};
const handleAddToCompare = (ranking: RankingEntry) => {
addSchool({
...ranking,
// Ensure required School fields are present
address: null,
postcode: null,
latitude: null,
longitude: null,
} as any);
};
// Get metric definition
const currentMetricDef = metrics.find((m) => m.key === selectedMetric);
const metricLabel = currentMetricDef?.label || selectedMetric;
const isProgressScore = selectedMetric.includes('progress');
const isPercentage = selectedMetric.includes('pct') || selectedMetric.includes('rate');
return (
<div className={styles.container}>
{/* Header */}
<header className={styles.header}>
<h1>School Rankings</h1>
<p className={styles.subtitle}>
Top-performing schools by {metricLabel.toLowerCase()}
</p>
</header>
{/* Filters */}
<section className={styles.filters}>
<div className={styles.filterGroup}>
<label htmlFor="metric-select" className={styles.filterLabel}>
Metric:
</label>
<select
id="metric-select"
value={selectedMetric}
onChange={(e) => handleMetricChange(e.target.value)}
className={styles.filterSelect}
>
<optgroup label="Expected Standard">
{metrics.filter(m => m.category === 'expected').map((metric) => (
<option key={metric.key} value={metric.key}>{metric.label}</option>
))}
</optgroup>
<optgroup label="Higher Standard">
{metrics.filter(m => m.category === 'higher').map((metric) => (
<option key={metric.key} value={metric.key}>{metric.label}</option>
))}
</optgroup>
<optgroup label="Progress Scores">
{metrics.filter(m => m.category === 'progress').map((metric) => (
<option key={metric.key} value={metric.key}>{metric.label}</option>
))}
</optgroup>
<optgroup label="Average Scores">
{metrics.filter(m => m.category === 'average').map((metric) => (
<option key={metric.key} value={metric.key}>{metric.label}</option>
))}
</optgroup>
<optgroup label="Gender Performance">
{metrics.filter(m => m.category === 'gender').map((metric) => (
<option key={metric.key} value={metric.key}>{metric.label}</option>
))}
</optgroup>
<optgroup label="Equity (Disadvantaged)">
{metrics.filter(m => m.category === 'disadvantaged').map((metric) => (
<option key={metric.key} value={metric.key}>{metric.label}</option>
))}
</optgroup>
<optgroup label="School Context">
{metrics.filter(m => m.category === 'context').map((metric) => (
<option key={metric.key} value={metric.key}>{metric.label}</option>
))}
</optgroup>
<optgroup label="3-Year Trends">
{metrics.filter(m => m.category === '3yr').map((metric) => (
<option key={metric.key} value={metric.key}>{metric.label}</option>
))}
</optgroup>
</select>
</div>
<div className={styles.filterGroup}>
<label htmlFor="area-select" className={styles.filterLabel}>
Area:
</label>
<select
id="area-select"
value={selectedArea || ''}
onChange={(e) => handleAreaChange(e.target.value)}
className={styles.filterSelect}
>
<option value="">All Areas</option>
{filters.local_authorities.map((area) => (
<option key={area} value={area}>
{area}
</option>
))}
</select>
</div>
<div className={styles.filterGroup}>
<label htmlFor="year-select" className={styles.filterLabel}>
Year:
</label>
<select
id="year-select"
value={selectedYear?.toString() || ''}
onChange={(e) => handleYearChange(e.target.value)}
className={styles.filterSelect}
>
<option value="">Latest</option>
{filters.years.map((year) => (
<option key={year} value={year}>
{year}
</option>
))}
</select>
</div>
</section>
{/* Rankings Table */}
<section className={styles.rankingsSection}>
{rankings.length === 0 ? (
<div className={styles.noResults}>
<p>No rankings available for the selected filters.</p>
</div>
) : (
<div className={styles.tableWrapper}>
<table className={styles.rankingsTable}>
<thead>
<tr>
<th className={styles.rankHeader}>Rank</th>
<th className={styles.schoolHeader}>School</th>
<th className={styles.areaHeader}>Area</th>
<th className={styles.typeHeader}>Type</th>
<th className={styles.valueHeader}>{metricLabel}</th>
<th className={styles.actionHeader}>Action</th>
</tr>
</thead>
<tbody>
{rankings.map((ranking, index) => {
const rank = index + 1;
const isTopThree = rank <= 3;
const alreadyInComparison = isSelected(ranking.urn);
// Format the value
let displayValue: string;
if (ranking.value === null || ranking.value === undefined) {
displayValue = '-';
} else if (isProgressScore) {
displayValue = formatProgress(ranking.value);
} else if (isPercentage) {
displayValue = formatPercentage(ranking.value);
} else {
displayValue = ranking.value.toFixed(1);
}
return (
<tr
key={ranking.urn}
className={isTopThree ? styles[`rank${rank}`] : ''}
>
<td className={styles.rankCell}>
{isTopThree ? (
<span className={`${styles.rankBadge} ${styles[`rankBadge${rank}`]}`}>
{rank}
</span>
) : (
<span className={styles.rankNumber}>{rank}</span>
)}
</td>
<td className={styles.schoolCell}>
<a href={`/school/${ranking.urn}`} className={styles.schoolLink}>
{ranking.school_name}
</a>
</td>
<td className={styles.areaCell}>{ranking.local_authority || '-'}</td>
<td className={styles.typeCell}>{ranking.school_type || '-'}</td>
<td className={styles.valueCell}>
<strong>{displayValue}</strong>
</td>
<td className={styles.actionCell}>
<button
onClick={() => handleAddToCompare(ranking)}
disabled={alreadyInComparison}
className={styles.addButton}
>
{alreadyInComparison ? '✓ Added' : '+ Add'}
</button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</section>
</div>
);
}