chore: organise frontend

This commit is contained in:
Stijnvandenbroek
2025-08-12 14:39:53 +02:00
parent 9d48a1012f
commit 470bf07608
2 changed files with 239 additions and 222 deletions

View File

@@ -2,41 +2,25 @@ import React, { useState, useEffect } from 'react';
import axios from 'axios'; import axios from 'axios';
const Home = ({ setSessionId, setQuizStarted }) => { const Home = ({ setSessionId, setQuizStarted }) => {
const [files, setFiles] = useState([]); const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:8000';
const [fileNames, setFileNames] = useState('');
const [settingsOpen, setSettingsOpen] = useState(true); // File state
const [setting1, setSetting1] = useState(false); const [files, setFiles] = useState([]);
const [setting2, setSetting2] = useState(false); const [fileNames, setFileNames] = useState('');
const [isFileListVisible, setIsFileListVisible] = useState(false); const [isFileListVisible, setIsFileListVisible] = useState(false);
// Settings state
const [settingsOpen, setSettingsOpen] = useState(true);
const [setting1, setSetting1] = useState(false);
const [setting2, setSetting2] = useState(false);
const [quizSettings, setQuizSettings] = useState({ const [quizSettings, setQuizSettings] = useState({
repeat_on_mistake: false, repeat_on_mistake: false,
shuffle_answers: false, shuffle_answers: false,
randomise_order: false, randomise_order: false,
question_count_multiplier: 1, question_count_multiplier: 1,
}); });
// Backend API URL - use environment variable or default
const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:8000';
// Add useEffect to handle file list visibility
useEffect(() => {
if (fileNames) {
// Slight delay to ensure smooth transition
const timer = setTimeout(() => {
setIsFileListVisible(true);
}, 50);
return () => clearTimeout(timer);
} else {
setIsFileListVisible(false);
}
}, [fileNames]);
const handleFileChange = (e) => {
const selectedFiles = Array.from(e.target.files);
setFiles(selectedFiles);
setFileNames(selectedFiles.map(file => file.name).join(', '));
};
// API functions
const handleFileUpload = async () => { const handleFileUpload = async () => {
if (files.length === 0) return alert("Please select files"); if (files.length === 0) return alert("Please select files");
@@ -68,6 +52,13 @@ const Home = ({ setSessionId, setQuizStarted }) => {
} }
}; };
// Event handlers
const handleFileChange = (e) => {
const selectedFiles = Array.from(e.target.files);
setFiles(selectedFiles);
setFileNames(selectedFiles.map(file => file.name).join(', '));
};
const toggleSettings = () => { const toggleSettings = () => {
setSettingsOpen(!settingsOpen); setSettingsOpen(!settingsOpen);
}; };
@@ -84,7 +75,27 @@ const Home = ({ setSessionId, setQuizStarted }) => {
setQuizSettings({ ...quizSettings, shuffle_answers: newSetting }); setQuizSettings({ ...quizSettings, shuffle_answers: newSetting });
}; };
// Main container style with flexbox to position elements side by side const handleRandomizeOrderToggle = () => {
setQuizSettings({ ...quizSettings, randomise_order: !quizSettings.randomise_order });
};
const handleQuestionCountChange = (e) => {
setQuizSettings({ ...quizSettings, question_count_multiplier: parseInt(e.target.value) });
};
// Effects
useEffect(() => {
if (fileNames) {
const timer = setTimeout(() => {
setIsFileListVisible(true);
}, 50);
return () => clearTimeout(timer);
} else {
setIsFileListVisible(false);
}
}, [fileNames]);
// Styles
const containerStyle = { const containerStyle = {
display: 'flex', display: 'flex',
justifyContent: 'center', justifyContent: 'center',
@@ -97,16 +108,14 @@ const Home = ({ setSessionId, setQuizStarted }) => {
boxSizing: 'border-box', boxSizing: 'border-box',
}; };
// Left side container for quiz setup
const leftContainerStyle = { const leftContainerStyle = {
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
marginRight: '30px', // Space between left and right containers marginRight: '30px',
}; };
// Right side container for file names with improved fade transition
const rightContainerStyle = { const rightContainerStyle = {
backgroundColor: '#333', backgroundColor: '#333',
padding: '20px', padding: '20px',
@@ -123,31 +132,6 @@ const Home = ({ setSessionId, setQuizStarted }) => {
position: 'relative', position: 'relative',
}; };
const fileNameHeaderStyle = {
fontWeight: 'bold',
marginBottom: '10px',
position: 'sticky',
top: '0',
backgroundColor: '#333',
zIndex: 1,
paddingBottom: '10px',
};
const fileNamesScrollContainerStyle = {
overflowY: 'auto',
flexGrow: 1,
};
const fileNamesContainerStyle = {
backgroundColor: '#444',
borderRadius: '5px',
padding: '10px',
marginTop: '10px',
fontSize: '0.9em',
color: 'white',
wordBreak: 'break-all',
};
const uploadContainerStyle = { const uploadContainerStyle = {
backgroundColor: '#333', backgroundColor: '#333',
padding: '20px', padding: '20px',
@@ -265,6 +249,31 @@ const Home = ({ setSessionId, setQuizStarted }) => {
marginLeft: '5px', marginLeft: '5px',
}; };
const fileNameHeaderStyle = {
fontWeight: 'bold',
marginBottom: '10px',
position: 'sticky',
top: '0',
backgroundColor: '#333',
zIndex: 1,
paddingBottom: '10px',
};
const fileNamesScrollContainerStyle = {
overflowY: 'auto',
flexGrow: 1,
};
const fileNamesContainerStyle = {
backgroundColor: '#444',
borderRadius: '5px',
padding: '10px',
marginTop: '10px',
fontSize: '0.9em',
color: 'white',
wordBreak: 'break-all',
};
return ( return (
<div style={containerStyle}> <div style={containerStyle}>
<div style={leftContainerStyle}> <div style={leftContainerStyle}>
@@ -284,7 +293,6 @@ const Home = ({ setSessionId, setQuizStarted }) => {
</button> </button>
</div> </div>
{/* Show/Hide Settings Button */}
<div <div
onClick={toggleSettings} onClick={toggleSettings}
style={{ style={{
@@ -297,7 +305,6 @@ const Home = ({ setSessionId, setQuizStarted }) => {
<span>{settingsOpen ? 'Hide Settings' : 'Show Settings'}</span> <span>{settingsOpen ? 'Hide Settings' : 'Show Settings'}</span>
</div> </div>
{/* Settings Section */}
<div style={settingsContainerStyle}> <div style={settingsContainerStyle}>
<div style={toggleStyle}> <div style={toggleStyle}>
<span style={settingTextStyle}>Repeat on Mistake</span> <span style={settingTextStyle}>Repeat on Mistake</span>
@@ -314,7 +321,7 @@ const Home = ({ setSessionId, setQuizStarted }) => {
<div style={toggleStyle}> <div style={toggleStyle}>
<span style={settingTextStyle}>Randomize Order</span> <span style={settingTextStyle}>Randomize Order</span>
<div <div
onClick={() => setQuizSettings({ ...quizSettings, randomise_order: !quizSettings.randomise_order })} onClick={handleRandomizeOrderToggle}
style={toggleSwitchStyle(quizSettings.randomise_order)} style={toggleSwitchStyle(quizSettings.randomise_order)}
> >
<div <div
@@ -327,16 +334,13 @@ const Home = ({ setSessionId, setQuizStarted }) => {
<input <input
type="number" type="number"
value={quizSettings.question_count_multiplier} value={quizSettings.question_count_multiplier}
onChange={(e) => onChange={handleQuestionCountChange}
setQuizSettings({ ...quizSettings, question_count_multiplier: parseInt(e.target.value) })
}
style={{ ...inputStyle, ...hideSpinnerStyle }} style={{ ...inputStyle, ...hideSpinnerStyle }}
/> />
</div> </div>
</div> </div>
</div> </div>
{/* Right side container for selected files with improved fade transition */}
{fileNames && ( {fileNames && (
<div style={rightContainerStyle}> <div style={rightContainerStyle}>
<div style={fileNameHeaderStyle}>Selected Files:</div> <div style={fileNameHeaderStyle}>Selected Files:</div>

View File

@@ -3,20 +3,184 @@ import axios from 'axios';
const Quiz = ({ sessionId, onGoHome, onRetry }) => { const Quiz = ({ sessionId, onGoHome, onRetry }) => {
const API_URL = process.env.REACT_APP_API_URL; const API_URL = process.env.REACT_APP_API_URL;
// Quiz state
const [questionData, setQuestionData] = useState(null); const [questionData, setQuestionData] = useState(null);
const [selectedAnswers, setSelectedAnswers] = useState([]); const [quizCompleted, setQuizCompleted] = useState(false);
const [userAnswer, setUserAnswer] = useState('');
const [totalQuestions, setTotalQuestions] = useState(0); const [totalQuestions, setTotalQuestions] = useState(0);
const [correctAnswerCount, setCorrectAnswersCount] = useState(0); const [correctAnswerCount, setCorrectAnswersCount] = useState(0);
const [incorrectAnswersCount, setIncorrectAnswersCount] = useState(0); const [incorrectAnswersCount, setIncorrectAnswersCount] = useState(0);
// Answer state
const [selectedAnswers, setSelectedAnswers] = useState([]);
const [userAnswer, setUserAnswer] = useState('');
const [correctAnswers, setCorrectAnswers] = useState(''); const [correctAnswers, setCorrectAnswers] = useState('');
const [quizCompleted, setQuizCompleted] = useState(false);
const [timer, setTimer] = useState(0); // UI state
const [timerId, setTimerId] = useState(null);
const [feedback, setFeedback] = useState(''); const [feedback, setFeedback] = useState('');
const [isFeedbackVisible, setIsFeedbackVisible] = useState(false); const [isFeedbackVisible, setIsFeedbackVisible] = useState(false);
const [fadeOut, setFadeOut] = useState(false); const [fadeOut, setFadeOut] = useState(false);
const [showResults, setShowResults] = useState(false); const [showResults, setShowResults] = useState(false);
// Timer state
const [timer, setTimer] = useState(0);
const [timerId, setTimerId] = useState(null);
// API functions
const fetchQuestion = async () => {
try {
const response = await axios.get(`${API_URL}/next-question/?session_id=${sessionId}`);
console.log("Fetched Question Data:", response.data);
if (response.data.message === 'Quiz complete!') {
setFadeOut(true);
setTimeout(() => {
setQuizCompleted(true);
setQuestionData(null);
setTimeout(() => {
setShowResults(true);
}, 100);
}, 500);
} else {
setQuestionData(response.data);
setSelectedAnswers([]);
setUserAnswer('');
setFeedback('');
setIsFeedbackVisible(false);
}
} catch (error) {
alert('Error fetching question: ' + (error.response?.data?.error || error.message));
}
};
const fetchQuizStats = async () => {
try {
const response = await axios.get(`${API_URL}/quiz-stats/?session_id=${sessionId}`);
setTotalQuestions(response.data.total_questions);
setCorrectAnswersCount(response.data.correct_answers);
setIncorrectAnswersCount(response.data.incorrect_answers);
} catch (error) {
console.error('Error fetching quiz stats:', error);
}
};
const handleSubmit = async () => {
let submissionData;
if (questionData.options.length > 1) {
submissionData = {
session_id: sessionId,
selected_answers: selectedAnswers,
};
} else {
submissionData = {
session_id: sessionId,
selected_answers: [userAnswer.trim()],
};
}
try {
const response = await axios.post(`${API_URL}/submit-answer/`, submissionData);
const result = response.data.result;
setFeedback(result);
setIsFeedbackVisible(true);
if (result === 'Correct') {
fetchQuestion();
} else {
setCorrectAnswers(response.data.correct_answers.join(', '));
await moveQuestionToBottom();
}
fetchQuizStats();
} catch (error) {
alert('Error submitting answer: ' + (error.response?.data?.error || error.message));
}
};
const moveQuestionToBottom = async () => {
try {
const questionIndex = questionData.question_index;
await axios.post(`${API_URL}/move-question-to-bottom/`, {
session_id: sessionId,
question_index: questionIndex,
});
} catch (error) {
alert('Error moving question to bottom: ' + (error.response?.data?.error || error.message));
}
};
// Event handlers
const handleMultipleAnswerChange = (answerText) => {
setSelectedAnswers((prev) => {
if (prev.includes(answerText)) {
return prev.filter((answer) => answer !== answerText);
}
return [...prev, answerText];
});
};
const handleAnswerChange = (answerText) => {
setSelectedAnswers([answerText]);
};
const handleUserAnswerChange = (event) => {
setUserAnswer(event.target.value);
};
const handleContinueClick = () => {
setIsFeedbackVisible(false);
setFeedback('');
fetchQuestion();
};
const handleKeyPress = (event) => {
if (event.key === 'Enter') {
if (isFeedbackVisible) {
handleContinueClick();
} else {
const canSubmit = questionData.options.length > 1
? selectedAnswers.length > 0
: userAnswer.trim() !== '';
if (canSubmit) {
handleSubmit();
}
}
}
};
// Utility functions
const formatTime = (totalSeconds) => {
const hours = String(Math.floor(totalSeconds / 3600)).padStart(2, '0');
const minutes = String(Math.floor((totalSeconds % 3600) / 60)).padStart(2, '0');
const seconds = String(totalSeconds % 60).padStart(2, '0');
return `${hours}:${minutes}:${seconds}`;
};
// Effects
useEffect(() => {
document.addEventListener('keypress', handleKeyPress);
return () => {
document.removeEventListener('keypress', handleKeyPress);
};
}, [isFeedbackVisible, selectedAnswers, userAnswer, questionData]);
useEffect(() => {
fetchQuestion();
fetchQuizStats();
const id = setInterval(() => setTimer((prev) => prev + 1), 1000);
setTimerId(id);
return () => {
clearInterval(id);
};
}, [sessionId]);
useEffect(() => {
if (quizCompleted) {
clearInterval(timerId);
}
}, [quizCompleted, timerId]);
// Styles // Styles
const pageStyle = { const pageStyle = {
@@ -118,160 +282,7 @@ const Quiz = ({ sessionId, onGoHome, onRetry }) => {
transition: 'opacity 0.5s ease-out, transform 0.5s ease-out', transition: 'opacity 0.5s ease-out, transform 0.5s ease-out',
}; };
// Keyboard event handler // Render quiz completion screen
const handleKeyPress = (event) => {
if (event.key === 'Enter') {
if (isFeedbackVisible) {
handleContinueClick();
} else {
const canSubmit = questionData.options.length > 1
? selectedAnswers.length > 0
: userAnswer.trim() !== '';
if (canSubmit) {
handleSubmit();
}
}
}
};
// Event listeners
useEffect(() => {
document.addEventListener('keypress', handleKeyPress);
return () => {
document.removeEventListener('keypress', handleKeyPress);
};
}, [isFeedbackVisible, selectedAnswers, userAnswer, questionData]);
const fetchQuestion = async () => {
try {
const response = await axios.get(`${API_URL}/next-question/?session_id=${sessionId}`);
console.log("Fetched Question Data:", response.data);
if (response.data.message === 'Quiz complete!') {
setFadeOut(true);
setTimeout(() => {
setQuizCompleted(true);
setQuestionData(null);
setTimeout(() => {
setShowResults(true);
}, 100);
}, 500);
} else {
setQuestionData(response.data);
setSelectedAnswers([]);
setUserAnswer('');
setFeedback('');
setIsFeedbackVisible(false);
}
} catch (error) {
alert('Error fetching question: ' + (error.response?.data?.error || error.message));
}
};
const fetchQuizStats = async () => {
try {
const response = await axios.get(`${API_URL}/quiz-stats/?session_id=${sessionId}`);
setTotalQuestions(response.data.total_questions);
setCorrectAnswersCount(response.data.correct_answers);
setIncorrectAnswersCount(response.data.incorrect_answers);
} catch (error) {
console.error('Error fetching quiz stats:', error);
}
};
const handleMultipleAnswerChange = (answerText) => {
setSelectedAnswers((prev) => {
if (prev.includes(answerText)) {
return prev.filter((answer) => answer !== answerText);
}
return [...prev, answerText];
});
};
const handleAnswerChange = (answerText) => {
setSelectedAnswers([answerText]);
};
const handleUserAnswerChange = (event) => {
setUserAnswer(event.target.value);
};
const handleContinueClick = () => {
setIsFeedbackVisible(false);
setFeedback('');
fetchQuestion();
};
const handleSubmit = async () => {
let submissionData;
if (questionData.options.length > 1) {
submissionData = {
session_id: sessionId,
selected_answers: selectedAnswers,
};
} else {
submissionData = {
session_id: sessionId,
selected_answers: [userAnswer.trim()],
};
}
try {
const response = await axios.post(`${API_URL}/submit-answer/`, submissionData);
const result = response.data.result;
setFeedback(result);
setIsFeedbackVisible(true);
if (result === 'Correct') {
fetchQuestion();
} else {
setCorrectAnswers(response.data.correct_answers.join(', '));
await moveQuestionToBottom();
}
fetchQuizStats();
} catch (error) {
alert('Error submitting answer: ' + (error.response?.data?.error || error.message));
}
};
const moveQuestionToBottom = async () => {
try {
const questionIndex = questionData.question_index;
await axios.post(`${API_URL}/move-question-to-bottom/`, {
session_id: sessionId,
question_index: questionIndex,
});
} catch (error) {
alert('Error moving question to bottom: ' + (error.response?.data?.error || error.message));
}
};
useEffect(() => {
fetchQuestion();
fetchQuizStats();
const id = setInterval(() => setTimer((prev) => prev + 1), 1000);
setTimerId(id);
return () => {
clearInterval(id);
};
}, [sessionId]);
useEffect(() => {
if (quizCompleted) {
clearInterval(timerId);
}
}, [quizCompleted, timerId]);
const formatTime = (totalSeconds) => {
const hours = String(Math.floor(totalSeconds / 3600)).padStart(2, '0');
const minutes = String(Math.floor((totalSeconds % 3600) / 60)).padStart(2, '0');
const seconds = String(totalSeconds % 60).padStart(2, '0');
return `${hours}:${minutes}:${seconds}`;
};
if (quizCompleted) { if (quizCompleted) {
const totalAnswered = correctAnswerCount + (totalQuestions - correctAnswerCount); const totalAnswered = correctAnswerCount + (totalQuestions - correctAnswerCount);
const percentageCorrect = totalAnswered > 0 ? (correctAnswerCount / (correctAnswerCount + incorrectAnswersCount)) * 100 : 0; const percentageCorrect = totalAnswered > 0 ? (correctAnswerCount / (correctAnswerCount + incorrectAnswersCount)) * 100 : 0;
@@ -306,8 +317,10 @@ const Quiz = ({ sessionId, onGoHome, onRetry }) => {
); );
} }
// Loading state
if (!questionData) return <div style={{ color: 'white', textAlign: 'center', paddingTop: '50px' }}>Loading...</div>; if (!questionData) return <div style={{ color: 'white', textAlign: 'center', paddingTop: '50px' }}>Loading...</div>;
// Quiz question rendering
const hasMultipleOptions = questionData.options.length > 1; const hasMultipleOptions = questionData.options.length > 1;
const hasMultipleCorrectAnswers = questionData.multiple_choice; const hasMultipleCorrectAnswers = questionData.multiple_choice;