updates for secondary schools
All checks were successful
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 46s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 1m15s
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

This commit is contained in:
2026-03-28 22:36:00 +00:00
parent f3a8ebdb4b
commit e8175561d5
16 changed files with 2170 additions and 42 deletions

View File

@@ -5,14 +5,15 @@
'use client';
import { useState, useEffect } from 'react';
import { useState, useEffect, useRef } from 'react';
import { useSearchParams } from 'next/navigation';
import { FilterBar } from './FilterBar';
import { SchoolRow } from './SchoolRow';
import { SecondarySchoolRow } from './SecondarySchoolRow';
import { SchoolMap } from './SchoolMap';
import { Pagination } from './Pagination';
import { EmptyState } from './EmptyState';
import { useComparisonContext } from '@/context/ComparisonContext';
import { fetchSchools, fetchLAaverages } from '@/lib/api';
import type { SchoolsResponse, Filters, School } from '@/lib/types';
import styles from './HomeView.module.css';
@@ -28,19 +29,67 @@ export function HomeView({ initialSchools, filters, totalSchools }: HomeViewProp
const [resultsView, setResultsView] = useState<'list' | 'map'>('list');
const [selectedMapSchool, setSelectedMapSchool] = useState<School | null>(null);
const [sortOrder, setSortOrder] = useState<string>('default');
const [allSchools, setAllSchools] = useState<School[]>(initialSchools.schools);
const [currentPage, setCurrentPage] = useState(initialSchools.page);
const [hasMore, setHasMore] = useState(initialSchools.total_pages > 1);
const [isLoadingMore, setIsLoadingMore] = useState(false);
const [laAverages, setLaAverages] = useState<Record<string, number>>({});
const prevSearchParamsRef = useRef(searchParams.toString());
const hasSearch = searchParams.get('search') || searchParams.get('postcode');
const isLocationSearch = !!searchParams.get('postcode');
const isSearchActive = !!(hasSearch || searchParams.get('local_authority') || searchParams.get('school_type'));
const currentPhase = searchParams.get('phase') || '';
const hasSecondaryResults = allSchools.some(s => s.attainment_8_score != null);
const isSecondaryView = currentPhase.toLowerCase().includes('secondary') || hasSecondaryResults;
// Reset pagination state when search params change
useEffect(() => {
const newParamsStr = searchParams.toString();
if (newParamsStr !== prevSearchParamsRef.current) {
prevSearchParamsRef.current = newParamsStr;
setAllSchools(initialSchools.schools);
setCurrentPage(initialSchools.page);
setHasMore(initialSchools.total_pages > 1);
}
}, [searchParams, initialSchools]);
// Close bottom sheet if we change views or search
useEffect(() => {
setSelectedMapSchool(null);
}, [resultsView, searchParams]);
const sortedSchools = [...initialSchools.schools].sort((a, b) => {
// Fetch LA averages when secondary schools are visible
useEffect(() => {
if (!isSecondaryView) return;
fetchLAaverages({ cache: 'force-cache' })
.then(data => setLaAverages(data.secondary.attainment_8_by_la))
.catch(() => {});
}, [isSecondaryView]);
const handleLoadMore = async () => {
if (isLoadingMore || !hasMore) return;
setIsLoadingMore(true);
try {
const params: Record<string, any> = {};
searchParams.forEach((value, key) => { params[key] = value; });
params.page = currentPage + 1;
const response = await fetchSchools(params, { cache: 'no-store' });
setAllSchools(prev => [...prev, ...response.schools]);
setCurrentPage(response.page);
setHasMore(response.page < response.total_pages);
} catch {
// silently ignore
} finally {
setIsLoadingMore(false);
}
};
const sortedSchools = [...allSchools].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 === 'att8_desc') return (b.attainment_8_score ?? -Infinity) - (a.attainment_8_score ?? -Infinity);
if (sortOrder === 'att8_asc') return (a.attainment_8_score ?? Infinity) - (b.attainment_8_score ?? 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;
@@ -134,8 +183,10 @@ export function HomeView({ initialSchools, filters, totalSchools }: HomeViewProp
</h2>
<select value={sortOrder} onChange={e => setSortOrder(e.target.value)} className={styles.sortSelect}>
<option value="default">Sort: Relevance</option>
<option value="rwm_desc">Highest R, W &amp; M %</option>
<option value="rwm_asc">Lowest R, W &amp; M %</option>
{!isSecondaryView && <option value="rwm_desc">Highest R, W &amp; M %</option>}
{!isSecondaryView && <option value="rwm_asc">Lowest R, W &amp; M %</option>}
{isSecondaryView && <option value="att8_desc">Highest Attainment 8</option>}
{isSecondaryView && <option value="att8_asc">Lowest Attainment 8</option>}
{isLocationSearch && <option value="distance">Nearest first</option>}
<option value="name_asc">Name AZ</option>
</select>
@@ -206,23 +257,44 @@ export function HomeView({ initialSchools, filters, totalSchools }: HomeViewProp
<>
<div className={styles.schoolList}>
{sortedSchools.map((school) => (
<SchoolRow
key={school.urn}
school={school}
isLocationSearch={isLocationSearch}
onAddToCompare={addSchool}
onRemoveFromCompare={removeSchool}
isInCompare={selectedSchools.some(s => s.urn === school.urn)}
/>
isSecondaryView || school.attainment_8_score != null ? (
<SecondarySchoolRow
key={school.urn}
school={school}
isLocationSearch={isLocationSearch}
onAddToCompare={addSchool}
onRemoveFromCompare={removeSchool}
isInCompare={selectedSchools.some(s => s.urn === school.urn)}
laAvgAttainment8={school.local_authority ? laAverages[school.local_authority] ?? null : null}
/>
) : (
<SchoolRow
key={school.urn}
school={school}
isLocationSearch={isLocationSearch}
onAddToCompare={addSchool}
onRemoveFromCompare={removeSchool}
isInCompare={selectedSchools.some(s => s.urn === school.urn)}
/>
)
))}
</div>
{initialSchools.total_pages > 1 && (
<Pagination
currentPage={initialSchools.page}
totalPages={initialSchools.total_pages}
total={initialSchools.total}
/>
{(hasMore || allSchools.length < initialSchools.total) && (
<div className={styles.loadMoreSection}>
<p className={styles.loadMoreCount}>
Showing {allSchools.length.toLocaleString()} of {initialSchools.total.toLocaleString()} schools
</p>
{hasMore && (
<button
onClick={handleLoadMore}
disabled={isLoadingMore}
className={`btn btn-secondary ${styles.loadMoreButton}`}
>
{isLoadingMore ? 'Loading...' : 'Load more schools'}
</button>
)}
</div>
)}
</>
)}