feat(admissions): replace bar + metric cards with Q&A tile
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 13s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 50s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 1m4s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s

The "How Hard to Get Into This School" tile mixed a progress bar
(places vs first-choice) with three text metric cards, making the
data feel fragmented and hiding the real narrative. The progress bar
also broke visually when undersubscribed and didn't scale to different
school sizes.

Replace with a typographic Q&A list that answers the questions
parents actually ask — "How many places were offered?", "How many
families wanted this school first?", "How many got their first
choice?", "How many applied in total?" — with a verdict footer
(Oversubscribed / Not oversubscribed + one-sentence explanation).

The third row now uses first_preference_offers (already in the API
response) to show "27 of 42 (64.3%)" instead of just the percentage,
giving the raw count parents actually want.

Each row is independently null-gated; rows stack vertically under
480px so the Playfair numeral stays legible.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Tudor Sitaru
2026-04-14 10:01:19 +01:00
parent 6d685b7e8a
commit 5abab067a1
2 changed files with 106 additions and 54 deletions
@@ -927,42 +927,85 @@
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
} }
/* ── Admissions progress bar ── */ /* ── Admissions Q&A list ── */
.admissionsBarWrap { .admissionsQa {
margin-bottom: 1rem; display: flex;
flex-direction: column;
margin: 0;
} }
.admissionsBarLabel { .admissionsQaRow {
font-size: 0.8125rem; display: flex;
justify-content: space-between;
align-items: baseline;
padding: 0.85rem 0;
border-bottom: 1px solid var(--border-color, #e5dfd5);
gap: 1rem;
}
.admissionsQaRow:first-child {
padding-top: 0.35rem;
}
.admissionsQaRow:last-child {
border-bottom: none;
padding-bottom: 0.35rem;
}
.admissionsQaQuestion {
font-size: 0.92rem;
color: var(--text-secondary, #5c564d); color: var(--text-secondary, #5c564d);
margin-bottom: 0.4rem; line-height: 1.35;
margin: 0;
} }
.admissionsBarLabel strong { .admissionsQaAnswer {
color: var(--text-primary, #1a1612); font-family: var(--font-playfair), 'Playfair Display', Georgia, serif;
font-size: 1.4rem;
font-weight: 700; font-weight: 700;
color: var(--text-primary, #1a1612);
line-height: 1;
white-space: nowrap;
flex-shrink: 0;
font-variant-numeric: tabular-nums;
margin: 0;
} }
.admissionsBarTrack { .admissionsQaAnswerSub {
height: 20px; font-size: 0.7rem;
background: var(--bg-secondary, #f3ede4); font-weight: 500;
border-radius: 10px; color: var(--text-muted, #6d685f);
overflow: hidden; margin-left: 0.35rem;
position: relative; font-family: var(--font-dm-sans), 'DM Sans', sans-serif;
} }
.admissionsBarFill { .admissionsVerdict {
height: 100%; margin-top: 1rem;
border-radius: 10px; padding-top: 0.9rem;
transition: width 0.6s ease; border-top: 1px solid var(--border-color, #e5dfd5);
display: flex;
align-items: center;
gap: 0.6rem;
font-size: 0.85rem;
color: var(--text-secondary, #5c564d);
flex-wrap: wrap;
} }
.admissionsBarOversubscribed { .admissionsVerdictText {
background: var(--accent-coral, #e07256); line-height: 1.4;
} }
.admissionsBarUndersubscribed { @media (max-width: 480px) {
background: var(--accent-teal, #2d7d7d); .admissionsQaRow {
flex-direction: column;
align-items: flex-start;
gap: 0.25rem;
padding: 0.7rem 0;
}
.admissionsQaAnswer {
white-space: normal;
}
} }
/* ── History accordion ── */ /* ── History accordion ── */
+41 -32
View File
@@ -787,46 +787,55 @@ export function SchoolDetailView({
{admissions && ( {admissions && (
<section id="admissions" className={styles.card}> <section id="admissions" className={styles.card}>
<h2 className={styles.sectionTitle}>How Hard to Get Into This School ({formatAcademicYear(admissions.year)})</h2> <h2 className={styles.sectionTitle}>How Hard to Get Into This School ({formatAcademicYear(admissions.year)})</h2>
{admissions.first_preference_applications != null && admissions.places_offered != null && (
<div className={styles.admissionsBarWrap}> <dl className={styles.admissionsQa}>
<div className={styles.admissionsBarLabel}>
<strong>{admissions.places_offered}</strong> places offered for <strong>{admissions.first_preference_applications}</strong> first-choice applications
</div>
<div className={styles.admissionsBarTrack}>
<div
className={`${styles.admissionsBarFill} ${admissions.oversubscribed ? styles.admissionsBarOversubscribed : styles.admissionsBarUndersubscribed}`}
style={{ width: `${Math.min(100, (admissions.places_offered / admissions.first_preference_applications) * 100)}%` }}
/>
</div>
</div>
)}
{admissions.first_preference_applications == null && admissions.oversubscribed != null && (
<div className={`${styles.admissionsBadge} ${admissions.oversubscribed ? styles.statusWarn : styles.statusGood}`}>
{admissions.oversubscribed
? '⚠ Oversubscribed'
: '✓ Not Oversubscribed'}
</div>
)}
<div className={styles.metricsGrid}>
{admissions.places_offered != null && ( {admissions.places_offered != null && (
<div className={styles.metricCard}> <div className={styles.admissionsQaRow}>
<div className={styles.metricLabel}>{isSecondary ? 'Year 7' : 'Reception'} places offered</div> <dt className={styles.admissionsQaQuestion}>How many places were offered?</dt>
<div className={styles.metricValue}>{admissions.places_offered}</div> <dd className={styles.admissionsQaAnswer}>{admissions.places_offered}</dd>
</div> </div>
)} )}
{admissions.total_applications != null && ( {admissions.first_preference_applications != null && (
<div className={styles.metricCard}> <div className={styles.admissionsQaRow}>
<div className={styles.metricLabel}>Applications received</div> <dt className={styles.admissionsQaQuestion}>How many families wanted this school first?</dt>
<div className={styles.metricValue}>{admissions.total_applications.toLocaleString()}</div> <dd className={styles.admissionsQaAnswer}>{admissions.first_preference_applications}</dd>
</div> </div>
)} )}
{admissions.first_preference_offer_pct != null && ( {admissions.first_preference_offer_pct != null && (
<div className={styles.metricCard}> <div className={styles.admissionsQaRow}>
<div className={styles.metricLabel}>Families who got their first-choice</div> <dt className={styles.admissionsQaQuestion}>How many got their first choice?</dt>
<div className={styles.metricValue}>{formatPercentage(admissions.first_preference_offer_pct)}</div> <dd className={styles.admissionsQaAnswer}>
{admissions.first_preference_offers != null && admissions.first_preference_applications != null ? (
<>
{admissions.first_preference_offers}
<span className={styles.admissionsQaAnswerSub}>
of {admissions.first_preference_applications} ({formatPercentage(admissions.first_preference_offer_pct)})
</span>
</>
) : (
formatPercentage(admissions.first_preference_offer_pct)
)}
</dd>
</div> </div>
)} )}
</div> {admissions.total_applications != null && (
<div className={styles.admissionsQaRow}>
<dt className={styles.admissionsQaQuestion}>How many applied in total?</dt>
<dd className={styles.admissionsQaAnswer}>{admissions.total_applications.toLocaleString()}</dd>
</div>
)}
</dl>
{admissions.oversubscribed != null && (
<div className={styles.admissionsVerdict}>
<span className={`${styles.admissionsBadge} ${admissions.oversubscribed ? styles.statusWarn : styles.statusGood}`}>
{admissions.oversubscribed ? 'Oversubscribed' : 'Not oversubscribed'}
</span>
<span className={styles.admissionsVerdictText}>
{admissions.oversubscribed ? 'Demand exceeds capacity.' : 'Supply meets demand.'}
</span>
</div>
)}
</section> </section>
)} )}