Skip to content

Commit 289347e

Browse files
committed
chore: add ability to create quizes with les then all cards or with some reused
1 parent 91340ae commit 289347e

1 file changed

Lines changed: 213 additions & 23 deletions

File tree

frontend/src/views/QuizPage.vue

Lines changed: 213 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,14 @@ const selectedLabels = ref<Label[]>([])
1616
const includeUnlabeled = ref(false)
1717
const showLabelsInQuiz = ref(true)
1818
const isUntimed = ref(false)
19+
const quizMode = ref<'one-per-card' | 'specific-amount'>('one-per-card')
20+
const questionCount = ref(10)
1921
const cards = ref<CardData[]>([])
22+
const allCards = ref<CardData[]>([])
23+
const usedCardIndices = ref<number[]>([])
2024
const currentCardIndex = ref(0)
25+
const totalQuestions = ref(0)
26+
const currentQuestionNumber = ref(0)
2127
const timeRemaining = ref(60)
2228
const showAnswer = ref(false)
2329
const quizStarted = ref(false)
@@ -31,10 +37,35 @@ const currentQuestionType = ref<QuestionType>('title')
3137
3238
const currentCard = computed(() => cards.value[currentCardIndex.value])
3339
const progress = computed(() => {
34-
if (cards.value.length === 0) return 0
35-
return ((currentCardIndex.value + 1) / cards.value.length) * 100
40+
if (quizMode.value === 'specific-amount') {
41+
if (totalQuestions.value === 0) return 0
42+
return (currentQuestionNumber.value / totalQuestions.value) * 100
43+
} else {
44+
if (cards.value.length === 0) return 0
45+
return ((currentCardIndex.value + 1) / cards.value.length) * 100
46+
}
3647
})
3748
49+
const getNextRandomCard = () => {
50+
if (allCards.value.length === 0) return null
51+
52+
// If we've used all cards, reset the used indices
53+
if (usedCardIndices.value.length >= allCards.value.length) {
54+
usedCardIndices.value = []
55+
}
56+
57+
// Get available card indices
58+
const availableIndices = allCards.value
59+
.map((_, index) => index)
60+
.filter(index => !usedCardIndices.value.includes(index))
61+
62+
// Pick a random available card
63+
const randomIndex = availableIndices[Math.floor(Math.random() * availableIndices.length)]
64+
usedCardIndices.value.push(randomIndex)
65+
66+
return allCards.value[randomIndex]
67+
}
68+
3869
const generateQuestion = () => {
3970
if (!currentCard.value) return
4071
@@ -77,26 +108,46 @@ const startQuiz = async () => {
77108
78109
const response = await fetch(url)
79110
if (response.ok) {
80-
cards.value = await response.json()
111+
const fetchedCards = await response.json()
81112
82-
console.log(`Loaded ${cards.value.length} cards for quiz`)
113+
console.log(`Loaded ${fetchedCards.length} cards for quiz`)
83114
84-
if (cards.value.length === 0) {
115+
if (fetchedCards.length === 0) {
85116
alert('No cards found for selected labels')
86117
return
87118
}
88119
89-
// Shuffle cards
90-
cards.value.sort(() => Math.random() - 0.5)
91-
92-
quizStarted.value = true
93-
timeRemaining.value = 60
94-
currentCardIndex.value = 0
95-
showAnswer.value = false
120+
// Reset state
96121
correctAnswers.value = 0
97122
wrongAnswers.value = 0
98123
quizEnded.value = false
99124
finalTime.value = 0
125+
showAnswer.value = false
126+
usedCardIndices.value = []
127+
128+
if (quizMode.value === 'one-per-card') {
129+
// Original mode: one question per card
130+
cards.value = [...fetchedCards]
131+
cards.value.sort(() => Math.random() - 0.5)
132+
currentCardIndex.value = 0
133+
totalQuestions.value = cards.value.length
134+
currentQuestionNumber.value = 1
135+
} else {
136+
// Specific amount mode
137+
allCards.value = [...fetchedCards]
138+
totalQuestions.value = questionCount.value
139+
currentQuestionNumber.value = 1
140+
141+
// Generate first random card
142+
const firstCard = getNextRandomCard()
143+
if (firstCard) {
144+
cards.value = [firstCard]
145+
currentCardIndex.value = 0
146+
}
147+
}
148+
149+
quizStarted.value = true
150+
timeRemaining.value = 60
100151
101152
generateQuestion()
102153
@@ -144,11 +195,31 @@ const handleCorrect = () => {
144195
}
145196
showAnswer.value = false
146197
147-
if (currentCardIndex.value < cards.value.length - 1) {
148-
currentCardIndex.value++
149-
generateQuestion()
198+
if (quizMode.value === 'specific-amount') {
199+
currentQuestionNumber.value++
200+
201+
if (currentQuestionNumber.value > totalQuestions.value) {
202+
endQuiz()
203+
} else {
204+
// Get next random card
205+
const nextCard = getNextRandomCard()
206+
if (nextCard) {
207+
cards.value = [nextCard]
208+
currentCardIndex.value = 0
209+
generateQuestion()
210+
} else {
211+
endQuiz()
212+
}
213+
}
150214
} else {
151-
endQuiz()
215+
// One per card mode
216+
if (currentCardIndex.value < cards.value.length - 1) {
217+
currentCardIndex.value++
218+
currentQuestionNumber.value++
219+
generateQuestion()
220+
} else {
221+
endQuiz()
222+
}
152223
}
153224
}
154225
@@ -160,11 +231,31 @@ const continueAfterWrong = () => {
160231
wrongAnswers.value++
161232
showAnswer.value = false
162233
163-
if (currentCardIndex.value < cards.value.length - 1) {
164-
currentCardIndex.value++
165-
generateQuestion()
234+
if (quizMode.value === 'specific-amount') {
235+
currentQuestionNumber.value++
236+
237+
if (currentQuestionNumber.value > totalQuestions.value) {
238+
endQuiz()
239+
} else {
240+
// Get next random card
241+
const nextCard = getNextRandomCard()
242+
if (nextCard) {
243+
cards.value = [nextCard]
244+
currentCardIndex.value = 0
245+
generateQuestion()
246+
} else {
247+
endQuiz()
248+
}
249+
}
166250
} else {
167-
endQuiz()
251+
// One per card mode
252+
if (currentCardIndex.value < cards.value.length - 1) {
253+
currentCardIndex.value++
254+
currentQuestionNumber.value++
255+
generateQuestion()
256+
} else {
257+
endQuiz()
258+
}
168259
}
169260
}
170261
@@ -175,8 +266,14 @@ const restartQuiz = () => {
175266
includeUnlabeled.value = false
176267
showLabelsInQuiz.value = true
177268
isUntimed.value = false
269+
quizMode.value = 'one-per-card'
270+
questionCount.value = 10
178271
cards.value = []
272+
allCards.value = []
273+
usedCardIndices.value = []
179274
currentCardIndex.value = 0
275+
totalQuestions.value = 0
276+
currentQuestionNumber.value = 0
180277
timeRemaining.value = 60
181278
correctAnswers.value = 0
182279
wrongAnswers.value = 0
@@ -218,6 +315,7 @@ watch(() => quizStarted.value, (newVal) => {
218315
Include cards without labels
219316
</label>
220317
</div>
318+
221319
<div class="checkbox-field">
222320
<label>
223321
<input type="checkbox" v-model="showLabelsInQuiz" />
@@ -230,6 +328,32 @@ watch(() => quizStarted.value, (newVal) => {
230328
Untimed mode (no timer)
231329
</label>
232330
</div>
331+
<div class="quiz-mode-section">
332+
<h3>Quiz Mode</h3>
333+
<div class="radio-field">
334+
<label>
335+
<input type="radio" v-model="quizMode" value="one-per-card" />
336+
One question per card
337+
</label>
338+
</div>
339+
<div class="radio-field">
340+
<label>
341+
<input type="radio" v-model="quizMode" value="specific-amount" />
342+
Specific number of questions
343+
</label>
344+
</div>
345+
<div v-if="quizMode === 'specific-amount'" class="question-count-field">
346+
<label for="questionCount">Number of questions:</label>
347+
<input
348+
id="questionCount"
349+
type="number"
350+
v-model.number="questionCount"
351+
min="1"
352+
max="100"
353+
class="question-count-input"
354+
/>
355+
</div>
356+
</div>
233357
<Button
234358
label="Start Quiz"
235359
icon="pi pi-play"
@@ -268,8 +392,8 @@ watch(() => quizStarted.value, (newVal) => {
268392
:card="currentCard"
269393
:all-cards="cards"
270394
:question-type="currentQuestionType"
271-
:question-number="currentCardIndex + 1"
272-
:total-questions="cards.length"
395+
:question-number="currentQuestionNumber"
396+
:total-questions="totalQuestions"
273397
:show-labels="showLabelsInQuiz"
274398
:show-answer="showAnswer"
275399
@correct="handleCorrect"
@@ -298,7 +422,7 @@ watch(() => quizStarted.value, (newVal) => {
298422
</div>
299423
<div class="result-stat">
300424
<i class="pi pi-clock" style="color: #3B82F6; font-size: 3rem;"></i>
301-
<h2>{{ cards.length }}</h2>
425+
<h2>{{ totalQuestions }}</h2>
302426
<p>Total Questions</p>
303427
</div>
304428
<div v-if="finalTime > 0" class="result-stat">
@@ -360,6 +484,59 @@ h1 {
360484
cursor: pointer;
361485
}
362486
487+
.quiz-mode-section {
488+
margin-top: 1.5rem;
489+
padding: 1rem;
490+
background: var(--surface-50);
491+
border-radius: 8px;
492+
}
493+
494+
.quiz-mode-section h3 {
495+
margin: 0 0 0.75rem 0;
496+
font-size: 1rem;
497+
color: #4b5563;
498+
}
499+
500+
.radio-field {
501+
padding: 0.5rem 0;
502+
}
503+
504+
.radio-field label {
505+
display: flex;
506+
align-items: center;
507+
gap: 0.5rem;
508+
cursor: pointer;
509+
font-size: 0.95rem;
510+
}
511+
512+
.radio-field input[type="radio"] {
513+
width: 1.2rem;
514+
height: 1.2rem;
515+
cursor: pointer;
516+
}
517+
518+
.question-count-field {
519+
margin-top: 0.75rem;
520+
padding: 0.75rem;
521+
border-radius: 6px;
522+
display: flex;
523+
align-items: center;
524+
gap: 0.75rem;
525+
}
526+
527+
.question-count-field label {
528+
font-size: 0.95rem;
529+
color: #4b5563;
530+
}
531+
532+
.question-count-input {
533+
width: 80px;
534+
padding: 0.5rem;
535+
border: 1px solid #d1d5db;
536+
border-radius: 6px;
537+
font-size: 0.95rem;
538+
}
539+
363540
.start-button {
364541
margin-top: 1rem;
365542
width: 100%;
@@ -404,6 +581,7 @@ h1 {
404581
}
405582
406583
.progress-bar {
584+
position: relative;
407585
width: 100%;
408586
height: 8px;
409587
background: #e5e7eb;
@@ -418,6 +596,18 @@ h1 {
418596
transition: width 0.3s ease;
419597
}
420598
599+
.progress-text {
600+
position: absolute;
601+
top: 50%;
602+
left: 50%;
603+
transform: translate(-50%, -50%);
604+
font-size: 0.75rem;
605+
font-weight: 600;
606+
color: #4b5563;
607+
white-space: nowrap;
608+
text-shadow: 0 0 2px white;
609+
}
610+
421611
.question-card {
422612
margin-bottom: 2rem;
423613
}

0 commit comments

Comments
 (0)