-
-
- Know Your Candidates
-
- {/* Only the empty states get a sentence. Where candidates have
- answered, the roster underneath names both halves of the
- ballot — "2 of 11 answered" was the same count, spelled out,
- immediately above the list it was counting. */}
- {respondents.length === 0 && (
-
- {councilCandidates.length === 0
- ? "No one has registered in this ward yet."
- : withheld
- ? ANSWERS_WITHHELD
- : "Nobody in this ward has answered yet. These are the questions we asked."}
-
- )}
- {councilCandidates.length > 0 && (
-
!surveyAnswers?.[candidate.key],
- )
- }
- respondentsLabel={
- withheld ? "On the ballot" : undefined
- }
- election={election.slug}
- race="councillor"
- ward={ward.n}
- wardName={ward.name}
- />
- )}
-
-
- {/* Absent entirely while the survey is closed. The column beside
- it is the heading and the ballot, which stand on their own —
- this was always the ask, not part of the ward's own facts. */}
- {surveyHref(election) && (
-
- )}
+ {/* ── Council candidates ─────────────────────────────── */}
+ {/* The ward's ballot, one card a candidate: who is standing, whether
+ they hold the seat, and the way to their own campaign. That is a
+ fact about the election rather than anything a candidate told us,
+ so it is what the page is made of until the questionnaire is
+ published. */}
+
+
+
+ Candidates
+
- {councilCandidates.length === 0 ? (
+ {councilRaces.length === 0 && (
- ) : (
- councilRaces.map((race) => (
-
- ))
)}
+ {councilRaces.map((race) => (
+
+ {showRaceHeadings && }
+ {race.candidates.length === 0 ? (
+
+ ) : (
+ race.candidates.map((cand) => (
+
+ ))
+ )}
+
+ ))}
+
- Registered candidates from the City Clerk’s list, less
- anyone who has withdrawn. The field is not final until nominations
- close{nominationCloseLabel ? ` on ${nominationCloseLabel}` : ""}.
+ Registered candidates from the City Clerk’s list. The field
+ is not final until nominations close
+ {nominationCloseLabel ? ` on ${nominationCloseLabel}` : ""}.
@@ -325,95 +234,6 @@ export function WardDetail({
);
}
-/**
- * One race's questionnaire, question by question.
- *
- * One per race rather than one for the ward, because a ward can elect more
- * than one councillor — Brampton's wards elect a city and a regional
- * councillor — and two rival fields read together would compare candidates who
- * are not running against each other.
- *
- * A race nobody answered has no questions to draw, since the questions come
- * from the returned questionnaires. That case still names the candidates: they
- * are on the ballot, and the page is now the only place that says so.
- */
-function RaceQuestionnaire({
- race,
- surveyAnswers,
- surveyShape,
- showHeading,
- withheld = false,
- issuesHref,
-}: {
- race: RaceView;
- surveyAnswers?: Record
;
- surveyShape?: ComparedGroup[];
- showHeading: boolean;
- /** the answers are being held back, so an empty grid is our doing and not
- * a field that stayed quiet — see `questionnaireHidden` in the registry */
- withheld?: boolean;
- issuesHref?: string;
-}) {
- /* Two lists, not one. The candidates who wrote back are the ones the
- questions can group, and the rest are named beside them — a reader can
- still see everyone on their ballot and go to their site, without a
- ward's dozen registrants turning thirty questions into three hundred
- cells of "did not respond". */
- const roster = surveyRoster(
- race.candidates.map((candidate) => ({
- ...candidate,
- // "" for most of the ballot; the grid only draws the row when something
- // in it is non-empty, so pass through rather than filtering here.
- bio: candidate.bio || undefined,
- })),
- surveyAnswers,
- );
- const answered = roster.filter((candidate) => candidate.answers);
- const silent = roster.filter((candidate) => !candidate.answers);
- const groups = comparedQuestions(
- answered.map((candidate) => candidate.answers!),
- answered,
- surveyShape,
- );
-
- return (
-
- {showHeading &&
}
-
- {groups.length > 0 ? (
-
-
-
- ) : (
- /* Nobody has filed for the seat, the questionnaire could not be
- fetched, or the answers are being held back. Either way there is
- no grid to draw, and the candidates are still worth naming — but
- only the first two let us say the field has yet to respond, which
- is why the third has to be told apart from them. */
-
- {roster.length === 0
- ? "No one has filed for this seat yet."
- : withheld
- ? /* Nothing. The notice is already up beside the ballot at the
- top of this section, and a ward with two races would
- otherwise print it once per race under the one that
- covers them all. */
- null
- : `On the ballot, and yet to respond to us: ${roster
- .map((candidate) => candidate.name)
- .join(", ")}.`}
-
- )}
-
-
- );
-}
-
function RaceHeading({ race }: { race: RaceView }) {
return (
@@ -450,8 +270,8 @@ function EmptyRace({
);
}
-/* A candidate card, which only the school-board races use now: the council
- ballot is the questionnaire grid, and trustees have no questionnaire. */
+/* A candidate card — one per candidate, in both the council and the
+ school-board races. */
function CouncilCandidate({
candidate,
election,
diff --git a/src/lib/elections/registry.ts b/src/lib/elections/registry.ts
index 4c6116e..4026f05 100644
--- a/src/lib/elections/registry.ts
+++ b/src/lib/elections/registry.ts
@@ -103,21 +103,35 @@ export type SupportedElection = {
* The candidates' questionnaire answers are off for now.
*
* The sibling of `surveyClosed` and the same bargain: temporary, one line,
- * and nothing deleted. What comes down is every published answer — the cards
- * on the ward, mayoral and candidate pages, the whole-field view on /issues,
- * and the read proxy that serves them to the browser. What stays up is the
- * ballot: who is running, in which ward, with their campaign site, which is
- * a fact about the election rather than anything a candidate told us.
+ * and nothing deleted. It shuts the answers off at the source — the read
+ * proxy that serves them to the browser, and `rosterSurvey`, which every
+ * page reading them goes through. Turning them back on is deleting this
+ * flag.
*
- * A candidate's own bio stays too, though it arrives in the same response.
- * It is a self-description rather than a position, and it is the only thing
+ * A candidate's own bio stays, though it arrives in the same response. It is
+ * a self-description rather than a position, and it is the only thing
* standing between most candidate pages and an empty one.
*
- * The pages need no empty states written for this: they already have the
- * ones built for the weeks before anybody had written back — "Nobody in this
- * ward has answered yet", "No answers from the mayoral field have been
- * published yet" — and hiding the answers at the source puts every page into
- * exactly that state.
+ * WHAT THE PAGES SHOW INSTEAD
+ *
+ * The pre-questionnaire ballot, which is what they showed before there were
+ * answers to publish: who is running, in which ward, with their campaign
+ * site — every one of which is a fact about the election rather than
+ * anything a candidate told us. The ward pages list their candidates,
+ * /mayor/candidates lists the mayoral field flat, and a candidate's own page
+ * keeps their bio and their links.
+ *
+ * None of them says anything about the answers. An empty state written for
+ * the weeks before anybody had written back — "Nobody in this ward has
+ * answered yet" — is a claim about candidates who answered months ago, and
+ * a notice that the answers are coming is a promise on a page that is
+ * complete without one.
+ *
+ * The two pages that exist only to publish answers, /issues and /mayor, have
+ * nothing left when those are gone, so they are switched off at the router
+ * instead and land on the nearest ballot (see next.config.ts). They stay in
+ * the repo exactly as built, which is why `ANSWERS_WITHHELD` below is still
+ * here for them.
*/
questionnaireHidden?: boolean;
};
@@ -125,20 +139,17 @@ export type SupportedElection = {
/**
* What a page says where the candidates' answers would be.
*
- * One phrase, in one place, because it appears on the ward pages, the mayoral
- * page, /issues and every candidate page, and four hand-written versions of it
- * would be four different accounts of the same fact.
- *
- * Once per page. It names what is missing rather than only promising a return,
- * so on a page that says it twice — in the masthead and again where the cards
- * would be — it reads as a stutter rather than as a fuller explanation. Each
- * page keeps it in the one place a reader looks for the thing that is gone.
+ * Nothing a reader can reach says it today: the pages that publish the ballot
+ * are complete without the answers, and are left to say what they do know
+ * rather than what they are not saying yet — see `questionnaireHidden` above.
+ * It is still here because /issues and /mayor, the two pages that were nothing
+ * but the answers, are kept in the repo as built and switched off at the
+ * router; this is the line they print if either is ever served again.
*
- * What it must not do is reuse the empty states these pages already had for
- * the weeks before anybody had written back. "Nobody in this ward has answered
- * yet", printed over a ward whose candidates answered months ago, is a claim
- * about those candidates and it is ours, not theirs — and on a candidate page
- * it sits under a named person's photograph.
+ * One phrase, in one place, because four hand-written versions of it would be
+ * four different accounts of the same fact. Once per page: it names what is
+ * missing rather than only promising a return, so on a page that says it twice
+ * it reads as a stutter rather than as a fuller explanation.
*/
export const ANSWERS_WITHHELD = "Candidate survey coming soon.";