feat: similar schools nearby on the detail page #150
No files matched your search
@@ -172,3 +172,39 @@ describe('buildSecondaryNavItems', () => {
|
||||
expect(ids).not.toContain('history');
|
||||
});
|
||||
});
|
||||
|
||||
describe('the similar-schools nav item', () => {
|
||||
const navInput = {
|
||||
ofsted: null, admissions: null, admissionDistance: null,
|
||||
hasLocation: true, yearlyDataLength: 1,
|
||||
};
|
||||
|
||||
it('appears on both templates when the section renders', () => {
|
||||
const primary = computeSchoolFlags(primaryFixture);
|
||||
const secondary = computeSecondaryFlags(secondaryFixture);
|
||||
const input = { ...navInput, hasSimilarSchools: true };
|
||||
|
||||
expect(buildNavItems(primary, input).map((i) => i.id)).toContain('similar');
|
||||
expect(buildSecondaryNavItems(secondary, input).map((i) => i.id)).toContain('similar');
|
||||
});
|
||||
|
||||
it('is absent when the section does not render', () => {
|
||||
const primary = computeSchoolFlags(primaryFixture);
|
||||
const secondary = computeSecondaryFlags(secondaryFixture);
|
||||
const input = { ...navInput, hasSimilarSchools: false };
|
||||
|
||||
expect(buildNavItems(primary, input).map((i) => i.id)).not.toContain('similar');
|
||||
expect(buildSecondaryNavItems(secondary, input).map((i) => i.id)).not.toContain('similar');
|
||||
});
|
||||
|
||||
it('is absent when nothing says either way', () => {
|
||||
const primary = computeSchoolFlags(primaryFixture);
|
||||
expect(buildNavItems(primary, navInput).map((i) => i.id)).not.toContain('similar');
|
||||
});
|
||||
|
||||
it('comes last, because the section renders last', () => {
|
||||
const primary = computeSchoolFlags(primaryFixture);
|
||||
const ids = buildNavItems(primary, { ...navInput, hasSimilarSchools: true }).map((i) => i.id);
|
||||
expect(ids[ids.length - 1]).toBe('similar');
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,7 @@ import { APIFetchError, fetchSchoolDetails, fetchSchools, fetchNationalAverages
|
||||
import { notFound, redirect } from 'next/navigation';
|
||||
import { SchoolDetailShell } from '@/components/school/SchoolDetailShell';
|
||||
import { NearbyPlaces } from '@/components/school/NearbyPlaces';
|
||||
import { shouldRenderSimilar } from '@/components/school/SimilarSchoolsSection';
|
||||
import { schoolBreadcrumbJsonLd, type SchoolPlace } from '@/lib/jsonld';
|
||||
import { PrimarySchoolSections } from '@/components/school/PrimarySchoolSections';
|
||||
import { SecondarySchoolSections } from '@/components/school/SecondarySchoolSections';
|
||||
@@ -155,6 +156,8 @@ export default async function SchoolPage({ params }: SchoolPageProps) {
|
||||
// nothing rather than throwing, which is how this shipped without a
|
||||
// lockstep deploy of the two images.
|
||||
const places: SchoolPlace[] = data.places ?? [];
|
||||
// Absent on an older API build, exactly like `places` above.
|
||||
const similarSchools = data.similar_schools ?? [];
|
||||
|
||||
// Redirect bare URN to canonical slug URL
|
||||
const canonicalSlug = schoolUrl(urn, school_info.school_name).replace('/school/', '');
|
||||
@@ -186,6 +189,7 @@ export default async function SchoolPage({ params }: SchoolPageProps) {
|
||||
admissions: admissions ?? null,
|
||||
admissionDistance: admission_distance ?? null,
|
||||
hasLocation: school_info.latitude != null && school_info.longitude != null,
|
||||
hasSimilarSchools: shouldRenderSimilar(similarSchools),
|
||||
yearlyDataLength: yearly_data.length,
|
||||
};
|
||||
const primaryNavItems = buildNavItems(primaryFlags, navInput);
|
||||
@@ -262,6 +266,7 @@ export default async function SchoolPage({ params }: SchoolPageProps) {
|
||||
finance={finance ?? null}
|
||||
nationalAvg={nationalAvg}
|
||||
destinations={destinations ?? null}
|
||||
similarSchools={similarSchools}
|
||||
flags={secondaryFlags}
|
||||
/>
|
||||
</SchoolDetailShell>
|
||||
@@ -284,6 +289,7 @@ export default async function SchoolPage({ params }: SchoolPageProps) {
|
||||
deprivation={deprivation ?? null}
|
||||
finance={finance ?? null}
|
||||
nationalAvg={nationalAvg}
|
||||
similarSchools={similarSchools}
|
||||
flags={primaryFlags}
|
||||
/>
|
||||
</SchoolDetailShell>
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
import type {
|
||||
School, SchoolResult, AbsenceData, OfstedInspection, SchoolCensus,
|
||||
SchoolAdmissions, SchoolAdmissionDistance, SchoolDeprivation, SchoolFinance, NationalAverages,
|
||||
SimilarSchool,
|
||||
} from '@/lib/types';
|
||||
import { ofstedLegacyAreas } from '@/lib/utils';
|
||||
import type { SchoolFlags } from '@/lib/schoolSections';
|
||||
@@ -26,6 +27,7 @@ import { HistorySection } from './HistorySection';
|
||||
import { SchoolLifeSection } from './SchoolLifeSection';
|
||||
import { LocalAreaSection } from './LocalAreaSection';
|
||||
import { FinancesSection } from './FinancesSection';
|
||||
import { SimilarSchoolsSection } from './SimilarSchoolsSection';
|
||||
|
||||
export interface PrimarySchoolSectionsProps {
|
||||
schoolInfo: School;
|
||||
@@ -39,13 +41,15 @@ export interface PrimarySchoolSectionsProps {
|
||||
deprivation: SchoolDeprivation | null;
|
||||
finance: SchoolFinance | null;
|
||||
nationalAvg: NationalAverages | null;
|
||||
/** Nearby schools of a comparable intake. Absent on an older API build. */
|
||||
similarSchools?: SimilarSchool[];
|
||||
flags: SchoolFlags;
|
||||
}
|
||||
|
||||
export function PrimarySchoolSections({
|
||||
schoolInfo, yearlyData, absenceData, ofsted, census,
|
||||
admissions, admissionsHistory, admissionDistance,
|
||||
deprivation, finance, nationalAvg, flags,
|
||||
deprivation, finance, nationalAvg, similarSchools, flags,
|
||||
}: PrimarySchoolSectionsProps) {
|
||||
const primaryAvg = nationalAvg?.primary ?? {};
|
||||
const secondaryAvg = nationalAvg?.secondary ?? {};
|
||||
@@ -146,6 +150,15 @@ export function PrimarySchoolSections({
|
||||
)}
|
||||
|
||||
{flags.hasFinance && finance && <FinancesSection finance={finance} />}
|
||||
|
||||
{/* Last: it is where the reader goes next, not part of this school. */}
|
||||
<SimilarSchoolsSection
|
||||
urn={schoolInfo.urn}
|
||||
schoolName={schoolInfo.school_name}
|
||||
phaseNoun={flags.isAllThrough ? 'all-through' : 'primary'}
|
||||
thisMetricValue={flags.latestResults?.rwm_expected_pct ?? null}
|
||||
similar={similarSchools}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
import type {
|
||||
School, SchoolResult, AbsenceData, OfstedInspection, SchoolCensus,
|
||||
SchoolAdmissions, SchoolAdmissionDistance, SchoolDeprivation, SchoolFinance, NationalAverages,
|
||||
SchoolDestinations,
|
||||
SchoolDestinations, SimilarSchool,
|
||||
} from '@/lib/types';
|
||||
import { ofstedLegacyAreas } from '@/lib/utils';
|
||||
import type { SecondaryFlags } from '@/lib/schoolSections';
|
||||
@@ -27,6 +27,7 @@ import { DistanceSection } from './DistanceSection';
|
||||
import { SecondaryHistorySection } from './SecondaryHistorySection';
|
||||
import { WellbeingSection } from './WellbeingSection';
|
||||
import { FinancesSection } from './FinancesSection';
|
||||
import { SimilarSchoolsSection } from './SimilarSchoolsSection';
|
||||
import styles from './schoolSections.module.css';
|
||||
|
||||
export interface SecondarySchoolSectionsProps {
|
||||
@@ -47,13 +48,15 @@ export interface SecondarySchoolSectionsProps {
|
||||
finance: SchoolFinance | null;
|
||||
nationalAvg: NationalAverages | null;
|
||||
destinations: SchoolDestinations | null;
|
||||
/** Nearby schools of a comparable intake. Absent on an older API build. */
|
||||
similarSchools?: SimilarSchool[];
|
||||
flags: SecondaryFlags;
|
||||
}
|
||||
|
||||
export function SecondarySchoolSections({
|
||||
schoolInfo, yearlyData, ofsted, census,
|
||||
admissions, admissionsHistory, admissionDistance,
|
||||
deprivation, finance, nationalAvg, destinations, flags,
|
||||
deprivation, finance, nationalAvg, destinations, similarSchools, flags,
|
||||
}: SecondarySchoolSectionsProps) {
|
||||
const secondaryAvg = nationalAvg?.secondary ?? {};
|
||||
|
||||
@@ -141,6 +144,15 @@ export function SecondarySchoolSections({
|
||||
{flags.hasFinance && finance && (
|
||||
<FinancesSection finance={finance} showPremises />
|
||||
)}
|
||||
|
||||
{/* Last: it is where the reader goes next, not part of this school. */}
|
||||
<SimilarSchoolsSection
|
||||
urn={schoolInfo.urn}
|
||||
schoolName={schoolInfo.school_name}
|
||||
phaseNoun="secondary"
|
||||
thisMetricValue={flags.latestResults?.attainment_8_score ?? null}
|
||||
similar={similarSchools}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -128,6 +128,10 @@ export interface NavItemsInput {
|
||||
* measure a postcode, so the nav must gate on them too or it will link to an
|
||||
* anchor that was never rendered. */
|
||||
hasLocation?: boolean;
|
||||
/** Whether the similar-schools section will render. Optional for the same
|
||||
* reason hasLocation is: the nav must never link to an anchor that was not
|
||||
* rendered, and absent has to mean "no section". */
|
||||
hasSimilarSchools?: boolean;
|
||||
yearlyDataLength: number;
|
||||
}
|
||||
|
||||
@@ -142,7 +146,10 @@ export interface NavItemsInput {
|
||||
*/
|
||||
export function buildNavItems(
|
||||
flags: SchoolFlags,
|
||||
{ ofsted, admissions, admissionDistance, hasLocation, yearlyDataLength }: NavItemsInput,
|
||||
{
|
||||
ofsted, admissions, admissionDistance, hasLocation,
|
||||
hasSimilarSchools, yearlyDataLength,
|
||||
}: NavItemsInput,
|
||||
): NavItem[] {
|
||||
const navItems: NavItem[] = [];
|
||||
if (ofsted) navItems.push({ id: 'ofsted', label: 'Ofsted' });
|
||||
@@ -161,6 +168,8 @@ export function buildNavItems(
|
||||
if (flags.hasSchoolLife) navItems.push({ id: 'school-life', label: 'School Life' });
|
||||
if (flags.hasDeprivation) navItems.push({ id: 'local-area', label: 'Local Area' });
|
||||
if (flags.hasFinance) navItems.push({ id: 'finances', label: 'Finances' });
|
||||
// Last, because the section renders last — the scroll-spy reads this order.
|
||||
if (hasSimilarSchools) navItems.push({ id: 'similar', label: 'Similar schools' });
|
||||
return navItems;
|
||||
}
|
||||
|
||||
@@ -239,7 +248,10 @@ export function computeSecondaryFlags({
|
||||
*/
|
||||
export function buildSecondaryNavItems(
|
||||
flags: SecondaryFlags,
|
||||
{ ofsted, admissions, admissionDistance, hasLocation, yearlyDataLength }: NavItemsInput,
|
||||
{
|
||||
ofsted, admissions, admissionDistance, hasLocation,
|
||||
hasSimilarSchools, yearlyDataLength,
|
||||
}: NavItemsInput,
|
||||
): NavItem[] {
|
||||
const navItems: NavItem[] = [];
|
||||
if (ofsted) navItems.push({ id: 'ofsted', label: 'Ofsted' });
|
||||
@@ -257,5 +269,7 @@ export function buildSecondaryNavItems(
|
||||
if (yearlyDataLength > 1) navItems.push({ id: 'history', label: 'History' });
|
||||
if (flags.hasWellbeing) navItems.push({ id: 'wellbeing', label: 'Wellbeing' });
|
||||
if (flags.hasFinance) navItems.push({ id: 'finances', label: 'Finances' });
|
||||
// Last, because the section renders last — the scroll-spy reads this order.
|
||||
if (hasSimilarSchools) navItems.push({ id: 'similar', label: 'Similar schools' });
|
||||
return navItems;
|
||||
}
|
||||
Reference in new issue
Block a user