feat(ux): implement comprehensive UX audit fixes across all pages
All checks were successful
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 1m8s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 1m5s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s

Addresses 28 issues identified in UX audit (P0–P3 severity):

P0 — Critical:
- Fix compare URL sharing: seed ComparisonContext from SSR initialData
  when localStorage is empty, making /compare?urns=... links shareable
- Remove permanently broken "Avg. Scaled Score" column from school
  detail historical data table

P1 — High priority:
- Add radius selector (0.5–10 mi) to postcode search in FilterBar
- Make Add to Compare a toggle (remove) on SchoolCards
- Hide hero title/description once a search is active
- Show school count + quick-search prompts on empty landing page
- Compare empty state opens in-page school search modal directly
- Remove URN from school detail header (irrelevant to end users)
- Move map above performance chart in school detail page
- Add ← Back navigation to school detail page
- Add sort controls to search results (RWM%, distance, A–Z)
- Show metric descriptions below metric selector
- Expand ComparisonToast to list school names with per-school remove
- Add progress score explainer (0 = national average) throughout

P2 — Medium:
- Remove console.log statements from ComparisonView
- Colour-code comparison school cards to match chart line colours
- Replace plain loading text with LoadingSkeleton in ComparisonView
- Rankings empty state uses shared EmptyState component
- Rankings year filter shows actual year e.g. "2023 (Latest)"
- Rankings subtitle shows top-N count
- Add View link alongside Add button in rankings table
- Remove placeholder Privacy Policy / Terms links from footer
- Replace untappable 10px info icons with visible metric hint text
- Show active filter chips in search results header

P3 — Polish:
- Remove redundant "Home" nav link (logo already links home)
- Add / and Ctrl+K keyboard shortcut to focus search input
- Add Share button to compare page (copies URL to clipboard)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-23 21:31:28 +00:00
parent d4abb56c22
commit 3d24050d11
17 changed files with 564 additions and 98 deletions

View File

@@ -9,6 +9,7 @@ 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 { EmptyState } from './EmptyState';
import styles from './RankingsView.module.css';
interface RankingsViewProps {
@@ -83,9 +84,17 @@ 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>}
</p>
</header>
{currentMetricDef?.description && (
<p className={styles.metricDescription}>{currentMetricDef.description}</p>
)}
{isProgressScore && (
<p className={styles.progressHint}>Progress scores: 0 = national average. Positive = above average.</p>
)}
{/* Filters */}
<section className={styles.filters}>
<div className={styles.filterGroup}>
@@ -170,7 +179,9 @@ export function RankingsView({
onChange={(e) => handleYearChange(e.target.value)}
className={styles.filterSelect}
>
<option value="">Latest</option>
<option value="">
{filters.years.length > 0 ? `${Math.max(...filters.years)} (Latest)` : 'Latest'}
</option>
{filters.years.map((year) => (
<option key={year} value={year}>
{year}
@@ -183,9 +194,14 @@ export function RankingsView({
{/* Rankings Table */}
<section className={styles.rankingsSection}>
{rankings.length === 0 ? (
<div className={styles.noResults}>
<p>No rankings available for the selected filters.</p>
</div>
<EmptyState
title="No rankings found"
message="Try selecting a different metric, area, or year."
action={{
label: 'Clear filters',
onClick: () => router.push(pathname),
}}
/>
) : (
<div className={styles.tableWrapper}>
<table className={styles.rankingsTable}>
@@ -242,6 +258,7 @@ export function RankingsView({
<strong>{displayValue}</strong>
</td>
<td className={styles.actionCell}>
<a href={`/school/${ranking.urn}`} className={styles.viewButton}>View</a>
<button
onClick={() => handleAddToCompare(ranking)}
disabled={alreadyInComparison}