feat: Add skip functionality and improve navigation
Torey Heinz
committed Jul 12, 2025
commit ad2dac5adaa51e712fb983a54c1515414d573124
Showing 1
changed file with
101 additions
and 21 deletions
src/views/Assessment.vue
+101
-21
| @@ | @@ -5,15 +5,39 @@ |
| <div class="progress-bar"> | |
| <div | |
| class="progress-fill" | |
| - | :style="{ width: `${(currentQuestionIndex / totalQuestions) * 100}%` }" |
| + | :style="{ width: `${(Math.min(currentQuestionIndex + questionsPerPage, totalQuestions) / totalQuestions) * 100}%` }" |
| ></div> | |
| </div> | |
| <p class="progress-text"> | |
| - | Question {{ currentQuestionIndex + 1 }} of {{ totalQuestions }} |
| + | 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 | |
| @@ | @@ -46,21 +70,19 @@ |
| :disabled="currentPage === 0" | |
| class="nav-button" | |
| > | |
| - | Previous |
| + | ← Previous |
| </button> | |
| <button | |
| v-if="!isLastPage" | |
| @click="nextPage" | |
| class="nav-button primary" | |
| - | :disabled="!currentPageAnswered" |
| > | |
| - | Next |
| + | Next → |
| </button> | |
| <button | |
| v-else | |
| @click="submitQuiz" | |
| class="nav-button primary" | |
| - | :disabled="!allQuestionsAnswered" |
| > | |
| Submit Quiz | |
| </button> | |
| @@ | @@ -71,14 +93,20 @@ |
| <h2>Quiz Results</h2> | |
| <div class="score-card"> | |
| <h3>Total Score: {{ score }} / {{ totalQuestions }}</h3> | |
| - | <p>{{ ((score / totalQuestions) * 100).toFixed(1) }}%</p> |
| + | <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 }}%)</span> |
| + | <span> |
| + | {{ subject.score }} / {{ subject.total }} |
| + | ({{ subject.percentage }}%{{ subject.skipped > 0 ? `, ${subject.skipped} skipped` : '' }}) |
| + | </span> |
| </div> | |
| </div> | |
| @@ | @@ -94,7 +122,7 @@ |
| </template> | |
| <script setup> | |
| - | import { ref, computed, onMounted } from 'vue' |
| + | import { ref, computed, onMounted, onUnmounted } from 'vue' |
| import { useRoute, useRouter } from 'vue-router' | |
| const route = useRoute() | |
| @@ | @@ -106,6 +134,8 @@ const currentPage = ref(0) |
| const showResults = ref(false) | |
| const score = ref(0) | |
| const subjectScores = ref([]) | |
| + | const skippedCount = ref(0) |
| + | const answeredQuestions = ref(0) |
| const questionsPerPage = 5 | |
| @@ | @@ -135,13 +165,6 @@ const isLastPage = computed(() => { |
| return (currentPage.value + 1) * questionsPerPage >= totalQuestions.value | |
| }) | |
| - | const currentPageAnswered = computed(() => { |
| - | return currentPageQuestions.value.every(q => answers.value[q.id] !== undefined) |
| - | }) |
| - | |
| - | const allQuestionsAnswered = computed(() => { |
| - | return allQuestions.value.every(q => answers.value[q.id] !== undefined) |
| - | }) |
| const getQuestionNumber = (index) => { | |
| return currentPage.value * questionsPerPage + index + 1 | |
| @@ | @@ -166,31 +189,67 @@ const submitQuiz = () => { |
| const calculateScore = () => { | |
| let totalScore = 0 | |
| + | let totalSkipped = 0 |
| + | let totalAnswered = 0 |
| const subjectData = {} | |
| assessmentData.value.subjects.forEach(subject => { | |
| - | subjectData[subject.name] = { correct: 0, total: 0 } |
| + | subjectData[subject.name] = { correct: 0, total: 0, skipped: 0, answered: 0 } |
| subject.questions.forEach(question => { | |
| subjectData[subject.name].total++ | |
| - | if (answers.value[question.id] === question.correctAnswer) { |
| - | totalScore++ |
| - | subjectData[subject.name].correct++ |
| + | |
| + | 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++ |
| + | } |
| } | |
| }) | |
| }) | |
| score.value = totalScore | |
| + | skippedCount.value = totalSkipped |
| + | answeredQuestions.value = totalAnswered |
| subjectScores.value = Object.entries(subjectData).map(([name, data]) => ({ | |
| name, | |
| score: data.correct, | |
| total: data.total, | |
| - | percentage: ((data.correct / data.total) * 100).toFixed(1) |
| + | 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 { | |
| @@ | @@ -214,6 +273,11 @@ onMounted(async () => { |
| router.push('/') | |
| } | |
| }) | |
| + | |
| + | onUnmounted(() => { |
| + | // Remove keyboard event listener |
| + | window.removeEventListener('keydown', handleKeydown) |
| + | }) |
| </script> | |
| <style scoped> | |
| @@ | @@ -308,11 +372,21 @@ onMounted(async () => { |
| .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; | |
| @@ | @@ -375,6 +449,12 @@ onMounted(async () => { |
| margin: 0; | |
| } | |
| + | .skipped-info { |
| + | margin-top: 0.5rem; |
| + | color: #ff9800; |
| + | font-size: 1rem; |
| + | } |
| + | |
| .subject-scores { | |
| text-align: left; | |
| margin-bottom: 2rem; | |