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:
parent
6971d0a3c3
commit
ec34ffed72
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue