First version with all questions.

This commit is contained in:
Valentin Deffaugt
2026-06-23 17:23:41 +02:00
commit c40082b73d
13 changed files with 11739 additions and 0 deletions
+55
View File
@@ -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
+1
View File
@@ -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
View File
@@ -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>
+1635
View File
File diff suppressed because it is too large Load Diff
+20
View File
@@ -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"
}
}
+68
View File
@@ -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
View File
@@ -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%' }
};
+163
View File
@@ -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' }
};
+9
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+21
View File
@@ -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[];
}[];
}
+12
View File
@@ -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"]
}
+6
View File
@@ -0,0 +1,6 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()]
})