feat: remove stamp_ prefix from directories

This commit is contained in:
Stijnvandenbroek
2025-06-29 11:34:40 +02:00
parent fd1229c8f3
commit 16e2032f9e
32 changed files with 7 additions and 7 deletions

38
frontend/src/App.css Normal file
View File

@@ -0,0 +1,38 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

145
frontend/src/App.js Normal file
View File

@@ -0,0 +1,145 @@
import React, { useState } from 'react';
import Home from './components/Home';
import Quiz from './components/Quiz';
import axios from 'axios';
function App() {
const [sessionId, setSessionId] = useState('');
const [quizStarted, setQuizStarted] = useState(false);
const [isTransitioning, setIsTransitioning] = useState(false);
const pageStyle = {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'flex-start',
minHeight: '100vh',
backgroundColor: '#000000',
color: 'white',
fontFamily: 'Arial, sans-serif',
overflowX: 'hidden',
margin: '0',
padding: '0',
};
const headerBarStyle = {
position: 'fixed',
top: 0,
width: '100%',
padding: '20px 40px',
background: '#000000',
display: 'flex',
alignItems: 'center',
boxShadow: '0px 4px 8px rgba(0, 0, 0, 0.2)',
zIndex: 1000,
};
const titleStyle = {
fontSize: '2.5em',
fontWeight: 'bold',
marginTop: 0,
marginBottom: 0,
marginLeft: 25,
textAlign: 'left',
cursor: 'pointer',
};
const mainContentStyle = {
width: '90%',
maxWidth: '600px',
textAlign: 'center',
flex: '1',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
position: 'relative',
};
const transitionStyles = {
container: {
position: 'absolute',
width: '100%',
opacity: isTransitioning ? 0 : 1,
transform: isTransitioning ? 'scale(0.95)' : 'scale(1)',
transition: 'opacity 300ms ease-in-out, transform 300ms ease-in-out',
}
};
const handleUploadClick = () => {
setIsTransitioning(true);
// Slight delay to allow transition to occur before changing state
setTimeout(() => {
setSessionId('');
setQuizStarted(false);
setIsTransitioning(false);
}, 300);
};
const handleTitleClick = () => {
handleUploadClick();
};
const handleStartQuiz = (sessionId) => {
setIsTransitioning(true);
setSessionId(sessionId);
// Slight delay to allow transition to occur before changing state
setTimeout(() => {
setQuizStarted(true);
setIsTransitioning(false);
}, 300);
};
const handleRetryQuiz = async () => {
try {
await axios.post('http://10.0.0.3:8000/reset-session/', {
session_id: sessionId,
});
setIsTransitioning(true);
setTimeout(() => {
setQuizStarted(false);
setIsTransitioning(false);
}, 300);
} catch (error) {
alert('Error resetting session: ' + (error.response?.data?.error || error.message));
}
};
return (
<div style={pageStyle}>
<div style={headerBarStyle}>
<h1 style={titleStyle} onClick={handleTitleClick}>Stamp</h1>
</div>
<div style={mainContentStyle}>
{!quizStarted && (
<div style={{
...transitionStyles.container,
visibility: isTransitioning ? 'hidden' : 'visible'
}}>
<Home
setSessionId={handleStartQuiz}
setQuizStarted={setQuizStarted}
/>
</div>
)}
{quizStarted && (
<div style={{
...transitionStyles.container,
visibility: isTransitioning ? 'hidden' : 'visible'
}}>
<Quiz
sessionId={sessionId}
onGoHome={handleUploadClick}
onRetry={handleRetryQuiz}
/>
</div>
)}
</div>
</div>
);
}
export default App;

8
frontend/src/App.test.js Normal file
View File

@@ -0,0 +1,8 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View 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;

View 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;

13
frontend/src/index.css Normal file
View File

@@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

17
frontend/src/index.js Normal file
View File

@@ -0,0 +1,17 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

1
frontend/src/logo.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,13 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

View File

@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';