|
1 | 1 | import { useState, useEffect } from 'react'; |
2 | 2 | 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'; |
4 | 4 | import { useAuth } from '../context/AuthContext'; |
5 | 5 | import api from '../services/api'; |
6 | 6 | import type { Submission } from '../types'; |
@@ -46,6 +46,18 @@ export default function Profile() { |
46 | 46 | const [passwordLoading, setPasswordLoading] = useState(false); |
47 | 47 | const [passwordMessage, setPasswordMessage] = useState<string | null>(null); |
48 | 48 | 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 | + }, []); |
49 | 61 |
|
50 | 62 | useEffect(() => { |
51 | 63 | async function load() { |
@@ -115,6 +127,11 @@ export default function Profile() { |
115 | 127 | const stats = profile.stats || { total: 0, accepted: 0 }; |
116 | 128 | const acceptanceRate = stats.total > 0 ? Math.round((stats.accepted / stats.total) * 100) : 0; |
117 | 129 | 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 | + }); |
118 | 135 |
|
119 | 136 | const formatDate = (dateStr: string) => { |
120 | 137 | if (!dateStr) return '-'; |
@@ -186,6 +203,38 @@ export default function Profile() { |
186 | 203 | </div> |
187 | 204 | </div> |
188 | 205 |
|
| 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 | + |
189 | 238 | {/* Password change */} |
190 | 239 | <div className="card p-6"> |
191 | 240 | <button |
|
0 commit comments