Skip to content

Commit fe2a710

Browse files
authored
Merge pull request #6 from mbianchidev/copilot/update-question-timer-functionality
Implement countdown timer, extra time pool, and self-evaluation for practice mode
2 parents d5faf84 + 3bc93d0 commit fe2a710

File tree

2 files changed

+244
-14
lines changed

2 files changed

+244
-14
lines changed

app/practice/page.tsx

Lines changed: 181 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
'use client';
22

3-
import { useState, useEffect } from 'react';
3+
import { useState, useEffect, useRef } from 'react';
44
import Link from 'next/link';
55
import { allQuestions } from '@/lib/questionsData';
6-
import { getResponse, saveResponse, deleteResponse } from '@/lib/responseStorage';
6+
import { saveResponse, deleteResponse } from '@/lib/responseStorage';
7+
import { saveEvaluation } from '@/lib/evaluationStorage';
78

89
interface Question {
910
id: string;
@@ -12,22 +13,44 @@ interface Question {
1213
subcategory?: string;
1314
}
1415

16+
const TIMER_DURATION = 300; // 5 minutes in seconds
17+
const QUESTIONS_PER_ROUND = 10;
18+
1519
export default function PracticePage() {
1620
const [questions] = useState<Question[]>(allQuestions);
1721
const [currentQuestion, setCurrentQuestion] = useState<Question | null>(null);
18-
const [timer, setTimer] = useState(0);
22+
const [timer, setTimer] = useState(TIMER_DURATION);
1923
const [isActive, setIsActive] = useState(false);
2024
const [usedQuestions, setUsedQuestions] = useState<Set<string>>(new Set());
2125
const [response, setResponse] = useState('');
26+
const [questionsInRound, setQuestionsInRound] = useState(0);
27+
const [extraTime, setExtraTime] = useState(0);
28+
const [showEvaluation, setShowEvaluation] = useState(false);
29+
const [evaluation, setEvaluation] = useState({
30+
confidence: 3,
31+
effectiveness: 3,
32+
knowledge: 3,
33+
});
34+
35+
const skipQuestionRef = useRef<(() => void) | null>(null);
2236

2337
useEffect(() => {
2438
let interval: NodeJS.Timeout | null = null;
2539

26-
if (isActive) {
40+
if (isActive && timer > 0) {
2741
interval = setInterval(() => {
28-
setTimer((seconds) => seconds + 1);
42+
setTimer((seconds) => {
43+
if (seconds <= 1) {
44+
// Time's up, auto-skip to next question
45+
if (skipQuestionRef.current) {
46+
skipQuestionRef.current();
47+
}
48+
return TIMER_DURATION;
49+
}
50+
return seconds - 1;
51+
});
2952
}, 1000);
30-
} else if (!isActive && timer !== 0) {
53+
} else if (!isActive && timer !== TIMER_DURATION) {
3154
if (interval) clearInterval(interval);
3255
}
3356

@@ -45,6 +68,13 @@ export default function PracticePage() {
4568
const getRandomQuestion = () => {
4669
if (questions.length === 0) return;
4770

71+
// Check if we've completed a round
72+
if (questionsInRound >= QUESTIONS_PER_ROUND) {
73+
setShowEvaluation(true);
74+
setIsActive(false);
75+
return;
76+
}
77+
4878
// Save current response before switching questions (only if not empty)
4979
if (currentQuestion && response.trim()) {
5080
saveResponse(currentQuestion.id, response);
@@ -74,8 +104,21 @@ export default function PracticePage() {
74104
setResponse(''); // Clear the text box for new question
75105
}
76106

77-
setTimer(0);
107+
// Add any remaining time to extra time pool, then reset timer
108+
if (timer > 0 && questionsInRound > 0) {
109+
setExtraTime(prev => prev + timer);
110+
}
111+
112+
// Use extra time if available
113+
let newTimer = TIMER_DURATION;
114+
if (extraTime > 0) {
115+
newTimer = TIMER_DURATION + extraTime;
116+
setExtraTime(0);
117+
}
118+
119+
setTimer(newTimer);
78120
setIsActive(true);
121+
setQuestionsInRound(prev => prev + 1);
79122
};
80123

81124
const skipQuestion = () => {
@@ -86,6 +129,11 @@ export default function PracticePage() {
86129
getRandomQuestion();
87130
};
88131

132+
// Update ref whenever skipQuestion changes
133+
useEffect(() => {
134+
skipQuestionRef.current = skipQuestion;
135+
});
136+
89137
const handleSaveResponse = () => {
90138
if (currentQuestion) {
91139
saveResponse(currentQuestion.id, response);
@@ -99,6 +147,25 @@ export default function PracticePage() {
99147
}
100148
};
101149

150+
const handleSubmitEvaluation = () => {
151+
saveEvaluation(evaluation);
152+
// Reset for new round
153+
setShowEvaluation(false);
154+
setQuestionsInRound(0);
155+
setExtraTime(0);
156+
setUsedQuestions(new Set());
157+
setCurrentQuestion(null);
158+
setTimer(TIMER_DURATION);
159+
setIsActive(false);
160+
};
161+
162+
const handleStartNewRound = () => {
163+
setQuestionsInRound(0);
164+
setExtraTime(0);
165+
setUsedQuestions(new Set());
166+
getRandomQuestion();
167+
};
168+
102169
return (
103170
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800">
104171
<div className="container mx-auto px-4 py-8 max-w-4xl">
@@ -115,13 +182,97 @@ export default function PracticePage() {
115182
Practice Mode
116183
</h1>
117184

118-
{!currentQuestion ? (
185+
{showEvaluation ? (
186+
<div className="bg-white dark:bg-slate-800 rounded-xl shadow-lg p-8 space-y-6">
187+
<h2 className="text-2xl font-bold text-slate-900 dark:text-slate-100 text-center">
188+
Round Complete! 🎉
189+
</h2>
190+
<p className="text-center text-slate-600 dark:text-slate-300">
191+
You&apos;ve completed {QUESTIONS_PER_ROUND} questions. Please evaluate yourself:
192+
</p>
193+
194+
<div className="space-y-6">
195+
<div>
196+
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
197+
Confidence (1-5)
198+
</label>
199+
<div className="flex gap-2 justify-center">
200+
{[1, 2, 3, 4, 5].map((val) => (
201+
<button
202+
key={val}
203+
onClick={() => setEvaluation(prev => ({ ...prev, confidence: val }))}
204+
className={`w-12 h-12 rounded-lg font-semibold transition-all ${
205+
evaluation.confidence === val
206+
? 'bg-blue-600 text-white scale-110'
207+
: 'bg-slate-200 dark:bg-slate-700 text-slate-700 dark:text-slate-300 hover:bg-slate-300 dark:hover:bg-slate-600'
208+
}`}
209+
>
210+
{val}
211+
</button>
212+
))}
213+
</div>
214+
</div>
215+
216+
<div>
217+
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
218+
Effectiveness (1-5)
219+
</label>
220+
<div className="flex gap-2 justify-center">
221+
{[1, 2, 3, 4, 5].map((val) => (
222+
<button
223+
key={val}
224+
onClick={() => setEvaluation(prev => ({ ...prev, effectiveness: val }))}
225+
className={`w-12 h-12 rounded-lg font-semibold transition-all ${
226+
evaluation.effectiveness === val
227+
? 'bg-green-600 text-white scale-110'
228+
: 'bg-slate-200 dark:bg-slate-700 text-slate-700 dark:text-slate-300 hover:bg-slate-300 dark:hover:bg-slate-600'
229+
}`}
230+
>
231+
{val}
232+
</button>
233+
))}
234+
</div>
235+
</div>
236+
237+
<div>
238+
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
239+
Knowledge (1-5)
240+
</label>
241+
<div className="flex gap-2 justify-center">
242+
{[1, 2, 3, 4, 5].map((val) => (
243+
<button
244+
key={val}
245+
onClick={() => setEvaluation(prev => ({ ...prev, knowledge: val }))}
246+
className={`w-12 h-12 rounded-lg font-semibold transition-all ${
247+
evaluation.knowledge === val
248+
? 'bg-purple-600 text-white scale-110'
249+
: 'bg-slate-200 dark:bg-slate-700 text-slate-700 dark:text-slate-300 hover:bg-slate-300 dark:hover:bg-slate-600'
250+
}`}
251+
>
252+
{val}
253+
</button>
254+
))}
255+
</div>
256+
</div>
257+
</div>
258+
259+
<button
260+
onClick={handleSubmitEvaluation}
261+
className="w-full px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-lg shadow-lg hover:shadow-xl transition-all duration-300"
262+
>
263+
Submit Evaluation & Start New Round
264+
</button>
265+
</div>
266+
) : !currentQuestion ? (
119267
<div className="text-center space-y-8">
120268
<p className="text-xl text-slate-600 dark:text-slate-300">
121-
Test your knowledge with random questions. A timer will start when you begin.
269+
Test your knowledge with random questions. Each round has {QUESTIONS_PER_ROUND} questions with 5 minutes per question.
270+
</p>
271+
<p className="text-md text-slate-500 dark:text-slate-400">
272+
Finish questions early? Your extra time rolls over to the next question!
122273
</p>
123274
<button
124-
onClick={getRandomQuestion}
275+
onClick={handleStartNewRound}
125276
disabled={questions.length === 0}
126277
className="px-8 py-4 bg-green-600 hover:bg-green-700 text-white font-semibold rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 text-lg disabled:opacity-50 disabled:cursor-not-allowed"
127278
>
@@ -141,14 +292,24 @@ export default function PracticePage() {
141292
{currentQuestion.category}
142293
</span>
143294
)}
295+
<div className="mt-2 text-sm font-medium text-slate-600 dark:text-slate-400">
296+
Question {questionsInRound} / {QUESTIONS_PER_ROUND}
297+
</div>
144298
</div>
145299
<div className="text-right">
146-
<div className="text-4xl font-bold text-slate-900 dark:text-slate-100 font-mono">
300+
<div className={`text-4xl font-bold font-mono ${
301+
timer <= 60 ? 'text-red-600 dark:text-red-400' : 'text-slate-900 dark:text-slate-100'
302+
}`}>
147303
{formatTime(timer)}
148304
</div>
149305
<div className="text-sm text-slate-500 dark:text-slate-400 mt-1">
150-
Time elapsed
306+
Time remaining
151307
</div>
308+
{extraTime > 0 && (
309+
<div className="text-xs text-green-600 dark:text-green-400 mt-1">
310+
+{formatTime(extraTime)} extra time available
311+
</div>
312+
)}
152313
</div>
153314
</div>
154315

@@ -192,9 +353,15 @@ export default function PracticePage() {
192353
<div className="flex gap-4 justify-center">
193354
<button
194355
onClick={skipQuestion}
195-
className="px-6 py-3 bg-slate-200 dark:bg-slate-700 hover:bg-slate-300 dark:hover:bg-slate-600 text-slate-900 dark:text-slate-100 font-semibold rounded-lg transition-colors"
356+
className="px-4 py-2 bg-slate-200 dark:bg-slate-700 hover:bg-slate-300 dark:hover:bg-slate-600 text-slate-900 dark:text-slate-100 text-sm font-medium rounded-lg transition-colors"
357+
>
358+
Skip
359+
</button>
360+
<button
361+
onClick={skipQuestion}
362+
className="px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-lg transition-colors shadow-lg hover:shadow-xl"
196363
>
197-
Skip Question →
364+
Next Question →
198365
</button>
199366
</div>
200367

lib/evaluationStorage.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Utility functions for managing self-evaluations in local storage
2+
3+
const EVALUATION_STORAGE_KEY = 'interview-practice-evaluations';
4+
5+
export interface SelfEvaluation {
6+
confidence: number; // 1-5 scale
7+
effectiveness: number; // 1-5 scale
8+
knowledge: number; // 1-5 scale
9+
timestamp: number;
10+
roundNumber: number;
11+
}
12+
13+
export interface EvaluationStorage {
14+
evaluations: SelfEvaluation[];
15+
}
16+
17+
// Get all evaluations from local storage
18+
export function getAllEvaluations(): SelfEvaluation[] {
19+
if (typeof window === 'undefined') return [];
20+
21+
try {
22+
const stored = localStorage.getItem(EVALUATION_STORAGE_KEY);
23+
const data: EvaluationStorage = stored ? JSON.parse(stored) : { evaluations: [] };
24+
return data.evaluations || [];
25+
} catch (error) {
26+
console.error('Error reading evaluations from local storage:', error);
27+
return [];
28+
}
29+
}
30+
31+
// Save a new evaluation
32+
export function saveEvaluation(evaluation: Omit<SelfEvaluation, 'timestamp' | 'roundNumber'>): void {
33+
if (typeof window === 'undefined') return;
34+
35+
try {
36+
const evaluations = getAllEvaluations();
37+
const roundNumber = evaluations.length + 1;
38+
39+
const newEvaluation: SelfEvaluation = {
40+
...evaluation,
41+
timestamp: Date.now(),
42+
roundNumber,
43+
};
44+
45+
evaluations.push(newEvaluation);
46+
47+
const data: EvaluationStorage = { evaluations };
48+
localStorage.setItem(EVALUATION_STORAGE_KEY, JSON.stringify(data));
49+
} catch (error) {
50+
console.error('Error saving evaluation to local storage:', error);
51+
}
52+
}
53+
54+
// Clear all evaluations
55+
export function clearAllEvaluations(): void {
56+
if (typeof window === 'undefined') return;
57+
58+
try {
59+
localStorage.removeItem(EVALUATION_STORAGE_KEY);
60+
} catch (error) {
61+
console.error('Error clearing evaluations from local storage:', error);
62+
}
63+
}

0 commit comments

Comments
 (0)