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:
@@ -3,7 +3,7 @@
|
||||
* Main landing page with school search and browsing
|
||||
*/
|
||||
|
||||
import { fetchSchools, fetchFilters } from '@/lib/api';
|
||||
import { fetchSchools, fetchFilters, fetchDataInfo } from '@/lib/api';
|
||||
import { HomeView } from '@/components/HomeView';
|
||||
|
||||
interface HomePageProps {
|
||||
@@ -43,7 +43,7 @@ export default async function HomePage({ searchParams }: HomePageProps) {
|
||||
|
||||
// Fetch data on server with error handling
|
||||
try {
|
||||
const filtersData = await fetchFilters();
|
||||
const [filtersData, dataInfo] = await Promise.all([fetchFilters(), fetchDataInfo().catch(() => null)]);
|
||||
|
||||
// Only fetch schools if there are search parameters
|
||||
let schoolsData;
|
||||
@@ -66,6 +66,7 @@ export default async function HomePage({ searchParams }: HomePageProps) {
|
||||
<HomeView
|
||||
initialSchools={schoolsData}
|
||||
filters={filtersData || { local_authorities: [], school_types: [], years: [] }}
|
||||
totalSchools={dataInfo?.total_schools ?? null}
|
||||
/>
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -76,6 +77,7 @@ export default async function HomePage({ searchParams }: HomePageProps) {
|
||||
<HomeView
|
||||
initialSchools={{ schools: [], page: 1, page_size: 50, total: 0, total_pages: 0 }}
|
||||
filters={{ local_authorities: [], school_types: [], years: [] }}
|
||||
totalSchools={null}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,14 +20,15 @@
|
||||
|
||||
.toastContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
padding: 0.75rem 1rem 0.75rem 1.25rem;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
padding: 1rem 1.25rem;
|
||||
background: var(--bg-accent, #1a1612);
|
||||
color: var(--text-inverse, #faf7f2);
|
||||
border-radius: 50px;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 10px 30px rgba(26, 22, 18, 0.3);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
min-width: 260px;
|
||||
}
|
||||
|
||||
.toastInfo {
|
||||
@@ -59,6 +60,8 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding-top: 0.25rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.btnClear {
|
||||
@@ -93,6 +96,65 @@
|
||||
background: var(--bg-secondary, #f3ede4);
|
||||
}
|
||||
|
||||
.toastHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.toastTitle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-inverse, #faf7f2);
|
||||
}
|
||||
|
||||
.schoolList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
margin-bottom: 0.75rem;
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.schoolItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
padding: 0.25rem 0.375rem;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-radius: var(--radius-sm, 4px);
|
||||
}
|
||||
|
||||
.schoolName {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-inverse, #faf7f2);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.removeSchoolBtn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: rgba(250, 247, 242, 0.5);
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
padding: 0 0.25rem;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.removeSchoolBtn:hover {
|
||||
color: var(--text-inverse, #faf7f2);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.toastContainer {
|
||||
bottom: 1.5rem;
|
||||
@@ -100,8 +162,7 @@
|
||||
}
|
||||
|
||||
.toastContent {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
gap: 0;
|
||||
border-radius: 16px;
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { usePathname } from 'next/navigation';
|
||||
import styles from './ComparisonToast.module.css';
|
||||
|
||||
export function ComparisonToast() {
|
||||
const { selectedSchools, clearAll } = useComparison();
|
||||
const { selectedSchools, clearAll, removeSchool } = useComparison();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const pathname = usePathname();
|
||||
|
||||
@@ -25,19 +25,29 @@ export function ComparisonToast() {
|
||||
return (
|
||||
<div className={styles.toastContainer}>
|
||||
<div className={styles.toastContent}>
|
||||
<div className={styles.toastInfo}>
|
||||
<span className={styles.toastBadge}>{selectedSchools.length}</span>
|
||||
<span className={styles.toastText}>
|
||||
<div className={styles.toastHeader}>
|
||||
<span className={styles.toastTitle}>
|
||||
<span className={styles.toastBadge}>{selectedSchools.length}</span>
|
||||
{selectedSchools.length === 1 ? 'school' : 'schools'} selected
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.schoolList}>
|
||||
{selectedSchools.map(school => (
|
||||
<div key={school.urn} className={styles.schoolItem}>
|
||||
<span className={styles.schoolName} title={school.school_name}>
|
||||
{school.school_name.length > 28 ? school.school_name.slice(0, 28) + '…' : school.school_name}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => removeSchool(school.urn)}
|
||||
className={styles.removeSchoolBtn}
|
||||
aria-label={`Remove ${school.school_name}`}
|
||||
>×</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.toastActions}>
|
||||
<button onClick={clearAll} className={styles.btnClear}>
|
||||
Clear
|
||||
</button>
|
||||
<Link href="/compare" className={styles.btnCompare}>
|
||||
Compare Now
|
||||
</Link>
|
||||
<button onClick={clearAll} className={styles.btnClear}>Clear all</button>
|
||||
<Link href="/compare" className={styles.btnCompare}>Compare Now</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
margin-bottom: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
box-shadow: var(--shadow-soft, 0 2px 8px rgba(26, 22, 18, 0.06));
|
||||
}
|
||||
@@ -346,6 +347,44 @@
|
||||
margin: 0 auto 1.5rem;
|
||||
}
|
||||
|
||||
.metricDescription {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
max-width: 600px;
|
||||
flex-basis: 100%;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.progressNote {
|
||||
background: var(--bg-secondary);
|
||||
border-left: 3px solid var(--accent-teal);
|
||||
padding: 0.75rem 1rem;
|
||||
margin: 0 0 1.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
|
||||
}
|
||||
|
||||
.shareButton {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color, #e0ddd8);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition);
|
||||
}
|
||||
|
||||
.shareButton:hover {
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.headerContent {
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -163,3 +163,26 @@
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.radiusWrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.radiusLabel {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.radiusSelect {
|
||||
padding: 0.375rem 0.75rem;
|
||||
border: 1px solid var(--border-color, #e0ddd8);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-card);
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback, useTransition } from 'react';
|
||||
import { useState, useCallback, useTransition, useRef, useEffect } from 'react';
|
||||
import { useRouter, useSearchParams, usePathname } from 'next/navigation';
|
||||
import { isValidPostcode } from '@/lib/utils';
|
||||
import type { Filters } from '@/lib/types';
|
||||
@@ -16,9 +16,11 @@ export function FilterBar({ filters, isHero }: FilterBarProps) {
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const currentSearch = searchParams.get('search') || '';
|
||||
const currentPostcode = searchParams.get('postcode') || '';
|
||||
const currentRadius = searchParams.get('radius') || '1.6';
|
||||
const initialOmniValue = currentPostcode || currentSearch;
|
||||
|
||||
const [omniValue, setOmniValue] = useState(initialOmniValue);
|
||||
@@ -26,6 +28,21 @@ export function FilterBar({ filters, isHero }: FilterBarProps) {
|
||||
const currentLA = searchParams.get('local_authority') || '';
|
||||
const currentType = searchParams.get('school_type') || '';
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
// Focus search on '/' or Ctrl+K, but not when typing in an input
|
||||
if ((e.key === '/' || (e.key === 'k' && (e.ctrlKey || e.metaKey))) &&
|
||||
document.activeElement?.tagName !== 'INPUT' &&
|
||||
document.activeElement?.tagName !== 'TEXTAREA' &&
|
||||
document.activeElement?.tagName !== 'SELECT') {
|
||||
e.preventDefault();
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||
}, []);
|
||||
|
||||
const updateURL = useCallback((updates: Record<string, string>) => {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
|
||||
@@ -52,8 +69,7 @@ export function FilterBar({ filters, isHero }: FilterBarProps) {
|
||||
}
|
||||
|
||||
if (isValidPostcode(omniValue)) {
|
||||
// Default to 1 mile radius (approx 1.6 km)
|
||||
updateURL({ postcode: omniValue.trim().toUpperCase(), radius: '1.6', search: '' });
|
||||
updateURL({ postcode: omniValue.trim().toUpperCase(), radius: currentRadius || '1.6', search: '' });
|
||||
} else {
|
||||
updateURL({ search: omniValue.trim(), postcode: '', radius: '' });
|
||||
}
|
||||
@@ -77,6 +93,7 @@ export function FilterBar({ filters, isHero }: FilterBarProps) {
|
||||
<form onSubmit={handleSearchSubmit} className={styles.searchSection}>
|
||||
<div className={styles.omniBoxContainer}>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="search"
|
||||
value={omniValue}
|
||||
onChange={(e) => setOmniValue(e.target.value)}
|
||||
@@ -87,6 +104,23 @@ export function FilterBar({ filters, isHero }: FilterBarProps) {
|
||||
{isPending ? <div className={styles.spinner}></div> : 'Search'}
|
||||
</button>
|
||||
</div>
|
||||
{currentPostcode && (
|
||||
<div className={styles.radiusWrapper}>
|
||||
<label className={styles.radiusLabel}>Within:</label>
|
||||
<select
|
||||
value={currentRadius}
|
||||
onChange={e => updateURL({ radius: e.target.value })}
|
||||
className={styles.radiusSelect}
|
||||
disabled={isPending}
|
||||
>
|
||||
<option value="0.8">0.5 miles</option>
|
||||
<option value="1.6">1 mile</option>
|
||||
<option value="4.8">3 miles</option>
|
||||
<option value="8.0">5 miles</option>
|
||||
<option value="16.0">10 miles</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
|
||||
<div className={styles.filters}>
|
||||
|
||||
@@ -33,12 +33,6 @@ export function Footer() {
|
||||
Data Source
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<span className={styles.linkDisabled}>Privacy Policy</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className={styles.linkDisabled}>Terms of Use</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -539,3 +539,104 @@
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.discoverySection {
|
||||
padding: 2rem var(--page-padding, 2rem);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.discoveryCount {
|
||||
font-size: 1.1rem;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.discoveryCount strong {
|
||||
color: var(--text-primary);
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.discoveryHints {
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.quickSearches {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.quickSearchLabel {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.quickSearchChip {
|
||||
padding: 0.375rem 0.875rem;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-color, #e0ddd8);
|
||||
border-radius: 999px;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
transition: all var(--transition);
|
||||
}
|
||||
|
||||
.quickSearchChip:hover {
|
||||
background: var(--accent-coral);
|
||||
color: white;
|
||||
border-color: var(--accent-coral);
|
||||
}
|
||||
|
||||
.resultsHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
padding: 0 0 1rem;
|
||||
}
|
||||
|
||||
.sortSelect {
|
||||
padding: 0.375rem 0.75rem;
|
||||
border: 1px solid var(--border-color, #e0ddd8);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-card);
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.activeFilters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.filterChip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.25rem 0.625rem;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color, #e0ddd8);
|
||||
border-radius: 999px;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.chipRemove {
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1;
|
||||
transition: color var(--transition, 0.2s ease);
|
||||
}
|
||||
|
||||
.chipRemove:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
@@ -19,13 +19,15 @@ import styles from './HomeView.module.css';
|
||||
interface HomeViewProps {
|
||||
initialSchools: SchoolsResponse;
|
||||
filters: Filters;
|
||||
totalSchools?: number | null;
|
||||
}
|
||||
|
||||
export function HomeView({ initialSchools, filters }: HomeViewProps) {
|
||||
export function HomeView({ initialSchools, filters, totalSchools }: HomeViewProps) {
|
||||
const searchParams = useSearchParams();
|
||||
const { addSchool, selectedSchools } = useComparisonContext();
|
||||
const { addSchool, removeSchool, selectedSchools } = useComparisonContext();
|
||||
const [resultsView, setResultsView] = useState<'list' | 'map'>('list');
|
||||
const [selectedMapSchool, setSelectedMapSchool] = useState<School | null>(null);
|
||||
const [sortOrder, setSortOrder] = useState<string>('default');
|
||||
|
||||
const hasSearch = searchParams.get('search') || searchParams.get('postcode');
|
||||
const isLocationSearch = !!searchParams.get('postcode');
|
||||
@@ -36,19 +38,43 @@ export function HomeView({ initialSchools, filters }: HomeViewProps) {
|
||||
setSelectedMapSchool(null);
|
||||
}, [resultsView, searchParams]);
|
||||
|
||||
const sortedSchools = [...initialSchools.schools].sort((a, b) => {
|
||||
if (sortOrder === 'rwm_desc') return (b.rwm_expected_pct ?? -Infinity) - (a.rwm_expected_pct ?? -Infinity);
|
||||
if (sortOrder === 'rwm_asc') return (a.rwm_expected_pct ?? Infinity) - (b.rwm_expected_pct ?? Infinity);
|
||||
if (sortOrder === 'distance') return (a.distance ?? Infinity) - (b.distance ?? Infinity);
|
||||
if (sortOrder === 'name_asc') return a.school_name.localeCompare(b.school_name);
|
||||
return 0;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.homeView}>
|
||||
{/* Combined Hero + Search and Filters */}
|
||||
<div className={styles.heroSection}>
|
||||
<h1 className={styles.heroTitle}>Compare Primary School Performance</h1>
|
||||
<p className={styles.heroDescription}>Search and compare KS2 results for thousands of schools across England</p>
|
||||
</div>
|
||||
{!isSearchActive && (
|
||||
<div className={styles.heroSection}>
|
||||
<h1 className={styles.heroTitle}>Compare Primary School Performance</h1>
|
||||
<p className={styles.heroDescription}>Search and compare KS2 results for thousands of schools across England</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<FilterBar
|
||||
filters={filters}
|
||||
isHero={!isSearchActive}
|
||||
/>
|
||||
|
||||
{/* Discovery section shown on landing page before any search */}
|
||||
{!isSearchActive && initialSchools.schools.length === 0 && (
|
||||
<div className={styles.discoverySection}>
|
||||
{totalSchools && <p className={styles.discoveryCount}><strong>{totalSchools.toLocaleString()}+</strong> primary schools across England</p>}
|
||||
<p className={styles.discoveryHints}>Try searching for a school name, or enter a postcode to find schools near you.</p>
|
||||
<div className={styles.quickSearches}>
|
||||
<span className={styles.quickSearchLabel}>Quick searches:</span>
|
||||
{['Manchester', 'Bristol', 'Leeds', 'Birmingham'].map(city => (
|
||||
<a key={city} href={`/?search=${city}`} className={styles.quickSearchChip}>{city}</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Location Info Banner with View Toggle */}
|
||||
{isLocationSearch && initialSchools.location_info && (
|
||||
<div className={styles.locationBannerWrapper}>
|
||||
@@ -106,6 +132,22 @@ export function HomeView({ initialSchools, filters }: HomeViewProps) {
|
||||
{initialSchools.total.toLocaleString()} school
|
||||
{initialSchools.total !== 1 ? 's' : ''} found
|
||||
</h2>
|
||||
<select value={sortOrder} onChange={e => setSortOrder(e.target.value)} className={styles.sortSelect}>
|
||||
<option value="default">Sort: Relevance</option>
|
||||
<option value="rwm_desc">Highest RWM%</option>
|
||||
<option value="rwm_asc">Lowest RWM%</option>
|
||||
{isLocationSearch && <option value="distance">Nearest first</option>}
|
||||
<option value="name_asc">Name A–Z</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isSearchActive && (
|
||||
<div className={styles.activeFilters}>
|
||||
{searchParams.get('search') && <span className={styles.filterChip}>Search: {searchParams.get('search')}<a href="/" className={styles.chipRemove} onClick={e => { e.preventDefault(); }}>×</a></span>}
|
||||
{searchParams.get('local_authority') && <span className={styles.filterChip}>{searchParams.get('local_authority')}</span>}
|
||||
{searchParams.get('school_type') && <span className={styles.filterChip}>{searchParams.get('school_type')}</span>}
|
||||
{searchParams.get('postcode') && <span className={styles.filterChip}>Near {searchParams.get('postcode')} ({(parseFloat(searchParams.get('radius') || '1.6') / 1.60934).toFixed(1)} mi)</span>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -163,11 +205,12 @@ export function HomeView({ initialSchools, filters }: HomeViewProps) {
|
||||
/* List View Layout */
|
||||
<>
|
||||
<div className={styles.grid}>
|
||||
{initialSchools.schools.map((school) => (
|
||||
{sortedSchools.map((school) => (
|
||||
<SchoolCard
|
||||
key={school.urn}
|
||||
school={school}
|
||||
onAddToCompare={addSchool}
|
||||
onRemoveFromCompare={removeSchool}
|
||||
isInCompare={selectedSchools.some(s => s.urn === school.urn)}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -36,12 +36,6 @@ export function Navigation() {
|
||||
</Link>
|
||||
|
||||
<nav className={styles.nav}>
|
||||
<Link
|
||||
href="/"
|
||||
className={isActive('/') ? `${styles.navLink} ${styles.active}` : styles.navLink}
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
<Link
|
||||
href="/compare"
|
||||
className={isActive('/compare') ? `${styles.navLink} ${styles.active}` : styles.navLink}
|
||||
|
||||
@@ -350,3 +350,39 @@
|
||||
min-width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.limitNote {
|
||||
color: var(--text-muted);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.metricDescription {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
margin: -1rem 0 1.5rem;
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
.progressHint {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
margin: -1rem 0 1.5rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.viewButton {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.625rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary);
|
||||
border: 1px solid var(--border-color, #e0ddd8);
|
||||
border-radius: var(--radius-sm, 4px);
|
||||
text-decoration: none;
|
||||
margin-right: 0.375rem;
|
||||
transition: all var(--transition, 0.2s ease);
|
||||
}
|
||||
|
||||
.viewButton:hover {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -187,11 +187,34 @@
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btnPrimary.btnAdded {
|
||||
|
||||
.btnRemove {
|
||||
flex: 1;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
background: var(--bg-secondary, #f3ede4);
|
||||
color: var(--text-secondary, #5c564d);
|
||||
border: 1px solid var(--border-color, #e5dfd5);
|
||||
cursor: default;
|
||||
border: 1px solid var(--border-color, #e0ddd8);
|
||||
}
|
||||
|
||||
.btnRemove:hover {
|
||||
background: var(--border-color, #e0ddd8);
|
||||
color: var(--text-primary, #1a1612);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.metricHint {
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-muted, #8a847a);
|
||||
display: block;
|
||||
margin-top: 1px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
|
||||
@@ -11,12 +11,13 @@ import styles from './SchoolCard.module.css';
|
||||
interface SchoolCardProps {
|
||||
school: School;
|
||||
onAddToCompare?: (school: School) => void;
|
||||
onRemoveFromCompare?: (urn: number) => void;
|
||||
showDistance?: boolean;
|
||||
distance?: number;
|
||||
isInCompare?: boolean;
|
||||
}
|
||||
|
||||
export function SchoolCard({ school, onAddToCompare, showDistance, distance, isInCompare = false }: SchoolCardProps) {
|
||||
export function SchoolCard({ school, onAddToCompare, onRemoveFromCompare, showDistance, distance, isInCompare = false }: SchoolCardProps) {
|
||||
const trend = calculateTrend(school.rwm_expected_pct, school.prev_rwm_expected_pct);
|
||||
const trendColor = getTrendColor(trend);
|
||||
|
||||
@@ -51,9 +52,9 @@ export function SchoolCard({ school, onAddToCompare, showDistance, distance, isI
|
||||
<div className={styles.metrics}>
|
||||
{school.rwm_expected_pct !== null && (
|
||||
<div className={styles.metric}>
|
||||
<span className={styles.metricLabel} title="Percentage of pupils achieving the expected standard in Reading, Writing, and Maths">
|
||||
<span className={styles.metricLabel}>
|
||||
RWM Expected
|
||||
<svg className="info-icon" style={{ marginLeft: '4px', width: '10px', height: '10px', verticalAlign: 'middle', color: 'var(--text-muted)' }} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>
|
||||
<span className={styles.metricHint}>% meeting expected standard</span>
|
||||
</span>
|
||||
<div className={styles.metricValue}>
|
||||
<strong>{formatPercentage(school.rwm_expected_pct)}</strong>
|
||||
@@ -91,9 +92,9 @@ export function SchoolCard({ school, onAddToCompare, showDistance, distance, isI
|
||||
|
||||
{school.reading_progress !== null && (
|
||||
<div className={styles.metric}>
|
||||
<span className={styles.metricLabel} title="Progress score from KS1 to KS2 in Reading. >0 is above average.">
|
||||
<span className={styles.metricLabel}>
|
||||
Reading
|
||||
<svg className="info-icon" style={{ marginLeft: '4px', width: '10px', height: '10px', verticalAlign: 'middle', color: 'var(--text-muted)' }} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>
|
||||
<span className={styles.metricHint}>progress score (0 = avg)</span>
|
||||
</span>
|
||||
<strong>{formatProgress(school.reading_progress)}</strong>
|
||||
</div>
|
||||
@@ -101,9 +102,9 @@ export function SchoolCard({ school, onAddToCompare, showDistance, distance, isI
|
||||
|
||||
{school.writing_progress !== null && (
|
||||
<div className={styles.metric}>
|
||||
<span className={styles.metricLabel} title="Progress score from KS1 to KS2 in Writing. >0 is above average.">
|
||||
<span className={styles.metricLabel}>
|
||||
Writing
|
||||
<svg className="info-icon" style={{ marginLeft: '4px', width: '10px', height: '10px', verticalAlign: 'middle', color: 'var(--text-muted)' }} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>
|
||||
<span className={styles.metricHint}>progress score (0 = avg)</span>
|
||||
</span>
|
||||
<strong>{formatProgress(school.writing_progress)}</strong>
|
||||
</div>
|
||||
@@ -111,9 +112,9 @@ export function SchoolCard({ school, onAddToCompare, showDistance, distance, isI
|
||||
|
||||
{school.maths_progress !== null && (
|
||||
<div className={styles.metric}>
|
||||
<span className={styles.metricLabel} title="Progress score from KS1 to KS2 in Maths. >0 is above average.">
|
||||
<span className={styles.metricLabel}>
|
||||
Maths
|
||||
<svg className="info-icon" style={{ marginLeft: '4px', width: '10px', height: '10px', verticalAlign: 'middle', color: 'var(--text-muted)' }} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>
|
||||
<span className={styles.metricHint}>progress score (0 = avg)</span>
|
||||
</span>
|
||||
<strong>{formatProgress(school.maths_progress)}</strong>
|
||||
</div>
|
||||
@@ -122,16 +123,15 @@ export function SchoolCard({ school, onAddToCompare, showDistance, distance, isI
|
||||
)}
|
||||
|
||||
<div className={styles.actions}>
|
||||
<Link href={`/school/${school.urn}`} className={styles.btnSecondary}>
|
||||
<Link href={`/school/${school.urn}`} className={styles.btnPrimary}>
|
||||
View Details
|
||||
</Link>
|
||||
{onAddToCompare && (
|
||||
<button
|
||||
onClick={() => onAddToCompare(school)}
|
||||
className={`${styles.btnPrimary} ${isInCompare ? styles.btnAdded : ''}`}
|
||||
disabled={isInCompare}
|
||||
onClick={() => isInCompare ? onRemoveFromCompare?.(school.urn) : onAddToCompare(school)}
|
||||
className={isInCompare ? styles.btnRemove : styles.btnSecondary}
|
||||
>
|
||||
{isInCompare ? 'Added ✓' : 'Add to Compare'}
|
||||
{isInCompare ? '✓ Remove' : 'Add to Compare'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -395,3 +395,32 @@
|
||||
padding: 0.5rem 0.375rem;
|
||||
}
|
||||
}
|
||||
|
||||
.backNav {
|
||||
padding: 1rem var(--page-padding, 2rem);
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.backButton {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
padding: 0.375rem 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
transition: color var(--transition);
|
||||
}
|
||||
|
||||
.backButton:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.progressNote {
|
||||
margin-top: 0.75rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useComparison } from '@/hooks/useComparison';
|
||||
import { PerformanceChart } from './PerformanceChart';
|
||||
import { SchoolMap } from './SchoolMap';
|
||||
@@ -19,6 +20,7 @@ interface SchoolDetailViewProps {
|
||||
}
|
||||
|
||||
export function SchoolDetailView({ schoolInfo, yearlyData, absenceData }: SchoolDetailViewProps) {
|
||||
const router = useRouter();
|
||||
const { addSchool, removeSchool, isSelected } = useComparison();
|
||||
const isInComparison = isSelected(schoolInfo.urn);
|
||||
|
||||
@@ -36,6 +38,11 @@ export function SchoolDetailView({ schoolInfo, yearlyData, absenceData }: School
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{/* Back Navigation */}
|
||||
<div className={styles.backNav}>
|
||||
<button onClick={() => router.back()} className={styles.backButton}>← Back</button>
|
||||
</div>
|
||||
|
||||
{/* Header Section */}
|
||||
<header className={styles.header}>
|
||||
<div className={styles.headerContent}>
|
||||
@@ -52,9 +59,6 @@ export function SchoolDetailView({ schoolInfo, yearlyData, absenceData }: School
|
||||
{schoolInfo.school_type}
|
||||
</span>
|
||||
)}
|
||||
<span className={styles.metaItem}>
|
||||
URN: {schoolInfo.urn}
|
||||
</span>
|
||||
</div>
|
||||
{schoolInfo.address && (
|
||||
<p className={styles.address}>
|
||||
@@ -137,6 +141,23 @@ export function SchoolDetailView({ schoolInfo, yearlyData, absenceData }: School
|
||||
)}
|
||||
|
||||
</div>
|
||||
{(latestResults.reading_progress !== null || latestResults.writing_progress !== null || latestResults.maths_progress !== null) && (
|
||||
<p className={styles.progressNote}>Progress scores: 0 = national average. Positive = above average, negative = below average.</p>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Map */}
|
||||
{schoolInfo.latitude && schoolInfo.longitude && (
|
||||
<section className={styles.mapSection}>
|
||||
<h2 className={styles.sectionTitle}>Location</h2>
|
||||
<div className={styles.mapContainer}>
|
||||
<SchoolMap
|
||||
schools={[schoolInfo]}
|
||||
center={[schoolInfo.latitude, schoolInfo.longitude]}
|
||||
zoom={15}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
@@ -269,20 +290,6 @@ export function SchoolDetailView({ schoolInfo, yearlyData, absenceData }: School
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Map */}
|
||||
{schoolInfo.latitude && schoolInfo.longitude && (
|
||||
<section className={styles.mapSection}>
|
||||
<h2 className={styles.sectionTitle}>Location</h2>
|
||||
<div className={styles.mapContainer}>
|
||||
<SchoolMap
|
||||
schools={[schoolInfo]}
|
||||
center={[schoolInfo.latitude, schoolInfo.longitude]}
|
||||
zoom={15}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* All Years Data Table */}
|
||||
{yearlyData.length > 0 && (
|
||||
<section className={styles.historySection}>
|
||||
@@ -297,7 +304,6 @@ export function SchoolDetailView({ schoolInfo, yearlyData, absenceData }: School
|
||||
<th>Reading Progress</th>
|
||||
<th>Writing Progress</th>
|
||||
<th>Maths Progress</th>
|
||||
<th>Avg. Scaled Score</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -309,7 +315,6 @@ export function SchoolDetailView({ schoolInfo, yearlyData, absenceData }: School
|
||||
<td>{result.reading_progress !== null ? formatProgress(result.reading_progress) : '-'}</td>
|
||||
<td>{result.writing_progress !== null ? formatProgress(result.writing_progress) : '-'}</td>
|
||||
<td>{result.maths_progress !== null ? formatProgress(result.maths_progress) : '-'}</td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
Reference in New Issue
Block a user