Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1373fb6df | ||
|
|
ccfa44389e |
No files matched your search
@@ -8,7 +8,6 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
nearbyNoun,
|
|
||||||
NearbySchoolsSection,
|
NearbySchoolsSection,
|
||||||
shouldRenderNearby,
|
shouldRenderNearby,
|
||||||
} from '@/components/school/NearbySchoolsSection';
|
} from '@/components/school/NearbySchoolsSection';
|
||||||
@@ -43,13 +42,7 @@ function school(overrides: Partial<NearbySchool> = {}): NearbySchool {
|
|||||||
|
|
||||||
function renderSection(nearby: NearbySchool[]) {
|
function renderSection(nearby: NearbySchool[]) {
|
||||||
return render(
|
return render(
|
||||||
<NearbySchoolsSection
|
<NearbySchoolsSection urn={100001} thisMetricValue={72} nearby={nearby} />,
|
||||||
urn={100001}
|
|
||||||
schoolName="Meadowbrook Primary School"
|
|
||||||
phase="Primary"
|
|
||||||
thisMetricValue={72}
|
|
||||||
nearby={nearby}
|
|
||||||
/>,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,15 +83,10 @@ describe('what the section claims', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('shows no chips at all when nothing is shared, rather than inventing one', () => {
|
it('shows no chips at all when nothing is shared, rather than inventing one', () => {
|
||||||
const { container } = render(
|
const { container } = renderSection([
|
||||||
<NearbySchoolsSection
|
school({ shared: [] }),
|
||||||
urn={100001}
|
school({ urn: 100003, shared: [] }),
|
||||||
schoolName="Meadowbrook Primary School"
|
]);
|
||||||
phase="Primary"
|
|
||||||
thisMetricValue={72}
|
|
||||||
nearby={[school({ shared: [] }), school({ urn: 100003, shared: [] })]}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
// The card still carries its distance, name, type and figure — just no
|
// The card still carries its distance, name, type and figure — just no
|
||||||
// claim of likeness.
|
// claim of likeness.
|
||||||
expect(container.querySelectorAll('li ul').length).toBe(0);
|
expect(container.querySelectorAll('li ul').length).toBe(0);
|
||||||
@@ -106,37 +94,6 @@ describe('what the section claims', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('what the lede calls the set', () => {
|
|
||||||
it.each([
|
|
||||||
['Primary', 'primary schools'],
|
|
||||||
['Middle deemed primary', 'primary schools'],
|
|
||||||
['Secondary', 'secondary schools'],
|
|
||||||
['Middle deemed secondary', 'secondary schools'],
|
|
||||||
['All-through', 'all-through schools'],
|
|
||||||
// GIAS phase 6. Its candidates span the whole secondary group, so no
|
|
||||||
// single noun fits and it takes the honest general one.
|
|
||||||
['16 plus', 'schools and colleges'],
|
|
||||||
['', 'schools'],
|
|
||||||
[null, 'schools'],
|
|
||||||
])('calls a %s school\'s neighbours "%s"', (phase, expected) => {
|
|
||||||
expect(nearbyNoun(phase)).toBe(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('never calls a sixth form college\'s neighbours primary schools', () => {
|
|
||||||
render(
|
|
||||||
<NearbySchoolsSection
|
|
||||||
urn={100001}
|
|
||||||
schoolName="Barnet Sixth Form College"
|
|
||||||
phase="16 plus"
|
|
||||||
thisMetricValue={null}
|
|
||||||
nearby={[school(), school({ urn: 100003 })]}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
expect(screen.getByText(/Other schools and colleges near Barnet Sixth Form College/)).toBeInTheDocument();
|
|
||||||
expect(screen.queryByText(/primary schools/)).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('cards', () => {
|
describe('cards', () => {
|
||||||
it('links each school to its canonical slug', () => {
|
it('links each school to its canonical slug', () => {
|
||||||
renderSection([school(), school({ urn: 100003, school_name: 'Oakfield Primary School' })]);
|
renderSection([school(), school({ urn: 100003, school_name: 'Oakfield Primary School' })]);
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
.heading { font-family: var(--font-display); font-size: 1.4rem; letter-spacing: -0.4px; margin: 0; }
|
.heading { font-family: var(--font-display); font-size: 1.4rem; letter-spacing: -0.4px; margin: 0; }
|
||||||
.lede { margin: 0.5rem 0 1.25rem; color: var(--text-secondary); max-width: 64ch; }
|
|
||||||
.caption { margin: 1rem 0 0; font-size: 0.72rem; color: var(--text-muted); }
|
.caption { margin: 1rem 0 0; font-size: 0.72rem; color: var(--text-muted); }
|
||||||
|
|
||||||
.top { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; }
|
.top { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: 1.25rem; }
|
||||||
.arrows { display: flex; gap: 0.5rem; flex: none; }
|
.arrows { display: flex; gap: 0.5rem; flex: none; }
|
||||||
.arrow { width: 44px; height: 44px; display: grid; place-items: center; cursor: pointer; border: 1px solid var(--border-strong); border-radius: 999px; background: var(--bg-card); color: var(--brand); }
|
.arrow { width: 44px; height: 44px; display: grid; place-items: center; cursor: pointer; border: 1px solid var(--border-strong); border-radius: 999px; background: var(--bg-card); color: var(--brand); }
|
||||||
.arrow:hover:not(:disabled) { border-color: var(--brand); background: var(--brand-bg); }
|
.arrow:hover:not(:disabled) { border-color: var(--brand); background: var(--brand-bg); }
|
||||||
@@ -15,10 +14,10 @@
|
|||||||
.scroller { display: grid; grid-auto-flow: column; grid-auto-columns: calc((100% - 1.8rem) / 3); gap: 0.9rem; overflow-x: auto; scroll-snap-type: x mandatory; padding: 2px; margin: -2px; list-style: none; scrollbar-width: none; -ms-overflow-style: none; }
|
.scroller { display: grid; grid-auto-flow: column; grid-auto-columns: calc((100% - 1.8rem) / 3); gap: 0.9rem; overflow-x: auto; scroll-snap-type: x mandatory; padding: 2px; margin: -2px; list-style: none; scrollbar-width: none; -ms-overflow-style: none; }
|
||||||
.scroller::-webkit-scrollbar { display: none; }
|
.scroller::-webkit-scrollbar { display: none; }
|
||||||
@media (max-width: 820px) { .scroller { grid-auto-columns: calc((100% - 0.9rem) / 2); } }
|
@media (max-width: 820px) { .scroller { grid-auto-columns: calc((100% - 0.9rem) / 2); } }
|
||||||
/* Touch widths (MOBILE.md): the arrows would take 96px from a 328px card and
|
/* Touch widths (MOBILE.md): the arrows would take 96px from a 328px card, for
|
||||||
crush the lede into four lines, for a control swiping already provides. They
|
a control swiping already provides. They go, and the documented right-edge
|
||||||
go, and the documented right-edge fade carries the affordance — lifting at
|
fade carries the affordance — lifting at the end of the travel, where there
|
||||||
the end of the travel, where there is nothing more to hint at. */
|
is nothing more to hint at. */
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.top { display: block; }
|
.top { display: block; }
|
||||||
.arrows { display: none; }
|
.arrows { display: none; }
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* The scroller and its arrows.
|
* The scroller and its arrows.
|
||||||
*
|
*
|
||||||
* `children` are the server-rendered cards and `header` the server-rendered
|
* `children` are the server-rendered cards and `header` the server-rendered
|
||||||
* heading and lede: both stay server components, passed through, so this file
|
* heading: both stay server components, passed through, so this file
|
||||||
* owns a DOM ref and nothing else. That is what keeps all six links in the
|
* owns a DOM ref and nothing else. That is what keeps all six links in the
|
||||||
* initial HTML — a carousel that mounted cards on click would put four of the
|
* initial HTML — a carousel that mounted cards on click would put four of the
|
||||||
* six beyond a crawler and beyond a reader with no JavaScript.
|
* six beyond a crawler and beyond a reader with no JavaScript.
|
||||||
|
|||||||
@@ -13,8 +13,10 @@
|
|||||||
* reached on their behalf.
|
* reached on their behalf.
|
||||||
*
|
*
|
||||||
* There is deliberately no "how these are chosen" panel: the method is already
|
* There is deliberately no "how these are chosen" panel: the method is already
|
||||||
* visible in the lede, the chips and the distances. The single caption line is
|
* visible in the chips and the distances. The single caption line is not a
|
||||||
* not a method note — it is the one thing a card cannot self-correct.
|
* method note — it is the one thing a card cannot self-correct.
|
||||||
|
*
|
||||||
|
* Nor is there a lede: "Other primary schools near X" only restated the heading.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
@@ -32,28 +34,6 @@ export function shouldRenderNearby(nearby?: NearbySchool[] | null): boolean {
|
|||||||
return (nearby?.length ?? 0) >= MINIMUM;
|
return (nearby?.length ?? 0) >= MINIMUM;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* What the lede calls the set of schools it is showing.
|
|
||||||
*
|
|
||||||
* Derived from the school's own GIAS phase rather than the template it renders
|
|
||||||
* with, because those disagree for "16 plus" (GIAS phase 6): a sixth-form
|
|
||||||
* college renders the primary template — computeSchoolFlags tests for the
|
|
||||||
* substring "secondary" — while the backend correctly matches it against the
|
|
||||||
* secondary group. Taking the noun from the template would print "Other primary
|
|
||||||
* schools near <sixth form college>" above a row of secondaries.
|
|
||||||
*
|
|
||||||
* A 16-plus school's candidates span the whole secondary group, so no single
|
|
||||||
* noun fits and it gets the honest general one.
|
|
||||||
*/
|
|
||||||
export function nearbyNoun(phase: string | null | undefined): string {
|
|
||||||
const text = (phase ?? '').trim().toLowerCase();
|
|
||||||
if (text === 'all-through') return 'all-through schools';
|
|
||||||
if (text === '16 plus') return 'schools and colleges';
|
|
||||||
if (text.includes('secondary')) return 'secondary schools';
|
|
||||||
if (text.includes('primary')) return 'primary schools';
|
|
||||||
return 'schools';
|
|
||||||
}
|
|
||||||
|
|
||||||
function metricLabel(key: string): string {
|
function metricLabel(key: string): string {
|
||||||
return key === 'attainment_8_score' ? 'Attainment 8' : 'Reading, writing & maths';
|
return key === 'attainment_8_score' ? 'Attainment 8' : 'Reading, writing & maths';
|
||||||
}
|
}
|
||||||
@@ -65,25 +45,16 @@ function formatMetric(value: number | null, key: string): string {
|
|||||||
|
|
||||||
export function NearbySchoolsSection({
|
export function NearbySchoolsSection({
|
||||||
urn,
|
urn,
|
||||||
schoolName,
|
|
||||||
phase,
|
|
||||||
thisMetricValue,
|
thisMetricValue,
|
||||||
nearby,
|
nearby,
|
||||||
}: {
|
}: {
|
||||||
urn: number;
|
urn: number;
|
||||||
schoolName: string;
|
|
||||||
/** The school's own GIAS phase, not the template it renders with. */
|
|
||||||
phase: string | null | undefined;
|
|
||||||
thisMetricValue: number | null;
|
thisMetricValue: number | null;
|
||||||
nearby?: NearbySchool[] | null;
|
nearby?: NearbySchool[] | null;
|
||||||
}) {
|
}) {
|
||||||
if (!shouldRenderNearby(nearby)) return null;
|
if (!shouldRenderNearby(nearby)) return null;
|
||||||
const schools = nearby as NearbySchool[];
|
const schools = nearby as NearbySchool[];
|
||||||
|
|
||||||
// One card matched on phase alone, so the section may not claim the set
|
|
||||||
// shares an intake with this school.
|
|
||||||
const metricKey = schools[0].metric_key;
|
const metricKey = schools[0].metric_key;
|
||||||
const noun = nearbyNoun(phase);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section id="nearby">
|
<Section id="nearby">
|
||||||
@@ -91,12 +62,9 @@ export function NearbySchoolsSection({
|
|||||||
count={schools.length}
|
count={schools.length}
|
||||||
labelledBy="nearby-schools-heading"
|
labelledBy="nearby-schools-heading"
|
||||||
header={
|
header={
|
||||||
<div>
|
<h2 id="nearby-schools-heading" className={styles.heading}>
|
||||||
<h2 id="nearby-schools-heading" className={styles.heading}>
|
Other schools nearby
|
||||||
Other schools nearby
|
</h2>
|
||||||
</h2>
|
|
||||||
<p className={styles.lede}>{`Other ${noun} near ${schoolName}.`}</p>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{schools.map((school) => (
|
{schools.map((school) => (
|
||||||
|
|||||||
@@ -154,8 +154,6 @@ export function PrimarySchoolSections({
|
|||||||
{/* Last: it is where the reader goes next, not part of this school. */}
|
{/* Last: it is where the reader goes next, not part of this school. */}
|
||||||
<NearbySchoolsSection
|
<NearbySchoolsSection
|
||||||
urn={schoolInfo.urn}
|
urn={schoolInfo.urn}
|
||||||
schoolName={schoolInfo.school_name}
|
|
||||||
phase={schoolInfo.phase}
|
|
||||||
thisMetricValue={flags.latestResults?.rwm_expected_pct ?? null}
|
thisMetricValue={flags.latestResults?.rwm_expected_pct ?? null}
|
||||||
nearby={nearbySchools}
|
nearby={nearbySchools}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -148,8 +148,6 @@ export function SecondarySchoolSections({
|
|||||||
{/* Last: it is where the reader goes next, not part of this school. */}
|
{/* Last: it is where the reader goes next, not part of this school. */}
|
||||||
<NearbySchoolsSection
|
<NearbySchoolsSection
|
||||||
urn={schoolInfo.urn}
|
urn={schoolInfo.urn}
|
||||||
schoolName={schoolInfo.school_name}
|
|
||||||
phase={schoolInfo.phase}
|
|
||||||
thisMetricValue={flags.latestResults?.attainment_8_score ?? null}
|
thisMetricValue={flags.latestResults?.attainment_8_score ?? null}
|
||||||
nearby={nearbySchools}
|
nearby={nearbySchools}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in new issue
Block a user