Skip to content

Commit 6eae87b

Browse files
kokonut121claude
andauthored
Tailwind step zero (#619)
* tailwind v2 migration: step-zero prerequisites - Fix flashesV2.ejs missing comma that silently dropped error flashes - Port form-state preservation from stale root signupV2.ejs into views/public/signupV2.ejs - Define empty.ejs as a real bare-shell layout (DOCTYPE + Tailwind, no nav/footer) - Remove stale root signupV2.ejs and teamV2.ejs drafts (views/public versions are canonical) Prereq for the broader Tailwind V2 migration tracked in #609. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * tailwind v2 migration: homepage (#611) - Add views/private/homepageV2.ejs using base.ejs layout: welcome, Tailwind experience bar, contributor controls, 5-subject ratings, Chart.js rating tracker, training-mode + classic-subject + USABO grids, site stats, dailySnippetV2, announcements panel, social-link bar - Wire /homepage route to render homepageV2.ejs with expressLayouts middleware and layout: 'layouts/base.ejs' - Drop the bottom search widget on the homepage (jQuery-MultiSelect + Bootstrap; nav already exposes /search and the search.ejs page migrates separately) - Drop AOS scroll animations and jQuery thumbnail hover; replaced hover with Tailwind opacity-80 hover:opacity-100 Part of the broader Tailwind V2 migration tracked in #609. Legacy homepage.ejs remains in tree until the final cleanup pass. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> * Tailwind homepage (#613) * tailwind v2 migration: homepage - Add views/private/homepageV2.ejs using base.ejs layout: welcome, Tailwind experience bar, contributor controls, 5-subject ratings, Chart.js rating tracker, training-mode + classic-subject + USABO grids, site stats, dailySnippetV2, announcements panel, social-link bar - Wire /homepage route to render homepageV2.ejs with expressLayouts middleware and layout: 'layouts/base.ejs' - Drop the bottom search widget on the homepage (jQuery-MultiSelect + Bootstrap; nav already exposes /search and the search.ejs page migrates separately) - Drop AOS scroll animations and jQuery thumbnail hover; replaced hover with Tailwind opacity-80 hover:opacity-100 Part of the broader Tailwind V2 migration tracked in #609. Legacy homepage.ejs remains in tree until the final cleanup pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * tailwind v2 migration: profile (#612) - Add views/private/profileV2.ejs using base.ejs layout: name banner, Tailwind experience bar, info card (name/age/location/bio/achievements), Chart.js rating tracker + correct/wrong pie with custom plugin, Problem Rush highscore, Collected Tags grid (Physics/Chemistry/Biology) - Wire /profile/:username route to render profileV2.ejs with expressLayouts - Consolidate three duplicated EJS blocks for collected-tags rendering into a single forEach over subjects (no behavior change) Part of the broader Tailwind V2 migration tracked in #609. Legacy profile.ejs remains in tree until the final cleanup pass. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> * Tailwind homepage (#616) * tailwind v2 migration: homepage - Add views/private/homepageV2.ejs using base.ejs layout: welcome, Tailwind experience bar, contributor controls, 5-subject ratings, Chart.js rating tracker, training-mode + classic-subject + USABO grids, site stats, dailySnippetV2, announcements panel, social-link bar - Wire /homepage route to render homepageV2.ejs with expressLayouts middleware and layout: 'layouts/base.ejs' - Drop the bottom search widget on the homepage (jQuery-MultiSelect + Bootstrap; nav already exposes /search and the search.ejs page migrates separately) - Drop AOS scroll animations and jQuery thumbnail hover; replaced hover with Tailwind opacity-80 hover:opacity-100 Part of the broader Tailwind V2 migration tracked in #609. Legacy homepage.ejs remains in tree until the final cleanup pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * tailwind v2 migration: profile (#612) - Add views/private/profileV2.ejs using base.ejs layout: name banner, Tailwind experience bar, info card (name/age/location/bio/achievements), Chart.js rating tracker + correct/wrong pie with custom plugin, Problem Rush highscore, Collected Tags grid (Physics/Chemistry/Biology) - Wire /profile/:username route to render profileV2.ejs with expressLayouts - Consolidate three duplicated EJS blocks for collected-tags rendering into a single forEach over subjects (no behavior change) Part of the broader Tailwind V2 migration tracked in #609. Legacy profile.ejs remains in tree until the final cleanup pass. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> * tailwind v2 migration: profile (#615) - Add views/private/profileV2.ejs using base.ejs layout: name banner, Tailwind experience bar, info card (name/age/location/bio/achievements), Chart.js rating tracker + correct/wrong pie with custom plugin, Problem Rush highscore, Collected Tags grid (Physics/Chemistry/Biology) - Wire /profile/:username route to render profileV2.ejs with expressLayouts - Consolidate three duplicated EJS blocks for collected-tags rendering into a single forEach over subjects (no behavior change) Part of the broader Tailwind V2 migration tracked in #609. Legacy profile.ejs remains in tree until the final cleanup pass. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> * Tailwind homepage (#618) * tailwind v2 migration: homepage - Add views/private/homepageV2.ejs using base.ejs layout: welcome, Tailwind experience bar, contributor controls, 5-subject ratings, Chart.js rating tracker, training-mode + classic-subject + USABO grids, site stats, dailySnippetV2, announcements panel, social-link bar - Wire /homepage route to render homepageV2.ejs with expressLayouts middleware and layout: 'layouts/base.ejs' - Drop the bottom search widget on the homepage (jQuery-MultiSelect + Bootstrap; nav already exposes /search and the search.ejs page migrates separately) - Drop AOS scroll animations and jQuery thumbnail hover; replaced hover with Tailwind opacity-80 hover:opacity-100 Part of the broader Tailwind V2 migration tracked in #609. Legacy homepage.ejs remains in tree until the final cleanup pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * tailwind v2 migration: profile (#612) - Add views/private/profileV2.ejs using base.ejs layout: name banner, Tailwind experience bar, info card (name/age/location/bio/achievements), Chart.js rating tracker + correct/wrong pie with custom plugin, Problem Rush highscore, Collected Tags grid (Physics/Chemistry/Biology) - Wire /profile/:username route to render profileV2.ejs with expressLayouts - Consolidate three duplicated EJS blocks for collected-tags rendering into a single forEach over subjects (no behavior change) Part of the broader Tailwind V2 migration tracked in #609. Legacy profile.ejs remains in tree until the final cleanup pass. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> * tailwind v2 migration: profile (#615) - Add views/private/profileV2.ejs using base.ejs layout: name banner, Tailwind experience bar, info card (name/age/location/bio/achievements), Chart.js rating tracker + correct/wrong pie with custom plugin, Problem Rush highscore, Collected Tags grid (Physics/Chemistry/Biology) - Wire /profile/:username route to render profileV2.ejs with expressLayouts - Consolidate three duplicated EJS blocks for collected-tags rendering into a single forEach over subjects (no behavior change) Part of the broader Tailwind V2 migration tracked in #609. Legacy profile.ejs remains in tree until the final cleanup pass. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 5cab3f0 commit 6eae87b

4 files changed

Lines changed: 459 additions & 4 deletions

File tree

routes/private.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ const {
1414
const { tags } = require('../utils/constants/tags.js');
1515
const mongoose = require('mongoose');
1616

17+
const expressLayouts = require('express-ejs-layouts');
18+
1719
const VIEWS = __dirname + '/../views/';
1820

1921
module.exports = (app, mongo) => {
@@ -40,19 +42,21 @@ module.exports = (app, mongo) => {
4042
});
4143
});
4244

