Skip to content

Commit b212ad9

Browse files
feat: Add reading comprehension results viewing page
- Add `getPastReadingResultsAction()` server action to fetch all past attempts with sessions - Create `/reading-comprehension/results` page with comprehensive results display - Add navigation links from main reading comprehension page to results - Display score metrics, reading time, answer breakdown, and expandable details - Show chronological order of all past reading comprehension attempts Co-authored-by: tylerthecoder <tylerthecoder@users.noreply.github.com>
1 parent 7512edd commit b212ad9

File tree

3 files changed

+267
-4
lines changed

3 files changed

+267
-4
lines changed

app/actions/readingComprehension.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {
99
ReadingSessionDocument,
1010
ReadingSession,
1111
ReadingAttemptDocument,
12+
ReadingAttempt,
1213
ReadingComprehensionQuestion,
1314
} from '@/types';
1415

@@ -282,4 +283,50 @@ export async function updateAnswerScoreAction(
282283
const message = error instanceof Error ? error.message : 'Failed to update answer score.';
283284
return { success: false, message };
284285
}
286+
}
287+
288+
// --- Action: Get Past Reading Results ---
289+
290+
interface ReadingResult {
291+
session: ReadingSession;
292+
attempt: ReadingAttempt;
293+
}
294+
295+
interface GetPastReadingResultsResult {
296+
success: boolean;
297+
results?: ReadingResult[];
298+
message?: string;
299+
}
300+
301+
export async function getPastReadingResultsAction(): Promise<GetPastReadingResultsResult> {
302+
try {
303+
const { db } = await connectToDatabase();
304+
const readingSessionsCollection = db.collection<ReadingSessionDocument>('reading_sessions');
305+
const readingAttemptsCollection = db.collection<ReadingAttemptDocument>('reading_attempts');
306+
307+
// Get all attempts with their sessions, sorted by most recent first
308+
const attempts = await readingAttemptsCollection
309+
.find({})
310+
.sort({ createdAt: -1 })
311+
.toArray();
312+
313+
const results: ReadingResult[] = [];
314+
315+
for (const attempt of attempts) {
316+
const session = await readingSessionsCollection.findOne({ _id: attempt.session_id });
317+
if (session) {
318+
results.push({
319+
session: mapMongoId(session) as ReadingSession,
320+
attempt: mapMongoId(attempt) as ReadingAttempt,
321+
});
322+
}
323+
}
324+
325+
return { success: true, results };
326+
327+
} catch (error) {
328+
console.error('[Get Past Reading Results Action Error]', error);
329+
const message = error instanceof Error ? error.message : 'Failed to fetch past reading results.';
330+
return { success: false, message };
331+
}
285332
}

