feat(ux): implement comprehensive UX audit fixes across all pages
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:
@@ -11,8 +11,9 @@ import { useComparison } from '@/hooks/useComparison';
|
||||
import { ComparisonChart } from './ComparisonChart';
|
||||
import { SchoolSearchModal } from './SchoolSearchModal';
|
||||
import { EmptyState } from './EmptyState';
|
||||
import { LoadingSkeleton } from './LoadingSkeleton';
|
||||
import type { ComparisonData, MetricDefinition } from '@/lib/types';
|
||||
import { formatPercentage, formatProgress } from '@/lib/utils';
|
||||
import { formatPercentage, formatProgress, CHART_COLORS } from '@/lib/utils';
|
||||
import { fetchComparison } from '@/lib/api';
|
||||
import styles from './ComparisonView.module.css';
|
||||
|
||||
@@ -32,11 +33,25 @@ export function ComparisonView({
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const { selectedSchools, removeSchool } = useComparison();
|
||||
const { selectedSchools, removeSchool, addSchool, isInitialized } = useComparison();
|
||||
|
||||
const [selectedMetric, setSelectedMetric] = useState(initialMetric);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [comparisonData, setComparisonData] = useState(initialData);
|
||||
const [shareConfirm, setShareConfirm] = useState(false);
|
||||
|
||||
// Seed context from initialData when component mounts and localStorage is empty
|
||||
useEffect(() => {
|
||||
if (!isInitialized) return;
|
||||
if (selectedSchools.length === 0 && initialUrns.length > 0 && initialData) {
|
||||
initialUrns.forEach(urn => {
|
||||
const data = initialData[String(urn)];
|
||||
if (data?.school_info) {
|
||||
addSchool(data.school_info);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [isInitialized]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// Sync URL with selected schools
|
||||
useEffect(() => {
|
||||
@@ -56,10 +71,8 @@ export function ComparisonView({
|
||||
|
||||
// Fetch comparison data
|
||||
if (selectedSchools.length > 0) {
|
||||
console.log('Fetching comparison data for URNs:', urns);
|
||||
fetchComparison(urns, { cache: 'no-store' })
|
||||
.then((data) => {
|
||||
console.log('Comparison data received:', data);
|
||||
setComparisonData(data.comparison);
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -79,6 +92,14 @@ export function ComparisonView({
|
||||
removeSchool(urn);
|
||||
};
|
||||
|
||||
const handleShare = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(window.location.href);
|
||||
setShareConfirm(true);
|
||||
setTimeout(() => setShareConfirm(false), 2000);
|
||||
} catch { /* fallback: do nothing */ }
|
||||
};
|
||||
|
||||
// Get metric definition
|
||||
const currentMetricDef = metrics.find((m) => m.key === selectedMetric);
|
||||
const metricLabel = currentMetricDef?.label || selectedMetric;
|
||||
@@ -98,10 +119,12 @@ export function ComparisonView({
|
||||
title="No schools selected"
|
||||
message="Add schools from the home page or search to start comparing."
|
||||
action={{
|
||||
label: 'Browse Schools',
|
||||
onClick: () => router.push('/'),
|
||||
label: '+ Add Schools to Compare',
|
||||
onClick: () => setIsModalOpen(true),
|
||||
}}
|
||||
/>
|
||||
|
||||
<SchoolSearchModal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -123,9 +146,15 @@ export function ComparisonView({
|
||||
Comparing {selectedSchools.length} school{selectedSchools.length !== 1 ? 's' : ''}
|
||||
</p>
|
||||
</div>
|
||||
<button onClick={() => setIsModalOpen(true)} className={styles.addButton}>
|
||||
+ Add School
|
||||
</button>
|
||||
<div style={{ display: 'flex', gap: '0.75rem', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<button onClick={() => setIsModalOpen(true)} className={styles.addButton}>
|
||||
+ Add School
|
||||
</button>
|
||||
<button onClick={handleShare} className={styles.shareButton} title="Copy comparison link">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" width="16" height="16"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><polyline points="16 6 12 2 8 6"/><line x1="12" y1="2" x2="12" y2="15"/></svg>
|
||||
{shareConfirm ? 'Copied!' : 'Share'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -181,17 +210,32 @@ export function ComparisonView({
|
||||
))}
|
||||
</optgroup>
|
||||
</select>
|
||||
{currentMetricDef?.description && (
|
||||
<p className={styles.metricDescription}>{currentMetricDef.description}</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Progress score explanation */}
|
||||
{selectedMetric.includes('progress') && (
|
||||
<p className={styles.progressNote}>
|
||||
Progress scores measure pupils' progress from KS1 to KS2. A score of 0 equals the national average; positive scores are above average.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* School Cards */}
|
||||
<section className={styles.schoolsSection}>
|
||||
<div className={styles.schoolsGrid}>
|
||||
{selectedSchools.map((school) => (
|
||||
<div key={school.urn} className={styles.schoolCard}>
|
||||
{selectedSchools.map((school, index) => (
|
||||
<div
|
||||
key={school.urn}
|
||||
className={styles.schoolCard}
|
||||
style={{ borderLeft: `3px solid ${CHART_COLORS[index % CHART_COLORS.length]}` }}
|
||||
>
|
||||
<button
|
||||
onClick={() => handleRemoveSchool(school.urn)}
|
||||
className={styles.removeButton}
|
||||
aria-label="Remove school"
|
||||
title="Remove from comparison"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
@@ -211,7 +255,18 @@ export function ComparisonView({
|
||||
{comparisonData && comparisonData[school.urn] && (
|
||||
<div className={styles.latestValue}>
|
||||
<div className={styles.latestLabel}>{metricLabel}</div>
|
||||
<div className={styles.latestNumber}>
|
||||
<div className={styles.latestNumber} style={{ color: CHART_COLORS[index % CHART_COLORS.length] }}>
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
width: '10px',
|
||||
height: '10px',
|
||||
borderRadius: '50%',
|
||||
background: CHART_COLORS[index % CHART_COLORS.length],
|
||||
marginRight: '0.4rem',
|
||||
verticalAlign: 'middle',
|
||||
}}
|
||||
/>
|
||||
{(() => {
|
||||
const yearlyData = comparisonData[school.urn].yearly_data;
|
||||
if (yearlyData.length === 0) return '-';
|
||||
@@ -252,7 +307,7 @@ export function ComparisonView({
|
||||
</section>
|
||||
) : selectedSchools.length > 0 ? (
|
||||
<section className={styles.chartSection}>
|
||||
<div className={styles.loadingMessage}>Loading comparison data...</div>
|
||||
<LoadingSkeleton type="list" />
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user