feat: Update login navigation and authentication flow

- Changed navigation from '/editor' to '/tests' after successful login.
- Introduced token state management in AuthContext for better handling of authentication.
- Updated login function to store JWT instead of Google token.
- Added error handling for login and test fetching processes.

style: Enhance UI with new footer and test list styles

- Added a footer component with copyright information.
- Created a new TestList component with improved styling and animations.
- Implemented responsive design for test cards and filter tabs.
- Added loading and error states for better user experience.

fix: Improve API interaction for test fetching and password verification

- Refactored API calls to use a centralized studentApi utility.
- Enhanced error handling for API responses, including unauthorized access.
- Implemented password verification for protected tests before starting them.
This commit is contained in:
2025-10-29 11:37:19 +05:30
parent 304761e258
commit 47f73681af
11 changed files with 1416 additions and 98 deletions

View File

@@ -1,8 +1,12 @@
import React, { useState, useEffect, useRef } from 'react';
import Editor from "@monaco-editor/react";
import { Play, Send } from 'lucide-react';
import { useAuth } from '../contexts/AuthContext';
import { useNavigate } from 'react-router-dom';
const CodeChallenge = () => {
const [test, setTest] = useState(null);
const [questions, setQuestions] = useState([]);
const [activeQuestion, setActiveQuestion] = useState("Q.1");
const [language, setLanguage] = useState("JavaScript");
const [code, setCode] = useState("");
@@ -11,7 +15,85 @@ const CodeChallenge = () => {
const [autoSelected, setAutoSelected] = useState(true);
const [activeSocket, setActiveSocket] = useState(null);
const [submissionId, setSubmissionId] = useState(null);
const [timeRemaining, setTimeRemaining] = useState(null);
const socketRef = useRef(null);
const { token } = useAuth();
const navigate = useNavigate();
// Load test data from localStorage
useEffect(() => {
const testData = localStorage.getItem('currentTest');
if (testData) {
try {
const parsedData = JSON.parse(testData);
setTest(parsedData);
if (parsedData.questions && parsedData.questions.length > 0) {
setQuestions(parsedData.questions);
// Set initial code from first question
const firstQuestion = parsedData.questions[0];
setLanguage(firstQuestion.programming_language || 'JavaScript');
setCode(firstQuestion.code_template || getDefaultTemplate(firstQuestion.programming_language || 'JavaScript'));
}
} catch (error) {
console.error('Error loading test data:', error);
}
} else {
// No test data, redirect back to tests
navigate('/tests');
}
}, [navigate]);
// Timer countdown
useEffect(() => {
if (!test || !test.end_time) return;
const updateTimer = () => {
const now = new Date();
const endTime = new Date(test.end_time);
const diff = endTime - now;
if (diff <= 0) {
setTimeRemaining('Time Up!');
// Optionally auto-submit or redirect
return;
}
const hours = Math.floor(diff / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
setTimeRemaining(`${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`);
};
updateTimer(); // Initial call
const timerId = setInterval(updateTimer, 1000);
return () => clearInterval(timerId);
}, [test]);
// Get default code template for a language
const getDefaultTemplate = (lang) => {
const templates = {
'JavaScript': '// Write your code here\n',
'Python': '# Write your code here\n',
'Java': 'public class Solution {\n public static void main(String[] args) {\n // Write your code here\n }\n}',
'C++': '#include <iostream>\nusing namespace std;\n\nint main() {\n // Write your code here\n return 0;\n}',
'C': '#include <stdio.h>\n\nint main() {\n // Write your code here\n return 0;\n}'
};
return templates[lang] || '// Write your code here\n';
};
// Map question index to Q.1, Q.2, Q.3 format
const getQuestionIndex = (questionKey) => {
const index = parseInt(questionKey.replace('Q.', '')) - 1;
return index;
};
// Get current question based on activeQuestion
const getCurrentQuestion = () => {
const index = getQuestionIndex(activeQuestion);
return questions[index] || null;
};
// Map frontend language names to backend language identifiers
const getLanguageIdentifier = (uiLanguage) => {
@@ -179,10 +261,34 @@ int main() {
// Set initial code based on active problem
useEffect(() => {
if (problems[activeQuestion]) {
const currentQuestion = getCurrentQuestion();
if (currentQuestion) {
// Check if there's a saved submission for this question
const savedSubmission = localStorage.getItem(`submission_${test?.id}_${currentQuestion.id}`);
if (savedSubmission) {
try {
const submission = JSON.parse(savedSubmission);
setCode(submission.code);
setLanguage(currentQuestion.programming_language || 'JavaScript');
setTerminalOutput([
{ type: 'system', content: `Loaded your previous submission from ${new Date(submission.timestamp).toLocaleString()}` }
]);
} catch (error) {
console.error('Error loading saved submission:', error);
setLanguage(currentQuestion.programming_language || 'JavaScript');
setCode(currentQuestion.code_template || getDefaultTemplate(currentQuestion.programming_language || 'JavaScript'));
}
} else {
setLanguage(currentQuestion.programming_language || 'JavaScript');
setCode(currentQuestion.code_template || getDefaultTemplate(currentQuestion.programming_language || 'JavaScript'));
setTerminalOutput([]);
}
} else if (problems[activeQuestion]) {
// Fallback to example problems if no real test data
setCode(getStarterCode(problems[activeQuestion], language));
setTerminalOutput([]);
}
}, [activeQuestion, language]);
}, [activeQuestion]);
// Cleanup WebSocket connection on unmount
useEffect(() => {
@@ -402,8 +508,9 @@ int main() {
}
setIsRunning(true);
const currentQuestion = getCurrentQuestion();
setTerminalOutput([
{ type: 'system', content: `Running ${problems[activeQuestion].id}...` }
{ type: 'system', content: `Running ${currentQuestion?.title || problems[activeQuestion]?.id || 'code'}...` }
]);
try {
@@ -445,34 +552,61 @@ int main() {
// Handle code submission
const submitCode = async () => {
setIsRunning(true);
const currentQuestion = getCurrentQuestion();
setTerminalOutput([
{ type: 'system', content: `Submitting solution for ${problems[activeQuestion].id}...` }
{ type: 'system', content: `Submitting solution for ${currentQuestion?.title || problems[activeQuestion]?.id || 'problem'}...` }
]);
try {
// Submit code to the backend
const response = await fetch('http://localhost:8080/api/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
code: code,
language: getLanguageIdentifier(language),
input: '',
problemId: problems[activeQuestion].id
}),
});
if (!response.ok) {
throw new Error(`Error: ${response.statusText}`);
// If we have real test data, submit to faculty backend
if (currentQuestion && test) {
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:5000';
const response = await fetch(`${apiUrl}/api/students/submissions`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
testId: test.id,
answers: [{
questionId: currentQuestion.id,
submittedAnswer: code
}]
}),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.success) {
setTerminalOutput(prev => [
...prev,
{ type: 'system', content: '✓ Submission successful!' },
{ type: 'output', content: `Your answer has been submitted for Question ${getQuestionIndex(activeQuestion) + 1}` },
{ type: 'output', content: `Test: ${test.title}` },
{ type: 'system', content: 'You can modify and resubmit your answer anytime before the test ends.' }
]);
// Store submission locally
localStorage.setItem(`submission_${test.id}_${currentQuestion.id}`, JSON.stringify({
code,
timestamp: new Date().toISOString()
}));
} else {
throw new Error(data.message || 'Submission failed');
}
setIsRunning(false);
return;
}
const data = await response.json();
setSubmissionId(data.id);
// Connect to WebSocket for real-time updates
connectToWebSocket(data.id);
// If no test data, show error
throw new Error('No test data available. Please start a test from the test list.');
} catch (error) {
console.error('Error submitting solution:', error);
@@ -486,6 +620,36 @@ int main() {
// Render the current problem
const renderProblem = () => {
const currentQuestion = getCurrentQuestion();
// If we have real test question, use it
if (currentQuestion) {
return (
<div className="problem-container">
<h1>{currentQuestion.title || `Question ${getQuestionIndex(activeQuestion) + 1}`}</h1>
<div className="problem-description">
<p>{currentQuestion.question_text || currentQuestion.description}</p>
{currentQuestion.constraints && <p><strong>Constraints:</strong> {currentQuestion.constraints}</p>}
{currentQuestion.marks && <p><strong>Points:</strong> {currentQuestion.marks}</p>}
</div>
{currentQuestion.test_cases && currentQuestion.test_cases.length > 0 && (
<div className="test-cases">
<h3>Example Test Cases:</h3>
{currentQuestion.test_cases.slice(0, 2).map((testCase, idx) => (
<div key={idx} className="test-case">
<p><strong>Input:</strong> {testCase.input}</p>
<p><strong>Expected Output:</strong> {testCase.expected_output}</p>
</div>
))}
</div>
)}
</div>
);
}
// Fallback to example problems
const problem = problems[activeQuestion];
if (!problem) return null;
@@ -518,8 +682,23 @@ int main() {
return (
<div className="code-challenge-container">
<header className="code-challenge-header">
<h1>OnScreen Test</h1>
<button className="sign-in-btn">Sign In</button>
<h1>{test?.title || 'OnScreen Test'}</h1>
{timeRemaining && (
<div className="timer-display" style={{
fontSize: '1.2rem',
fontWeight: 'bold',
color: timeRemaining === 'Time Up!' ? '#ef4444' : '#10b981',
display: 'flex',
alignItems: 'center',
gap: '0.5rem'
}}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="12" cy="12" r="10"/>
<polyline points="12 6 12 12 16 14"/>
</svg>
{timeRemaining}
</div>
)}
</header>
{/* <div className="code-challenge-problem-nav">
@@ -528,24 +707,19 @@ int main() {
<div className="code-challenge-main">
<div className="problem-tabs">
<button
className={activeQuestion === "Q.1" ? "tab-active" : ""}
onClick={() => setActiveQuestion("Q.1")}
>
Q.1
</button>
<button
className={activeQuestion === "Q.2" ? "tab-active" : ""}
onClick={() => setActiveQuestion("Q.2")}
>
Q.2
</button>
<button
className={activeQuestion === "Q.3" ? "tab-active" : ""}
onClick={() => setActiveQuestion("Q.3")}
>
Q.3
</button>
{(questions.length > 0 ? questions : [1, 2, 3]).map((q, idx) => {
const questionKey = `Q.${idx + 1}`;
return (
<button
key={questionKey}
className={activeQuestion === questionKey ? "tab-active" : ""}
onClick={() => setActiveQuestion(questionKey)}
disabled={questions.length > 0 && idx >= questions.length}
>
{questionKey}
</button>
);
})}
</div>
<div className="problem-content">