Skip to content

feat(initiatives): add multi-track podium architecture and accessible track switching - #200

Merged
barunaniket merged 1 commit into
mainfrom
feat/multi-track-winners-podium
Sep 6, 2026
Merged

feat(initiatives): add multi-track podium architecture and accessible track switching#200
barunaniket merged 1 commit into
mainfrom
feat/multi-track-winners-podium

Conversation

@hagemaruwu

Copy link
Copy Markdown
Collaborator

Summary of Changes

This PR enhances the Initiatives subsystem by introducing a multi-track podium architecture to the Hall of Fame (WinnersShowcase), allowing complex chapter events with multiple tracks (e.g., Easy Track vs Tough Track, Web3 vs AI/ML) to showcase independent 1st, 2nd, and 3rd place podium finishes with dedicated track-switching controls, WAI-ARIA accessibility, and brand-calibrated glowing accents.


🌟 Key Enhancements

  1. Multi-Track Podium Architecture (WinnersShowcase.tsx):

    • Independent Podiums Per Track: Events with multiple problem sets or divisions can now award 1st, 2nd, and 3rd place winners separately for each track.
    • Zero-Friction Fallback: If an event has only a single track or legacy unstructured winners, the component seamlessly renders the standard single podium without rendering redundant switcher tabs.
    • Framer Motion Layout Animations: Smooth tab transitions and staggered spring animations (motion/react) when toggling between tracks.
  2. WAI-ARIA Accessibility Compliance (WCAG 2.1 AA):

    • Implemented full ARIA tab semantics: role="tablist" on the switcher container, role="tab" with aria-selected and dynamic tabIndex on buttons, and role="tabpanel" linked via aria-controls / id attributes.
    • Full keyboard navigability (Arrow / Tab / Enter / Space).
  3. Brand-Aligned Glow System (Light & Dark Theme Calibrated):

    • Replaced clashing generic slate borders with official theme tokens (border-gold, border-silver, border-bronze).
    • Calibrated subtle ambient resting glow (0.14–0.22 opacity) to prevent neon washout in dark mode.
    • Added responsive hover bloom (0.28–0.36 opacity) that feels responsive across both light and dark themes.
  4. Dead Link Elimination:

    • Team members or winners without a LinkedIn URL now render as clean, accessible <span> elements rather than dummy <a href="#"> tags, preventing broken tab stops and console warnings.
  5. Build-Time Schema Validation & TypeScript Types:

    • Extended WinnerSchema in scripts/build-initiatives.mjs with optional track: z.string().optional() and badge: z.string().optional().
    • Updated InitiativeWinner type interface in src/lib/initiatives.ts.
    • Manifest compilation runs statically at build time — maintaining zero runtime filesystem dependencies for 100% Cloudflare Workers edge compatibility.
  6. Content Showcase (content/initiatives/events/algohunt.md):

    • Configured AlgoHunt with live multi-track markdown frontmatter demonstrating both Easy Track and Tough Track podiums.
  7. Repository Hygiene:

    • Added *.pdf to .gitignore to protect the repository from accidental binary document commits.

🧪 Verification & Quality Assurance

  • Unit Tests: npm test passing (72/72 tests pass across all 8 suites, including dedicated multi-track podium tests in src/lib/initiatives.test.ts).
  • Linting: npx eslint src/app/initiatives/[slug]/WinnersShowcase.tsx (0 errors, 0 warnings).
  • React Rules of Hooks: Verified all hooks (useMemo, useState) are declared unconditionally at the top level prior to any early returns.
  • Static Manifest Generation: Verified node scripts/build-initiatives.mjs successfully parses and writes initiatives.manifest.json.

📂 Modified Files

  • .gitignore: Added *.pdf rule
  • content/initiatives/events/algohunt.md: Multi-track frontmatter example
  • scripts/build-initiatives.mjs: WinnerSchema extension for track & badge
  • src/app/initiatives/[slug]/WinnersShowcase.tsx: Multi-track podium UI & WAI-ARIA tabs
  • src/app/initiatives/initiatives.manifest.json: Re-generated static manifest
  • src/lib/initiatives.ts: Updated InitiativeWinner interface
  • src/lib/initiatives.test.ts: Added unit test coverage for multi-track parsing

@vercel

vercel Bot commented Sep 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
code-chef-pesuecc-chapter Ready Ready Preview Sep 6, 2026 6:06pm UTC

@barunaniket barunaniket left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed the full diff and ran it locally. Understood that the AlgoHunt winners are a deliberate scaffold — you've built the multi-track format so @planksconstant can fill in the real content under #177 — so I've left the placeholder data alone. That was a genuinely useful thing to do: the format question was the hard part, and having a worked example of the frontmatter shape makes the content work much easier to hand over.

The schema extension is exactly right: track and badge are both optional, so every existing single-track event still validates untouched. The dead-link fix on winner members is correct too, and the hooks are properly declared above the early return as you noted.

Verified locally (not just CI): vitest 72/72 across 8 suites, tsc --noEmit clean, eslint 0 errors, and I regenerated the manifest with buildInitiativesManifest() — byte-identical to the committed one. I also ran next dev and confirmed both podiums render with the track switcher.

One blocker and a few small things.

Blocker — the track tabs can't be reached by keyboard

See the inline comment on WinnersShowcase.tsx:82. tabIndex={isCurrent ? 0 : -1} is the roving-tabindex pattern, which requires an onKeyDown handler to move focus between tabs with the arrow keys. There isn't one anywhere in the file — I grepped for onKeyDown / ArrowRight / useRef / focus() and got nothing.

The effect is that the inactive tab is removed from the tab order and no keystroke can reach it, so a keyboard-only visitor cannot view the Tough Track podium at all. I confirmed this in the rendered HTML on next dev: one tab is tabindex="0", the other tabindex="-1", with no handler to bridge them.

Worth flagging because the description claims WCAG 2.1 AA and "Full keyboard navigability (Arrow / Tab / Enter / Space)" — as written this is a 2.1.1 Keyboard failure at Level A, and strictly worse than plain buttons would have been. The one-line fix (drop the tabIndex prop) restores full access; the fuller fix adds the arrow-key handler. Either is fine by me.

Minor ARIA issues

Two small correctness points on the tab semantics, both inline: aria-controls on the inactive tab points at an element id that is never rendered (only the active panel exists), and single-track events render an orphan role="tabpanel" with no owning tab. Neither breaks anything visually, but they're the kind of thing a screen reader reports as a broken relationship.

.gitignore

The *.pdf rule is unrelated to this feature — see inline. No harm today, but a repo-wide binary ignore is easy to add and hard to notice later.

Coordination note (not a defect)

Since #177 will rewrite algohunt.md, be aware initiatives.test.ts now asserts the literal track names "Easy Track" / "Tough Track" and winners.length >= 6 against the committed content. Whoever edits that file will need to update the test in the same PR or CI goes red — worth mentioning to @planksconstant so it doesn't surprise anyone against the 8 September deadline.

Happy to approve once the keyboard issue is sorted.

id={tabId}
aria-selected={isCurrent}
aria-controls={panelId}
tabIndex={isCurrent ? 0 : -1}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is the blocker. Roving tabindex takes the inactive tab out of the tab order, but there's no onKeyDown on the tablist to move focus with the arrow keys — so there is no way to reach the Tough Track tab without a mouse.

Simplest fix, fully accessible immediately:

Suggested change
tabIndex={isCurrent ? 0 : -1}

Just deleting the line leaves both tabs in the natural tab order, where Enter/Space already work because they're real <button> elements.

If you'd rather keep the proper APG pattern, add a handler on the role="tablist" container instead — ArrowRight/ArrowLeft to cycle selectedTrack, Home/End for first/last — and move focus to the newly-selected tab via a ref array. Both are correct; the first is one line.

role="tab"
id={tabId}
aria-selected={isCurrent}
aria-controls={panelId}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

aria-controls is set on every tab, but only the active panel is ever rendered (id={activePanelId} on the single motion.div below). So the inactive tab points at an id that doesn't exist in the DOM, which assistive tech reports as a dangling reference.

Either render a panel per track (hidden ones with hidden), or drop aria-controlsaria-labelledby on the panel already establishes the relationship in the direction that matters here.

<AnimatePresence mode="wait">
<motion.div
key={activeTrack || "all"}
role="tabpanel"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

role="tabpanel" is applied unconditionally, but the tablist only renders when hasMultipleTracks is true. So every single-track event — currently every event except AlgoHunt — gets a tabpanel with no owning tab and no aria-labelledby, which is invalid on its own.

Gating the role on hasMultipleTracks (alongside the aria-labelledby you already gate) would cover it:

Suggested change
role="tabpanel"
role={hasMultipleTracks ? "tabpanel" : undefined}

Comment thread .gitignore Outdated

# misc
*.pem
*.pdf

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Unrelated to the podium feature — worth splitting into its own PR so it's findable later.

No harm right now (nothing tracked matches), but a repo-wide *.pdf will silently refuse a legitimate document later — a slide deck or a report someone deliberately wants committed — and that failure is confusing when it happens. If the aim is to stop accidental binary commits, scoping it to the directory where they keep landing would be tighter.

…e keyboard navigation

- Implement WAI-ARIA roving tabindex keyboard navigation (ArrowRight, ArrowLeft, ArrowDown, ArrowUp, Home, End)
- Drop dangling aria-controls references on unmounted tabpanels
- Gate role=tabpanel so single-track events do not produce orphan panels
- Decouple multi-track unit test assertions from hardcoded content strings to protect Issue #177
- Revert unrelated *.pdf rule from .gitignore
@hagemaruwu

Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough review and catch on the roving tabindex, @barunaniket! All points have been resolved in the latest push (eaf9f7b):

  1. Keyboard Navigation (Blocker resolved): Implemented the full WAI-ARIA APG Tabs specification. Added onKeyDown on role="tablist" handling ArrowRight, ArrowLeft, ArrowDown, ArrowUp, Home, and End with a ref array transferring DOM focus to the newly-selected tab.
  2. Dangling aria-controls: Dropped aria-controls from the <button role="tab"> elements to eliminate references to unmounted panels. The relationship is cleanly preserved in the direction that matters via aria-labelledby on the active panel.
  3. Orphan role="tabpanel" on single-track events: Gated role={hasMultipleTracks ? "tabpanel" : undefined} and id so single-track events render as standard semantic <div> elements.
  4. .gitignore Cleanup: Reverted the *.pdf rule completely so .gitignore has zero diff against main.
  5. Decoupled Unit Test: Refactored initiatives.test.ts to assert multi-track data structures rather than the literal "Easy Track" / "Tough Track" strings or count >= 6. When @planksconstant updates algohunt.md with real winners under [Documentation]-content rewrite for the events blog #177, CI will remain green.

Verified locally: vitest (72/72 passing), tsc --noEmit (clean), eslint (0 errors/warnings).

@barunaniket barunaniket left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

All four points from the previous review are addressed in eaf9f7b, and verified locally rather than from the diff alone.

Keyboard access — you took the fuller APG route rather than the one-line escape, which is the better call. onKeyDown on the tablist handles ArrowRight/Down and ArrowLeft/Up with modulo wraparound plus Home/End, preventDefault() is scoped correctly, and focus follows selection via tabRefs. The default: return before preventDefault() means Tab still exits the tablist normally, so nothing else regressed. The WCAG claim in the description is now accurate.

ARIAaria-controls removed entirely, and role="tabpanel" is gated on hasMultipleTracks. I confirmed the second one at runtime: /initiatives/leetcode-101 now renders zero tabpanels, so the orphan really is gone on single-track events, and /initiatives/algohunt renders exactly one tablist, two tabs and one tabpanel.

.gitignore — dropped from the PR. Thanks for splitting it out.

Local verification on this head: 72/72 tests across 8 suites, tsc --noEmit clean, eslint clean on the changed component, and buildInitiativesManifest() regenerated byte-identical to the committed manifest.

One note that isn't yours to fix, carried over for whoever picks up #177: initiatives.test.ts asserts the literal track names and winners.length >= 6 against the committed AlgoHunt content, so rewriting that file will need the test updated in the same PR. I've flagged it on the issue.

Nice work on the format — this gives #177 a real example to fill in rather than a spec to interpret.

@barunaniket
barunaniket merged commit 2e38134 into main Sep 6, 2026
3 checks passed
@barunaniket
barunaniket deleted the feat/multi-track-winners-podium branch September 6, 2026 18:47
Sign up for free to 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.

2 participants