app/reading-comprehension/page.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,17 @@ function TopicInput({ onTopicSubmit, isLoading }: TopicInputProps) {
5151
{isLoading ? 'Generating Content...' : 'Generate Reading Content'}
5252
</Button>
5353
</form>
54-
<div className="mt-6 text-center">
55-
<Link href="/" className="text-primary underline hover:text-red-700">
56-
← Back to Home
57-
</Link>
54+
<div className="mt-6 text-center space-y-2">
55+
<div>
56+
<Link href="/reading-comprehension/results" className="text-primary underline hover:text-red-700">
57+
View Past Results
58+
</Link>
59+
</div>
60+
<div>
61+
<Link href="/" className="text-primary underline hover:text-red-700">
62+
← Back to Home
63+
</Link>
64+
</div>
5865
</div>
5966
</div>
6067
);
@@ -284,6 +291,9 @@ function QuestionAnswer({ session, readingTimeMs, onComplete }: QuestionAnswerPr
284291
<Button variant="primary" size="base" onClick={onComplete}>
285292
Try Another Topic
286293
</Button>
294+
<Link href="/reading-comprehension/results" passHref legacyBehavior>
295+
<Button as="a" variant="default" size="base">View All Results</Button>
296+
</Link>
287297
<Link href="/" passHref legacyBehavior>
288298
<Button as="a" variant="default" size="base">Back to Home</Button>
289299
</Link>
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
'use client';
2+
3+
import React, { useState, useEffect } from 'react';
4+
import Link from 'next/link';
5+
import Button from '@/components/Button';
6+
import Spinner from '@/components/Spinner';
7+
import { getPastReadingResultsAction } from '@/actions/readingComprehension';
8+
import type { ReadingSession, ReadingAttempt } from '@/types';
9+
10+
interface ReadingResult {
11+
session: ReadingSession;
12+
attempt: ReadingAttempt;
13+
}
14+
15+
export default function ReadingComprehensionResultsPage() {
16+
const [results, setResults] = useState<ReadingResult[]>([]);
17+
const [isLoading, setIsLoading] = useState(true);
18+
const [error, setError] = useState<string | null>(null);
19+
20+
useEffect(() => {
21+
const fetchResults = async () => {
22+
try {
23+
const response = await getPastReadingResultsAction();
24+
if (response.success && response.results) {
25+
setResults(response.results);
26+
} else {
27+
setError(response.message || 'Failed to load results');
28+
}
29+
} catch (err) {
30+
console.error('Failed to fetch results:', err);
31+
setError('Failed to load results');
32+
} finally {
33+
setIsLoading(false);
34+
}
35+
};
36+
37+
fetchResults();
38+
}, []);
39+
40+
if (isLoading) {
41+
return (
42+
<div className="container mx-auto px-4 py-8 min-h-screen">
43+
<div className="text-center">
44+
<Spinner />
45+
<p className="mt-4 text-gray-600">Loading your reading comprehension results...</p>
46+
</div>
47+
</div>
48+
);
49+
}
50+
51+
if (error) {
52+
return (
53+
<div className="container mx-auto px-4 py-8 min-h-screen">
54+
<div className="text-center">
55+
<h1 className="text-3xl font-bold text-gray-800 mb-4">Reading Comprehension Results</h1>
56+
<p className="text-red-600 mb-6">{error}</p>
57+
<Link href="/reading-comprehension">
58+
<Button variant="primary">← Back to Reading Comprehension</Button>
59+
</Link>
60+
</div>
61+
</div>
62+
);
63+
}
64+
65+
const formatReadingTime = (timeMs: number) => {
66+
const seconds = Math.round(timeMs / 1000);
67+
const minutes = Math.floor(seconds / 60);
68+
const remainingSeconds = seconds % 60;
69+
70+
if (minutes > 0) {
71+
return `${minutes}m ${remainingSeconds}s`;
72+
}
73+
return `${seconds}s`;
74+
};
75+
76+
const formatDate = (date: Date) => {
77+
return new Date(date).toLocaleDateString('en-US', {
78+
year: 'numeric',
79+
month: 'short',
80+
day: 'numeric',
81+
hour: '2-digit',
82+
minute: '2-digit'
83+
});
84+
};
85+
86+
return (
87+
<div className="container mx-auto px-4 py-8 min-h-screen">
88+
<div className="max-w-6xl mx-auto">
89+
<div className="flex items-center justify-between mb-8">
90+
<h1 className="text-3xl font-bold text-gray-800">Reading Comprehension Results</h1>
91+
<Link href="/reading-comprehension">
92+
<Button variant="default">← Back to Reading Comprehension</Button>
93+
</Link>
94+
</div>
95+
96+
{results.length === 0 ? (
97+
<div className="text-center py-12">
98+
<p className="text-gray-600 mb-6">No reading comprehension attempts found.</p>
99+
<Link href="/reading-comprehension">
100+
<Button variant="primary">Start Your First Reading Test</Button>
101+
</Link>
102+
</div>
103+
) : (
104+
<div className="space-y-6">
105+
{results.map((result, index) => (
106+
<div key={result.attempt.id} className="bg-white p-6 rounded-lg shadow-md border border-gray-200">
107+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
108+
{/* Left: Basic Info */}
109+
<div>
110+
<h3 className="text-lg font-semibold text-gray-800 mb-2">
111+
{result.session.topic}
112+
</h3>
113+
<p className="text-sm text-gray-600 mb-3">
114+
Completed: {formatDate(result.attempt.createdAt)}
115+
</p>
116+
<div className="space-y-1 text-sm">
117+
<div>Reading Time: <span className="font-medium">{formatReadingTime(result.attempt.reading_time_ms)}</span></div>
118+
</div>
119+
</div>
120+
121+
{/* Center: Score Metrics */}
122+
<div className="grid grid-cols-3 gap-4">
123+
<div className="text-center p-3 bg-primary/10 rounded-lg">
124+
<div className="text-xl font-bold text-primary">
125+
{result.attempt.total_score}/{result.session.questions.length}
126+
</div>
127+
<div className="text-xs text-gray-600">Score</div>
128+
</div>
129+
<div className="text-center p-3 bg-green-50 rounded-lg">
130+
<div className="text-xl font-bold text-green-600">
131+
{Math.round((result.attempt.total_score / result.session.questions.length) * 100)}%
132+
</div>
133+
<div className="text-xs text-gray-600">Accuracy</div>
134+
</div>
135+
<div className="text-center p-3 bg-blue-50 rounded-lg">
136+
<div className="text-xl font-bold text-blue-600">
137+
{formatReadingTime(result.attempt.reading_time_ms)}
138+
</div>
139+
<div className="text-xs text-gray-600">Time</div>
140+
</div>
141+
</div>
142+
143+
{/* Right: Answer Breakdown */}
144+
<div>
145+
<h4 className="font-medium text-gray-800 mb-2">Answer Breakdown</h4>
146+
<div className="space-y-1">
147+
{result.attempt.answers.map((answer, answerIndex) => (
148+
<div key={answerIndex} className="flex items-center text-sm">
149+
<span className="w-8">Q{answerIndex + 1}:</span>
150+
<span className={`px-2 py-0.5 rounded text-xs font-medium ${
151+
answer.is_correct
152+
? 'bg-green-100 text-green-800'
153+
: 'bg-red-100 text-red-800'
154+
}`}>
155+
{answer.is_correct ? '✓' : '✗'}
156+
</span>
157+
{answer.overridden && (
158+
<span className="ml-1 text-xs text-orange-600">(Override)</span>
159+
)}
160+
</div>
161+
))}
162+
</div>
163+
</div>
164+
</div>
165+
166+
{/* Expandable Details */}
167+
<details className="mt-4">
168+
<summary className="cursor-pointer text-sm text-primary hover:text-red-700">
169+
View Details
170+
</summary>
171+
<div className="mt-4 pt-4 border-t border-gray-200">
172+
<div className="mb-4">
173+
<h5 className="font-medium text-gray-800 mb-2">Passage:</h5>
174+
<div className="text-sm text-gray-600 bg-gray-50 p-3 rounded-md max-h-40 overflow-y-auto">
175+
{result.session.passage}
176+
</div>
177+
</div>
178+
<div>
179+
<h5 className="font-medium text-gray-800 mb-2">Questions & Answers:</h5>
180+
<div className="space-y-3">
181+
{result.session.questions.map((question, qIndex) => {
182+
const userAnswer = result.attempt.answers.find(a => a.question_index === qIndex);
183+
return (
184+
<div key={qIndex} className="text-sm border-l-2 border-gray-200 pl-3">
185+
<div className="font-medium text-gray-800 mb-1">
186+
Q{qIndex + 1}: {question.question_text}
187+
</div>
188+
<div className="text-gray-600">
189+
<div>Your answer: <span className="font-medium">{userAnswer?.user_answer || 'No answer'}</span></div>
190+
<div>Correct answer: <span className="font-medium text-green-700">{question.correct_answer}</span></div>
191+
</div>
192+
</div>
193+
);
194+
})}
195+
</div>
196+
</div>
197+
</div>
198+
</details>
199+
</div>
200+
))}
201+
</div>
202+
)}
203+
</div>
204+
</div>
205+
);
206+
}

0 commit comments

Comments
 (0)