Clone
<template>
<div class="assessment-container" v-if="assessmentData">
<div class="assessment-header">
<h1>{{ assessmentData.title }}</h1>
<div class="progress-bar">
<div
class="progress-fill"
:style="{ width: `${(Math.min(currentQuestionIndex + questionsPerPage, totalQuestions) / totalQuestions) * 100}%` }"
></div>
</div>
<p class="progress-text">
Questions {{ currentQuestionIndex + 1 }}-{{ Math.min(currentQuestionIndex + questionsPerPage, totalQuestions) }} of {{ totalQuestions }}
</p>
</div>
<div v-if="!showResults" class="quiz-content">
<div class="navigation top-nav">
<button
@click="previousPage"
:disabled="currentPage === 0"
class="nav-button"
>
← Previous
</button>
<button
v-if="!isLastPage"
@click="nextPage"
class="nav-button primary"
>
Next →
</button>
<button
v-else
@click="submitQuiz"
class="nav-button primary"
>
Submit Quiz
</button>
</div>
<div class="question-group">
<h2>{{ currentSubject.name }}</h2>
<div
v-for="(question, index) in currentPageQuestions"
:key="question.id"
class="question"
>
<h3>{{ getQuestionNumber(index) }}. {{ question.question }}</h3>
<div class="options">
<label
v-for="(option, optionIndex) in question.options"
:key="optionIndex"
class="option"
>
<input
type="radio"
:name="`q${question.id}`"
:value="optionIndex"
v-model="answers[question.id]"
/>
<span>{{ option }}</span>
</label>
</div>
</div>
</div>
<div class="navigation">
<button
@click="previousPage"
:disabled="currentPage === 0"
class="nav-button"
>
← Previous
</button>
<button
v-if="!isLastPage"
@click="nextPage"
class="nav-button primary"
>
Next →
</button>
<button
v-else
@click="submitQuiz"
class="nav-button primary"
>
Submit Quiz
</button>
</div>
</div>
<div v-else class="results">
<h2>Quiz Results</h2>
<div class="score-card">
<h3>Total Score: {{ score }} / {{ totalQuestions }}</h3>
<p>{{ answeredQuestions > 0 ? ((score / answeredQuestions) * 100).toFixed(1) : 0 }}% of answered questions</p>
<div v-if="skippedCount > 0" class="skipped-info">
<small>{{ skippedCount }} question{{ skippedCount === 1 ? '' : 's' }} skipped</small>
</div>
</div>
<div class="subject-scores">
<h3>Score by Subject:</h3>
<div v-for="subject in subjectScores" :key="subject.name" class="subject-score">
<span>{{ subject.name }}:</span>
<span>
{{ subject.score }} / {{ subject.total }}
({{ subject.percentage }}%{{ subject.skipped > 0 ? `, ${subject.skipped} skipped` : '' }})
</span>
</div>
</div>
<div v-if="incorrectAnswers.length > 0" class="incorrect-answers">
<h3>Review Incorrect Answers:</h3>
<div v-for="(item, index) in incorrectAnswers" :key="index" class="incorrect-item">
<div class="question-header">
<span class="question-number">Question {{ item.questionNumber }}</span>
<span class="subject-tag">{{ item.subject }}</span>
</div>
<p class="question-text">{{ item.question }}</p>
<div class="answer-comparison">
<div class="user-answer wrong">
<span class="label">Your answer:</span>
<span class="answer-text">{{ item.userAnswer }}</span>
</div>
<div class="correct-answer">
<span class="label">Correct answer:</span>
<span class="answer-text">{{ item.correctAnswerText || item.correctAnswer }}</span>
</div>
</div>
<div v-if="item.explanation" class="explanation">
<span class="label">Explanation:</span>
<p class="explanation-text">{{ item.explanation }}</p>
</div>
</div>
</div>
<router-link to="/" class="nav-button primary">
Back to Home
</router-link>
</div>
</div>
<div v-else class="loading">
Loading assessment...
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
const assessmentData = ref(null)
const answers = ref({})
const currentPage = ref(0)
const showResults = ref(false)
const score = ref(0)
const subjectScores = ref([])
const skippedCount = ref(0)
const answeredQuestions = ref(0)
const incorrectAnswers = ref([])
const questionsPerPage = 5
const allQuestions = computed(() => {
if (!assessmentData.value) return []
return assessmentData.value.subjects.flatMap(subject =>
subject.questions.map(q => ({ ...q, subject: subject.name }))
)
})
const totalQuestions = computed(() => allQuestions.value.length)
const currentPageQuestions = computed(() => {
const start = currentPage.value * questionsPerPage
const end = start + questionsPerPage
return allQuestions.value.slice(start, end)
})
const currentQuestionIndex = computed(() => currentPage.value * questionsPerPage)
const currentSubject = computed(() => {
if (currentPageQuestions.value.length === 0) return { name: '' }
return { name: currentPageQuestions.value[0].subject }
})
const isLastPage = computed(() => {
return (currentPage.value + 1) * questionsPerPage >= totalQuestions.value
})
const getQuestionNumber = (index) => {
return currentPage.value * questionsPerPage + index + 1
}
const previousPage = () => {
if (currentPage.value > 0) {
currentPage.value--
window.scrollTo({ top: 0, behavior: 'smooth' })
}
}
const nextPage = () => {
if (!isLastPage.value) {
currentPage.value++
window.scrollTo({ top: 0, behavior: 'smooth' })
}
}
const submitQuiz = () => {
calculateScore()
showResults.value = true
}
const calculateScore = () => {
let totalScore = 0
let totalSkipped = 0
let totalAnswered = 0
const subjectData = {}
const incorrectList = []
assessmentData.value.subjects.forEach(subject => {
subjectData[subject.name] = { correct: 0, total: 0, skipped: 0, answered: 0 }
subject.questions.forEach(question => {
subjectData[subject.name].total++
if (answers.value[question.id] === undefined) {
// Question was skipped
totalSkipped++
subjectData[subject.name].skipped++
} else {
// Question was answered
totalAnswered++
subjectData[subject.name].answered++
if (answers.value[question.id] === question.correctAnswer) {
totalScore++
subjectData[subject.name].correct++
} else {
// Track incorrect answers
incorrectList.push({
questionNumber: question.id,
question: question.question,
userAnswer: question.options[answers.value[question.id]],
correctAnswer: question.options[question.correctAnswer],
correctAnswerText: question.correctAnswerText,
explanation: question.explanation,
subject: subject.name
})
}
}
})
})
score.value = totalScore
skippedCount.value = totalSkipped
answeredQuestions.value = totalAnswered
incorrectAnswers.value = incorrectList
subjectScores.value = Object.entries(subjectData).map(([name, data]) => ({
name,
score: data.correct,
total: data.total,
skipped: data.skipped,
percentage: data.answered > 0
? ((data.correct / data.answered) * 100).toFixed(1)
: '0.0'
}))
}
const handleKeydown = (event) => {
// Only handle arrow keys when not in results view
if (showResults.value) return
// Don't interfere with form inputs
if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') return
if (event.key === 'ArrowLeft' && currentPage.value > 0) {
previousPage()
} else if (event.key === 'ArrowRight') {
if (!isLastPage.value) {
nextPage()
}
}
}
onMounted(async () => {
// Add keyboard event listener
window.addEventListener('keydown', handleKeydown)
const assessmentId = route.params.assessment
try {
// Check if it's an uploaded assessment first
if (assessmentId.startsWith('uploaded-')) {
const storedData = localStorage.getItem(`assessment-${assessmentId}`)
if (storedData) {
assessmentData.value = JSON.parse(storedData)
return
}
}
// Otherwise, try to fetch from public folder
const response = await fetch(`/assessments/${assessmentId}.json`)
if (!response.ok) {
throw new Error('Assessment not found')
}
assessmentData.value = await response.json()
} catch (error) {
console.error('Error loading assessment:', error)
router.push('/')
}
})
onUnmounted(() => {
// Remove keyboard event listener
window.removeEventListener('keydown', handleKeydown)
})
</script>
<style scoped>
.assessment-container {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
.assessment-header {
text-align: center;
margin-bottom: 2rem;
}
.assessment-header h1 {
color: #2c3e50;
margin-bottom: 1rem;
}
.progress-bar {
width: 100%;
height: 8px;
background-color: #e0e0e0;
border-radius: 4px;
overflow: hidden;
margin-bottom: 0.5rem;
}
.progress-fill {
height: 100%;
background-color: #42b883;
transition: width 0.3s ease;
}
.progress-text {
color: #666;
font-size: 0.9rem;
}
.quiz-content {
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.question-group h2 {
color: #2c3e50;
margin-bottom: 1.5rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid #e0e0e0;
}
.question {
margin-bottom: 2rem;
}
.question:last-child {
margin-bottom: 0;
}
.question h3 {
color: #333;
margin-bottom: 1rem;
font-size: 1.1rem;
}
.options {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.option {
display: flex;
align-items: center;
padding: 0.75rem 1rem;
background: #f8f9fa;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
.option:hover {
background: #e9ecef;
}
.option input[type="radio"] {
margin-right: 0.75rem;
}
.navigation {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 2rem;
padding-top: 2rem;
border-top: 1px solid #e0e0e0;
}
.top-nav {
margin-top: 0;
margin-bottom: 1rem;
padding-top: 0;
padding-bottom: 0;
border-top: none;
border-bottom: none;
}
.nav-button {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s;
background: #e0e0e0;
color: #333;
}
.nav-button:hover:not(:disabled) {
background: #d0d0d0;
}
.nav-button.primary {
background: #42b883;
color: white;
}
.nav-button.primary:hover:not(:disabled) {
background: #35a372;
}
.nav-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.results {
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
text-align: center;
}
.results h2 {
color: #2c3e50;
margin-bottom: 2rem;
}
.score-card {
background: #f0f9ff;
padding: 2rem;
border-radius: 8px;
margin-bottom: 2rem;
}
.score-card h3 {
color: #2c3e50;
margin-bottom: 0.5rem;
font-size: 1.5rem;
}
.score-card p {
color: #42b883;
font-size: 2rem;
font-weight: bold;
margin: 0;
}
.skipped-info {
margin-top: 0.5rem;
color: #ff9800;
font-size: 1rem;
}
.subject-scores {
text-align: left;
margin-bottom: 2rem;
}
.subject-scores h3 {
color: #2c3e50;
margin-bottom: 1rem;
}
.subject-score {
display: flex;
justify-content: space-between;
padding: 0.5rem 0;
border-bottom: 1px solid #e0e0e0;
}
.loading {
text-align: center;
padding: 4rem;
color: #666;
}
.incorrect-answers {
margin-top: 2rem;
text-align: left;
}
.incorrect-answers h3 {
color: #2c3e50;
margin-bottom: 1.5rem;
font-size: 1.2rem;
}
.incorrect-item {
background: #f8f9fa;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 1rem;
}
.question-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
}
.question-number {
font-weight: bold;
color: #495057;
}
.subject-tag {
background: #e9ecef;
color: #495057;
padding: 0.25rem 0.75rem;
border-radius: 4px;
font-size: 0.875rem;
}
.question-text {
color: #333;
margin-bottom: 1rem;
font-size: 1.05rem;
}
.answer-comparison {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.user-answer,
.correct-answer {
padding: 1rem;
border-radius: 6px;
}
.user-answer.wrong {
background: #fee;
border: 1px solid #fcc;
}
.correct-answer {
background: #e8f5e9;
border: 1px solid #c8e6c9;
}
.label {
display: block;
font-size: 0.875rem;
font-weight: bold;
margin-bottom: 0.5rem;
}
.user-answer .label {
color: #d32f2f;
}
.correct-answer .label {
color: #388e3c;
}
.answer-text {
color: #333;
font-size: 1rem;
}
.explanation {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px dashed #e0e0e0;
}
.explanation .label {
color: #42b883;
font-weight: 600;
display: block;
margin-bottom: 0.5rem;
}
.explanation-text {
color: #495057;
line-height: 1.6;
margin: 0;
}
@media (max-width: 600px) {
.answer-comparison {
grid-template-columns: 1fr;
}
}
</style>