feat: similar schools nearby on the detail page #150
No files matched your search
@@ -41,6 +41,11 @@ Playwright.
|
||||
`"Mixed"`, `"Boys"` or `"Girls"`.
|
||||
- **Copy rule:** the lede says "with a similar intake" only when no card is
|
||||
tier 3. A missing metric renders the exact string `Not published`.
|
||||
- **No "how these schools are chosen" disclosure.** One caption line only:
|
||||
distances are straight-line, not road distance.
|
||||
- **Three cards maximum, with no overflow affordance.** Surplus qualifying
|
||||
schools are dropped silently; `NearbyPlaces` below already leads to the full
|
||||
lists.
|
||||
- **The neighbour's metric never carries a valence colour.** No
|
||||
`--status-above` / `--status-below` anywhere in this feature.
|
||||
- **Backend tests:**
|
||||
@@ -868,6 +873,12 @@ describe('cards', () => {
|
||||
renderSection([school(), school({ urn: 100003 })]);
|
||||
expect(screen.getByTestId('compare-bar')).toHaveTextContent('bar for 100001');
|
||||
});
|
||||
|
||||
it('says distances are straight-line, and offers no method panel', () => {
|
||||
const { container } = renderSection([school(), school({ urn: 100003 })]);
|
||||
expect(screen.getByText(/straight-line from this school/i)).toBeInTheDocument();
|
||||
expect(container.querySelector('details')).toBeNull();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
@@ -1040,6 +1051,10 @@ Create `nextjs-app/components/school/SimilarSchoolsSection.tsx`:
|
||||
* The lede only claims a similar intake when no card came from tier 3, and a
|
||||
* card's chips list what that school actually shares rather than a match it
|
||||
* did not earn.
|
||||
*
|
||||
* 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
|
||||
* not a method note — it is the one thing a card cannot self-correct.
|
||||
*/
|
||||
|
||||
import Link from 'next/link';
|
||||
@@ -1095,27 +1110,6 @@ export function SimilarSchoolsSection({
|
||||
: `Other ${phaseNoun} schools near ${schoolName}, with a similar intake.`}
|
||||
</p>
|
||||
|
||||
<details className={styles.method}>
|
||||
<summary>How these schools are chosen</summary>
|
||||
<div>
|
||||
<p>
|
||||
Schools of the same phase, nearest first, preferring those whose intake
|
||||
resembles this one — gender, religious character, and whether the school
|
||||
selects by ability. Special schools are only ever compared with other
|
||||
special schools.
|
||||
</p>
|
||||
<p>
|
||||
The labels on each card list what that school actually shares with this
|
||||
one. Where nothing close enough was found nearby, the match is loosened
|
||||
and the labels say less.
|
||||
</p>
|
||||
<p>
|
||||
Distances are straight-line from this school, not road distance and not
|
||||
from your home. Being listed here is not a recommendation.
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<ul className={styles.grid}>
|
||||
{schools.map((school) => (
|
||||
<li key={school.urn} className={styles.school}>
|
||||
@@ -1158,6 +1152,13 @@ export function SimilarSchoolsSection({
|
||||
</ul>
|
||||
|
||||
<SimilarSchoolsCompareBar thisUrn={urn} candidates={schools} />
|
||||
|
||||
{/* The one caveat the cards cannot make on their own: a reader who takes
|
||||
"0.6 miles away" for the walk has been misled, and nothing else here
|
||||
corrects that. */}
|
||||
<p className={styles.caption}>
|
||||
Distances are straight-line from this school, not road distance.
|
||||
</p>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
@@ -1170,13 +1171,8 @@ Create `nextjs-app/components/school/SimilarSchools.module.css`. Tokens only —
|
||||
|
||||
```css
|
||||
.heading { font-family: var(--font-display); font-size: 1.4rem; letter-spacing: -0.4px; margin: 0; }
|
||||
.lede { margin: 0.5rem 0 0; color: var(--text-secondary); max-width: 64ch; }
|
||||
|
||||
.method { margin: 0.9rem 0 1.4rem; }
|
||||
.method summary { cursor: pointer; color: var(--brand); font-size: 0.85rem; font-weight: 500; width: fit-content; }
|
||||
.method div { margin-top: 0.6rem; padding: 0.9rem 1rem; background: var(--bg-secondary); border-radius: 8px; }
|
||||
.method p { margin: 0 0 0.55rem; font-size: 0.82rem; color: var(--text-secondary); max-width: 70ch; }
|
||||
.method p:last-child { margin-bottom: 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); }
|
||||
|
||||
.grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0.9rem; list-style: none; margin: 0; padding: 0; }
|
||||
@media (max-width: 820px) { .grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
|
||||
@@ -105,6 +105,13 @@ the promise in the heading and a reader scanning the row reads the first card as
|
||||
the closest. A tier-2 school at 0.4 miles therefore appears above a tier-1
|
||||
school at 2.9 miles, and the chips explain the difference in match quality.
|
||||
|
||||
**More than three qualifying schools are dropped, not paginated.** In a dense
|
||||
urban area dozens of schools clear tier 1, and the section shows the three
|
||||
nearest of them. There is no "show more" and no count of what was left out,
|
||||
because `NearbyPlaces` sits directly beneath and already answers "more schools
|
||||
near here" by linking to the place pages — which are the pages built for
|
||||
browsing a full list, and which the school page exists to feed.
|
||||
|
||||
**Fewer than two results renders nothing.** Not an empty state, not a single
|
||||
lonely card, not padding with schools that failed the hard filters. The section
|
||||
is absent, the nav item is absent, and the page is unchanged from today. A page
|
||||
@@ -170,7 +177,7 @@ match the selection did not actually make.
|
||||
|
||||
`components/school/SimilarSchoolsSection.tsx` — a server component wrapped in
|
||||
the shared `Section` shell from `sectionShared.tsx`. It renders the heading,
|
||||
the lede, the `<details>` disclosure, the card grid and the footer CTA. Every
|
||||
the lede, the card grid, the footer CTA and one caption line. Every
|
||||
card's title is an `<a>` to the school's canonical slug URL via `schoolUrl()`.
|
||||
|
||||
`components/school/AddToCompareButton.tsx` — the only `'use client'` file this
|
||||
@@ -228,11 +235,17 @@ reference line, and the reader draws their own conclusion.
|
||||
em dash. This follows the same rule the rest of the detail page uses: a school
|
||||
with no published result has not scored zero.
|
||||
|
||||
**The disclosure states the method plainly** — same phase, nearest first,
|
||||
preferring a similar intake, special schools only ever compared with special
|
||||
schools, straight-line distance from the school rather than road distance or
|
||||
distance from the reader's home, and that being listed here is not a
|
||||
recommendation.
|
||||
**There is no "how these are chosen" disclosure.** The method is visible in what
|
||||
the section already shows — the phase in the lede, the shared characteristics on
|
||||
each card, the distance above each name — and a collapsed panel restating it
|
||||
earns less than the space it costs.
|
||||
|
||||
**One caption line survives, and only one:** that distances are straight-line
|
||||
from the school and not road distance. This is not a method note. A reader who
|
||||
sees "0.6 miles away" and takes it for the walk has been misled by us, and no
|
||||
other element on the card corrects that. The remaining notes — that listing is
|
||||
not a recommendation, that special schools only meet special schools — are
|
||||
statements the selection rules already keep true without being narrated.
|
||||
|
||||
## Degradation
|
||||
|
||||
|
||||
@@ -73,14 +73,7 @@ body {
|
||||
border-radius:var(--radius-lg); padding:28px; box-shadow:var(--shadow);
|
||||
}
|
||||
.card > h2 { font:700 22px/1.25 var(--font-display); letter-spacing:-.4px; margin:0; }
|
||||
.lede { margin:8px 0 0; color:var(--text-secondary); font-size:14.5px; max-width:64ch; }
|
||||
details.method { margin:14px 0 22px; }
|
||||
details.method summary { cursor:pointer; color:var(--brand); font-size:13.5px; font-weight:500; width:fit-content; }
|
||||
details.method summary:hover { color:var(--brand-strong); }
|
||||
details.method div { margin-top:10px; padding:14px 16px; background:var(--bg-secondary); border-radius:var(--radius-md); }
|
||||
details.method p { margin:0 0 9px; font-size:13px; color:var(--text-secondary); max-width:70ch; }
|
||||
details.method p:last-child { margin-bottom:0; }
|
||||
|
||||
.lede { margin:8px 0 20px; color:var(--text-secondary); font-size:14.5px; max-width:64ch; }
|
||||
.grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:14px; }
|
||||
@media (max-width:820px) { .grid { grid-template-columns:repeat(2,minmax(0,1fr)); } }
|
||||
@media (max-width:560px) { .grid { grid-template-columns:1fr; } .card { padding:20px; } body { padding-left:16px; padding-right:16px; } }
|
||||
@@ -244,21 +237,6 @@ function render() {
|
||||
<div class="card">
|
||||
<h2>Similar schools nearby</h2>
|
||||
<p class="lede">${v.lede}</p>
|
||||
<details class="method">
|
||||
<summary>How these schools are chosen</summary>
|
||||
<div>
|
||||
<p>Schools of the same phase, nearest first, preferring those whose intake
|
||||
resembles this one — gender, religious character, and whether the school
|
||||
selects by ability. Special schools are only ever compared with other
|
||||
special schools.</p>
|
||||
<p>The chips on each card list what that school actually shares with this
|
||||
one. Where nothing close enough was found nearby, the match is loosened
|
||||
and the chips say less.</p>
|
||||
<p>Distances are straight-line from this school, not road distance and not
|
||||
from your home. Being listed here is not a recommendation, and the
|
||||
results shown are context rather than a measure of fit.</p>
|
||||
</div>
|
||||
</details>
|
||||
<div class="grid">${v.schools.map((s, i) => schoolCard(v, s, i)).join('')}</div>
|
||||
<div class="footer">
|
||||
<p aria-live="polite">
|
||||
@@ -269,7 +247,8 @@ function render() {
|
||||
${count ? `Compare ${count + 1} schools` : 'Compare'} →
|
||||
</a>
|
||||
</div>
|
||||
<p class="caption">${v.caption} Fictional schools and figures for this mockup.</p>
|
||||
<p class="caption">Distances are straight-line from this school, not road distance.
|
||||
${v.caption} Fictional schools and figures for this mockup.</p>
|
||||
</div>
|
||||
<p class="note">${v.note}</p>
|
||||
</section>`;
|
||||
|
||||
Reference in new issue
Block a user