gauth-0003-new-program-structure · frontend/src
Reference: Mentoring programs Antd.dc.html + mentoring-antd.jsx (Mentorship, the complete case) and Onboarding programs Antd.dc.html + onboarding-antd.jsx (the variant where a session is one week of a fixed path). Those files are the visual and behavioural source of truth; this document is the contract behind them.
Everything is Ant Design 5.16.2 with the codebase's own setup — ConfigProvider prefixCls="rc", componentSize="large", theme.cssVar.prefix="rc", tokens from frontend/src/layouts/config.tsx. SetupItem, Section, RadioCards and the reporting number cards are the existing components from frontend/src/components/, not new ones. No new visual primitives are introduced.
Read these first. Every screen below is a consequence of one of them.
1.1 — A program has no status. Remove Draft / Live / Paused from the program entity in the UI. A program is a configuration. What has status is a session. Consequences:
badge, no Publish button, no unpublished-changes counter, no Cancel/Save bar;
1.2 — A session ends when the matches are sent. Sending the pairs is the end of the session. For Mentorship the pairs then work through their milestones past the session's end, each at its own pace. So a session has no end date: never display one, and never let one be configured.
1.3 — Settings is replaced by Journey. The Settings tab and its sub-tabs (Audience, Matchmaking, Communication, Milestones, Booking, Schedule) are retired. Their fields are redistributed into one unnumbered program block plus five numbered blocks that read as the member's experience, each opening a drawer. See §4.
1.4 — Program type changes block content, never block structure. The blocks and their order are identical for Mentorship, Coffee Meetup and Onboarding. Only what each says changes. See §9.
1.5 — Terminology. Round → Session everywhere. Sessions have numbers, not names ("Session 6"). The steps a pair works through are milestones, not meetings — a meeting is the encounter that fulfils a milestone, and the word survives only where real encounters are counted (MEETINGS HELD, "has not booked their first meeting"). Say email, never "broadcast": broadcast is the internal feature name, and it must not surface in this UI, nor its orange colour code.
/account/:slug/admin/programs/:id → Overview (default)
/account/:slug/admin/programs/:id/journey → Journey
/account/:slug/admin/programs/:id/sessions → Sessions
/account/:slug/admin/programs/:id/sessions/:sessionId
/account/:slug/admin/programs/:id/insights → Insights
Journey replaces the old settings routes; keep redirects. A tab is disabled (#BFBFBF, not clickable) when it is meaningless: Insights until at least one session has completed. Sessions is always enabled — it is where the gating explanation lives.
Content area background is #FAFAFA (colorBgLayout); the header and the sider stay white.
Full width, no side column, in this order and nothing else:
<CurrentSessionCard>** (§5.1)NumberCards in a flex row (flex: 1 each, not Col span, since24/5 and 24/6 do not divide evenly). Session-scoped only: nothing cumulative, and nothing that requires the matches to be out. Every tile is clickable and drills into the people it covers. Card title "The numbers so far", with a text link "Open Insights".
<AttentionList>** (§5.2)The split with Insights is the whole point: Overview answers "is it healthy right now", Insights answers "does it perform". Cumulative totals, session-to-session comparison and history live in Insights and must not be duplicated here.
A centred 840px column. Blocks are joined by a 2px #D9D9D9 vertical connector.
The spine starts at block 1 and ends at block 5.
steps 2 to 5 run again"*. Blocks 2–5 are per-session; block 1 is not.
the chips are the description. The chip row must be display: flex, not antd's default inline-flex, or it renders beside the title instead of under it.
| # | Title | Absorbs from today |
|---|---|---|
| — | The program | name, goal, banner, channel |
| 1 | How people join the program | Audience tab, invitation link, roles, welcome email |
| 2 | Do people have to give their agreement | opt-in rule + opt-in message |
| 3 | How the pairs are generated | Matchmaking rules, who confirms, participation-vs-quality, match announcement |
| 4 | What the pairs do together, once paired | Milestones sub-tab + the pair's shared page |
| 5 | How the session ends | closing emails (new) |
Blocks 2 is tagged Optional. Empty blocks render dashed with a one-line incentive, a Required or Optional tag, and a primary CTA instead of Edit.
Right-side antd Drawer, getContainer={false}, rootStyle={{position:'absolute'}} so it stays inside the frame. Width 560px default, 600px when a drawer holds a rule builder, 800px for block 4. Structure: header (Step N eyebrow / title / one-line purpose) → scrollable body of fields, gap 18px → footer.
Footer: Cancel + Apply on the right. Left slot carries either a plain note or a discreet inline info alert — block 4 uses the alert:
These changes are live. Modifying milestones applies straight away to the pairs currently in the session.
Density is deliberate: field labels 13px/500, helper 12px #8C8C8C, controls 32px, group gap 18px. Do not use antd Form's default vertical rhythm; it is too airy here.
Explanations belong in the field's own subtitle, not in a coloured box. A drawer should not open on a large info alert.
<CurrentSessionCard>type Props = {
session?: Session;
steps: SessionStep[]; // derived, see below
primaryAction?: { label: string; onClick(): void };
};
The step list is derived from the program's configuration. Opt-in (block 2) and match review (block 3) are both optional, which yields four configurations and 2 to 4 steps:
| Config | Steps |
|---|---|
| opt-in ✓, review ✓ | Session started → Opt-ins collected → Match review → Matches sent |
| opt-in ✓, review ✗ | Session started → Opt-ins collected → Matches sent |
| opt-in ✗, review ✓ | Session started → Match review → Matches sent |
| opt-in ✗, review ✗ | Session started → Matches sent |
Three rules the states must respect — each was a real bug when violated:
session that has not started yet does not exist; that is the empty state's job.
"Collecting opt-ins · Closes Aug 8", not a final count. The review step has three forms: not reached → "Match review · After the opt-ins close" (no pair count); current → "Waiting for your review · 62 pairs, held 2 days"; past → "Matches reviewed · 62 pairs approved".
#4451EA, an orange Needs you tag and a primarybutton. Never an amber card border or an amber step marker.
Empty states: nothing running and nothing planned → one-line card, "No session planned" + primary "Plan a session". Nothing running but one planned → "No session running · Session 7 starts on Aug 24, 2026" + "Open Session 7".
<AttentionList>type AttentionItem = {
severity: 'blocking' | 'drifting' | 'housekeeping';
text: string; // a verdict, not a metric
action: { label: string; primary?: boolean; onClick(): void };
};
#F04438, drifting #F79009, housekeeping #8C8C8C.never empties stops being read.
reassurance, secondary "See the session".
the usual 70%"* — not "Opt-in: 56%".
The full inventory of 20 items across the three severities is in the reference file (Overview column v3). Implement detectors incrementally; the component does not care how many exist.
<MessageSlot> — an email or a surveyUsed in blocks 1, 3 and 5. Connects an already-existing email; it is never an editor.
type Props = {
kind?: 'email' | 'survey';
value?: { id: string; title: string; subject: string };
removable?: boolean; // only where the email is optional
bare?: boolean; // drop the card frame when the parent owns it
};
to recognise it), a neutral Email / Survey tag with its icon, the title, the subject line, then icon actions. No author, no timestamp.
Preview (eye) + Edit (pencil) always; Remove (bin) **only when the email isoptional**. Block 1's welcome email is optional and removable; block 2's agreement message and block 3's match announcement are required and cannot be removed, and have no empty state.
<AudienceSelector>One row per role: a label, a multi-value Select of existing audiences only (removable chips, + Add), and a headcount tag. Several audiences per role compose additively. Coffee Meetup renders a single row (§9.2).
<RadioCards> — every experience choicecomponents/radio-cards/index.tsx, list mode. Selected: 2px solid #4451EA. Unselected: 1px solid #d9d9d9. Compact: padding 6-7px 11-12px, marginBottom 5, line-height 1.35, and no gap between the option title and its description.
Every option carries a leading icon, so the choice reads as a fork in the experience rather than a form field:
| Choice | Icon |
|---|---|
| Email / Slack / Microsoft Teams | MailOutlined / SlackIcon / MsTeamsIcon |
| From selected audiences / By invitation link | TeamOutlined / LinkOutlined |
| No, match everyone | ThunderboltOutlined |
| Yes, everyone confirms | CheckCircleOutlined |
| Only some people confirm | UserSwitchOutlined |
| At random / With rules | RetweetOutlined / NodeIndexOutlined |
| You review first / Automatic | SafetyCertificateFilled / SendOutlined |
| Participation / Match quality | TeamOutlined / TrophyOutlined |
Options are named by their consequence, not their internal flag. Verbatim from the reference:
Coming soon.<MilestonesEditor> (block 4)An accordion on a white background — no blue fill on the open row.
none, so nothing anywhere may display one (this includes the Journey block's summary table and the session detail's milestone card).
Pages the pair sees at this milestone: zero or more linked notes, each rendered as a <MessageSlot>-shaped card (96×74 thumbnail, Page tag, title, description, preview / edit / remove) plus a Link a page button. There is no limit on pages.
Add a milestone button. There is no milestone library.A concept that does not exist in the product today (the member side has About / Sessions / History per program, nothing per pair). As soon as a pair is matched, mentor and mentee share one page for the whole session, reachable from any email they receive. It lists the milestones with where the pair stands on each.
In the drawer it is one field, Their shared page, whose subtitle carries the explanation, and a card shaped like <MessageSlot>: 96×74 thumbnail (banner, title, three milestone rows with status dots), Page tag, title, subtitle, then Preview and Customise icon actions. Title, intro and banner are edited behind Customise, not inline. The banner falls back to the program's.
Nothing is scheduled at session level. A pair finishes when it has held all its milestones. The field subtitle says so; there is no info alert and no end-date control.
The field holds N email cards plus Add an email. Each card is a <MessageSlot bare> over a delay row:
Sent [ 24 ] [ hours ▾ ] after their last meeting, to [ both of them ▾ ]
InputNumber (min 1) joined to a unit Select (hours / days / weeks) via Space.Compact;Select: both of them / the mentor / the mentee;white-space: nowrap so the sentence never breaks mid-phrase;Reference examples: "Your mentorship is complete" → 24 hours, both; "How did it go for you?" → 5 days, the mentor only. Empty state names the cost: "Nothing is sent when a pair finishes. You will have no satisfaction score for this program."
<TimeNavigator>Above the sessions table, replacing the old status filter. Today button (active = #4451EA border on #E2ECFF), a joined prev/next pair, and the range as plain text — "Aug 10 to Sep 6, 2026". One step = 4 weeks. No scale selector.
<SessionsTable>Columns: Session (number) · Matching date · Status · Participants · Pairs · row-end →.
completed = the date it happened.
— until the session is in progress.In progress (blue) · Planned (neutral) · Completed (green).background: rgba(16,81,224,.04).that is in progress or completed.
1px solid #F0F0F0, radius 8, 0 1px 2px rgba(0,0,0,.03). antd ships no border, so frame the .rc-table-container.
<SessionDetail>Back link → title row (number, status pill, matching date only — never an end date, see §1.2) → Export CSV as the only action → 6 KPI tiles → content.
The content depends on the status, and this is the part that was most often got wrong:
In progress = the matches are not out yet, so there are no pairs and no milestone progress. Two moments live inside that status, each with its own screen:
an info card stating explicitly that pairs are generated on DATE and there is nothing to review before then, and a table of who has answered. In link mode there is no audience and therefore no roster of non-participants: the two states are Signed up / Not signed up, nobody can decline, the table lists only sign-ups with the role they chose, and the actions are Copy the link / Share it again.
Needs you tag, Regenerate + Send the N matches, tiles on the upstream funnel, an info card saying milestones start once the matches are sent, and the held pairs with why this pair and any flags (Weak match, Same team) with Replace mentor.
Completed — the milestone card (name, progress bar, N pairs · P%; the bar turns green at 100%) and the pairs table ending in an Action column, not a chevron, derived from the pair's state:
| Pair state | Action |
|---|---|
| all milestones, high rating | Ask for a testimonial |
| all milestones | Pair the mentor again |
| stopped mid-path | Relaunch the pair (primary) |
| one milestone, low rating | Ask what happened (primary) |
| never met | Rematch next session (primary) |
Corrective actions are primary, opportunistic ones secondary — the eye lands on what is broken first.
<SessionGate>Shown in Sessions when the program cannot run yet.
satisfied ones with their default value and a green check.
generic "Untitled program" is fine), an audience, at least one milestone.
checklist. Its button says "Plan a session", not "Plan the first session": it may not be the first.
KPI tiles → a drop-off chart → a session-by-session table. Read-only plus Export CSV.
Everything here describes completed sessions only. The filter says "Completed sessions", and a session whose matches are not out reports — for every meeting-derived column and has no per-milestone sub-rows. Totals across tiles, chart and table must reconcile.
a duration on a collapsed row after the duration field was removed, "held 2 days" on a session already closed, an inherited-default note after the default was deleted. When you delete a control, grep every surface that quotes it.
These read as pedantry and were all real contradictions caught in review. Assert them in fixtures.
with a previous mentor.
Badge colours are part of the contract: Mentorship = purple, Coffee Meetup = blue, Onboarding = cyan.
Roles (mentor / mentee), a milestone path per pair, satisfaction at the end.
a disabled 3-row preview of what milestones would look like, plus the CTA. Visible scarcity, never hidden.
(Held / Booked / Booked not held / Not booked); there is no "next meeting" column.
4 / 6` on the sessions table, a 6-step current-session card, and a session detail that is one weekly meeting with its own pairs.
(week 1 their manager, week 3 the same job in another country). Block 3 keeps only what holds across all six (never the same host twice, one newcomer per host, who confirms) and says so.
widen to the closest match / skip the week / hold and tell me.
square (met / booked / waiting for you / missed / not sent). This is the only way a 0/6 newcomer becomes visible.
Round → Session, milestones, email-not-broadcast) and removal ofprogram status. Cheap, unblocks all copy.
<SessionGate> + gating the create button; delete the Settings tab.<CurrentSessionCard>, the numbers strip, <AttentionList> with blocking detectors only.<TimeNavigator>, table columns, the three session-detail states, the Action column.