First version with all questions.
This commit is contained in:
+55
@@ -0,0 +1,55 @@
|
|||||||
|
# Node.js dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Build output directories (Vite default)
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Vite cache and temporary files
|
||||||
|
.vite/
|
||||||
|
.vitepress/cache
|
||||||
|
.vitepress/dist
|
||||||
|
|
||||||
|
# Environment variable files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
.env.*.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# OS generated files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# IDE/editor specific files
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm/
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Coverage reports
|
||||||
|
coverage/
|
||||||
|
.nyc_output/
|
||||||
|
|
||||||
|
# Yarn files
|
||||||
|
.yarn/
|
||||||
|
.yarnrc.yml
|
||||||
|
|
||||||
|
# pnpm files
|
||||||
|
.pnpm-store/
|
||||||
|
|
||||||
|
# Miscellaneous
|
||||||
|
*.log
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Questions and answers are from the following repo : [https://github.com/Ditectrev/Amazon-Web-Services-AWS-Developer-Associate-DVA-C02-Practice-Tests-Exams-Questions-Answers](https://github.com/Ditectrev/Amazon-Web-Services-AWS-Developer-Associate-DVA-C02-Practice-Tests-Exams-Questions-Answers). Explainations linked to each answers have been generated using GPT 5.4 and have not been verified.
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>AWS Quiz</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Generated
+1635
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "aws-dva-c02-exam-sample-interface",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"start": "vite",
|
||||||
|
"build": "tsc && vite build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.3.3",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
|
"typescript": "^5.2.2",
|
||||||
|
"vite": "^5.3.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface MarkdownTextProps {
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MarkdownText({ text }: MarkdownTextProps) {
|
||||||
|
if (!text) return null;
|
||||||
|
|
||||||
|
// Split text by code blocks: ```code```
|
||||||
|
const blocks = text.split(/(```[\s\S]*?```)/g);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{blocks.map((block, index) => {
|
||||||
|
if (block.startsWith('```') && block.endsWith('```')) {
|
||||||
|
// Extract the actual code inside the backticks
|
||||||
|
const codeContent = block.slice(3, -3).replace(/^(typescript|json|javascript|yaml|bash)\n/, '');
|
||||||
|
return (
|
||||||
|
<pre key={index} style={styles.codeBlock}>
|
||||||
|
<code>{codeContent.trim()}</code>
|
||||||
|
</pre>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle inline code `like this` inside normal sentences
|
||||||
|
const inlineParts = block.split(/(`[^`\n]+`)/g);
|
||||||
|
return (
|
||||||
|
<span key={index}>
|
||||||
|
{inlineParts.map((part, pIdx) => {
|
||||||
|
if (part.startsWith('`') && part.endsWith('`')) {
|
||||||
|
return (
|
||||||
|
<code key={pIdx} style={styles.inlineCode}>
|
||||||
|
{part.slice(1, -1)}
|
||||||
|
</code>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return part;
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
codeBlock: {
|
||||||
|
backgroundColor: '#1e1e1e',
|
||||||
|
color: '#d4d4d4',
|
||||||
|
padding: '12px',
|
||||||
|
borderRadius: '6px',
|
||||||
|
overflowX: 'auto' as const,
|
||||||
|
fontFamily: 'Consolas, Monaco, "Andale Mono", monospace',
|
||||||
|
fontSize: '0.9rem',
|
||||||
|
lineHeight: '1.4',
|
||||||
|
margin: '10px 0',
|
||||||
|
border: '1px solid #333'
|
||||||
|
},
|
||||||
|
inlineCode: {
|
||||||
|
backgroundColor: '#f1f1f1',
|
||||||
|
color: '#a31515',
|
||||||
|
padding: '2px 6px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: '0.95rem'
|
||||||
|
}
|
||||||
|
};
|
||||||
+226
@@ -0,0 +1,226 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { QuizQuestion, UserSubmission } from './types';
|
||||||
|
import ReviewResults from './ReviewResults';
|
||||||
|
|
||||||
|
import quizData from './questions.json';
|
||||||
|
import MarkdownText from './MarkdownText';
|
||||||
|
const allQuestions: QuizQuestion[] = quizData as unknown as QuizQuestion[];
|
||||||
|
|
||||||
|
|
||||||
|
function getRandomizedQuestions(questions: QuizQuestion[]): QuizQuestion[] {
|
||||||
|
let shuffled = [...questions].sort(() => Math.random() - 0.5);
|
||||||
|
return shuffled
|
||||||
|
.map(q => ({
|
||||||
|
...q,
|
||||||
|
possible_answers: [...q.possible_answers].sort(() => Math.random() - 0.5)
|
||||||
|
}))
|
||||||
|
.slice(0, 40);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function QuizApp() {
|
||||||
|
const [questions, setQuestions] = useState<QuizQuestion[] | null>(null);
|
||||||
|
const [currentIndex, setCurrentIndex] = useState<number>(0);
|
||||||
|
const [selectedLabels, setSelectedLabels] = useState<string[]>([]);
|
||||||
|
const [confirmedAnswers, setConfirmedAnswers] = useState<{ [key: number]: string[] }>({});
|
||||||
|
const [submission, setSubmission] = useState<UserSubmission | null>(null);
|
||||||
|
const [forceReviewMode, setForceReviewMode] = useState<boolean>(false);
|
||||||
|
|
||||||
|
// If user clicked the button to jump straight to Review/Upload interface
|
||||||
|
if (forceReviewMode) {
|
||||||
|
// Build an empty dummy submission object so ReviewResults loads its uploader interface cleanly
|
||||||
|
const dummySubmission: UserSubmission = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
score: 0,
|
||||||
|
totalQuestions: 0,
|
||||||
|
results: []
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<ReviewResults
|
||||||
|
submission={submission || dummySubmission}
|
||||||
|
onImport={(imported) => setSubmission(imported)}
|
||||||
|
onReset={() => {
|
||||||
|
setForceReviewMode(false);
|
||||||
|
handleReset();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!questions) {
|
||||||
|
const randomizedQuestions = getRandomizedQuestions(allQuestions);
|
||||||
|
setQuestions(randomizedQuestions);
|
||||||
|
return (
|
||||||
|
<div style={{ textAlign: 'center', marginTop: '50px', fontFamily: 'system-ui, sans-serif' }}>
|
||||||
|
<div>Loading quiz...</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setForceReviewMode(true)}
|
||||||
|
style={{ ...styles.button, width: 'auto', marginTop: '20px', backgroundColor: '#6c757d' }}
|
||||||
|
>
|
||||||
|
Go directly to Review / Import Results
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const currentQuestion = questions[currentIndex];
|
||||||
|
|
||||||
|
// Detect if question has multiple correct options
|
||||||
|
const correctCount = currentQuestion?.possible_answers.filter(a => a.is_correct).length || 0;
|
||||||
|
const isMultipleChoice = correctCount > 1;
|
||||||
|
|
||||||
|
const handleOptionToggle = (label: string) => {
|
||||||
|
if (isMultipleChoice) {
|
||||||
|
setSelectedLabels(prev =>
|
||||||
|
prev.includes(label) ? prev.filter(l => l !== label) : [...prev, label]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setSelectedLabels([label]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
setConfirmedAnswers(prev => ({ ...prev, [currentIndex]: selectedLabels }));
|
||||||
|
|
||||||
|
if (currentIndex + 1 < questions.length) {
|
||||||
|
setCurrentIndex(prev => prev + 1);
|
||||||
|
setSelectedLabels([]);
|
||||||
|
} else {
|
||||||
|
processFinalResults({ ...confirmedAnswers, [currentIndex]: selectedLabels });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Stop the quiz immediately and process whatever answers are ready
|
||||||
|
const handleStopQuiz = () => {
|
||||||
|
if (window.confirm("Are you sure you want to stop the quiz? Your current answers will be saved.")) {
|
||||||
|
// Include currently selected choice on screen if it exists
|
||||||
|
const finalAnswersState = { ...confirmedAnswers };
|
||||||
|
if (selectedLabels.length > 0) {
|
||||||
|
finalAnswersState[currentIndex] = selectedLabels;
|
||||||
|
}
|
||||||
|
processFinalResults(finalAnswersState);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const processFinalResults = (finalAnswers: { [key: number]: string[] }) => {
|
||||||
|
let score = 0;
|
||||||
|
const totalQuestions = Object.keys(finalAnswers).length;
|
||||||
|
|
||||||
|
// FIX: Changed currentIndex to currentIndex + 1 to include the last/current item
|
||||||
|
const results = questions.slice(0, currentIndex + 1).map((q, idx) => {
|
||||||
|
const userLabels = finalAnswers[idx] || [];
|
||||||
|
const correctLabels = q.possible_answers.filter(a => a.is_correct).map(a => a.label);
|
||||||
|
|
||||||
|
const isCorrect =
|
||||||
|
userLabels.length === correctLabels.length &&
|
||||||
|
userLabels.every(l => correctLabels.includes(l));
|
||||||
|
|
||||||
|
if (isCorrect) score += 1;
|
||||||
|
|
||||||
|
return {
|
||||||
|
question: q.question,
|
||||||
|
selectedLabels: userLabels,
|
||||||
|
possible_answers: q.possible_answers
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const finalSubmission: UserSubmission = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
score,
|
||||||
|
totalQuestions,
|
||||||
|
results
|
||||||
|
};
|
||||||
|
|
||||||
|
setSubmission(finalSubmission);
|
||||||
|
downloadSubmissionFile(finalSubmission);
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadSubmissionFile = (data: UserSubmission) => {
|
||||||
|
const filename = `submission_${new Date().toISOString().replace(/[:.]/g, '-')}.json`;
|
||||||
|
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = filename;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
setQuestions(null);
|
||||||
|
setCurrentIndex(0);
|
||||||
|
setSelectedLabels([]);
|
||||||
|
setConfirmedAnswers({});
|
||||||
|
setSubmission(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (submission) {
|
||||||
|
return <ReviewResults submission={submission} onImport={setSubmission} onReset={handleReset} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={styles.container}>
|
||||||
|
<div style={styles.header}>
|
||||||
|
<span>Question {currentIndex + 1} of {questions.length}</span>
|
||||||
|
{isMultipleChoice && <span style={styles.badge}>Multiple choices available</span>}
|
||||||
|
<button
|
||||||
|
onClick={() => setForceReviewMode(true)}
|
||||||
|
style={{ padding: '4px 10px', fontSize: '0.8rem', backgroundColor: '#6c757d', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
Go to Review Page
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 style={styles.questionText}>
|
||||||
|
<MarkdownText text={currentQuestion.question} />
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div style={styles.optionsContainer}>
|
||||||
|
{currentQuestion.possible_answers.map((answer, index) => {
|
||||||
|
const isSelected = selectedLabels.includes(answer.label);
|
||||||
|
let btnStyle = { ...styles.optionButton };
|
||||||
|
if (isSelected) {
|
||||||
|
btnStyle.backgroundColor = '#e7f1ff';
|
||||||
|
btnStyle.borderColor = '#b3d7ff';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button key={index} onClick={() => handleOptionToggle(answer.label)} style={btnStyle}>
|
||||||
|
<span style={{ marginRight: '10px', flexShrink: 0 }}>
|
||||||
|
{isMultipleChoice ? (isSelected ? '☑' : '☐') : (isSelected ? '●' : '○')}
|
||||||
|
</span>
|
||||||
|
<div style={{ textAlign: 'left' }}>
|
||||||
|
<MarkdownText text={answer.label} />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleConfirm}
|
||||||
|
disabled={selectedLabels.length === 0}
|
||||||
|
style={{ ...styles.button, opacity: selectedLabels.length === 0 ? 0.6 : 1 }}
|
||||||
|
>
|
||||||
|
{currentIndex + 1 === questions.length ? "Confirm & Finish" : "Confirm & Next"}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Stop Quiz Action */}
|
||||||
|
<button
|
||||||
|
onClick={handleStopQuiz}
|
||||||
|
style={{ ...styles.button, backgroundColor: '#dc3545', marginTop: '10px' }}
|
||||||
|
>
|
||||||
|
Stop Quiz
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles: { [key: string]: React.CSSProperties } = {
|
||||||
|
container: { maxWidth: '600px', margin: '40px auto', fontFamily: 'system-ui, sans-serif', padding: '20px', border: '1px solid #ccc', borderRadius: '8px', backgroundColor: '#fff', boxShadow: '0 2px 10px rgba(0,0,0,0.1)' },
|
||||||
|
header: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: '0.9rem', color: '#666', borderBottom: '1px solid #eee', paddingBottom: '10px', marginBottom: '20px', gap: '10px' },
|
||||||
|
badge: { backgroundColor: '#fff3cd', color: '#856404', padding: '4px 8px', borderRadius: '4px', fontWeight: 'bold', fontSize: '0.8rem' },
|
||||||
|
questionText: { fontSize: '1.2rem', lineHeight: '1.5', marginBottom: '20px' },
|
||||||
|
optionsContainer: { display: 'flex', flexDirection: 'column', gap: '10px' },
|
||||||
|
optionButton: { padding: '12px 16px', fontSize: '1rem', textAlign: 'left', cursor: 'pointer', backgroundColor: '#f9f9f9', border: '1px solid #ddd', borderRadius: '4px', display: 'flex', alignItems: 'center' },
|
||||||
|
button: { marginTop: '20px', padding: '12px 24px', backgroundColor: '#28a745', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '1rem', width: '100%' }
|
||||||
|
};
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
import React, { ChangeEvent } from 'react';
|
||||||
|
import { UserSubmission } from './types';
|
||||||
|
import MarkdownText from './MarkdownText';
|
||||||
|
|
||||||
|
interface ReviewResultsProps {
|
||||||
|
submission: UserSubmission;
|
||||||
|
onImport: (submission: UserSubmission) => void;
|
||||||
|
onReset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ReviewResults({ submission, onImport, onReset }: ReviewResultsProps) {
|
||||||
|
|
||||||
|
const handleFileUpload = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (event) => {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(event.target?.result as string) as UserSubmission;
|
||||||
|
if (parsed.timestamp && Array.isArray(parsed.results)) {
|
||||||
|
onImport(parsed);
|
||||||
|
} else {
|
||||||
|
alert("Invalid submission file format.");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
alert("Error parsing JSON file.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={styles.container}>
|
||||||
|
<div style={styles.uploadSection}>
|
||||||
|
<label style={{ fontWeight: 'bold', display: 'block', marginBottom: '8px' }}>
|
||||||
|
Import historical submission file:
|
||||||
|
</label>
|
||||||
|
<input type="file" accept=".json" onChange={handleFileUpload} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr style={{ border: '0', borderTop: '1px solid #eee', margin: '20px 0' }} />
|
||||||
|
|
||||||
|
<h2>Quiz Review</h2>
|
||||||
|
<p style={{ color: '#555', fontSize: '0.9rem' }}>
|
||||||
|
Submitted at: {new Date(submission.timestamp).toLocaleString()}
|
||||||
|
</p>
|
||||||
|
<p style={{ fontSize: '1.2rem' }}>
|
||||||
|
Final Score: <strong>{submission.score}</strong> / <strong>{submission.totalQuestions}</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Color & Icon Legend */}
|
||||||
|
<div style={styles.legendContainer}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
|
<span style={{ fontSize: '1.1rem' }}>✅</span>
|
||||||
|
<span style={{ ...styles.legendBadge, backgroundColor: '#d1e7dd', border: '2px solid #0f5132' }} />
|
||||||
|
<strong>Answered and correct</strong>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
|
<span style={{ fontSize: '1.1rem' }}>❌</span>
|
||||||
|
<span style={{ ...styles.legendBadge, backgroundColor: '#f8d7da', border: '2px solid #842029' }} />
|
||||||
|
<strong>Answered but incorrect</strong>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
|
<span style={{ fontSize: '1.1rem' }}>💡</span>
|
||||||
|
<span style={{ ...styles.legendBadge, backgroundColor: '#fff3cd', border: '2px solid #664d03' }} />
|
||||||
|
<strong>Didn't answer but correct</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{submission.results.map((item, qIdx) => {
|
||||||
|
const correctAnswers = item.possible_answers.filter(a => a.is_correct);
|
||||||
|
const correctLabels = correctAnswers.map(a => a.label);
|
||||||
|
const isUserCorrect =
|
||||||
|
item.selectedLabels.length === correctLabels.length &&
|
||||||
|
item.selectedLabels.every(l => correctLabels.includes(l));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={qIdx} style={{ ...styles.reviewCard, borderLeft: `5px solid ${isUserCorrect ? '#28a745' : '#dc3545'}` }}>
|
||||||
|
<h4 style={{ margin: '0 0 10px 0' }}>
|
||||||
|
Q{qIdx + 1}: <MarkdownText text={item.question} />
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||||||
|
{item.possible_answers.map((ans, aIdx) => {
|
||||||
|
const wasSelected = item.selectedLabels.includes(ans.label);
|
||||||
|
|
||||||
|
let rowBg = '#ffffff';
|
||||||
|
let borderColor = '#cccccc';
|
||||||
|
let textColor = '#212529';
|
||||||
|
let icon = '🔲'; // Default unselected icon
|
||||||
|
|
||||||
|
if (wasSelected && ans.is_correct) {
|
||||||
|
// Green : answered and is correct -> Checkmark
|
||||||
|
rowBg = '#d1e7dd';
|
||||||
|
borderColor = '#0f5132';
|
||||||
|
textColor = '#0f5132';
|
||||||
|
icon = '✅';
|
||||||
|
} else if (wasSelected && !ans.is_correct) {
|
||||||
|
// Red : answered but is incorrect -> Cross
|
||||||
|
rowBg = '#f8d7da';
|
||||||
|
borderColor = '#842029';
|
||||||
|
textColor = '#842029';
|
||||||
|
icon = '❌';
|
||||||
|
} else if (!wasSelected && ans.is_correct) {
|
||||||
|
// Yellow : didn't answer but is correct -> Light Bulb
|
||||||
|
rowBg = '#fff3cd';
|
||||||
|
borderColor = '#664d03';
|
||||||
|
textColor = '#664d03';
|
||||||
|
icon = '💡';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={aIdx}
|
||||||
|
style={{
|
||||||
|
padding: '10px 14px',
|
||||||
|
borderRadius: '6px',
|
||||||
|
backgroundColor: rowBg,
|
||||||
|
border: `2px solid ${borderColor}`,
|
||||||
|
color: textColor,
|
||||||
|
fontWeight: wasSelected || ans.is_correct ? '700' : '400',
|
||||||
|
marginBottom: '4px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '10px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ fontSize: '1.2rem', flexShrink: 0 }}>{icon}</span>
|
||||||
|
<span>
|
||||||
|
<MarkdownText text={ans.label} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={styles.explanationBox}>
|
||||||
|
<strong>Explanations:</strong>
|
||||||
|
{item.possible_answers.map((ans, idx) => (
|
||||||
|
<p key={idx} style={{ fontSize: '0.9rem', margin: '6px 0' }}>
|
||||||
|
<em><MarkdownText text={ans.label} />:</em> <MarkdownText text={ans.explaination} />
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<button onClick={onReset} style={styles.button}>Take Quiz Again</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles: { [key: string]: React.CSSProperties } = {
|
||||||
|
container: { maxWidth: '700px', margin: '40px auto', fontFamily: 'system-ui, sans-serif', padding: '20px', border: '1px solid #ccc', borderRadius: '8px', backgroundColor: '#fff' },
|
||||||
|
uploadSection: { padding: '15px', backgroundColor: '#f8f9fa', borderRadius: '6px', border: '1px dashed #ccc' },
|
||||||
|
legendContainer: { display: 'flex', flexDirection: 'column', gap: '8px', padding: '12px', backgroundColor: '#f8f9fa', borderRadius: '6px', fontSize: '0.85rem', color: '#333', marginBottom: '15px' },
|
||||||
|
legendBadge: { width: '16px', height: '16px', borderRadius: '4px', display: 'inline-block' },
|
||||||
|
reviewCard: { padding: '15px', margin: '20px 0', backgroundColor: '#fafafa', borderRadius: '4px', boxShadow: '0 1px 3px rgba(0,0,0,0.05)' },
|
||||||
|
explanationBox: { marginTop: '12px', padding: '10px', backgroundColor: '#e9ecef', borderRadius: '4px' },
|
||||||
|
button: { marginTop: '15px', padding: '10px 20px', backgroundColor: '#007bff', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import QuizApp from './QuizApp'
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<QuizApp />
|
||||||
|
</React.StrictMode>,
|
||||||
|
)
|
||||||
+9512
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,21 @@
|
|||||||
|
export interface Answer {
|
||||||
|
label: string;
|
||||||
|
is_correct: boolean;
|
||||||
|
explaination: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QuizQuestion {
|
||||||
|
question: string;
|
||||||
|
possible_answers: Answer[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserSubmission {
|
||||||
|
timestamp: string;
|
||||||
|
score: number;
|
||||||
|
totalQuestions: number;
|
||||||
|
results: {
|
||||||
|
question: string;
|
||||||
|
selectedLabels: string[];
|
||||||
|
possible_answers: Answer[];
|
||||||
|
}[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()]
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user