feat(ux): 8 UX improvements — simpler home, advanced filters, phase tabs, 4-line rows
All checks were successful
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 48s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 1m13s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 32s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
All checks were successful
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 48s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 1m13s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 32s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
1. Simpler home page: only search box on landing, no filter dropdowns 2. Advanced filters: hidden behind toggle on results page, auto-open if active 3. Per-school phase rendering: each row renders based on its own data 4. Taller 4-line rows with context line (type, age range, denomination, gender) 5. Result-scoped filters: dropdown values reflect current search results 6. Fix blank filter values: exclude empty strings and "Not applicable" 7. Rankings: Primary/Secondary phase tabs with phase-specific metrics 8. Compare: Primary/Secondary tabs with school counts and phase metrics Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* RankingsView Component
|
||||
* Client-side rankings interface with filters
|
||||
* Client-side rankings interface with phase tabs and filters
|
||||
*/
|
||||
|
||||
'use client';
|
||||
@@ -12,6 +12,25 @@ import { formatPercentage, formatProgress } from '@/lib/utils';
|
||||
import { EmptyState } from './EmptyState';
|
||||
import styles from './RankingsView.module.css';
|
||||
|
||||
const PRIMARY_CATEGORIES = ['expected', 'higher', 'progress', 'average', 'gender', 'equity', 'context', 'absence', 'trends'];
|
||||
const SECONDARY_CATEGORIES = ['gcse'];
|
||||
|
||||
const PRIMARY_OPTGROUPS: { label: string; category: string }[] = [
|
||||
{ label: 'Expected Standard', category: 'expected' },
|
||||
{ label: 'Higher Standard', category: 'higher' },
|
||||
{ label: 'Progress Scores', category: 'progress' },
|
||||
{ label: 'Average Scores', category: 'average' },
|
||||
{ label: 'Gender Performance', category: 'gender' },
|
||||
{ label: 'Equity (Disadvantaged)', category: 'equity' },
|
||||
{ label: 'School Context', category: 'context' },
|
||||
{ label: 'Absence', category: 'absence' },
|
||||
{ label: '3-Year Trends', category: 'trends' },
|
||||
];
|
||||
|
||||
const SECONDARY_OPTGROUPS: { label: string; category: string }[] = [
|
||||
{ label: 'GCSE Performance', category: 'gcse' },
|
||||
];
|
||||
|
||||
interface RankingsViewProps {
|
||||
rankings: RankingEntry[];
|
||||
filters: Filters;
|
||||
@@ -19,6 +38,7 @@ interface RankingsViewProps {
|
||||
selectedMetric: string;
|
||||
selectedArea?: string;
|
||||
selectedYear?: number;
|
||||
selectedPhase?: string;
|
||||
}
|
||||
|
||||
export function RankingsView({
|
||||
@@ -28,12 +48,17 @@ export function RankingsView({
|
||||
selectedMetric,
|
||||
selectedArea,
|
||||
selectedYear,
|
||||
selectedPhase = 'primary',
|
||||
}: RankingsViewProps) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const { addSchool, isSelected } = useComparison();
|
||||
|
||||
const isPrimary = selectedPhase === 'primary';
|
||||
const allowedCategories = isPrimary ? PRIMARY_CATEGORIES : SECONDARY_CATEGORIES;
|
||||
const optgroups = isPrimary ? PRIMARY_OPTGROUPS : SECONDARY_OPTGROUPS;
|
||||
|
||||
const updateFilters = (updates: Record<string, string | undefined>) => {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
|
||||
@@ -48,6 +73,11 @@ export function RankingsView({
|
||||
router.push(`${pathname}?${params.toString()}`);
|
||||
};
|
||||
|
||||
const handlePhaseChange = (phase: string) => {
|
||||
const defaultMetric = phase === 'secondary' ? 'attainment_8_score' : 'rwm_expected_pct';
|
||||
updateFilters({ phase, metric: defaultMetric });
|
||||
};
|
||||
|
||||
const handleMetricChange = (metric: string) => {
|
||||
updateFilters({ metric });
|
||||
};
|
||||
@@ -63,7 +93,6 @@ export function RankingsView({
|
||||
const handleAddToCompare = (ranking: RankingEntry) => {
|
||||
addSchool({
|
||||
...ranking,
|
||||
// Ensure required School fields are present
|
||||
address: null,
|
||||
postcode: null,
|
||||
latitude: null,
|
||||
@@ -77,6 +106,9 @@ export function RankingsView({
|
||||
const isProgressScore = selectedMetric.includes('progress');
|
||||
const isPercentage = selectedMetric.includes('pct') || selectedMetric.includes('rate');
|
||||
|
||||
// Filter metrics to only show relevant categories
|
||||
const filteredMetrics = metrics.filter(m => allowedCategories.includes(m.category));
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{/* Header */}
|
||||
@@ -84,10 +116,26 @@ export function RankingsView({
|
||||
<h1>School Rankings</h1>
|
||||
<p className={styles.subtitle}>
|
||||
Top-performing schools by {metricLabel.toLowerCase()}
|
||||
{!selectedArea && <span className={styles.limitNote}> — showing top {rankings.length}</span>}
|
||||
{!selectedArea && rankings.length > 0 && <span className={styles.limitNote}> — showing top {rankings.length}</span>}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{/* Phase Tabs */}
|
||||
<div className={styles.phaseTabs}>
|
||||
<button
|
||||
className={`${styles.phaseTab} ${isPrimary ? styles.phaseTabActive : ''}`}
|
||||
onClick={() => handlePhaseChange('primary')}
|
||||
>
|
||||
Primary (KS2)
|
||||
</button>
|
||||
<button
|
||||
className={`${styles.phaseTab} ${!isPrimary ? styles.phaseTabActive : ''}`}
|
||||
onClick={() => handlePhaseChange('secondary')}
|
||||
>
|
||||
Secondary (GCSE)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{currentMetricDef?.description && (
|
||||
<p className={styles.metricDescription}>{currentMetricDef.description}</p>
|
||||
)}
|
||||
@@ -107,46 +155,17 @@ export function RankingsView({
|
||||
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>
|
||||
{optgroups.map(({ label, category }) => {
|
||||
const groupMetrics = filteredMetrics.filter(m => m.category === category);
|
||||
if (groupMetrics.length === 0) return null;
|
||||
return (
|
||||
<optgroup key={category} label={label}>
|
||||
{groupMetrics.map((metric) => (
|
||||
<option key={metric.key} value={metric.key}>{metric.label}</option>
|
||||
))}
|
||||
</optgroup>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -199,7 +218,7 @@ export function RankingsView({
|
||||
message="Try selecting a different metric, area, or year."
|
||||
action={{
|
||||
label: 'Clear filters',
|
||||
onClick: () => router.push(pathname),
|
||||
onClick: () => router.push(`${pathname}?phase=${selectedPhase}`),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user