Repository navigation
Complete Tailwind V2 UI migration across all remaining EJS views #609
Description
Problem Statement
The Mutorials.org frontend is divided. A previous Tailwind UI overhaul ("V2") only made it across the finish line for a handful of public pages — index, signin, signup, team, plus shared partials (head, navigation, footer, flashes, dailySnippet). The remaining ~86 EJS views (~10,500 LOC) across private/, private/train/, private/class/, private/references/, admin/, admin/train/, contributors/, usabo/, usabo/train/, and the public stragglers (forgotPassword, termsOfService, error, zetamac, etc.) still ship the legacy design.
Logged-in users hit this hardest: they sign in through the V2 form and immediately land on a legacy homepage, profile, leaderboard, or stats page — a jarring style break on the highest-traffic surface. Admins, contributors, and USABO users see the same divide on their internal tools. There are also a few left-behind footguns from the partial migration: a syntax bug in flashesV2.ejs line 15 silently dropping error flashes, and two stale *V2.ejs drafts at the repo root (newer than the actually-rendered versions in views/public/) that need reconciling.
Solution
Migrate every remaining legacy .ejs view to the V2 Tailwind design system, then perform a final cleanup pass that drops the V2 filename suffix, deletes legacy partials, and flips expressLayouts to a global default in index.js. End state: one coherent UI, one source of truth per view, no V2 markers in the tree.
This is a pure visual migration. No behavior changes, no script extraction, no shared question-display partial, no route restructuring, no schema changes. Inline scripts stay inline; the only JS edits allowed are fixing selectors that break because V2 markup uses different classes/IDs.
The V2 filename suffix is preserved during migration as a visual progress marker — engineers can tell at a glance which files have been migrated. The suffix only drops in the final cleanup PR after the last legacy page is gone.
User Stories
- As a Mutorials user signing in, I want every page after sign-in to share the same modern Tailwind look as the sign-in page, so that I'm not jarred by a sudden style break.
- As a logged-in user, I want my homepage, profile, leaderboard, settings, and stats pages to use the V2 design, so that the core authenticated surfaces feel consistent.
- As a logged-in user training problems, I want the train hub, chooseSubject, chooseUnits, displayQuestion, answerExplanation, rush, dailyQuestion, and setProficiency pages to use the V2 design, so that my study flow is visually coherent.
- As a logged-in user, I want the references hub (achievements, equations, constants, taglist, about) and class views (create/join/dash/details) to use V2, so that ancillary surfaces match the rest of the site.
- As a public visitor, I want the public stragglers (forgotPassword, termsOfService, error, zetamac, latexcompiler, confirmationEmail, robots) to use V2 styling, so that the public surface is uniformly modern.
- As an admin, I want the admin dashboard, analytics, contributor stats, announcements, and question-management pages (addQuestion, editQuestion, reviewQuestion, allQuestions, reviewHomepage, reviewSubjects, subjectQuestions) to use V2, so that internal tooling is on parity with the public-facing site.
- As a contributor, I want the contributor add-question and stats pages to use V2, so that my contribution workflow looks like the rest of the site.
- As a USABO admin/contributor/student, I want the USABO homepage and training views (chooseCategories, displayQuestion, answerExplanation, allQuestions, editQuestion, reviewHomepage) to use V2, so that the USABO silo isn't visually orphaned from the rest of the site.
- As a Mutorials user on a 375px-wide phone, I want every page to lay out cleanly without horizontal scroll and with a working collapsing nav, so that I can use the site on mobile.
- As a user submitting a form, I want flash messages (success and error) to render reliably with the V2 styling, so that I get clear feedback after submission. (Today, error flashes are silently dropped due to a syntax bug in
flashesV2.ejs.) - As a user reading an answer explanation, I want LaTeX/MathJax to render correctly on the V2 train and USABO answerExplanation pages, so that math expressions remain legible after the markup changes.
- As a user viewing the stats page, I want the inline Chart.js graphs to keep working after the migration, so that I don't lose my rating history visualization.
- As a future contributor reading the codebase, I want a single canonical filename per view (no
V2suffix), so that I'm not confused about which file to edit. - As a developer reviewing a migration PR, I want to see exactly one page (or a tightly-coupled group like
displayQuestion + answerExplanation, orreferences/*) per PR, so that diffs stay reviewable. - As a developer rolling back a regression, I want to revert a single route flip rather than untangle a giant migration commit, so that recovery is fast.
- As an engineer running
npm run build, I want the Tailwind output to write to a single, predictable CSS file with no collisions, so that production styles are deterministic. - As a tester verifying a migrated page, I want a simple six-item checklist recorded in each PR description, so that verification is consistent across pages and reviewers.
- As an admin, reviewer, contributor, or USABO admin, I want to verify migrated role-gated pages with my staging account, so that I can catch regressions specific to my role.
- As a developer touching a special bare-shell page (email confirmations, error pages, robots.txt, LaTeX compiler iframe), I want a real
views/layouts/empty.ejsto use, so that those pages render with Tailwind but without nav/footer chrome. - As the team finishing the migration, I want the
expressLayoutsmiddleware to flip to a global default inindex.jsonly at the very end, so that mid-migration we don't have to add per-route opt-outs to every still-legacy page.
Implementation Decisions
These decisions came out of a focused design session and are locked. The rationale for each is given in parentheses.
Scope and end state
- Pure visual migration. No new features, no behavior changes, no functional refactors, no script extraction. Inline scripts stay inline; only fix selectors that break against V2 markup. (Smallest blast radius; aligns with the goal of finishing the migration, not redesigning the app.)
- Done = every route serves V2 + every legacy
.ejsdeleted + theV2filename suffix dropped in a final rename pass. (Single source of truth; no permanent confusion for future contributors.)
Sequencing
Migration proceeds in this order:
- Step-zero prerequisite PR (one small PR — see below)
- Core authed shell:
homepage,profile,settings,leaderboard,stats, plusannouncements,search,question,emailConfirmation - Train: the train hub,
chooseSubject,chooseUnits,dailyQuestion,displayQuestion + answerExplanation(PR group),rush,setProficiency - References + Class: small static pages, quick wins, group
references/*andclass/*into one PR each - Public stragglers:
forgotPassword,termsOfService,whoWeAre,zetamac,latexcompiler,error,confirmationEmail,robots - Admin: dashboard, analytics, contributor stats, announcements, and the question-management family
- Contributors: contributor add-question and stats
- USABO: homepage, admin homepage, contributor add-question, and the USABO training family
(Logged-in users get visual consistency fastest because their highest-traffic pages migrate first; admins and USABO tolerate the divide longest.)
File pattern during migration
- Each migrated page is created as a parallel
pageV2.ejsnext to its legacypage.ejs. The route is flipped to render the V2 version. The legacy file remains in the tree until the final cleanup pass. (Per-page rollback by reverting the route flip; reviewable PR diffs; theV2suffix doubles as a visual progress marker.)
Layout middleware
app.use(expressLayouts)stays commented out inindex.jsduring the migration. Each migrated route opts in by registering theexpressLayoutsmiddleware and passinglayout: 'layouts/base.ejs'in render data, matching the existing V2 routes pattern. (Mid-migration global flip would force every still-legacy page to add an explicit opt-out, which is a flag-day risk with zero tests.)- In the final cleanup pass, the global is enabled, the per-route boilerplate is removed, and
app.set('layout', 'layouts/base.ejs')becomes the default.
Step-zero prerequisite PR
Before any page migration, a single small PR:
- Fixes the syntax error at
views/partials/flashesV2.ejsline 15 (missing comma after[messages.error, 'error']) that's silently dropping error flashes today. Without working flashes, per-page verification (which depends on triggering known-failing forms) is unreliable. - Reconciles two stale drafts at the repo root: ports the form-state preservation from root
signupV2.ejsintoviews/public/signupV2.ejs, ports the updated team roster from rootteamV2.ejsintoviews/public/teamV2.ejs, then removes both root files. - Defines
views/layouts/empty.ejs(currently empty) as a real bare-shell layout (DOCTYPE + Tailwind link, no nav/footer/flashes) for emails, error pages, robots, and the LaTeX compiler. - Verifies that
npm run buildproduces clean Tailwind output atpublic/css/style.csswith no collision against any legacy CSS still imported intotailwind.css.
Final cleanup pass (one PR after the last page migrates)
- Drop the
V2suffix from every filename and update every route reference. - Delete every remaining legacy partial (
partials/header.ejs,navigationPublic.ejs,navigationPrivate.ejs,footer.ejs,flashes.ejs,contact.ejs,components/aboutMutorials.ejs,components/dailySnippet.ejs,fancyLogo.ejs,experienceBar.ejs,iconBar.ejs,components/search.ejs,siteStats.ejs) after verifying each is unreferenced. - Enable
app.use(expressLayouts)globally and remove per-route layout boilerplate. - Audit source CSS files (
public/css/base.css,about.css,components/*.css) and remove rules tied to deleted legacy markup.
PR granularity
- One PR per page, or per tightly-coupled group (e.g.,
displayQuestion + answerExplanationshare state;references/*are all small static pages). No feature flags — the parallel-V2-file pattern provides per-page rollback by reverting the route flip.
Components and CSS
- The V2 design system is already in place:
views/layouts/base.ejsmaster layout,views/partials/headV2.ejs,navigationV2.ejs,flashesV2.ejs,footerV2.ejs,components/dailySnippetV2.ejs. Reuse these — do not fork. - Reusable component CSS already defined:
.btn/.btn-primary/.btn-outline(public/css/components/button.css), native input styling (input.css), dropdown styling (dropdown-menu.css), table styling (table.css), base layer rules (base.css). - Libraries available in the V2 head: Inter font (preloaded), FontAwesome, Alpine.js, Flowbite, jQuery (loaded by
dailySnippetV2). No Bootstrap. - Compiled CSS goes to
public/css/style.cssvianpm run build(overwritten on every build, so no separate "delete legacy CSS" step is needed mid-migration). Source CSS files get audited only in the final cleanup pass.
Out-of-band scope items
- USABO and train remain duplicated — copy the V2 patterns from train into USABO when migrating USABO. No shared question-display partial. (Pure-visual scope.)
- The big inline
<script>blocks onstats.ejs(Chart.js),train/displayQuestion.ejs,train/rush.ejs, andprivate/homepage.ejsstay inline. Only update selectors that break against new markup.
Testing Decisions
No automated tests are being added as part of this migration (consistent with the repo's existing zero-test posture and the pure-visual scope).
Per-PR manual verification — 6-item checklist recorded in PR description:
- Page loads at desktop width (1280px) with no console errors.
- Page loads at mobile width (375px) — no horizontal scroll, navigation collapses correctly.
- Logged-out vs. logged-in render correctly (where applicable).
- Forms round-trip end-to-end: submit a known-good payload (success flash visible) AND a known-bad payload (error flash visible).
- Page-specific JS works: charts render, timers tick, LaTeX/MathJax renders, dropdowns toggle, etc.
- View-source contains exactly one
<head>and one<body>(catches double-layout regressions where both the legacypartials/header.ejsandlayouts/base.ejsget rendered).
Role-gated pages:
- Admin, reviewer, contributor, and USABO-admin staging accounts are confirmed available.
- The tester signs in with the role-appropriate staging account before running the checklist for
admin/*,contributors/*, andusabo/*pages.
Step-zero verification:
- Trigger a known-failing form (e.g., signin with bad credentials) and confirm the V2 error flash now renders (regression fix for
flashesV2.ejs:15). - Confirm
npm run buildproduces a cleanpublic/css/style.csswith the expected Tailwind utilities and no missing component classes.
There is no prior art for automated tests in this codebase to model from. Manual verification is the bar.
Out of Scope
- New features, behavior changes, or UX redesigns beyond applying existing V2 design tokens.
- Extracting big inline
<script>blocks (Chart.js onstats, timer onrush, handlers ondisplayQuestion) intopublic/js/— they stay inline. - Creating a shared
questionDisplayV2.ejspartial across train/USABO/admin — train and USABO stay duplicated and parallel. - Functional refactor or de-duplication of route handlers, controllers, or DB queries.
- Adding automated tests (unit, integration, E2E).
- Schema changes, route restructuring, or auth changes.
- Migrating any pages currently rendered by V2 routes (those are already done; verify they still pass the 6-item checklist as part of the final cleanup pass).
- Performance optimization, accessibility audits, or SEO work — separate efforts.
Further Notes
Risks and watchpoints
- Flash bug masks verification. The step-zero fix is non-negotiable; without working error flashes, every subsequent page's verification step 4 is unreliable.
- Forgetting
expressLayoutsmiddleware on a migrated route is the most likely regression mode (page renders without nav/footer/head). Verification step 6 (single<head>/<body>) catches this. - MathJax/LaTeX selectors on
train/answerExplanationandusabo/train/answerExplanationare fragile if container class names change — verify rendering manually on those pages. stats.ejsis the single largest page in the migration (~624 LOC including 184 lines of inline Chart.js); budget extra verification time and consider it a single-page PR.- CSS source-file pruning is final-pass only. Don't delete rules from
public/css/base.css,about.css, orcomponents/*.cssmid-migration — too easy to break a still-active legacy page that depends on a class. - Velocity is bounded by tester availability. No tests means every PR is human-verified; resist the temptation to batch PRs to "save time."
Reference: design tokens at a glance
- Font: Inter (preloaded by
headV2.ejs) - Primary color: blue-500/blue-600
- Container max-widths:
max-w-sm(auth forms),max-w-4xl(most pages),max-w-6xl(team) - Buttons:
.btn+.btn-primary/.btn-outline - Inputs: native input selectors (no
.form-controlclass) - Layout: pages provide
<%- contentFor('main') %>blocks (and optionally<%- contentFor('head') %>for page-specific scripts) insideviews/layouts/base.ejs
Success criteria
- Every route in
routes/*.jsrenders a V2 template (no legacy.ejsreferenced). - All legacy partials and legacy
.ejsview files are deleted from the repo. - The
V2filename suffix is gone after the final rename pass. app.use(expressLayouts)is enabled globally and per-route layout boilerplate is removed.npm run buildproduces a clean Tailwind CSS output with no warnings.- Every page passes the 6-item manual checklist at the time it shipped.
- No regressions reported on any role-gated surface (admin, reviewer, contributor, USABO-admin) within one release cycle of the final cleanup pass.
This is tracked as an epic. Each migrated page (or tightly-coupled group) ships as its own PR linking back to this issue.
Activity
tailwind v2 migration: step-zero prerequisites (#610)
tailwind v2 migration: homepage (#611)
tailwind v2 migration: profile (#612)
Tailwind homepage (#613)
tailwind v2 migration: profile (#615)
Tailwind homepage (#616)
Tailwind homepage (#618)
Tailwind step zero (#619)
tailwind v2 staging: all batch-2 page migrations (step-zero + homepag…