Skip to content

Complete Tailwind V2 UI migration across all remaining EJS views #609

Open

Description

@kokonut121

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. 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.
  9. 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.
  10. 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.)
  11. 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.
  12. 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.
  13. As a future contributor reading the codebase, I want a single canonical filename per view (no V2 suffix), so that I'm not confused about which file to edit.
  14. As a developer reviewing a migration PR, I want to see exactly one page (or a tightly-coupled group like displayQuestion + answerExplanation, or references/*) per PR, so that diffs stay reviewable.
  15. 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.
  16. 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.
  17. 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.
  18. 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.
  19. 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.ejs to use, so that those pages render with Tailwind but without nav/footer chrome.
  20. As the team finishing the migration, I want the expressLayouts middleware to flip to a global default in index.js only 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 .ejs deleted + the V2 filename suffix dropped in a final rename pass. (Single source of truth; no permanent confusion for future contributors.)

Sequencing

Migration proceeds in this order:

  1. Step-zero prerequisite PR (one small PR — see below)
  2. Core authed shell: homepage, profile, settings, leaderboard, stats, plus announcements, search, question, emailConfirmation
  3. Train: the train hub, chooseSubject, chooseUnits, dailyQuestion, displayQuestion + answerExplanation (PR group), rush, setProficiency
  4. References + Class: small static pages, quick wins, group references/* and class/* into one PR each
  5. Public stragglers: forgotPassword, termsOfService, whoWeAre, zetamac, latexcompiler, error, confirmationEmail, robots
  6. Admin: dashboard, analytics, contributor stats, announcements, and the question-management family
  7. Contributors: contributor add-question and stats
  8. 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.ejs next to its legacy page.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; the V2 suffix doubles as a visual progress marker.)

Layout middleware

  • app.use(expressLayouts) stays commented out in index.js during the migration. Each migrated route opts in by registering the expressLayouts middleware and passing layout: '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:

  1. Fixes the syntax error at views/partials/flashesV2.ejs line 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.
  2. Reconciles two stale drafts at the repo root: ports the form-state preservation from root signupV2.ejs into views/public/signupV2.ejs, ports the updated team roster from root teamV2.ejs into views/public/teamV2.ejs, then removes both root files.
  3. 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.
  4. Verifies that npm run build produces clean Tailwind output at public/css/style.css with no collision against any legacy CSS still imported into tailwind.css.

Final cleanup pass (one PR after the last page migrates)

  • Drop the V2 suffix 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 + answerExplanation share 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.ejs master 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.css via npm 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 on stats.ejs (Chart.js), train/displayQuestion.ejs, train/rush.ejs, and private/homepage.ejs stay 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:

  1. Page loads at desktop width (1280px) with no console errors.
  2. Page loads at mobile width (375px) — no horizontal scroll, navigation collapses correctly.
  3. Logged-out vs. logged-in render correctly (where applicable).
  4. Forms round-trip end-to-end: submit a known-good payload (success flash visible) AND a known-bad payload (error flash visible).
  5. Page-specific JS works: charts render, timers tick, LaTeX/MathJax renders, dropdowns toggle, etc.
  6. View-source contains exactly one <head> and one <body> (catches double-layout regressions where both the legacy partials/header.ejs and layouts/base.ejs get 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/*, and usabo/* 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 build produces a clean public/css/style.css with 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 on stats, timer on rush, handlers on displayQuestion) into public/js/ — they stay inline.
  • Creating a shared questionDisplayV2.ejs partial 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 expressLayouts middleware 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/answerExplanation and usabo/train/answerExplanation are fragile if container class names change — verify rendering manually on those pages.
  • stats.ejs is 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, or components/*.css mid-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-control class)
  • Layout: pages provide <%- contentFor('main') %> blocks (and optionally <%- contentFor('head') %> for page-specific scripts) inside views/layouts/base.ejs

Success criteria

  • Every route in routes/*.js renders a V2 template (no legacy .ejs referenced).
  • All legacy partials and legacy .ejs view files are deleted from the repo.
  • The V2 filename suffix is gone after the final rename pass.
  • app.use(expressLayouts) is enabled globally and per-route layout boilerplate is removed.
  • npm run build produces 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.

Metadata

Metadata

Assignees

Labels

cleaningMakes the repository better and cleaner for everyone.front-endIssues that relate to front-end features or bugs

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions