Skip to content

Commit 5253e45

Browse files
committed
feat(frontend): add initial history dashboards UI and logic
1 parent 7871b9b commit 5253e45

File tree

11 files changed

+1922
-76
lines changed

11 files changed

+1922
-76
lines changed

frontend/src/components/userMenu.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,9 @@ const UserMenu: React.FC = () => {
5353
</>
5454
) : (
5555
<>
56-
<button className="icon-button">
56+
{/* <button className="icon-button">
5757
<img src={NotificationIcon} alt="Notifications" className="notification-icon" />
58-
</button>
58+
</button> */}
5959

6060
{isVerified ? (
6161
<Link to="/profile/" className="icon-link">

frontend/src/features/progress/RecentSessionsList.tsx

Lines changed: 117 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
import React, { useState, useEffect } from 'react';
22
import { useNavigate } from 'react-router-dom';
3-
import { getAllAttemptSummaries } from '../../lib/api';
3+
import { getAllAttemptSummaries, getOtherUser } from '../../lib/api';
44
// --- FIX: Import both time formatters ---
55
import { formatDuration, formatTimeAgo } from '../../lib/timeFormatters';
6-
// import useAuth from '../hooks/useAuth'; // Added useAuth to get the user
76

87
// --- UI Imports from New UI ---
98
import { Card } from "@/components/ui/card";
109
import { Badge } from "@/components/ui/badge";
1110
import {
12-
User,
13-
Clock,
14-
Calendar // Added for "time ago"
11+
Calendar,
12+
Code
1513
} from "lucide-react";
1614
// ------------------------------------
1715

@@ -22,17 +20,19 @@ interface SessionSummary {
2220
question_title: string;
2321
question_difficulty: "Easy" | "Medium" | "Hard";
2422
partner_id: string;
25-
is_solved_successfully: boolean;
23+
is_solved_successfully: boolean | null;
2624
has_penalty: boolean; // We need this to determine "Incomplete"
2725
started_at: string;
28-
time_taken_ms: number;
26+
time_taken_ms: number | null;
2927
}
3028

3129
const RecentSessionsList: React.FC<{ userId: string, limit: number }> = ({ userId, limit }) => {
3230
const navigate = useNavigate();
3331
const [sessions, setSessions] = useState<SessionSummary[]>([]);
3432
const [loading, setLoading] = useState(true);
33+
const [usernames, setUsernames] = useState<Map<string, string>>(new Map());
3534

35+
// Fetch sessions
3636
useEffect(() => {
3737
if (!userId) return;
3838

@@ -53,6 +53,39 @@ const RecentSessionsList: React.FC<{ userId: string, limit: number }> = ({ userI
5353
fetchRecent();
5454
}, [userId, limit]);
5555

56+
// Fetch usernames for all unique partner IDs
57+
useEffect(() => {
58+
if (sessions.length === 0) return;
59+
60+
const fetchUsernames = async () => {
61+
const uniquePartnerIds = [...new Set(sessions.map(s => s.partner_id).filter(id => id))];
62+
const usernameMap = new Map<string, string>();
63+
64+
// Fetch usernames in parallel
65+
await Promise.allSettled(
66+
uniquePartnerIds.map(async (partnerId) => {
67+
try {
68+
const response = await getOtherUser(partnerId);
69+
// getOtherUser returns { data: { username, ... } }
70+
if (response?.data?.username) {
71+
usernameMap.set(partnerId, response.data.username);
72+
} else {
73+
usernameMap.set(partnerId, partnerId);
74+
}
75+
} catch (error) {
76+
console.error(`Failed to fetch username for partner ${partnerId}:`, error);
77+
// Keep partner_id as fallback
78+
usernameMap.set(partnerId, partnerId);
79+
}
80+
})
81+
);
82+
83+
setUsernames(usernameMap);
84+
};
85+
86+
fetchUsernames();
87+
}, [sessions]);
88+
5689
const handleSessionClick = (questionId: string) => {
5790
// Navigate to the detail page for that question
5891
navigate(`/history/attempts/${questionId}`);
@@ -68,56 +101,100 @@ const RecentSessionsList: React.FC<{ userId: string, limit: number }> = ({ userI
68101

69102
// --- This is the new UI, adapted to the REAL API data ---
70103
return (
71-
<div className="space-y-4">
104+
<div className="space-y-3">
72105
{/* The title "Recent Sessions" is in your userProfile.tsx, so we don't repeat it here */}
73-
<div className="space-y-3">
74-
{sessions.map((session) => (
106+
{sessions.map((session) => {
107+
const statusText = session.is_solved_successfully === true ? "Passed" : (session.has_penalty ? "Incomplete" : "Failed");
108+
const statusColor = session.is_solved_successfully === true ? 'bg-green-500' : (session.has_penalty ? 'bg-orange-500' : 'bg-red-500');
109+
const difficultyColors: Record<string, { bg: string; text: string }> = {
110+
Easy: { bg: 'bg-green-100', text: 'text-green-800' },
111+
Medium: { bg: 'bg-yellow-100', text: 'text-yellow-800' },
112+
Hard: { bg: 'bg-red-100', text: 'text-red-800' }
113+
};
114+
const difficultyStyle = difficultyColors[session.question_difficulty] || difficultyColors.Medium;
115+
116+
return (
75117
<Card
76118
key={session.session_id}
77-
className="p-4 shadow-soft hover:shadow-card transition-smooth cursor-pointer"
119+
className="p-4 cursor-pointer"
120+
style={{
121+
background: 'white',
122+
borderRadius: '12px',
123+
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
124+
transition: 'all 0.3s ease',
125+
border: '1px solid #e5e7eb'
126+
}}
127+
onMouseEnter={(e) => {
128+
e.currentTarget.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.15)';
129+
}}
130+
onMouseLeave={(e) => {
131+
e.currentTarget.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.1)';
132+
}}
78133
onClick={() => handleSessionClick(session.question_id)}
79134
>
80-
<div className="flex items-center justify-between">
81-
<div className="space-y-1">
82-
<div className="flex items-center gap-2">
135+
<div className="flex items-center gap-4">
136+
{/* Left: Code Icon */}
137+
<div
138+
className="flex-shrink-0 flex items-center justify-center"
139+
style={{
140+
width: '48px',
141+
height: '48px',
142+
backgroundColor: '#addaf7',
143+
borderRadius: '8px'
144+
}}
145+
>
146+
<Code className="h-6 w-6 text-white" style={{ strokeWidth: 2.5 }} />
147+
</div>
148+
149+
{/* Middle: Topic, Difficulty, Partner, Time */}
150+
<div className="flex-1 space-y-1">
151+
{/* Topic in bold */}
152+
<div className="font-bold text-base text-gray-900">
153+
{session.question_title}
154+
</div>
155+
156+
{/* Difficulty badge and Partner */}
157+
<div className="flex items-center gap-2 flex-wrap">
83158
<Badge
84-
variant={session.question_difficulty} // Use Easy, Medium, Hard variants
159+
className={`${difficultyStyle.bg} ${difficultyStyle.text} border-0 font-medium`}
160+
style={{ fontSize: '12px', padding: '2px 8px' }}
85161
>
86162
{session.question_difficulty}
87163
</Badge>
88-
{/* API provides question_title, not topic */}
89-
<span className="font-medium text-sm">{session.question_title}</span>
164+
<span className="text-sm text-gray-500">
165+
with {usernames.get(session.partner_id) || session.partner_id}
166+
</span>
90167
</div>
91-
<div className="flex items-center gap-4 text-sm text-muted-foreground">
92-
<div className="flex items-center gap-1">
93-
<User className="h-3 w-3" />
94-
{session.partner_id}
95-
</div>
168+
169+
{/* Time taken and Time ago */}
170+
<div className="flex items-center gap-4 text-sm text-gray-500">
171+
{session.time_taken_ms && (
172+
<span>{formatDuration(session.time_taken_ms)}</span>
173+
)}
96174
<div className="flex items-center gap-1">
97-
<Clock className="h-3 w-3" />
98-
{/* API provides time_taken_ms, format it */}
99-
{formatDuration(session.time_taken_ms)}
100-
</div>
101-
{/* API provides started_at, let's show time ago */}
102-
<div className="flex items-center gap-1">
103175
<Calendar className="h-3 w-3" />
104-
{formatTimeAgo(session.started_at)}
176+
<span>{formatTimeAgo(session.started_at)}</span>
105177
</div>
106178
</div>
107179
</div>
108-
109-
{/* API provides booleans, we derive the status */}
110-
<Badge className={
111-
session.is_solved_successfully ? 'bg-green-500 text-white' : // "Passed" (blue)
112-
session.has_penalty ? 'bg-orange-500 text-white' : // "Incomplete" (gray)
113-
'bg-red-500 text-white' // "Failed" (red)
114-
}>
115-
{session.is_solved_successfully ? "Passed" : (session.has_penalty ? "Incomplete" : "Failed")}
116-
</Badge>
180+
181+
{/* Right: Status Badge */}
182+
<div className="flex-shrink-0">
183+
<Badge
184+
className={`${statusColor} text-white border-0 font-medium`}
185+
style={{
186+
fontSize: '13px',
187+
padding: '4px 12px',
188+
borderRadius: '9999px'
189+
}}
190+
>
191+
{statusText}
192+
</Badge>
193+
</div>
117194
</div>
118195
</Card>
119-
))}
120-
</div>
196+
);
197+
})}
121198
</div>
122199
);
123200
};

frontend/src/pages/HistoryDashboardPage.tsx

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515

1616
// --- Logic Imports (from Cursor's file) ---
1717
import useAuth from '../hooks/useAuth';
18-
import { getHistoryProgress, getAllAttemptSummaries } from '../lib/api';
18+
import { getHistoryProgress, getAllAttemptSummaries, getOtherUser } from '../lib/api';
1919
import { formatTimeAgo } from '../lib/timeFormatters';
2020

2121
// --- Main Component ---
@@ -28,6 +28,7 @@ export default function HistoryDashboardPage() {
2828
const [progress, setProgress] = useState<any>(null);
2929
const [summaries, setSummaries] = useState<any[]>([]);
3030
const [loading, setLoading] = useState(true);
31+
const [usernames, setUsernames] = useState<Map<string, string>>(new Map());
3132

3233
// --- Data Fetching (from Cursor's file) ---
3334
useEffect(() => {
@@ -49,6 +50,37 @@ export default function HistoryDashboardPage() {
4950
});
5051
}, [userId]);
5152

53+
// Fetch usernames for all unique partner IDs
54+
useEffect(() => {
55+
if (summaries.length === 0) return;
56+
57+
const fetchUsernames = async () => {
58+
const uniquePartnerIds = [...new Set(summaries.map((s: any) => s.partner_id).filter((id: string) => id))];
59+
const usernameMap = new Map<string, string>();
60+
61+
// Fetch usernames in parallel
62+
await Promise.allSettled(
63+
uniquePartnerIds.map(async (partnerId: string) => {
64+
try {
65+
const response = await getOtherUser(partnerId);
66+
if (response?.data?.username) {
67+
usernameMap.set(partnerId, response.data.username);
68+
} else {
69+
usernameMap.set(partnerId, partnerId);
70+
}
71+
} catch (error) {
72+
console.error(`Failed to fetch username for partner ${partnerId}:`, error);
73+
usernameMap.set(partnerId, partnerId);
74+
}
75+
})
76+
);
77+
78+
setUsernames(usernameMap);
79+
};
80+
81+
fetchUsernames();
82+
}, [summaries]);
83+
5284
// --- Stat Calculation (from Cursor's file, adapted for Lovable's UI) ---
5385
const totals = useMemo(() => {
5486
const uniqueQuestions = summaries.length; // Renamed to match UI
@@ -185,7 +217,7 @@ export default function HistoryDashboardPage() {
185217
<div className="flex flex-wrap items-center gap-4 text-sm text-muted-foreground">
186218
<span className="flex items-center gap-1">
187219
<User className="h-3 w-3" />
188-
Partner: {question.partner_id}
220+
Partner: {usernames.get(question.partner_id) || question.partner_id}
189221
</span>
190222
<span>
191223
{/* Real data */}

frontend/src/pages/QuestionDetailPage.tsx

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
1919

2020
// --- API & Auth Imports (from Logic) ---
2121
import useAuth from '../hooks/useAuth';
22-
import { getQuestionAttempts } from '../lib/api';
22+
import { getQuestionAttempts, getOtherUser } from '../lib/api';
2323
import { formatDurationMinutes } from '../lib/timeFormatters';
2424

2525
// ... (interface Attempt definition is correct) ...
@@ -45,6 +45,7 @@ export const QuestionDetail = () => {
4545
const userId = (user as any)?._id ?? (user as any)?.uid ?? '';
4646
const [attempts, setAttempts] = useState<Attempt[]>([]);
4747
const [loading, setLoading] = useState(true);
48+
const [usernames, setUsernames] = useState<Map<string, string>>(new Map());
4849

4950
useEffect(() => {
5051
if (!userId || !questionId) return;
@@ -62,6 +63,37 @@ export const QuestionDetail = () => {
6263
});
6364
}, [userId, questionId]);
6465

66+
// Fetch usernames for all unique partner IDs
67+
useEffect(() => {
68+
if (attempts.length === 0) return;
69+
70+
const fetchUsernames = async () => {
71+
const uniquePartnerIds = [...new Set(attempts.map(a => a.partner_id).filter(id => id))];
72+
const usernameMap = new Map<string, string>();
73+
74+
// Fetch usernames in parallel
75+
await Promise.allSettled(
76+
uniquePartnerIds.map(async (partnerId: string) => {
77+
try {
78+
const response = await getOtherUser(partnerId);
79+
if (response?.data?.username) {
80+
usernameMap.set(partnerId, response.data.username);
81+
} else {
82+
usernameMap.set(partnerId, partnerId);
83+
}
84+
} catch (error) {
85+
console.error(`Failed to fetch username for partner ${partnerId}:`, error);
86+
usernameMap.set(partnerId, partnerId);
87+
}
88+
})
89+
);
90+
91+
setUsernames(usernameMap);
92+
};
93+
94+
fetchUsernames();
95+
}, [attempts]);
96+
6597
const questionTitle = attempts[0]?.question_title;
6698

6799
// --- Helper function to guess language ---
@@ -164,7 +196,7 @@ export const QuestionDetail = () => {
164196
<div className="flex flex-wrap items-center gap-4 text-sm text-muted-foreground mb-4">
165197
<span className="flex items-center gap-1">
166198
<User className="h-4 w-4" />
167-
Partner: {attempt.partner_id}
199+
Partner: {usernames.get(attempt.partner_id) || attempt.partner_id}
168200
</span>
169201
<span className="flex items-center gap-1">
170202
<Calendar className="h-4 w-4" />

0 commit comments

Comments
 (0)