From 46c44b975c5cce3a14d4677497bc51a3e570a641 Mon Sep 17 00:00:00 2001 From: Patrick Feng Date: Thu, 7 May 2026 16:57:03 -0700 Subject: [PATCH] tailwind v2 migration: leaderboard - Add views/private/leaderboardV2.ejs using base.ejs layout: rating leaderboard 4-col grid (Physics/Chemistry/Biology/USABO), Rush leaderboard, Experience leaderboard - Consolidate the four near-identical rating tables into a single forEach over [title, list, valueGetter] tuples; preserves row colors (1st/2nd/3rd/4th+) and medal SVGs - Wire /leaderboard route to render leaderboardV2.ejs with expressLayouts Part of the broader Tailwind V2 migration tracked in #609. Co-Authored-By: Claude Opus 4.7 --- routes/stats.js | 5 +- views/private/leaderboardV2.ejs | 138 ++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 views/private/leaderboardV2.ejs diff --git a/routes/stats.js b/routes/stats.js index d27dd036..4a07f236 100644 --- a/routes/stats.js +++ b/routes/stats.js @@ -24,11 +24,12 @@ module.exports = (app, mongo) => { } }); - app.get('/leaderboard', async (req, res) => { + app.get('/leaderboard', expressLayouts, async (req, res) => { const leaderboard = await generateLeaderboard(mongo.User, 10); - res.render(VIEWS + 'private/leaderboard.ejs', { + res.render(VIEWS + 'private/leaderboardV2.ejs', { rankings: leaderboard, pageName: 'Leaderboard', + layout: 'layouts/base.ejs', }); }); diff --git a/views/private/leaderboardV2.ejs b/views/private/leaderboardV2.ejs new file mode 100644 index 00000000..b1e81e24 --- /dev/null +++ b/views/private/leaderboardV2.ejs @@ -0,0 +1,138 @@ +<% + const rankBg = (r) => { + if (r === 1) return 'bg-sky-500 text-white'; + if (r === 2) return 'bg-yellow-400'; + if (r === 3) return 'bg-red-500 text-white'; + return 'bg-green-500 text-white'; + }; + const medalSrc = (r) => { + if (r === 1) return 'https://cdn.mutorials.org/images/icons/Leaderboard_First.svg'; + if (r === 2) return 'https://cdn.mutorials.org/images/icons/Leaderboard_Second.svg'; + if (r === 3) return 'https://cdn.mutorials.org/images/icons/Leaderboard_Third.svg'; + return null; + }; + const truncate = (s, n) => s.length <= n ? s : s.substring(0, n - 3) + '...'; +%> + +<%- contentFor('main') %> +
+ +

Rating Leaderboard

+ +
+ <% [ + ['Physics', rankings.physics, (u) => u.rating.physics], + ['Chemistry', rankings.chem, (u) => u.rating.chemistry], + ['Biology', rankings.bio, (u) => u.rating.biology], + ['USABO', rankings.usabo, (u) => u.rating.usabo], + ].forEach(([title, list, getValue]) => { %> +
+

<%= title %>

+
+ + + + + + + + + + <% let rank = 1; %> + <% list.forEach((user) => { %> + + + + + + <% rank++; %> + <% }) %> + +
RankUsernameRating
+ <% const medal = medalSrc(rank); %> + <% if (medal) { %> + + <% } else { %> + <%= rank %> + <% } %> + <%= truncate(user.ign, 15) %><%= getValue(user) %>
+
+
+ <% }) %> +
+ +

Problem Rush Leaderboard

+ +
+

Highest Rush Scores

+
+ + + + + + + + + + + <% let rankRush = 1; %> + <% rankings.rush.forEach((user) => { %> + + + + + + + <% rankRush++; %> + <% }) %> + +
RankUsernameHigh ScoreAttempts
+ <% const medal = medalSrc(rankRush); %> + <% if (medal) { %> + + <% } else { %> + <%= rankRush %> + <% } %> + <%= truncate(user.ign, 33) %><%= user.stats.rush.highscore %><%= user.stats.rush.attempts %>
+
+
+ +

Experience Leaderboard

+ +
+

Highest Level Mutorials Users

+
+ + + + + + + + + + + <% let rankExp = 1; %> + <% rankings.experience.forEach((user) => { %> + + + + + + + <% rankExp++; %> + <% }) %> + +
RankUsernameLevelExperience
+ <% const medal = medalSrc(rankExp); %> + <% if (medal) { %> + + <% } else { %> + <%= rankExp %> + <% } %> + <%= truncate(user.ign, 33) %><%= user.level.level %><%= Math.round(user.experience) %>
+
+
+ +