@@ -16,8 +16,14 @@ const selectedLabels = ref<Label[]>([])
1616const includeUnlabeled = ref (false )
1717const showLabelsInQuiz = ref (true )
1818const isUntimed = ref (false )
19+ const quizMode = ref <' one-per-card' | ' specific-amount' >(' one-per-card' )
20+ const questionCount = ref (10 )
1921const cards = ref <CardData []>([])
22+ const allCards = ref <CardData []>([])
23+ const usedCardIndices = ref <number []>([])
2024const currentCardIndex = ref (0 )
25+ const totalQuestions = ref (0 )
26+ const currentQuestionNumber = ref (0 )
2127const timeRemaining = ref (60 )
2228const showAnswer = ref (false )
2329const quizStarted = ref (false )
@@ -31,10 +37,35 @@ const currentQuestionType = ref<QuestionType>('title')
3137
3238const currentCard = computed (() => cards .value [currentCardIndex .value ])
3339const 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+
3869const 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% ;
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