Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions frontend/src/components/userMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ const UserMenu: React.FC = () => {
</>
) : (
<>
<button className="icon-button">
{/* <button className="icon-button">
<img src={NotificationIcon} alt="Notifications" className="notification-icon" />
</button>
</button> */}

{isVerified ? (
<Link to="/profile/" className="icon-link">
Expand Down
157 changes: 117 additions & 40 deletions frontend/src/features/progress/RecentSessionsList.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { getAllAttemptSummaries } from '../../lib/api';
import { getAllAttemptSummaries, getOtherUser } from '../../lib/api';
// --- FIX: Import both time formatters ---
import { formatDuration, formatTimeAgo } from '../../lib/timeFormatters';
// import useAuth from '../hooks/useAuth'; // Added useAuth to get the user

// --- UI Imports from New UI ---
import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import {
User,
Clock,
Calendar // Added for "time ago"
Calendar,
Code
} from "lucide-react";
// ------------------------------------

Expand All @@ -22,17 +20,19 @@ interface SessionSummary {
question_title: string;
question_difficulty: "Easy" | "Medium" | "Hard";
partner_id: string;
is_solved_successfully: boolean;
is_solved_successfully: boolean | null;
has_penalty: boolean; // We need this to determine "Incomplete"
started_at: string;
time_taken_ms: number;
time_taken_ms: number | null;
}

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

// Fetch sessions
useEffect(() => {
if (!userId) return;

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

// Fetch usernames for all unique partner IDs
useEffect(() => {
if (sessions.length === 0) return;

const fetchUsernames = async () => {
const uniquePartnerIds = [...new Set(sessions.map(s => s.partner_id).filter(id => id))];
const usernameMap = new Map<string, string>();

// Fetch usernames in parallel
await Promise.allSettled(
uniquePartnerIds.map(async (partnerId) => {
try {
const response = await getOtherUser(partnerId);
// getOtherUser returns { data: { username, ... } }
if (response?.data?.username) {
usernameMap.set(partnerId, response.data.username);
} else {
usernameMap.set(partnerId, partnerId);
}
} catch (error) {
console.error(`Failed to fetch username for partner ${partnerId}:`, error);
// Keep partner_id as fallback
usernameMap.set(partnerId, partnerId);
}
})
);

setUsernames(usernameMap);
};

fetchUsernames();
}, [sessions]);

const handleSessionClick = (questionId: string) => {
// Navigate to the detail page for that question
navigate(`/history/attempts/${questionId}`);
Expand All @@ -68,56 +101,100 @@ const RecentSessionsList: React.FC<{ userId: string, limit: number }> = ({ userI

// --- This is the new UI, adapted to the REAL API data ---
return (
<div className="space-y-4">
<div className="space-y-3">
{/* The title "Recent Sessions" is in your userProfile.tsx, so we don't repeat it here */}
<div className="space-y-3">
{sessions.map((session) => (
{sessions.map((session) => {
const statusText = session.is_solved_successfully === true ? "Passed" : (session.has_penalty ? "Incomplete" : "Failed");
const statusColor = session.is_solved_successfully === true ? 'bg-green-500' : (session.has_penalty ? 'bg-orange-500' : 'bg-red-500');
const difficultyColors: Record<string, { bg: string; text: string }> = {
Easy: { bg: 'bg-green-100', text: 'text-green-800' },
Medium: { bg: 'bg-yellow-100', text: 'text-yellow-800' },
Hard: { bg: 'bg-red-100', text: 'text-red-800' }
};
const difficultyStyle = difficultyColors[session.question_difficulty] || difficultyColors.Medium;

return (
<Card
key={session.session_id}
className="p-4 shadow-soft hover:shadow-card transition-smooth cursor-pointer"
className="p-4 cursor-pointer"
style={{
background: 'white',
borderRadius: '12px',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
transition: 'all 0.3s ease',
border: '1px solid #e5e7eb'
}}
onMouseEnter={(e) => {
e.currentTarget.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.15)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.1)';
}}
onClick={() => handleSessionClick(session.question_id)}
>
<div className="flex items-center justify-between">
<div className="space-y-1">
<div className="flex items-center gap-2">
<div className="flex items-center gap-4">
{/* Left: Code Icon */}
<div
className="flex-shrink-0 flex items-center justify-center"
style={{
width: '48px',
height: '48px',
backgroundColor: '#addaf7',
borderRadius: '8px'
}}
>
<Code className="h-6 w-6 text-white" style={{ strokeWidth: 2.5 }} />
</div>

{/* Middle: Topic, Difficulty, Partner, Time */}
<div className="flex-1 space-y-1">
{/* Topic in bold */}
<div className="font-bold text-base text-gray-900">
{session.question_title}
</div>

{/* Difficulty badge and Partner */}
<div className="flex items-center gap-2 flex-wrap">
<Badge
variant={session.question_difficulty} // Use Easy, Medium, Hard variants
className={`${difficultyStyle.bg} ${difficultyStyle.text} border-0 font-medium`}
style={{ fontSize: '12px', padding: '2px 8px' }}
>
{session.question_difficulty}
</Badge>
{/* API provides question_title, not topic */}
<span className="font-medium text-sm">{session.question_title}</span>
<span className="text-sm text-gray-500">
with {usernames.get(session.partner_id) || session.partner_id}
</span>
</div>
<div className="flex items-center gap-4 text-sm text-muted-foreground">
<div className="flex items-center gap-1">
<User className="h-3 w-3" />
{session.partner_id}
</div>

{/* Time taken and Time ago */}
<div className="flex items-center gap-4 text-sm text-gray-500">
{session.time_taken_ms && (
<span>{formatDuration(session.time_taken_ms)}</span>
)}
<div className="flex items-center gap-1">
<Clock className="h-3 w-3" />
{/* API provides time_taken_ms, format it */}
{formatDuration(session.time_taken_ms)}
</div>
{/* API provides started_at, let's show time ago */}
<div className="flex items-center gap-1">
<Calendar className="h-3 w-3" />
{formatTimeAgo(session.started_at)}
<span>{formatTimeAgo(session.started_at)}</span>
</div>
</div>
</div>

{/* API provides booleans, we derive the status */}
<Badge className={
session.is_solved_successfully ? 'bg-green-500 text-white' : // "Passed" (blue)
session.has_penalty ? 'bg-orange-500 text-white' : // "Incomplete" (gray)
'bg-red-500 text-white' // "Failed" (red)
}>
{session.is_solved_successfully ? "Passed" : (session.has_penalty ? "Incomplete" : "Failed")}
</Badge>

{/* Right: Status Badge */}
<div className="flex-shrink-0">
<Badge
className={`${statusColor} text-white border-0 font-medium`}
style={{
fontSize: '13px',
padding: '4px 12px',
borderRadius: '9999px'
}}
>
{statusText}
</Badge>
</div>
</div>
</Card>
))}
</div>
);
})}
</div>
);
};
Expand Down
36 changes: 34 additions & 2 deletions frontend/src/pages/HistoryDashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {

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

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

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

// Fetch usernames for all unique partner IDs
useEffect(() => {
if (summaries.length === 0) return;

const fetchUsernames = async () => {
const uniquePartnerIds = [...new Set(summaries.map((s: any) => s.partner_id).filter((id: string) => id))];
const usernameMap = new Map<string, string>();

// Fetch usernames in parallel
await Promise.allSettled(
uniquePartnerIds.map(async (partnerId: string) => {
try {
const response = await getOtherUser(partnerId);
if (response?.data?.username) {
usernameMap.set(partnerId, response.data.username);
} else {
usernameMap.set(partnerId, partnerId);
}
} catch (error) {
console.error(`Failed to fetch username for partner ${partnerId}:`, error);
usernameMap.set(partnerId, partnerId);
}
})
);

setUsernames(usernameMap);
};

fetchUsernames();
}, [summaries]);

// --- Stat Calculation (from Cursor's file, adapted for Lovable's UI) ---
const totals = useMemo(() => {
const uniqueQuestions = summaries.length; // Renamed to match UI
Expand Down Expand Up @@ -185,7 +217,7 @@ export default function HistoryDashboardPage() {
<div className="flex flex-wrap items-center gap-4 text-sm text-muted-foreground">
<span className="flex items-center gap-1">
<User className="h-3 w-3" />
Partner: {question.partner_id}
Partner: {usernames.get(question.partner_id) || question.partner_id}
</span>
<span>
{/* Real data */}
Expand Down
36 changes: 34 additions & 2 deletions frontend/src/pages/QuestionDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';

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

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

useEffect(() => {
if (!userId || !questionId) return;
Expand All @@ -62,6 +63,37 @@ export const QuestionDetail = () => {
});
}, [userId, questionId]);

// Fetch usernames for all unique partner IDs
useEffect(() => {
if (attempts.length === 0) return;

const fetchUsernames = async () => {
const uniquePartnerIds = [...new Set(attempts.map(a => a.partner_id).filter(id => id))];
const usernameMap = new Map<string, string>();

// Fetch usernames in parallel
await Promise.allSettled(
uniquePartnerIds.map(async (partnerId: string) => {
try {
const response = await getOtherUser(partnerId);
if (response?.data?.username) {
usernameMap.set(partnerId, response.data.username);
} else {
usernameMap.set(partnerId, partnerId);
}
} catch (error) {
console.error(`Failed to fetch username for partner ${partnerId}:`, error);
usernameMap.set(partnerId, partnerId);
}
})
);

setUsernames(usernameMap);
};

fetchUsernames();
}, [attempts]);

const questionTitle = attempts[0]?.question_title;

// --- Helper function to guess language ---
Expand Down Expand Up @@ -164,7 +196,7 @@ export const QuestionDetail = () => {
<div className="flex flex-wrap items-center gap-4 text-sm text-muted-foreground mb-4">
<span className="flex items-center gap-1">
<User className="h-4 w-4" />
Partner: {attempt.partner_id}
Partner: {usernames.get(attempt.partner_id) || attempt.partner_id}
</span>
<span className="flex items-center gap-1">
<Calendar className="h-4 w-4" />
Expand Down
Loading
Loading