Skip to content

Toronto Voter Survey - #52

Merged
mikaalnaik merged 14 commits into
mainfrom
mikaal/toronto-issue-survey
Sep 8, 2026
Merged

Toronto Voter Survey#52
mikaalnaik merged 14 commits into
mainfrom
mikaal/toronto-issue-survey

Conversation

@mikaalnaik

@mikaalnaikmikaalnaik commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Builds out the 2026 municipal election section around the candidate questionnaire: voters answer the same questions the candidates did, then see where they line up with the field.

Rebased on main now that the Toronto voter-guide/SEO pages (#74) have landed, so this PR is just the survey and candidate-answer work.

What's here

Voter survey (/toronto/vote/2026/survey)

  • SurveyClient renders the York Factory-hosted questionnaire rather than a local copy, so the questions voters answer are the same ones sent to candidates. Multi-step form, one question at a time, with staggered field transitions (.step-field in globals.css).
  • On submit, AlignmentResults scores the voter against every candidate who wrote back (src/lib/elections/alignment.ts) and shows the closest matches per race.
  • AgreementChart / AnswerChart show how the field answered each question next to the voter's own pick.
  • Server routes: /api/elections/survey (fetch + submit) and /api/elections/candidate-responses.

Candidate answers

  • /toronto/vote/2026/mayor — the questionnaire read across the mayoral field as a SurveyGrid: candidates as columns, questions as rows, non-respondents marked as such.
  • /toronto/vote/2026/mayor/candidates — new. The mayoral roster on a page of its own: the field is fifty-odd people and it was eating half the landing page. Grouped answered-first (a name vs. a position is the most useful sort available), surname order within each group, withdrawn candidates kept in a group at the foot.
  • /toronto/vote/2026/issues — new. The whole field read issue-by-issue via FieldSentiment, one card per question, for the city-wide "where do they converge and split" view that a grid can't hold at 32 respondents.
  • /toronto/vote/2026/wards/[ward]WardDetail now carries each race's questionnaire answers, charted with CandidateDotPlot (a dot per candidate on the option they picked) rather than a dial — shares are meaningless at two-to-six respondents, and the dial could only ever mark one position.

Trilemma dial (src/components/charts/trilemma/) ported in for the three-way tradeoff questions; its palette is also what the dot plots and answer charts colour from, so the pages read as one system.

Shared piecesSurveyGrid, AnswerChart, AnswerOptionList, CandidateDotPlot, CandidateSurveyAnswers, SurveyCta, FieldSentiment, plus src/lib/elections/{alignment,candidate-answers,candidate-responses,field-sentiment,survey,survey-answers,names,postal-code}.ts. ElectionLanding reworked around the new race entry points; postal-code normalization pulled out of the pledge route so the survey can share it.

Notes

  • Candidate responses are fetched at request time from York Factory, so pages reflect late replies without a redeploy.
  • tsc --noEmit clean; pnpm lint reports only the four pre-existing warnings on bills/* and state-of-the-nation/og-card.tsx.

@greptile-apps

greptile-appsBot commented Aug 12, 2026

Copy link
Copy Markdown

Greptile Summary

The PR adds a York Factory-backed Toronto voter questionnaire, candidate-alignment results, and several candidate-answer views.

  • Adds survey loading, submission, scoring, and candidate-response APIs.
  • Adds mayoral, ward, and issue-level questionnaire visualizations.
  • Introduces shared election components and trilemma chart primitives.
  • Reorganizes Toronto’s election landing and candidate roster pages.

Confidence Score: 4/5

The PR is not yet safe to merge because its survey load and submission flows still target York Factory routes that are not registered.

The outstanding backend-route issue remains: opening the survey depends on an unavailable remote GET route and converts that failure into a not-found page, while submitting answers targets an unavailable POST route and returns an error.

Files Needing Attention: src/lib/elections/survey.ts and src/app/api/elections/survey/route.ts

Important Files Changed

FilenameOverview
src/lib/elections/survey.tsDefines and fetches the remote questionnaire, but its York Factory GET dependency remains unavailable in the related backend repository.
src/app/api/elections/survey/route.tsAdds the submission proxy, but forwards completed surveys to an endpoint not registered in the related backend repository.
src/app/toronto/vote/2026/survey/SurveyClient.tsxImplements the multi-step voter questionnaire and transitions into candidate-alignment results.
src/lib/elections/alignment.tsAdds candidate scoring and race-level alignment calculations.
src/app/api/elections/candidate-responses/route.tsAdds a ward-scoped response proxy that joins respondent data with Toronto candidate rosters.
src/components/elections/SurveyGrid.tsxAdds the candidate-by-question grid used to present mayoral questionnaire answers.

Sequence Diagram

sequenceDiagram
participant V as Voter
participant T as Tradingpost
participant Y as York Factory
V->>T: Open survey page
T->>Y: "GET /elections/{election}/surveys/{survey}"
Y-->>T: Survey definition
T-->>V: Questionnaire
V->>T: Submit answers
T->>Y: "POST /elections/{election}/survey_responses"
Y-->>T: Region and submission result
T->>Y: GET candidate responses
Y-->>T: Candidate answers
T-->>V: Alignment results
Loading

Reviews (3): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

Comment on lines +108 to +111
): Promise<Survey> {
const { data } = await apiFetch<{ data: Survey }>(
`/elections/${electionSlug}/surveys/${surveySlug}`,
{ revalidate: 300, tags: [`survey:${electionSlug}:${surveySlug}`] },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1Backend survey routes are missing

The survey page always requests GET /elections/{election}/surveys/{survey}, but the current York Factory election routes register only pledges, so the request returns 404 and loadSurvey turns the new page into a not-found response. The submission proxy likewise targets an unregistered POST /elections/{election}/survey_responses route, preventing responses from being recorded.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/elections/survey.ts
Line: 108-111
Comment:
**Backend survey routes are missing**
The survey page always requests `GET /elections/{election}/surveys/{survey}`, but the current York Factory election routes register only pledges, so the request returns 404 and `loadSurvey` turns the new page into a not-found response. The submission proxy likewise targets an unregistered `POST /elections/{election}/survey_responses` route, preventing responses from being recorded.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in CodexFix in Claude Code

@mikaalnaik
mikaalnaikforce-pushed the mikaal/toronto-issue-survey branch from 3c283cd to 0e73123CompareSeptember 3, 2026 13:54
mikaalnaikand others added 14 commits September 4, 2026 12:05
The questions lived here in surveyData.ts, which was a deliberate choice
while this was the only thing rendering them: rewording a question needed
no deploy on the API side. Candidates now answer surveys too, and a
candidate questionnaire is authored in York Factory's CMS — a question set
that only exists in the front end can't be rendered as an admin form. So
the database became the source of truth and this follows it.
The renderer is unchanged in spirit: it still derives step count, progress,
validation and the submitted payload from whatever steps it's given, only
now they arrive over the wire. Adding a question is a CMS edit with no
deploy here at all, which is more than the old arrangement managed.
Three things follow from the move:
- The submitted slug and version come from the survey that was actually
rendered rather than constants, so answers can't be filed under a
question set the respondent never saw.
- yesno options come from the API like any other choice list, so the
values published results group by have one definition rather than a copy
on each side.
- Ward choices are resolved from the election's councillor races when the
survey is served, so the list can't drift from the ward pages the way a
frozen copy in this repo did.
The page 404s when the definition can't be fetched and the ISR cache is
cold. A half-rendered form is worse than an honest miss, and keeping a
hard-coded fallback here is the drift this change exists to end.
Depends on york_factory#100: production has no /surveys endpoint until
that ships.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mikaalnaik
mikaalnaikforce-pushed the mikaal/toronto-issue-survey branch from 0e73123 to eb1c18dCompareSeptember 8, 2026 19:42
@mikaalnaik
mikaalnaik merged commit 770d434 into mainSep 8, 2026
2 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@mikaalnaik