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 ?? []);
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 {
const a = mgr.quizSession?.answers[qId];
@ -38,21 +47,52 @@
<div class="quiz-question-view">
<div class="quiz-question-header">
{#if questions.length > 0}
<div class="quiz-navigator" aria-label="Navigasi soal">
{#each questions as q, i}
{@const answered = isAnswered(q.id)}
<div class="quiz-nav-row">
<button
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
type="button"
class="nav-dot"
class:active={i === mgr.quizCurrentIndex}
class:answered
aria-label={`Soal ${i + 1}${answered ? ' (dijawab)' : ' (belum dijawab)'}`}
aria-current={i === mgr.quizCurrentIndex ? 'step' : undefined}
onclick={() => mgr.goToQuizQuestion(i)}
class="btn btn-primary quiz-nav-btn"
onclick={() => mgr.nextQuizQuestion()}
aria-label="Soal selanjutnya"
>
{i + 1}
<span class="quiz-nav-label">Selanjutnya</span> →
</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>
{/if}
<button type="button" class="btn-exit-quiz" onclick={() => mgr.submitQuiz()} disabled={submitting}>Keluar Kuis</button>
@ -74,31 +114,4 @@
{:else}
<p class="quiz-empty">Tidak ada soal.</p>
{/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>

View File

@ -452,9 +452,29 @@
.quiz-question-header {
display: flex;
align-items: center;
gap: 1rem;
gap: 0.75rem;
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 {
@ -465,6 +485,7 @@
font-size: 0.85rem;
cursor: pointer;
padding: 0;
flex-shrink: 0;
}
.quiz-question-prose {
@ -504,11 +525,21 @@
/* ── Quiz navigator & controls (dipindah dari QuizTab.svelte) ── */
.quiz-navigator {
display: flex;
flex-wrap: wrap;
flex-wrap: nowrap;
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 {
flex-shrink: 0;
width: 34px;
height: 34px;
border-radius: 8px;
@ -534,29 +565,23 @@
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 {
background: white;
border: 1px solid var(--color-border);
color: var(--color-text);
}
.quiz-controls .btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
@media (max-width: 600px) {
.quiz-question-view {
padding: 1rem 0;
}
.quiz-question-header {
gap: 0.5rem;
}
.quiz-nav-label {
display: none;
}
.quiz-nav-btn {
padding: 0.35rem 0.5rem;
}
}