feat(kuis): navigator soal horizontal di mobile (poin 3)

Satukan prev/dots/next-Selesai dalam satu baris scroll-x dan hapus kontrol duplikat di bawah soal.
This commit is contained in:
a2nr 2026-10-27 16:08:36 +07:00
parent 6971d0a3c3
commit ec34ffed72
2 changed files with 94 additions and 56 deletions

View File

@ -10,6 +10,15 @@
const questions = $derived(mgr.quizSession?.questions ?? []); const questions = $derived(mgr.quizSession?.questions ?? []);
let submitting = $state(false); let submitting = $state(false);
let navigatorEl = $state<HTMLDivElement | null>(null);
// Jaga dot aktif tetap terlihat saat navigasi (terutama mobile, scroll-x).
$effect(() => {
const idx = mgr.quizCurrentIndex;
const dots = navigatorEl?.querySelectorAll<HTMLElement>('.nav-dot');
if (!dots || dots.length === 0) return;
dots[idx]?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
});
function isAnswered(qId: string): boolean { function isAnswered(qId: string): boolean {
const a = mgr.quizSession?.answers[qId]; const a = mgr.quizSession?.answers[qId];
@ -38,21 +47,52 @@
<div class="quiz-question-view"> <div class="quiz-question-view">
<div class="quiz-question-header"> <div class="quiz-question-header">
{#if questions.length > 0} {#if questions.length > 0}
<div class="quiz-navigator" aria-label="Navigasi soal"> <div class="quiz-nav-row">
{#each questions as q, i} <button
{@const answered = isAnswered(q.id)} type="button"
class="btn btn-outline quiz-nav-btn"
onclick={() => mgr.prevQuizQuestion()}
disabled={mgr.quizCurrentIndex === 0}
aria-label="Soal sebelumnya"
>
← <span class="quiz-nav-label">Sebelumnya</span>
</button>
<div class="quiz-navigator" bind:this={navigatorEl} aria-label="Navigasi soal">
{#each questions as q, i}
{@const answered = isAnswered(q.id)}
<button
type="button"
class="nav-dot"
class:active={i === mgr.quizCurrentIndex}
class:answered
data-dot-index={i}
aria-label={`Soal ${i + 1}${answered ? ' (dijawab)' : ' (belum dijawab)'}`}
aria-current={i === mgr.quizCurrentIndex ? 'step' : undefined}
onclick={() => mgr.goToQuizQuestion(i)}
>
{i + 1}
</button>
{/each}
</div>
{#if mgr.quizCurrentIndex < questions.length - 1}
<button <button
type="button" type="button"
class="nav-dot" class="btn btn-primary quiz-nav-btn"
class:active={i === mgr.quizCurrentIndex} onclick={() => mgr.nextQuizQuestion()}
class:answered aria-label="Soal selanjutnya"
aria-label={`Soal ${i + 1}${answered ? ' (dijawab)' : ' (belum dijawab)'}`}
aria-current={i === mgr.quizCurrentIndex ? 'step' : undefined}
onclick={() => mgr.goToQuizQuestion(i)}
> >
{i + 1} <span class="quiz-nav-label">Selanjutnya</span> →
</button> </button>
{/each} {:else}
<button
type="button"
class="btn btn-success quiz-nav-btn quiz-nav-finish"
onclick={handleFinish}
disabled={!mgr.quizAllAnswered || submitting}
>
{submitting ? 'Menyimpan…' : 'Selesai ✓'}
</button>
{/if}
</div> </div>
{/if} {/if}
<button type="button" class="btn-exit-quiz" onclick={() => mgr.submitQuiz()} disabled={submitting}>Keluar Kuis</button> <button type="button" class="btn-exit-quiz" onclick={() => mgr.submitQuiz()} disabled={submitting}>Keluar Kuis</button>
@ -74,31 +114,4 @@
{:else} {:else}
<p class="quiz-empty">Tidak ada soal.</p> <p class="quiz-empty">Tidak ada soal.</p>
{/if} {/if}
{#if questions.length > 0}
<div class="quiz-controls">
<button
type="button"
class="btn btn-outline"
onclick={() => mgr.prevQuizQuestion()}
disabled={mgr.quizCurrentIndex === 0}
>
← Sebelumnya
</button>
{#if mgr.quizCurrentIndex < questions.length - 1}
<button type="button" class="btn btn-primary" onclick={() => mgr.nextQuizQuestion()}>
Selanjutnya →
</button>
{:else}
<button
type="button"
class="btn btn-success"
onclick={handleFinish}
disabled={!mgr.quizAllAnswered || submitting}
>
{submitting ? 'Menyimpan...' : 'Selesai Kuis'}
</button>
{/if}
</div>
{/if}
</div> </div>

View File

@ -452,9 +452,29 @@
.quiz-question-header { .quiz-question-header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 1rem; gap: 0.75rem;
font-size: 0.9rem; font-size: 0.9rem;
flex-wrap: wrap; flex-wrap: nowrap;
min-width: 0;
}
.quiz-nav-row {
display: flex;
align-items: center;
gap: 0.4rem;
flex: 1;
min-width: 0;
}
.quiz-nav-btn {
flex-shrink: 0;
white-space: nowrap;
font-size: 0.8rem;
padding: 0.35rem 0.6rem;
}
.quiz-nav-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
} }
.quiz-question-header .btn-exit-quiz { .quiz-question-header .btn-exit-quiz {
@ -465,6 +485,7 @@
font-size: 0.85rem; font-size: 0.85rem;
cursor: pointer; cursor: pointer;
padding: 0; padding: 0;
flex-shrink: 0;
} }
.quiz-question-prose { .quiz-question-prose {
@ -504,11 +525,21 @@
/* ── Quiz navigator & controls (dipindah dari QuizTab.svelte) ── */ /* ── Quiz navigator & controls (dipindah dari QuizTab.svelte) ── */
.quiz-navigator { .quiz-navigator {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: nowrap;
gap: 0.4rem; gap: 0.4rem;
flex: 1;
min-width: 0;
overflow-x: auto;
overscroll-behavior-x: contain;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
.quiz-navigator::-webkit-scrollbar {
display: none;
} }
.nav-dot { .nav-dot {
flex-shrink: 0;
width: 34px; width: 34px;
height: 34px; height: 34px;
border-radius: 8px; border-radius: 8px;
@ -534,29 +565,23 @@
color: white; color: white;
} }
.quiz-controls {
display: flex;
justify-content: space-between;
gap: 1rem;
position: relative;
z-index: 10;
background: var(--color-bg);
padding-top: 1rem;
}
.btn-outline { .btn-outline {
background: white; background: white;
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
color: var(--color-text); color: var(--color-text);
} }
.quiz-controls .btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
@media (max-width: 600px) { @media (max-width: 600px) {
.quiz-question-view { .quiz-question-view {
padding: 1rem 0; padding: 1rem 0;
} }
.quiz-question-header {
gap: 0.5rem;
}
.quiz-nav-label {
display: none;
}
.quiz-nav-btn {
padding: 0.35rem 0.5rem;
}
} }