Skip to content

Commit ee83110

Browse files
committed
improve quiz
1 parent 509d0d7 commit ee83110

1 file changed

Lines changed: 159 additions & 33 deletions

File tree

src/routes/quiz/+page.svelte

Lines changed: 159 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@
22
import { base } from '$app/paths';
33
import type { QuizMode, Sample, SampleManifest } from '$lib/types';
44
import GifPlayer from '$lib/components/GifPlayer.svelte';
5-
import { onMount } from 'svelte';
5+
import { onMount, onDestroy } from 'svelte';
66
77
// Constants
8-
const QUESTIONS_PER_ROUND = 5;
98
const OPTIONS_PER_QUESTION = 4;
109
const POINTS_CORRECT = 10;
1110
const STREAK_BONUS = 5;
@@ -24,6 +23,7 @@
2423
// State
2524
let gamePhase = $state<GamePhase>('setup');
2625
let selectedMode = $state<QuizMode>('phase-to-intensity');
26+
let selectedQuestionCount = $state<number>(10);
2727
let allSamples = $state<Sample[]>([]);
2828
let questions = $state<QuizQuestion[]>([]);
2929
let currentQuestionIndex = $state(0);
@@ -33,11 +33,15 @@
3333
let answers = $state<boolean[]>([]);
3434
let loading = $state(true);
3535
let error = $state<string | null>(null);
36+
let startTime = $state<number>(0);
37+
let elapsedTime = $state<number>(0);
38+
let timerInterval = $state<number | null>(null);
3639
3740
// Derived
3841
let currentQuestion = $derived(questions[currentQuestionIndex]);
3942
let isLastQuestion = $derived(currentQuestionIndex === questions.length - 1);
4043
let streakBonus = $derived(Math.min(streak * STREAK_BONUS, MAX_STREAK_BONUS));
44+
let formattedTime = $derived(formatTime(elapsedTime));
4145
4246
onMount(async () => {
4347
try {
@@ -55,6 +59,32 @@
5559
}
5660
});
5761
62+
onDestroy(() => {
63+
stopTimer();
64+
});
65+
66+
function formatTime(seconds: number): string {
67+
const mins = Math.floor(seconds / 60);
68+
const secs = seconds % 60;
69+
return `${mins}:${secs.toString().padStart(2, '0')}`;
70+
}
71+
72+
function startTimer() {
73+
startTime = Date.now();
74+
elapsedTime = 0;
75+
if (timerInterval) clearInterval(timerInterval);
76+
timerInterval = window.setInterval(() => {
77+
elapsedTime = Math.floor((Date.now() - startTime) / 1000);
78+
}, 1000);
79+
}
80+
81+
function stopTimer() {
82+
if (timerInterval) {
83+
clearInterval(timerInterval);
84+
timerInterval = null;
85+
}
86+
}
87+
5888
function shuffleArray<T>(array: T[]): T[] {
5989
const shuffled = [...array];
6090
for (let i = shuffled.length - 1; i > 0; i--) {
@@ -68,7 +98,7 @@
6898
const shuffledSamples = shuffleArray(allSamples);
6999
const generatedQuestions: QuizQuestion[] = [];
70100
71-
for (let i = 0; i < QUESTIONS_PER_ROUND; i++) {
101+
for (let i = 0; i < selectedQuestionCount; i++) {
72102
// Pick correct answer (cycle through shuffled samples)
73103
const correctSample = shuffledSamples[i % shuffledSamples.length];
74104
@@ -99,6 +129,7 @@
99129
selectedAnswer = null;
100130
answers = [];
101131
gamePhase = 'playing';
132+
startTimer();
102133
}
103134
104135
function selectAnswer(index: number) {
@@ -121,6 +152,7 @@
121152
122153
function nextQuestion() {
123154
if (isLastQuestion) {
155+
stopTimer();
124156
gamePhase = 'results';
125157
} else {
126158
currentQuestionIndex += 1;
@@ -130,13 +162,15 @@
130162
}
131163
132164
function resetQuiz() {
165+
stopTimer();
133166
gamePhase = 'setup';
134167
questions = [];
135168
currentQuestionIndex = 0;
136169
score = 0;
137170
streak = 0;
138171
selectedAnswer = null;
139172
answers = [];
173+
elapsedTime = 0;
140174
}
141175
142176
function resolvePath(path: string): string {
@@ -236,11 +270,33 @@
236270
</div>
237271
</section>
238272

273+
<section class="option-group">
274+
<h2>Number of Questions</h2>
275+
<div class="options difficulty-options">
276+
<button
277+
class="option-btn small"
278+
class:active={selectedQuestionCount === 5}
279+
onclick={() => selectedQuestionCount = 5}
280+
>
281+
<span class="option-title">5</span>
282+
<span class="option-desc">Quick</span>
283+
</button>
284+
<button
285+
class="option-btn small"
286+
class:active={selectedQuestionCount === 10}
287+
onclick={() => selectedQuestionCount = 10}
288+
>
289+
<span class="option-title">10</span>
290+
<span class="option-desc">Standard</span>
291+
</button>
292+
</div>
293+
</section>
294+
239295
<div class="start-section">
240296
<button class="start-btn primary" onclick={startQuiz}>
241297
Start Quiz
242298
</button>
243-
<p class="hint">{QUESTIONS_PER_ROUND} questions per round</p>
299+
<p class="hint">{selectedQuestionCount} questions per round</p>
244300
</div>
245301
</div>
246302
{:else if gamePhase === 'playing' || gamePhase === 'feedback'}
@@ -255,6 +311,10 @@
255311
<span class="score-label">Score</span>
256312
<span class="score-value">{score}</span>
257313
</div>
314+
<div class="score-item timer-item">
315+
<span class="score-label">Time</span>
316+
<span class="score-value timer-value">{formattedTime}</span>
317+
</div>
258318
<div class="score-item">
259319
<span class="score-label">Streak</span>
260320
<span class="score-value">{streak}{streak > 0 ? ` (+${streakBonus})` : ''}</span>
@@ -286,44 +346,55 @@
286346
<p class="options-prompt">Select the matching {selectedMode === 'phase-to-intensity' ? 'intensity' : 'phase'}:</p>
287347
<div class="options-grid">
288348
{#each currentQuestion.options as option, index}
289-
<button
290-
class="option-card {getOptionClass(index)}"
291-
onclick={() => selectAnswer(index)}
292-
disabled={gamePhase === 'feedback'}
293-
>
294-
<GifPlayer
295-
src={getOptionGif(option)}
296-
alt="Option {index + 1}"
297-
/>
298-
{#if gamePhase === 'feedback'}
299-
<span class="option-label">{option.name}</span>
349+
<div class="option-wrapper">
350+
<button
351+
class="option-card {getOptionClass(index)}"
352+
onclick={() => selectAnswer(index)}
353+
disabled={gamePhase === 'feedback'}
354+
id="option-{index}"
355+
>
356+
<GifPlayer
357+
src={getOptionGif(option)}
358+
alt="Option {index + 1}"
359+
/>
360+
{#if gamePhase === 'feedback'}
361+
<span class="option-label">{option.name}</span>
362+
{/if}
363+
</button>
364+
365+
<!-- Feedback overlay on selected option -->
366+
{#if gamePhase === 'feedback' && selectedAnswer === index}
367+
<div class="feedback-overlay">
368+
<div class="feedback-content">
369+
{#if selectedAnswer === currentQuestion.correctIndex}
370+
<p class="feedback-text correct">Correct! +{POINTS_CORRECT}{streak > 1 ? ` +${Math.min((streak - 1) * STREAK_BONUS, MAX_STREAK_BONUS)} streak bonus` : ''}</p>
371+
{:else}
372+
<p class="feedback-text incorrect">Incorrect. The answer was: {currentQuestion.correctSample.name}</p>
373+
{/if}
374+
<button class="next-btn primary" onclick={nextQuestion}>
375+
{isLastQuestion ? 'See Results' : 'Next Question'}
376+
</button>
377+
</div>
378+
</div>
300379
{/if}
301-
</button>
380+
</div>
302381
{/each}
303382
</div>
304383
</div>
305-
306-
<!-- Feedback / Next -->
307-
{#if gamePhase === 'feedback'}
308-
<div class="feedback-section">
309-
{#if selectedAnswer === currentQuestion.correctIndex}
310-
<p class="feedback-text correct">Correct! +{POINTS_CORRECT}{streak > 1 ? ` +${Math.min((streak - 1) * STREAK_BONUS, MAX_STREAK_BONUS)} streak bonus` : ''}</p>
311-
{:else}
312-
<p class="feedback-text incorrect">Incorrect. The answer was: {currentQuestion.correctSample.name}</p>
313-
{/if}
314-
<button class="next-btn primary" onclick={nextQuestion}>
315-
{isLastQuestion ? 'See Results' : 'Next Question'}
316-
</button>
317-
</div>
318-
{/if}
319384
</div>
320385
{:else if gamePhase === 'results'}
321386
<div class="results">
322387
<div class="results-card">
323388
<h2>Quiz Complete</h2>
324-
<div class="final-score">
325-
<span class="score-number">{score}</span>
326-
<span class="score-label">points</span>
389+
<div class="final-scores">
390+
<div class="final-score">
391+
<span class="score-number">{score}</span>
392+
<span class="score-label">points</span>
393+
</div>
394+
<div class="final-score">
395+
<span class="score-number">{formattedTime}</span>
396+
<span class="score-label">time</span>
397+
</div>
327398
</div>
328399
<div class="results-stats">
329400
<div class="stat">
@@ -489,6 +560,10 @@
489560
gap: 2px;
490561
}
491562
563+
.timer-item {
564+
align-items: center;
565+
}
566+
492567
.score-label {
493568
font-size: 0.75rem;
494569
text-transform: uppercase;
@@ -501,6 +576,12 @@
501576
color: var(--accent);
502577
}
503578
579+
.timer-value {
580+
font-size: 1.3rem;
581+
font-weight: 600;
582+
color: var(--text-primary);
583+
}
584+
504585
.question-section {
505586
display: flex;
506587
flex-direction: column;
@@ -550,6 +631,10 @@
550631
}
551632
}
552633
634+
.option-wrapper {
635+
position: relative;
636+
}
637+
553638
.option-card {
554639
display: flex;
555640
flex-direction: column;
@@ -559,6 +644,7 @@
559644
border: 2px solid var(--border);
560645
cursor: pointer;
561646
transition: border-color 0.15s, opacity 0.15s;
647+
width: 100%;
562648
}
563649
564650
.option-card:hover:not(:disabled) {
@@ -592,6 +678,32 @@
592678
white-space: nowrap;
593679
}
594680
681+
.feedback-overlay {
682+
position: absolute;
683+
top: 0;
684+
left: 0;
685+
right: 0;
686+
bottom: 0;
687+
display: flex;
688+
align-items: center;
689+
justify-content: center;
690+
pointer-events: none;
691+
z-index: 10;
692+
}
693+
694+
.feedback-content {
695+
display: flex;
696+
flex-direction: column;
697+
align-items: center;
698+
gap: var(--spacing-md);
699+
padding: var(--spacing-lg);
700+
background-color: rgba(0, 0, 0, 0.85);
701+
backdrop-filter: blur(4px);
702+
border-radius: 8px;
703+
pointer-events: auto;
704+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
705+
}
706+
595707
.feedback-section {
596708
display: flex;
597709
flex-direction: column;
@@ -605,6 +717,8 @@
605717
.feedback-text {
606718
font-size: 1.1rem;
607719
font-weight: 500;
720+
margin: 0;
721+
text-align: center;
608722
}
609723
610724
.feedback-text.correct {
@@ -617,6 +731,12 @@
617731
618732
.next-btn {
619733
padding: var(--spacing-sm) var(--spacing-lg);
734+
background-color: rgba(255, 255, 255, 0.5);
735+
backdrop-filter: blur(4px);
736+
}
737+
738+
.next-btn:hover {
739+
background-color: rgba(255, 255, 255, 0.7);
620740
}
621741
622742
/* Results */
@@ -640,6 +760,12 @@
640760
margin: 0;
641761
}
642762
763+
.final-scores {
764+
display: flex;
765+
gap: var(--spacing-xl);
766+
align-items: center;
767+
}
768+
643769
.final-score {
644770
display: flex;
645771
flex-direction: column;

0 commit comments

Comments
 (0)