Skip to content

Commit d5faf84

Browse files
authored
Merge pull request #5 from mbianchidev/copilot/add-user-response-feature
Add local storage persistence for user responses to interview questions
2 parents d997d07 + d03273a commit d5faf84

4 files changed

Lines changed: 234 additions & 20 deletions

File tree

app/practice/page.tsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { useState, useEffect } from 'react';
44
import Link from 'next/link';
55
import { allQuestions } from '@/lib/questionsData';
6+
import { getResponse, saveResponse, deleteResponse } from '@/lib/responseStorage';
67

78
interface Question {
89
id: string;
@@ -17,6 +18,7 @@ export default function PracticePage() {
1718
const [timer, setTimer] = useState(0);
1819
const [isActive, setIsActive] = useState(false);
1920
const [usedQuestions, setUsedQuestions] = useState<Set<string>>(new Set());
21+
const [response, setResponse] = useState('');
2022

2123
useEffect(() => {
2224
let interval: NodeJS.Timeout | null = null;
@@ -43,6 +45,11 @@ export default function PracticePage() {
4345
const getRandomQuestion = () => {
4446
if (questions.length === 0) return;
4547

48+
// Save current response before switching questions (only if not empty)
49+
if (currentQuestion && response.trim()) {
50+
saveResponse(currentQuestion.id, response);
51+
}
52+
4653
// If all questions have been used, reset
4754
if (usedQuestions.size >= questions.length) {
4855
setUsedQuestions(new Set());
@@ -58,21 +65,40 @@ export default function PracticePage() {
5865
const newQuestion = questions[randomIndex];
5966
setCurrentQuestion(newQuestion);
6067
setUsedQuestions(new Set([newQuestion.id]));
68+
setResponse(''); // Clear the text box for new question
6169
} else {
6270
const randomIndex = Math.floor(Math.random() * availableQuestions.length);
6371
const newQuestion = availableQuestions[randomIndex];
6472
setCurrentQuestion(newQuestion);
6573
setUsedQuestions(prev => new Set([...prev, newQuestion.id]));
74+
setResponse(''); // Clear the text box for new question
6675
}
6776

6877
setTimer(0);
6978
setIsActive(true);
7079
};
7180

7281
const skipQuestion = () => {
82+
// Save current response before skipping (only if not empty)
83+
if (currentQuestion && response.trim()) {
84+
saveResponse(currentQuestion.id, response);
85+
}
7386
getRandomQuestion();
7487
};
7588

89+
const handleSaveResponse = () => {
90+
if (currentQuestion) {
91+
saveResponse(currentQuestion.id, response);
92+
}
93+
};
94+
95+
const handleClearResponse = () => {
96+
if (currentQuestion) {
97+
deleteResponse(currentQuestion.id);
98+
setResponse('');
99+
}
100+
};
101+
76102
return (
77103
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800">
78104
<div className="container mx-auto px-4 py-8 max-w-4xl">
@@ -131,6 +157,36 @@ export default function PracticePage() {
131157
{currentQuestion.text}
132158
</p>
133159
</div>
160+
161+
<div className="mt-6">
162+
<div className="flex justify-between items-center mb-2">
163+
<label
164+
htmlFor="response"
165+
className="block text-sm font-medium text-slate-700 dark:text-slate-300"
166+
>
167+
Your Response:
168+
</label>
169+
{response.trim() && (
170+
<button
171+
onClick={handleClearResponse}
172+
className="text-xs px-3 py-1 bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-200 hover:bg-red-200 dark:hover:bg-red-800 rounded transition-colors"
173+
>
174+
Clear Response
175+
</button>
176+
)}
177+
</div>
178+
<textarea
179+
id="response"
180+
value={response}
181+
onChange={(e) => setResponse(e.target.value)}
182+
onBlur={handleSaveResponse}
183+
placeholder="Type your answer here... (auto-saves)"
184+
className="w-full h-48 px-4 py-3 bg-slate-50 dark:bg-slate-900 border-2 border-slate-200 dark:border-slate-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-slate-900 dark:text-slate-100 placeholder-slate-400 dark:placeholder-slate-500 resize-y"
185+
/>
186+
<p className="mt-2 text-xs text-slate-500 dark:text-slate-400">
187+
💾 Your response is automatically saved to local storage when you type or switch questions
188+
</p>
189+
</div>
134190
</div>
135191

136192
<div className="flex gap-4 justify-center">
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
'use client';
2+
3+
import { useState, useEffect } from 'react';
4+
import { getResponse, saveResponse, deleteResponse } from '@/lib/responseStorage';
5+
6+
interface QuestionItemProps {
7+
questionId: string;
8+
questionText: string;
9+
index: number;
10+
}
11+
12+
export default function QuestionItem({ questionId, questionText, index }: QuestionItemProps) {
13+
const [isExpanded, setIsExpanded] = useState(false);
14+
const [response, setResponse] = useState('');
15+
const [isClient, setIsClient] = useState(false);
16+
17+
useEffect(() => {
18+
setIsClient(true);
19+
setResponse(getResponse(questionId));
20+
}, [questionId]);
21+
22+
const hasResponse = isClient && response.length > 0;
23+
24+
const handleSaveResponse = () => {
25+
saveResponse(questionId, response);
26+
};
27+
28+
const handleClearResponse = () => {
29+
deleteResponse(questionId);
30+
setResponse('');
31+
};
32+
33+
return (
34+
<li className="bg-slate-50 dark:bg-slate-700 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-600 transition-colors">
35+
<div
36+
className="p-4 cursor-pointer"
37+
onClick={() => setIsExpanded(!isExpanded)}
38+
>
39+
<div className="flex items-start justify-between">
40+
<div className="flex-1">
41+
<span className="font-medium text-slate-700 dark:text-slate-300 mr-2">
42+
{index + 1}.
43+
</span>
44+
<span className="text-slate-900 dark:text-slate-100">
45+
{questionText}
46+
</span>
47+
</div>
48+
<div className="flex items-center gap-2 ml-4">
49+
{hasResponse && (
50+
<span className="text-xs px-2 py-1 bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 rounded">
51+
✓ Answered
52+
</span>
53+
)}
54+
<span className="text-slate-400 dark:text-slate-500">
55+
{isExpanded ? '▲' : '▼'}
56+
</span>
57+
</div>
58+
</div>
59+
</div>
60+
61+
{isExpanded && (
62+
<div className="px-4 pb-4 border-t border-slate-200 dark:border-slate-600 pt-4">
63+
<div className="flex justify-between items-center mb-2">
64+
<label
65+
htmlFor={`response-${questionId}`}
66+
className="block text-sm font-medium text-slate-700 dark:text-slate-300"
67+
>
68+
Your Response:
69+
</label>
70+
{response.trim() && (
71+
<button
72+
onClick={handleClearResponse}
73+
className="text-xs px-3 py-1 bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-200 hover:bg-red-200 dark:hover:bg-red-800 rounded transition-colors"
74+
>
75+
Clear Response
76+
</button>
77+
)}
78+
</div>
79+
<textarea
80+
id={`response-${questionId}`}
81+
value={response}
82+
onChange={(e) => setResponse(e.target.value)}
83+
onBlur={handleSaveResponse}
84+
placeholder="Type your answer here... (auto-saves)"
85+
className="w-full h-32 px-3 py-2 bg-white dark:bg-slate-800 border-2 border-slate-200 dark:border-slate-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-slate-900 dark:text-slate-100 placeholder-slate-400 dark:placeholder-slate-500 resize-y"
86+
/>
87+
<p className="mt-2 text-xs text-slate-500 dark:text-slate-400">
88+
💾 Auto-saves when you finish typing
89+
</p>
90+
</div>
91+
)}
92+
</li>
93+
);
94+
}

app/topics/[categoryId]/page.tsx

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Link from 'next/link';
22
import { parseReadme } from '@/lib/parseQuestions';
33
import { notFound } from 'next/navigation';
4+
import QuestionItem from './QuestionItem';
45

56
export default async function CategoryPage({ params }: { params: Promise<{ categoryId: string }> }) {
67
const { categoryId } = await params;
@@ -35,17 +36,12 @@ export default async function CategoryPage({ params }: { params: Promise<{ categ
3536
</h2>
3637
<ul className="space-y-3">
3738
{category.questions.map((question, index) => (
38-
<li
39+
<QuestionItem
3940
key={question.id}
40-
className="p-4 bg-slate-50 dark:bg-slate-700 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-600 transition-colors"
41-
>
42-
<span className="font-medium text-slate-700 dark:text-slate-300 mr-2">
43-
{index + 1}.
44-
</span>
45-
<span className="text-slate-900 dark:text-slate-100">
46-
{question.text}
47-
</span>
48-
</li>
41+
questionId={question.id}
42+
questionText={question.text}
43+
index={index}
44+
/>
4945
))}
5046
</ul>
5147
</div>
@@ -61,17 +57,12 @@ export default async function CategoryPage({ params }: { params: Promise<{ categ
6157
</h2>
6258
<ul className="space-y-3">
6359
{subcategory.questions.map((question, index) => (
64-
<li
60+
<QuestionItem
6561
key={question.id}
66-
className="p-4 bg-slate-50 dark:bg-slate-700 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-600 transition-colors"
67-
>
68-
<span className="font-medium text-slate-700 dark:text-slate-300 mr-2">
69-
{index + 1}.
70-
</span>
71-
<span className="text-slate-900 dark:text-slate-100">
72-
{question.text}
73-
</span>
74-
</li>
62+
questionId={question.id}
63+
questionText={question.text}
64+
index={index}
65+
/>
7566
))}
7667
</ul>
7768
</div>

lib/responseStorage.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Utility functions for managing question responses in local storage
2+
3+
const STORAGE_KEY = 'interview-question-responses';
4+
5+
export interface QuestionResponse {
6+
questionId: string;
7+
response: string;
8+
timestamp: number;
9+
}
10+
11+
export interface ResponseStorage {
12+
[questionId: string]: QuestionResponse;
13+
}
14+
15+
// Get all responses from local storage
16+
export function getAllResponses(): ResponseStorage {
17+
if (typeof window === 'undefined') return {};
18+
19+
try {
20+
const stored = localStorage.getItem(STORAGE_KEY);
21+
return stored ? JSON.parse(stored) : {};
22+
} catch (error) {
23+
console.error('Error reading from local storage:', error);
24+
return {};
25+
}
26+
}
27+
28+
// Get response for a specific question
29+
export function getResponse(questionId: string): string {
30+
const responses = getAllResponses();
31+
return responses[questionId]?.response || '';
32+
}
33+
34+
// Save response for a specific question
35+
export function saveResponse(questionId: string, response: string): void {
36+
if (typeof window === 'undefined') return;
37+
38+
try {
39+
const responses = getAllResponses();
40+
responses[questionId] = {
41+
questionId,
42+
response,
43+
timestamp: Date.now(),
44+
};
45+
localStorage.setItem(STORAGE_KEY, JSON.stringify(responses));
46+
} catch (error) {
47+
console.error('Error saving to local storage:', error);
48+
}
49+
}
50+
51+
// Delete response for a specific question
52+
export function deleteResponse(questionId: string): void {
53+
if (typeof window === 'undefined') return;
54+
55+
try {
56+
const responses = getAllResponses();
57+
delete responses[questionId];
58+
localStorage.setItem(STORAGE_KEY, JSON.stringify(responses));
59+
} catch (error) {
60+
console.error('Error deleting from local storage:', error);
61+
}
62+
}
63+
64+
// Clear all responses
65+
export function clearAllResponses(): void {
66+
if (typeof window === 'undefined') return;
67+
68+
try {
69+
localStorage.removeItem(STORAGE_KEY);
70+
} catch (error) {
71+
console.error('Error clearing local storage:', error);
72+
}
73+
}

0 commit comments

Comments
 (0)