mirror of
https://github.com/Stijnvandenbroek/stamp.git
synced 2026-01-15 23:36:55 +01:00
feat: initial commit
This commit is contained in:
358
stamp_frontend/src/components/Home.js
Normal file
358
stamp_frontend/src/components/Home.js
Normal file
@@ -0,0 +1,358 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
|
||||
const Home = ({ setSessionId, setQuizStarted }) => {
|
||||
const [files, setFiles] = useState([]);
|
||||
const [fileNames, setFileNames] = useState('');
|
||||
const [settingsOpen, setSettingsOpen] = useState(true);
|
||||
const [setting1, setSetting1] = useState(false);
|
||||
const [setting2, setSetting2] = useState(false);
|
||||
const [isFileListVisible, setIsFileListVisible] = useState(false);
|
||||
const [quizSettings, setQuizSettings] = useState({
|
||||
repeat_on_mistake: false,
|
||||
shuffle_answers: false,
|
||||
randomise_order: false,
|
||||
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(', '));
|
||||
};
|
||||
|
||||
const handleFileUpload = async () => {
|
||||
if (files.length === 0) return alert("Please select files");
|
||||
|
||||
const formData = new FormData();
|
||||
files.forEach(file => {
|
||||
formData.append('files', file);
|
||||
});
|
||||
|
||||
formData.append('settings', JSON.stringify(quizSettings));
|
||||
|
||||
console.log('API URL:', API_URL);
|
||||
console.log('Uploading files:', files.map(f => f.name));
|
||||
console.log('Quiz settings:', quizSettings);
|
||||
|
||||
try {
|
||||
console.log('Sending request to:', `${API_URL}/upload-csv-with-settings/`);
|
||||
const response = await axios.post(`${API_URL}/upload-csv-with-settings/`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
console.log('Response received:', response.data);
|
||||
setSessionId(response.data.session_id);
|
||||
setQuizStarted(true);
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
console.error('Response data:', error.response?.data);
|
||||
alert('Error uploading files: ' + (error.response?.data?.error || error.message));
|
||||
}
|
||||
};
|
||||
|
||||
const toggleSettings = () => {
|
||||
setSettingsOpen(!settingsOpen);
|
||||
};
|
||||
|
||||
const handleToggle1 = () => {
|
||||
const newSetting = !setting1;
|
||||
setSetting1(newSetting);
|
||||
setQuizSettings({ ...quizSettings, repeat_on_mistake: newSetting });
|
||||
};
|
||||
|
||||
const handleToggle2 = () => {
|
||||
const newSetting = !setting2;
|
||||
setSetting2(newSetting);
|
||||
setQuizSettings({ ...quizSettings, shuffle_answers: newSetting });
|
||||
};
|
||||
|
||||
// Main container style with flexbox to position elements side by side
|
||||
const containerStyle = {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '100vh',
|
||||
backgroundColor: '#000000',
|
||||
color: 'white',
|
||||
fontFamily: 'Arial, sans-serif',
|
||||
padding: '20px',
|
||||
boxSizing: 'border-box',
|
||||
};
|
||||
|
||||
// Left side container for quiz setup
|
||||
const leftContainerStyle = {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: '30px', // Space between left and right containers
|
||||
};
|
||||
|
||||
// Right side container for file names with improved fade transition
|
||||
const rightContainerStyle = {
|
||||
backgroundColor: '#333',
|
||||
padding: '20px',
|
||||
borderRadius: '10px',
|
||||
boxShadow: '0 4px 15px rgba(0, 0, 0, 0.3)',
|
||||
width: '300px',
|
||||
height: '360px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
opacity: isFileListVisible ? 1 : 0,
|
||||
transform: isFileListVisible ? 'translateX(0)' : 'translateX(20px)',
|
||||
transition: 'opacity 0.5s ease, transform 0.5s ease',
|
||||
pointerEvents: isFileListVisible ? 'auto' : 'none',
|
||||
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 = {
|
||||
backgroundColor: '#333',
|
||||
padding: '20px',
|
||||
borderRadius: '10px',
|
||||
boxShadow: '0 4px 15px rgba(0, 0, 0, 0.3)',
|
||||
textAlign: 'center',
|
||||
width: '300px',
|
||||
marginBottom: '30px',
|
||||
};
|
||||
|
||||
const settingsContainerStyle = {
|
||||
backgroundColor: '#333',
|
||||
padding: '20px',
|
||||
borderRadius: '10px',
|
||||
boxShadow: '0 4px 15px rgba(0, 0, 0, 0.3)',
|
||||
width: '300px',
|
||||
maxHeight: settingsOpen ? '200px' : '0',
|
||||
opacity: settingsOpen ? '1' : '0',
|
||||
transition: 'max-height 0.3s ease, opacity 0.3s ease',
|
||||
overflow: 'hidden',
|
||||
};
|
||||
|
||||
const buttonStyle = {
|
||||
padding: '10px 20px',
|
||||
backgroundColor: '#0072ff',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '5px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '1em',
|
||||
transition: 'background-color 0.3s',
|
||||
width: '100%',
|
||||
height: '40px',
|
||||
};
|
||||
|
||||
const fileInputContainerStyle = {
|
||||
marginBottom: '20px',
|
||||
padding: '5px',
|
||||
backgroundColor: '#444',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '5px',
|
||||
width: '97%',
|
||||
cursor: 'pointer',
|
||||
height: '30px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '1em',
|
||||
};
|
||||
|
||||
const hiddenFileInput = {
|
||||
display: 'none',
|
||||
};
|
||||
|
||||
const inputStyle = {
|
||||
width: '30px',
|
||||
height: '8px',
|
||||
padding: '5px',
|
||||
marginRight: '5px',
|
||||
textAlign: 'center',
|
||||
border: '1px solid #555',
|
||||
borderRadius: '5px',
|
||||
backgroundColor: '#444',
|
||||
color: 'white',
|
||||
appearance: 'none',
|
||||
outline: 'none',
|
||||
};
|
||||
|
||||
const hideSpinnerStyle = {
|
||||
'&::-webkit-inner-spin-button, &::-webkit-outer-spin-button': {
|
||||
display: 'none',
|
||||
'-webkit-appearance': 'none',
|
||||
},
|
||||
'&[type=number]': {
|
||||
'-moz-appearance': 'textfield',
|
||||
},
|
||||
};
|
||||
|
||||
const toggleStyle = {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: '10px',
|
||||
padding: '5px',
|
||||
borderRadius: '5px',
|
||||
backgroundColor: '#555',
|
||||
cursor: 'pointer',
|
||||
};
|
||||
|
||||
const toggleSwitchStyle = (isActive) => ({
|
||||
width: '40px',
|
||||
height: '20px',
|
||||
backgroundColor: isActive ? '#0072ff' : '#444',
|
||||
borderRadius: '20px',
|
||||
marginRight: '5px',
|
||||
position: 'relative',
|
||||
transition: 'background-color 0.3s',
|
||||
});
|
||||
|
||||
const toggleCircleStyle = (isActive) => ({
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '50%',
|
||||
position: 'absolute',
|
||||
top: '2px',
|
||||
left: isActive ? '20px' : '2px',
|
||||
transition: 'left 0.3s',
|
||||
});
|
||||
|
||||
const settingTextStyle = {
|
||||
fontSize: '0.9em',
|
||||
color: 'white',
|
||||
marginLeft: '5px',
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={containerStyle}>
|
||||
<div style={leftContainerStyle}>
|
||||
<div style={uploadContainerStyle}>
|
||||
<label style={{ cursor: 'pointer', marginBottom: '20px', width: '100%' }}>
|
||||
<input
|
||||
type="file"
|
||||
accept=".csv"
|
||||
onChange={handleFileChange}
|
||||
style={hiddenFileInput}
|
||||
multiple
|
||||
/>
|
||||
<div style={fileInputContainerStyle}>Choose Files</div>
|
||||
</label>
|
||||
<button onClick={handleFileUpload} style={buttonStyle}>
|
||||
Start Quiz
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Show/Hide Settings Button */}
|
||||
<div
|
||||
onClick={toggleSettings}
|
||||
style={{
|
||||
color: 'white',
|
||||
cursor: 'pointer',
|
||||
fontSize: '1em',
|
||||
marginBottom: '10px',
|
||||
}}
|
||||
>
|
||||
<span>{settingsOpen ? 'Hide Settings' : 'Show Settings'}</span>
|
||||
</div>
|
||||
|
||||
{/* Settings Section */}
|
||||
<div style={settingsContainerStyle}>
|
||||
<div style={toggleStyle}>
|
||||
<span style={settingTextStyle}>Repeat on Mistake</span>
|
||||
<div onClick={handleToggle1} style={toggleSwitchStyle(setting1)}>
|
||||
<div style={toggleCircleStyle(setting1)} />
|
||||
</div>
|
||||
</div>
|
||||
<div style={toggleStyle}>
|
||||
<span style={settingTextStyle}>Shuffle Answers</span>
|
||||
<div onClick={handleToggle2} style={toggleSwitchStyle(setting2)}>
|
||||
<div style={toggleCircleStyle(setting2)} />
|
||||
</div>
|
||||
</div>
|
||||
<div style={toggleStyle}>
|
||||
<span style={settingTextStyle}>Randomize Order</span>
|
||||
<div
|
||||
onClick={() => setQuizSettings({ ...quizSettings, randomise_order: !quizSettings.randomise_order })}
|
||||
style={toggleSwitchStyle(quizSettings.randomise_order)}
|
||||
>
|
||||
<div
|
||||
style={toggleCircleStyle(quizSettings.randomise_order)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div style={toggleStyle}>
|
||||
<span style={settingTextStyle}>Question Count Multiplier</span>
|
||||
<input
|
||||
type="number"
|
||||
value={quizSettings.question_count_multiplier}
|
||||
onChange={(e) =>
|
||||
setQuizSettings({ ...quizSettings, question_count_multiplier: parseInt(e.target.value) })
|
||||
}
|
||||
style={{ ...inputStyle, ...hideSpinnerStyle }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right side container for selected files with improved fade transition */}
|
||||
{fileNames && (
|
||||
<div style={rightContainerStyle}>
|
||||
<div style={fileNameHeaderStyle}>Selected Files:</div>
|
||||
<div style={fileNamesScrollContainerStyle}>
|
||||
<div style={fileNamesContainerStyle}>
|
||||
{fileNames.split(', ').map((fileName, index) => (
|
||||
<div key={index} style={{ marginBottom: '5px' }}>
|
||||
{fileName}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
408
stamp_frontend/src/components/Quiz.js
Normal file
408
stamp_frontend/src/components/Quiz.js
Normal file
@@ -0,0 +1,408 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
|
||||
const Quiz = ({ sessionId, onGoHome, onRetry }) => {
|
||||
const [questionData, setQuestionData] = useState(null);
|
||||
const [selectedAnswers, setSelectedAnswers] = useState([]);
|
||||
const [userAnswer, setUserAnswer] = useState('');
|
||||
const [totalQuestions, setTotalQuestions] = useState(0);
|
||||
const [correctAnswerCount, setCorrectAnswersCount] = useState(0);
|
||||
const [incorrectAnswersCount, setIncorrectAnswersCount] = useState(0);
|
||||
const [correctAnswers, setCorrectAnswers] = useState('');
|
||||
const [quizCompleted, setQuizCompleted] = useState(false);
|
||||
const [timer, setTimer] = useState(0);
|
||||
const [timerId, setTimerId] = useState(null);
|
||||
const [feedback, setFeedback] = useState('');
|
||||
const [isFeedbackVisible, setIsFeedbackVisible] = useState(false);
|
||||
const [fadeOut, setFadeOut] = useState(false);
|
||||
const [showResults, setShowResults] = useState(false);
|
||||
|
||||
// Styles
|
||||
const pageStyle = {
|
||||
backgroundColor: 'black',
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
color: 'white',
|
||||
fontFamily: 'Arial, sans-serif',
|
||||
flexDirection: 'column',
|
||||
};
|
||||
|
||||
const timerStyle = {
|
||||
color: 'white',
|
||||
fontSize: '24px',
|
||||
marginBottom: '20px',
|
||||
};
|
||||
|
||||
const containerStyle = {
|
||||
backgroundColor: '#333',
|
||||
padding: '30px',
|
||||
borderRadius: '15px',
|
||||
width: '400px',
|
||||
height: '550px',
|
||||
boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.5)',
|
||||
textAlign: 'center',
|
||||
overflowY: 'auto',
|
||||
opacity: fadeOut ? 0 : 1,
|
||||
transition: 'opacity 0.5s ease-out',
|
||||
};
|
||||
|
||||
const buttonStyle = {
|
||||
backgroundColor: '#555',
|
||||
color: 'white',
|
||||
padding: '10px 20px',
|
||||
border: 'none',
|
||||
borderRadius: '5px',
|
||||
cursor: 'pointer',
|
||||
marginTop: '15px',
|
||||
fontWeight: 'bold',
|
||||
width: '100%',
|
||||
};
|
||||
|
||||
const optionStyle = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '12px',
|
||||
backgroundColor: '#444',
|
||||
borderRadius: '5px',
|
||||
margin: '5px 0',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
outline: 'none',
|
||||
border: '2px solid transparent',
|
||||
};
|
||||
|
||||
const selectedOptionStyle = {
|
||||
...optionStyle,
|
||||
border: '2px solid #0072ff',
|
||||
};
|
||||
|
||||
const inputStyle = {
|
||||
width: '95%',
|
||||
padding: '10px',
|
||||
borderRadius: '5px',
|
||||
backgroundColor: '#444',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
textAlign: 'center',
|
||||
marginTop: '15px',
|
||||
};
|
||||
|
||||
const feedbackContainerStyle = {
|
||||
backgroundColor: '#ffcccc',
|
||||
padding: '15px',
|
||||
borderRadius: '5px',
|
||||
marginTop: '20px',
|
||||
color: 'white',
|
||||
border: '1px solid #d8000c',
|
||||
textAlign: 'center',
|
||||
};
|
||||
|
||||
const disabledOptionStyle = {
|
||||
...optionStyle,
|
||||
opacity: 0.5,
|
||||
pointerEvents: 'none',
|
||||
};
|
||||
|
||||
const resultContainerStyle = {
|
||||
backgroundColor: '#333',
|
||||
padding: '30px',
|
||||
borderRadius: '15px',
|
||||
width: '400px',
|
||||
textAlign: 'center',
|
||||
boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.5)',
|
||||
opacity: showResults ? 1 : 0,
|
||||
transform: showResults ? 'translateY(0)' : 'translateY(20px)',
|
||||
transition: 'opacity 0.5s ease-out, transform 0.5s ease-out',
|
||||
};
|
||||
|
||||
// Keyboard event handler
|
||||
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(`http://10.0.0.3:8000/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(`http://10.0.0.3:8000/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('http://10.0.0.3:8000/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(`http://10.0.0.3:8000/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) {
|
||||
const totalAnswered = correctAnswerCount + (totalQuestions - correctAnswerCount);
|
||||
const percentageCorrect = totalAnswered > 0 ? (correctAnswerCount / (correctAnswerCount + incorrectAnswersCount)) * 100 : 0;
|
||||
|
||||
return (
|
||||
<div style={pageStyle}>
|
||||
<div style={resultContainerStyle}>
|
||||
<h2>Quiz Completed!</h2>
|
||||
<p>{formatTime(timer)}</p>
|
||||
<div style={{ margin: '20px auto', height: '300px', width: '50px', background: '#444', borderRadius: '5px', position: 'relative', overflow: 'hidden' }}>
|
||||
<div
|
||||
style={{
|
||||
background: '#4caf50',
|
||||
height: showResults ? `${percentageCorrect}%` : '0%',
|
||||
width: '100%',
|
||||
borderRadius: '5px',
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
transition: 'height 1s ease-out',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<p>{percentageCorrect.toFixed(0)}% Correct</p>
|
||||
<button onClick={onGoHome} style={{...buttonStyle, opacity: showResults ? 1 : 0, transition: 'opacity 0.5s ease-out', transitionDelay: '0.5s'}}>
|
||||
Go Home
|
||||
</button>
|
||||
<button onClick={onRetry} style={{...buttonStyle, opacity: showResults ? 1 : 0, transition: 'opacity 0.5s ease-out', transitionDelay: '0.7s'}}>
|
||||
Retry Quiz
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!questionData) return <div style={{ color: 'white', textAlign: 'center', paddingTop: '50px' }}>Loading...</div>;
|
||||
|
||||
const hasMultipleOptions = questionData.options.length > 1;
|
||||
const hasMultipleCorrectAnswers = questionData.multiple_choice;
|
||||
|
||||
return (
|
||||
<div style={pageStyle}>
|
||||
<div style={timerStyle}>{formatTime(timer)}</div>
|
||||
<div style={containerStyle}>
|
||||
<h2>{questionData.question}</h2>
|
||||
<div>
|
||||
{hasMultipleOptions ? (
|
||||
hasMultipleCorrectAnswers ? (
|
||||
questionData.options.map((option, index) => (
|
||||
<label
|
||||
key={index}
|
||||
style={isFeedbackVisible ? disabledOptionStyle : selectedAnswers.includes(option) ? selectedOptionStyle : optionStyle}
|
||||
onClick={() => !isFeedbackVisible && handleMultipleAnswerChange(option)}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
value={option}
|
||||
checked={selectedAnswers.includes(option)}
|
||||
onChange={() => !isFeedbackVisible && handleMultipleAnswerChange(option)}
|
||||
style={{ display: 'none' }}
|
||||
disabled={isFeedbackVisible}
|
||||
/>
|
||||
{option}
|
||||
</label>
|
||||
))
|
||||
) : (
|
||||
questionData.options.map((option, index) => (
|
||||
<label
|
||||
key={index}
|
||||
style={isFeedbackVisible ? disabledOptionStyle : selectedAnswers.includes(option) ? selectedOptionStyle : optionStyle}
|
||||
onClick={() => !isFeedbackVisible && handleAnswerChange(option)}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="single-answer"
|
||||
value={option}
|
||||
checked={selectedAnswers.includes(option)}
|
||||
onChange={() => !isFeedbackVisible && handleAnswerChange(option)}
|
||||
style={{ display: 'none' }}
|
||||
disabled={isFeedbackVisible}
|
||||
/>
|
||||
{option}
|
||||
</label>
|
||||
))
|
||||
)
|
||||
) : (
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
value={userAnswer}
|
||||
onChange={handleUserAnswerChange}
|
||||
placeholder="Type your answer here"
|
||||
style={inputStyle}
|
||||
disabled={isFeedbackVisible}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={isFeedbackVisible || (hasMultipleOptions ? selectedAnswers.length === 0 : userAnswer.trim() === '')}
|
||||
style={buttonStyle}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
{feedback && feedback !== 'Correct' && (
|
||||
<div style={feedbackContainerStyle}>
|
||||
<p>Correct answer: {correctAnswers}</p>
|
||||
<button
|
||||
onClick={handleContinueClick}
|
||||
style={{ background: 'none', border: 'none', color: 'white', cursor: 'pointer', padding: 0, textDecoration: 'underline' }}
|
||||
>
|
||||
Continue
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ marginTop: '20px', color: 'white' }}>
|
||||
<div style={{ background: '#555', borderRadius: '5px', height: '20px', width: '100%' }}>
|
||||
<div
|
||||
style={{
|
||||
background: '#4caf50',
|
||||
height: '100%',
|
||||
width: `${(correctAnswerCount / totalQuestions) * 100}%`,
|
||||
borderRadius: '5px',
|
||||
transition: 'width 0.3s ease',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<p>{correctAnswerCount}/{totalQuestions}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Quiz;
|
||||
Reference in New Issue
Block a user