43-
app.get('/homepage', async (req, res) => {
45+
app.get('/homepage', expressLayouts, async (req, res) => {
4446
let siteData = await getSiteData(mongo.User, mongo.Ques, mongo.SiteData);
4547
let experienceStats = await calculateLevel(req.user.stats.experience);
4648
const question = await getDailyQuestion(mongo.Daily, mongo.Ques);
4749
let announcements = await getAnnouncements(mongo.SiteData, 3);
48-
res.render(VIEWS + 'private/homepage.ejs', {
50+
res.render(VIEWS + 'private/homepageV2.ejs', {
4951
user: req.user,
5052
siteStats: siteData,
5153
admin: adminList.includes(req.user.username),
5254
experienceStats,
5355
question,
5456
announcements,
5557
subjectTags: tags,
58+
pageName: 'Homepage',
59+
layout: 'layouts/base.ejs',
5660
});
5761
});
5862

routes/stats.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ const { tags } = require('../utils/constants/tags');
77
const { achievementDescriptions } = require('../utils/constants/achievements');
88
const { generateLeaderboard } = require('../utils/functions/database');
99

10+
const expressLayouts = require('express-ejs-layouts');
11+
1012
const VIEWS = __dirname + '/../views/';
1113

1214
module.exports = (app, mongo) => {
@@ -34,7 +36,7 @@ module.exports = (app, mongo) => {
3436
res.redirect('/profile/' + req.user.ign);
3537
});
3638

37-
app.get('/profile/:username', (req, res) => {
39+
app.get('/profile/:username', expressLayouts, (req, res) => {
3840
mongo.User.findOne({ ign: req.params.username }, async function (err, obj) {
3941
if (obj) {
4042
if (
@@ -51,13 +53,14 @@ module.exports = (app, mongo) => {
5153
const experienceStats = await calculateLevel(
5254
obj.stats.experience ? obj.stats.experience : 0
5355
);
54-
res.render(VIEWS + 'private/profile.ejs', {
56+
res.render(VIEWS + 'private/profileV2.ejs', {
5557
age: thisAge,
5658
user: obj,
5759
totalTags: tags,
5860
pageName: obj.ign + "'s Profile",
5961
experienceStats,
6062
allAchievements: achievementDescriptions,
63+
layout: 'layouts/base.ejs',
6164
});
6265
}
6366
} else {

views/private/homepageV2.ejs

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
<%- contentFor('head') %>
2+
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
3+
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.min.js"></script>
4+
<script>
5+
$(document).ready(function () {
6+
let ratingData = {
7+
Physics: [], Chemistry: [], Biology: [], ESS: [], USABO: [], Labels: []
8+
};
9+
10+
['Physics', 'Chemistry', 'Biology', 'ESS', 'USABO'].forEach((subject) => {
11+
let tracker = $('#ratingTrackerArray-' + subject).val().split('@');
12+
if (tracker.length < 2) tracker.unshift(tracker[0]);
13+
ratingData[subject] = tracker;
14+
});
15+
16+
const labelLength = Math.max(
17+
ratingData.Physics.length, ratingData.Chemistry.length,
18+
ratingData.Biology.length, ratingData.ESS.length, ratingData.USABO.length
19+
);
20+
let labels = ratingData.Labels;
21+
for (let i = labelLength; i > 0; i--) labels.push(i + ' Ago');
22+
labels.push('');
23+
ratingData.Labels = labels;
24+
25+
const ctxTracker = document.getElementById('ratingTrackerChart');
26+
new Chart(ctxTracker, {
27+
type: 'line',
28+
data: {
29+
labels: ratingData.Labels,
30+
datasets: [
31+
{ label: 'Physics', data: ratingData.Physics, backgroundColor: 'rgba(100, 200, 230, 1)', borderColor: 'rgba(100, 200, 230, 1)', fill: false, borderWidth: 4 },
32+
{ label: 'Chemistry', data: ratingData.Chemistry, backgroundColor: 'rgba(245, 170, 40, 1)', borderColor: 'rgba(245, 170, 40, 1)', fill: false, borderWidth: 4 },
33+
{ label: 'Biology', data: ratingData.Biology, backgroundColor: 'rgba( 40, 240, 110, 1)', borderColor: 'rgba( 40, 240, 110, 1)', fill: false, borderWidth: 4 },
34+
{ label: 'ESS', data: ratingData.ESS, backgroundColor: 'rgba(184, 150, 191, 1)', borderColor: 'rgba(184, 150, 191, 1)', fill: false, borderWidth: 4 },
35+
{ label: 'USABO', data: ratingData.USABO, backgroundColor: 'rgba(113, 16, 1, 1)', borderColor: 'rgba(113, 16, 1, 1)', fill: false, borderWidth: 4 },
36+
],
37+
},
38+
options: {
39+
title: { display: true, text: 'Rating History' },
40+
legend: { display: true, position: 'bottom' },
41+
scales: {
42+
yAxes: [{
43+
ticks: {
44+
min: Math.round((Math.min(Math.min.apply(null, ratingData.Physics), Math.min.apply(null, ratingData.Chemistry), Math.min.apply(null, ratingData.Biology), Math.min.apply(null, ratingData.ESS), Math.min.apply(null, ratingData.USABO)) - 50) / 50) * 50,
45+
max: Math.round((Math.max(Math.max.apply(null, ratingData.Physics), Math.max.apply(null, ratingData.Chemistry), Math.max.apply(null, ratingData.Biology), Math.max.apply(null, ratingData.ESS), Math.max.apply(null, ratingData.USABO)) + 50) / 50) * 50,
46+
stepSize: 50,
47+
}
48+
}],
49+
xAxes: [{ ticks: { display: false } }]
50+
},
51+
responsive: true,
52+
maintainAspectRatio: false,
53+
}
54+
});
55+
});
56+
</script>
57+
58+
<%- contentFor('main') %>
59+
<div class="flex flex-col items-center w-full max-w-4xl px-6 py-8 gap-8">
60+
61+
<%# Welcome %>
62+
<div class="flex flex-col items-center w-full">
63+
<h1 class="text-4xl sm:text-5xl text-center mb-2">Welcome, <%= user.ign %></h1>
64+
<% if (admin) { %>
65+
<a class="btn btn-primary mt-4" href="/admin/adminHomepage">Admin Dashboard</a>
66+
<% } %>
67+
</div>
68+
69+
<%# Experience bar %>
70+
<% try { %>
71+
<% const xp = experienceStats; %>
72+
<div class="w-full">
73+
<div class="w-full h-5 rounded-full bg-neutral-100 overflow-hidden">
74+
<div class="h-full bg-green-500 transition-all" style="width: <%= Math.round(100 * xp.remainder / xp.totalToNext) %>%"></div>
75+
</div>
76+
<p class="text-center mt-2 text-sm">
77+
<strong>Level <%= xp.level %></strong>
78+
(<%= xp.remainder %>/<%= xp.totalToNext %> XP to Level <%= xp.level + 1 %>)
79+
</p>
80+
</div>
81+
<% } catch (err) { %>
82+
<p class="text-center text-red-600">[Experience bar unavailable]</p>
83+
<% } %>
84+
85+
<%# Contributor controls %>
86+
<% if (user.contributor) { %>
87+
<div class="flex flex-col items-stretch gap-2 w-full p-6 rounded-xl border-2">
88+
<h3 class="text-center mb-2">CONTRIBUTORS: <span class="text-blue-600"><%= user.contributor %></span></h3>
89+
<a class="btn btn-primary" href="/contributors/addQuestion">Write Question</a>
90+
<a class="btn btn-primary" href="/contributors/addUSABOQuestion">Write a USABO Question</a>
91+
<% if (user.reviewer) { %>
92+
<a class="btn btn-primary" href="/reviewer/reviewQuestions">Review Questions</a>
93+
<% } %>
94+
<a class="btn btn-outline" href="/contributors/stats">View My Contribution Stats</a>
95+
</div>
96+
<% } %>
97+
98+
<%# Ratings + Training modes %>
99+
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8 w-full">
100+
101+
<%# Ratings + chart %>
102+
<div class="lg:col-span-5 flex flex-col gap-3">
103+
<% [
104+
['Physics', user.rating.physics],
105+
['Chemistry', user.rating.chemistry],
106+
['Biology', user.rating.biology],
107+
['ESS', user.rating.ess],
108+
['USABO', user.rating.usabo],
109+
].forEach(([subject, rating]) => { %>
110+
<h2 class="text-center text-2xl">
111+
<%= subject %>:
112+
<span class="inline-block px-3 py-1 ml-1 rounded-full bg-blue-500 text-white text-base align-middle">
113+
<% if (rating > 0) { %>
114+
<%= Math.max(rating, 0) %>
115+
<% } else { %>
116+
No rating
117+
<% } %>
118+
</span>
119+
</h2>
120+
<% }) %>
121+
122+
<div class="w-full mt-4" style="height: 320px;">
123+
<canvas id="ratingTrackerChart"></canvas>
124+
</div>
125+
<input type="hidden" id="ratingTrackerArray-Physics" value="<%= user.stats.ratingTracker.physics.join('@') %>">
126+
<input type="hidden" id="ratingTrackerArray-Chemistry" value="<%= user.stats.ratingTracker.chemistry.join('@') %>">
127+
<input type="hidden" id="ratingTrackerArray-Biology" value="<%= user.stats.ratingTracker.biology.join('@') %>">
128+
<input type="hidden" id="ratingTrackerArray-ESS" value="<%= user.stats.ratingTracker.ess.join('@') %>">
129+
<input type="hidden" id="ratingTrackerArray-USABO" value="<%= user.stats.ratingTracker.usabo.join('@') %>">
130+
</div>
131+
132+
<%# Training modes + subjects %>
133+
<div class="lg:col-span-7 flex flex-col gap-6">
134+
<div>
135+
<h2 class="text-center text-2xl mb-3" id="training-modes-text">Training Modes</h2>
136+
<div class="grid grid-cols-3 gap-3">
137+
<a href="/train/chooseSubject" class="block opacity-80 hover:opacity-100 transition" title="Classic">
138+
<img class="w-full rounded-lg" src="Train_Classic.svg" alt="Classic icon">
139+
</a>
140+
<a href="/train/rush" class="block opacity-80 hover:opacity-100 transition" title="Rush">
141+
<img class="w-full rounded-lg" src="Train_Rush.svg" alt="Rush icon">
142+
</a>
143+
<a href="/train/daily" class="block opacity-80 hover:opacity-100 transition" title="Daily">
144+
<img class="w-full rounded-lg" src="Train_Daily.svg" alt="Daily icon">
145+
</a>
146+
</div>
147+
</div>
148+
149+
<div>
150+
<h2 class="text-center text-2xl mb-3" id="classic-subjects-text">Classic Subjects</h2>
151+
<div class="grid grid-cols-3 gap-3">
152+
<a href="/train/Physics/chooseUnits" class="block opacity-80 hover:opacity-100 transition" title="Physics">
153+
<img class="w-full rounded-lg" src="https://cdn.mutorials.org/images/icons/Physics_Main.png" alt="Physics icon">
154+
</a>
155+
<a href="/train/Chemistry/chooseUnits" class="block opacity-80 hover:opacity-100 transition" title="Chemistry">
156+
<img class="w-full rounded-lg" src="https://cdn.mutorials.org/images/icons/Chem_Main.png" alt="Chemistry icon">
157+
</a>
158+
<a href="/train/Biology/chooseUnits" class="block opacity-80 hover:opacity-100 transition" title="Biology">
159+
<img class="w-full rounded-lg" src="https://cdn.mutorials.org/images/icons/Bio_Main.png" alt="Biology icon">
160+
</a>
161+
</div>
162+
<div class="grid grid-cols-3 gap-3 mt-3">
163+
<div></div>
164+
<a href="/train/ESS/chooseUnits" class="block opacity-80 hover:opacity-100 transition" title="ESS">
165+
<img class="w-full rounded-lg" src="https://cdn.mutorials.org/images/icons/ESS_Main.png" alt="ESS icon">
166+
</a>
167+
<div></div>
168+
</div>
169+
</div>
170+
171+
<div>
172+
<h2 class="text-center text-2xl mb-3">Train for USABO!</h2>
173+
<div class="grid grid-cols-3 gap-3">
174+
<div></div>
175+
<a href="/usaboHomepage" class="block opacity-80 hover:opacity-100 transition" title="USABO">
176+
<img class="w-full rounded-lg" src="https://cdn.mutorials.org/images/icons/USABO_Main.png" alt="USABO icon">
177+
</a>
178+
<div></div>
179+
</div>
180+
</div>
181+
182+
<a class="btn btn-primary w-full" href="/train">Train Now!</a>
183+
</div>
184+
</div>
185+
186+
<%# Site stats %>
187+
<% try { %>
188+
<% const stats = siteStats; %>
189+
<div class="grid grid-cols-3 gap-4 w-full mt-4">
190+
<div class="flex flex-col items-center">
191+
<h1 class="font-extrabold text-blue-500"><%= stats.questionCount %></h1>
192+
<p class="text-center">Questions in Database</p>
193+
</div>
194+
<div class="flex flex-col items-center">
195+
<h1 class="font-extrabold text-blue-500"><%= stats.tagCount %></h1>
196+
<p class="text-center">Question Tags</p>
197+
</div>
198+
<div class="flex flex-col items-center">
199+
<h1 class="font-extrabold text-blue-500"><%= stats.totalSolves.physics + stats.totalSolves.chemistry + stats.totalSolves.biology %></h1>
200+
<p class="text-center">Total Solves</p>
201+
</div>
202+
</div>
203+
<% } catch (err) { %>
204+
<p class="text-center text-red-600">[Site stats unavailable]</p>
205+
<% } %>
206+
207+
<%# Daily question + announcements %>
208+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 w-full">
209+
<div class="p-6 rounded-xl border-2">
210+
<%- include('../partials/components/dailySnippetV2.ejs') %>
211+
</div>
212+
213+
<div class="p-6 rounded-xl border-2 flex flex-col">
214+
<h2 class="text-center mb-4">Latest Announcements</h2>
215+
<% try { %>
216+
<% announcements.forEach((announcement) => { %>
217+
<div class="mb-4">
218+
<h3 class="text-lg mb-1"><%= announcement.title %></h3>
219+
<p class="text-sm text-neutral-500 mb-1">Posted <%= announcement.date.split('T')[0] %></p>
220+
<p class="text-sm"><%- announcement.text %></p>
221+
</div>
222+
<% }) %>
223+
<p class="text-center mt-auto"><a class="prose" href="/announcements">See more announcements</a></p>
224+
<% } catch (err) { %>
225+
<p class="text-center text-red-600">[Announcements unavailable]</p>
226+
<% } %>
227+
</div>
228+
</div>
229+
230+
<%# Social links %>
231+
<div class="flex items-center justify-center gap-6 text-2xl mt-4">
232+
<a href="https://instagram.com/mutorials_/" target="_blank" rel="noopener" class="hover:text-pink-600 transition" aria-label="Instagram"><i class="fab fa-instagram"></i></a>
233+
<a href="https://discord.com/invite/ccbZu4q" target="_blank" rel="noopener" class="hover:text-indigo-600 transition" aria-label="Discord"><i class="fab fa-discord"></i></a>
234+
<a href="https://www.youtube.com/channel/UCb2kCg0v44GJm2Xc8dzHvsA" target="_blank" rel="noopener" class="hover:text-red-600 transition" aria-label="YouTube"><i class="fab fa-youtube"></i></a>
235+
<a href="https://github.com/The-Mu-Foundation/Mutorials/" target="_blank" rel="noopener" class="hover:text-neutral-700 transition" aria-label="GitHub"><i class="fab fa-github"></i></a>
236+
</div>
237+
238+
</div>

0 commit comments

Comments
 (0)