feat(map): rebuild popup as mini card with Ofsted badge and headline metric
Replaces the bare Leaflet popup with a mini card showing school name, 3-state Ofsted badge (OEIF grade / ReportCard / pending), phase tag, headline metric (Att8 for secondary, RWM% for primary) with delta vs LA/national average, and a styled View Details button. Threads nationalAvgRwm and laAverages from HomeView → SchoolMap → LeafletMapInner. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -261,6 +261,7 @@ export function HomeView({ initialSchools, filters, totalSchools }: HomeViewProp
|
||||
referencePoint={initialSchools.location_info?.coordinates}
|
||||
onMarkerClick={setSelectedMapSchool}
|
||||
nationalAvgRwm={nationalAvgRwm}
|
||||
laAverages={laAverages}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.compactList}>
|
||||
|
||||
@@ -25,9 +25,39 @@ interface LeafletMapInnerProps {
|
||||
zoom: number;
|
||||
referencePoint?: [number, number];
|
||||
onMarkerClick?: (school: School) => void;
|
||||
nationalAvgRwm?: number | null;
|
||||
laAverages?: Record<string, number | null>;
|
||||
}
|
||||
|
||||
export default function LeafletMapInner({ schools, center, zoom, referencePoint, onMarkerClick }: LeafletMapInnerProps) {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Popup helpers (must work in plain JS string templates — no React / CSS Modules)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface PopupBadge {
|
||||
label: string;
|
||||
style: string;
|
||||
}
|
||||
|
||||
function buildPopupBadge(school: School): PopupBadge {
|
||||
const year = school.ofsted_date ? new Date(school.ofsted_date).getFullYear() : null;
|
||||
const yearStr = year ? ` · ${year}` : '';
|
||||
if (school.ofsted_grade) {
|
||||
const labels: Record<number, string> = { 1: 'Outstanding', 2: 'Good', 3: 'Req. Improvement', 4: 'Inadequate' };
|
||||
const colours: Record<number, string> = {
|
||||
1: 'background:#d4f0ea;color:#2d7d7d',
|
||||
2: 'background:rgba(60,140,60,0.12);color:#3c8c3c',
|
||||
3: 'background:#fef3cd;color:#b8920e',
|
||||
4: 'background:#fde8e0;color:#e07256',
|
||||
};
|
||||
return { label: `${labels[school.ofsted_grade]}${yearStr}`, style: colours[school.ofsted_grade] };
|
||||
}
|
||||
if (school.ofsted_framework === 'ReportCard') {
|
||||
return { label: `Report Card${yearStr}`, style: 'background:#5a3a6e;color:#fff' };
|
||||
}
|
||||
return { label: 'Not yet inspected', style: 'background:#e0e0e0;color:#666' };
|
||||
}
|
||||
|
||||
export default function LeafletMapInner({ schools, center, zoom, referencePoint, onMarkerClick, nationalAvgRwm, laAverages }: LeafletMapInnerProps) {
|
||||
const mapRef = useRef<L.Map | null>(null);
|
||||
const mapContainerRef = useRef<HTMLDivElement>(null);
|
||||
const refMarkerRef = useRef<L.Marker | null>(null);
|
||||
@@ -81,14 +111,71 @@ export default function LeafletMapInner({ schools, center, zoom, referencePoint,
|
||||
const marker = L.marker([school.latitude, school.longitude]).addTo(mapRef.current);
|
||||
|
||||
// Create popup content
|
||||
const popupContent = `
|
||||
<div style="min-width: 200px;">
|
||||
<strong style="font-size: 14px; display: block; margin-bottom: 8px;">${school.school_name}</strong>
|
||||
${school.local_authority ? `<div style="font-size: 12px; color: #666; margin-bottom: 4px;">${school.local_authority}</div>` : ''}
|
||||
${school.school_type ? `<div style="font-size: 12px; color: #666; margin-bottom: 8px;">${school.school_type}</div>` : ''}
|
||||
<a href="${schoolUrl(school.urn, school.school_name)}" style="display: inline-block; margin-top: 8px; padding: 6px 12px; background: #e07256; color: white; text-decoration: none; border-radius: 4px; font-size: 12px;">View Details</a>
|
||||
</div>
|
||||
`;
|
||||
const badge = buildPopupBadge(school);
|
||||
const isSecondary = school.attainment_8_score != null;
|
||||
|
||||
// Phase label
|
||||
const rawPhase = (school.phase ?? '').toLowerCase();
|
||||
const phaseLabel =
|
||||
rawPhase.includes('secondary') ? 'Secondary' :
|
||||
rawPhase === 'all-through' ? 'All-through' :
|
||||
rawPhase.includes('primary') ? 'Primary' :
|
||||
isSecondary ? 'Secondary' : 'Primary';
|
||||
|
||||
// Distance string
|
||||
const distanceStr =
|
||||
school.distance != null ? ` · ${school.distance.toFixed(1)} mi` : '';
|
||||
|
||||
// Headline metric
|
||||
let metricHtml = '';
|
||||
if (isSecondary) {
|
||||
const score = school.attainment_8_score!;
|
||||
const laAvg = school.local_authority ? (laAverages?.[school.local_authority] ?? null) : null;
|
||||
let deltaLine = '';
|
||||
if (laAvg != null) {
|
||||
const diff = Math.round((score - laAvg) * 10) / 10;
|
||||
const sign = diff >= 0 ? '+' : '';
|
||||
const colour = diff >= 0.5 ? '#2d7d7d' : diff <= -0.5 ? '#e07256' : '#8a847a';
|
||||
const laName = school.local_authority ?? 'LA';
|
||||
deltaLine = `<div style="font-size:11px;font-weight:600;color:${colour}">${sign}${diff} vs ${laName} avg</div>`;
|
||||
}
|
||||
metricHtml = `<div style="margin-bottom:4px">
|
||||
<span style="font-size:20px;font-weight:700;color:#1a1612;font-family:Georgia,serif">${score.toFixed(1)}</span>
|
||||
<span style="font-size:11px;color:#8a847a;margin-left:4px">Attainment 8</span>
|
||||
${deltaLine}
|
||||
</div>`;
|
||||
} else if (school.rwm_expected_pct != null) {
|
||||
const rwm = school.rwm_expected_pct;
|
||||
let deltaLine = '';
|
||||
if (nationalAvgRwm != null) {
|
||||
const diff = Math.round(rwm - nationalAvgRwm);
|
||||
const colour = diff >= 2 ? '#2d7d7d' : diff <= -2 ? '#e07256' : '#8a847a';
|
||||
const text =
|
||||
diff >= 2 ? `+${diff} pts vs national` :
|
||||
diff <= -2 ? `${diff} pts vs national` :
|
||||
'≈ national avg';
|
||||
deltaLine = `<div style="font-size:11px;font-weight:600;color:${colour}">${text}</div>`;
|
||||
}
|
||||
metricHtml = `<div style="margin-bottom:4px">
|
||||
<span style="font-size:20px;font-weight:700;color:#1a1612;font-family:Georgia,serif">${rwm}%</span>
|
||||
<span style="font-size:11px;color:#8a847a;margin-left:4px">Reading, Writing & Maths</span>
|
||||
${deltaLine}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const slug = schoolUrl(school.urn, school.school_name);
|
||||
|
||||
const popupContent = `<div style="font-family:system-ui,sans-serif;min-width:240px;max-width:280px;padding:0">
|
||||
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:8px;margin-bottom:6px">
|
||||
<strong style="font-size:13px;color:#1a1612;line-height:1.3">${school.school_name}</strong>
|
||||
<span style="font-size:10px;font-weight:700;padding:2px 6px;border-radius:3px;white-space:nowrap;flex-shrink:0;${badge.style}">${badge.label}</span>
|
||||
</div>
|
||||
<div style="font-size:11px;color:#8a847a;margin-bottom:8px">
|
||||
${phaseLabel}${school.local_authority ? ` · ${school.local_authority}` : ''}${distanceStr}
|
||||
</div>
|
||||
${metricHtml}
|
||||
<a href="${slug}" style="display:block;text-align:center;padding:6px;background:#2d7d7d;color:white;border-radius:5px;text-decoration:none;font-size:12px;font-weight:600;margin-top:8px">View Details →</a>
|
||||
</div>`;
|
||||
|
||||
marker.bindPopup(popupContent);
|
||||
|
||||
|
||||
@@ -28,10 +28,10 @@ interface SchoolMapProps {
|
||||
referencePoint?: [number, number];
|
||||
onMarkerClick?: (school: School) => void;
|
||||
nationalAvgRwm?: number | null;
|
||||
laAverages?: Record<string, number | null>;
|
||||
}
|
||||
|
||||
export function SchoolMap({ schools, center, zoom = 13, referencePoint, onMarkerClick, nationalAvgRwm: _nationalAvgRwm }: SchoolMapProps) {
|
||||
// TODO: thread _nationalAvgRwm to LeafletMapInner (Task 7)
|
||||
export function SchoolMap({ schools, center, zoom = 13, referencePoint, onMarkerClick, nationalAvgRwm, laAverages }: SchoolMapProps) {
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
|
||||
@@ -91,6 +91,8 @@ export function SchoolMap({ schools, center, zoom = 13, referencePoint, onMarker
|
||||
zoom={zoom}
|
||||
referencePoint={referencePoint}
|
||||
onMarkerClick={onMarkerClick}
|
||||
nationalAvgRwm={nationalAvgRwm}
|
||||
laAverages={laAverages}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user