Skip to content

Commit a8cf008

Browse files
author
CodeJudge
committed
feat: 解题日历热力图
1 parent 8a8fd67 commit a8cf008

2 files changed

Lines changed: 58 additions & 1 deletion

File tree

backend/src/routes/auth.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ router.post('/login', login);
88
router.get('/profile', authenticate, getProfile);
99
router.put('/password', authenticate, changePassword);
1010

11+
router.get('/solved-calendar', authenticate, (req, res) => {
12+
const rows = queryAll(
13+
'SELECT DISTINCT DATE(created_at) as date FROM submissions WHERE user_id = ? ORDER BY date',
14+
[req.user.id]
15+
);
16+
res.json({ dates: rows.map(r => r.date) });
17+
});
18+
1119
router.get('/leaderboard', async (req, res) => {
1220
try {
1321
const rows = queryAll(`

frontend/src/pages/Profile.tsx

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useState, useEffect } from 'react';
22
import { Link } from 'react-router-dom';
3-
import { User, Mail, Shield, Award, TrendingUp, Loader2, AlertTriangle, CheckCircle2, XCircle, Clock, Zap, Lock, Key } from 'lucide-react';
3+
import { User, Mail, Shield, Award, TrendingUp, Loader2, AlertTriangle, CheckCircle2, XCircle, Clock, Zap, Lock, Key, CalendarDays } from 'lucide-react';
44
import { useAuth } from '../context/AuthContext';
55
import api from '../services/api';
66
import type { Submission } from '../types';
@@ -46,6 +46,18 @@ export default function Profile() {
4646
const [passwordLoading, setPasswordLoading] = useState(false);
4747
const [passwordMessage, setPasswordMessage] = useState<string | null>(null);
4848
const [passwordError, setPasswordError] = useState<string | null>(null);
49+
const [calendarDates, setCalendarDates] = useState<Set<string>>(new Set());
50+
51+
useEffect(() => {
52+
const token = localStorage.getItem('oj_token');
53+
if (!token) return;
54+
fetch('/api/auth/solved-calendar', { headers: { Authorization: `Bearer ${token}` } })
55+
.then(r => r.json())
56+
.then(data => {
57+
setCalendarDates(new Set(data.dates || []));
58+
})
59+
.catch(() => {});
60+
}, []);
4961

5062
useEffect(() => {
5163
async function load() {
@@ -115,6 +127,11 @@ export default function Profile() {
115127
const stats = profile.stats || { total: 0, accepted: 0 };
116128
const acceptanceRate = stats.total > 0 ? Math.round((stats.accepted / stats.total) * 100) : 0;
117129
const uniqueProblems = new Set(submissions.map((s) => s.problem_id)).size;
130+
const days = Array.from({ length: 30 }, (_, i) => {
131+
const d = new Date();
132+
d.setDate(d.getDate() - (29 - i));
133+
return d.toISOString().slice(0, 10);
134+
});
118135

119136
const formatDate = (dateStr: string) => {
120137
if (!dateStr) return '-';
@@ -186,6 +203,38 @@ export default function Profile() {
186203
</div>
187204
</div>
188205

206+
{/* Solved calendar */}
207+
<div className="card p-6">
208+
<h2 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
209+
<CalendarDays className="w-5 h-5 text-blue-400" />
210+
解题日历
211+
</h2>
212+
<div className="flex flex-wrap gap-1.5">
213+
{days.map((day) => {
214+
const hasSubmission = calendarDates.has(day);
215+
return (
216+
<div
217+
key={day}
218+
title={`${day} ${hasSubmission ? '已打卡' : '未打卡'}`}
219+
className={`w-3.5 h-3.5 rounded-sm transition-colors ${
220+
hasSubmission ? 'bg-emerald-500' : 'bg-dark-700'
221+
}`}
222+
/>
223+
);
224+
})}
225+
</div>
226+
<div className="flex items-center gap-4 mt-3 text-xs text-dark-400">
227+
<span className="flex items-center gap-1.5">
228+
<span className="w-3 h-3 rounded-sm bg-emerald-500" />
229+
已打卡
230+
</span>
231+
<span className="flex items-center gap-1.5">
232+
<span className="w-3 h-3 rounded-sm bg-dark-700" />
233+
未打卡
234+
</span>
235+
</div>
236+
</div>
237+
189238
{/* Password change */}
190239
<div className="card p-6">
191240
<button

0 commit comments

Comments
 (0)