Lompat ke konten
dokumentasi.id
Commit 120a248
v1
Star0

Ada commit baru di branch utama.

0792dcf

9

Tes Pemahaman Tajwid

Tes Pemahaman Tajwid adalah latihan pilihan ganda yang soalnya diambil acak dari bank soal tajwid. Satu ronde selalu berisi QUIZ_LENGTH soal, opsi setiap soal diacak, dan skor terbaik per kategori disimpan di peramban src/lib/quiz.ts:4-5. Halaman ini menjelaskan bank soal, fungsi drawQuiz, alur satu ronde, dan pencatatan skor. Pola penyimpanan di peramban dibahas di Progres Belajar di Perangkat, sedangkan metadata halaman /tes dibahas di SEO dan Metadata.

Dua rute, satu bank soal

/tes hanya menampilkan kartu kategori. Setiap kartu menautkan /tes/{kategori}, mencetak jumlah soal lewat {quizBanks[category].length} soal, dan memasang <QuizScoreBadge category={category} /> src/app/tes/page.tsx:33-53. Rute dinamis menyerahkan quizBanks[kategori] ke QuizRunner src/app/tes/[kategori]/page.tsx:58.

Daftar kategori dibekukan saat build: generateStaticParams memetakan materiCategories dan dynamicParams = false src/app/tes/[kategori]/page.tsx:12-18. Kategori di luar daftar itu memanggil notFound() src/app/tes/[kategori]/page.tsx:32-35. Saat ini materiCategories hanya berisi "tajwid" src/lib/categories.ts:6-8.

RuteKomponen utamaSumber soal
/tesQuizListPagequizBanks[category].length untuk label jumlah soal
/tes/tajwidQuizRunnerquizBanks["tajwid"], yaitu tajwidQuiz

Bank soal tajwid

Setiap entri QuizQuestion membawa empat opsi bertipe tuple, satu pembahasan, dan satu slug pelajaran src/lib/quiz.ts:7-14. Opsi pertama selalu jawaban benar; drawQuiz yang memindahkan posisinya. Bank dirakit di quizBanks sebagai Record<MateriCategory, QuizQuestion[]> dan baru berisi satu kategori tajwid src/lib/quiz-data/index.ts:1-7.

tajwidQuiz disusun dari materi kelas Lanjutan, dan tiap soal menautkan pertemuan yang membahasnya. Berkasnya dibagi komentar per tahap: makhraj, nun sukun dan mim sukun, mad, tebal-tipis serta qalqalah, lalu waqaf dan gharib src/lib/quiz-data/tajwid.ts:3-11.

FieldTipePeran
questionstringteks soal, sekaligus key pada daftar jawaban salah
options[string, string, string, string]empat pilihan; indeks 0 adalah jawaban benar
explanationstringpembahasan yang tampil setelah menjawab
lessonstringslug pertemuan untuk tautan /lanjutan/{lesson}

QuizItem adalah soal yang sudah ditampilkan: options menjadi string[] dan answer menyimpan posisi jawaban benar setelah opsi diacak src/lib/quiz.ts:16-20.

drawQuiz: satu ronde sepuluh soal

drawQuiz(bank, length = QUIZ_LENGTH, random = Math.random) mengacak bank, memotong length soal pertama, lalu mengacak indeks opsi tiap soal src/lib/quiz.ts:33-41. Karena opsi pertama adalah jawaban benar, posisi barunya dihitung dengan order.indexOf(0) src/lib/quiz.ts:37-40. Pengacakan memakai algoritma Fisher-Yates di atas salinan array, sehingga bank asli tidak berubah src/lib/quiz.ts:24-31.

graph LR
  bank["tajwidQuiz"] --> shuf["shuffle(bank, random)"]
  shuf --> cut["slice(0, QUIZ_LENGTH)"]
  cut --> opt["shuffle indeks opsi"]
  opt --> ans["answer = order.indexOf(0)"]
  ans --> items["QuizItem array"]
  items --> runner["QuizRunner.items"]
  style bank fill:#336791,color:#fff
  style shuf fill:#8b5cf6,color:#fff
  style cut fill:#8b5cf6,color:#fff
  style opt fill:#8b5cf6,color:#fff
  style ans fill:#f59e0b,color:#000
  style items fill:#2d7d46,color:#fff
  style runner fill:#2d7d46,color:#fff
%% fig: Jalur drawQuiz dari bank soal ke QuizItem
%% desc: Bank diacak, dipotong sepuluh soal, opsi tiap soal diacak ulang, lalu posisi jawaban benar disimpan di answer.
Fig. 1: Jalur drawQuiz dari bank soal ke QuizItemBank diacak, dipotong sepuluh soal, opsi tiap soal diacak ulang, lalu posisi jawaban benar disimpan di answer.

Setiap soal hanya diambil sekali dari hasil shuffle, jadi satu ronde tidak memuat soal kembar src/lib/quiz.ts:34-36.

Satu ronde: klik sampai skor

QuizRunner menyimpan items sebagai useState<QuizItem[] | null>(null) dan baru mengisinya di start() src/components/quiz/quiz-ui.tsx:44-56.

⚠️ drawQuiz sengaja tidak dipanggil saat render. Komentar di QuizRunner menyatakan soal ditarik saat klik supaya markup server dan client selalu sama src/components/quiz/quiz-ui.tsx:47-56.

Sebelum items terisi, yang tampil adalah kartu pembuka: disebutkan bahwa QUIZ_LENGTH soal diambil dari bank.length soal, dengan tombol "Mulai tes" src/components/quiz/quiz-ui.tsx:58-74.

sequenceDiagram
  actor Murid
  participant Page as halaman dinamis
  participant Runner as QuizRunner
  participant Quiz as drawQuiz
  participant Store as useQuizScores
  participant Badge as QuizScoreBadge
  Page->>Runner: category dan bank
  Murid->>Runner: klik Mulai tes, start()
  Runner->>Quiz: drawQuiz(bank)
  Quiz-->>Runner: QuizItem sebanyak QUIZ_LENGTH
  loop tiap soal sampai sepuluh
    Runner->>Murid: pertanyaan dan empat opsi
    Murid->>Runner: setPicked(i)
    Runner->>Murid: pembahasan dan tautan lanjutan
    Murid->>Runner: next()
  end
  Runner->>Store: updateScores(recordScore(saved, category, benar))
  Store-->>Badge: best, last, attempts
%% fig: Satu ronde tes dari pengambilan soal sampai penyimpanan skor
%% desc: Klik Mulai tes menarik sepuluh soal, setiap jawaban memunculkan pembahasan, dan ronde terakhir menulis skor lewat recordScore.
Fig. 2: Satu ronde tes dari pengambilan soal sampai penyimpanan skorKlik Mulai tes menarik sepuluh soal, setiap jawaban memunculkan pembahasan, dan ronde terakhir menulis skor lewat recordScore.

Saat satu opsi dipilih, picked menyimpan indeksnya, tombol opsi dinonaktifkan, dan pembahasan muncul bersama tautan "Buka materinya" ke /lanjutan/{item.lesson} yang dibuka di tab baru src/components/quiz/quiz-ui.tsx:210-223. next() menambahkan { item, picked } ke answers; ketika jumlahnya sudah sama dengan items.length, ia memanggil updateScores dengan hasil recordScore(...) src/components/quiz/quiz-ui.tsx:141-149.

Hasil dan materi yang perlu diulang

Nilai akhir adalah correct * 10, sehingga rentangnya 0 sampai 100 untuk sepuluh soal, disertai correct dari items.length jawaban benar src/components/quiz/quiz-ui.tsx:86-89. Kalimat penutupnya dipilih verdict(correct) src/components/quiz/quiz-ui.tsx:35-40:

Nilai correctKalimat verdict
correct === QUIZ_LENGTH"Sempurna! Semua jawaban benar."
correct >= 8"Bagus sekali, pemahamanmu sudah kuat."
correct >= 6"Cukup baik. Ulangi materi yang masih keliru."
lainnya"Tetap semangat. Pelajari lagi materinya, lalu coba kembali."

Jawaban yang salah dikumpulkan di bagian "Yang perlu dipelajari lagi": opsi yang dipilih ditampilkan dicoret, jawaban benar ditebalkan, lalu pembahasan dan tautan /lanjutan/{item.lesson} ditampilkan src/components/quiz/quiz-ui.tsx:107-131.

Skor terbaik per kategori

recordScore mengembalikan objek baru: best adalah Math.max(previous?.best ?? 0, correct), last berisi hasil ronde terakhir, dan attempts bertambah satu src/lib/quiz.ts:67-78.

Field QuizScoreCara recordScore mengisiTampilan di QuizScoreBadge
bestnilai tertinggi antara skor lama dan correctscore.best * 10
lastcorrect dari ronde terakhirscore.last * 10
attemptsjumlah percobaan sebelumnya ditambah satu{score.attempts}× mencoba

Tempat penyimpanannya dibuat sekali di tingkat modul: createLocalStore(QUIZ_SCORES_KEY, parseScores, emptyScores) src/components/quiz/quiz-ui.tsx:19, dengan key localStorage bernama tes-pemahaman src/lib/quiz.ts:5. createLocalStore merender nilai empty di server, mengisinya setelah hydration, dan menyiarkan perubahan ke komponen lain di halaman yang sama src/components/use-local-store.ts:9-22.

parseScores membuang entri yang tidak lengkap atau bukan bilangan bulat non-negatif, serta mengembalikan emptyScores bila string kosong, bukan JSON, atau bukan objek src/lib/quiz.ts:46-65.

stateDiagram-v2
  [*] --> Kosong
  Kosong --> Tersimpan: "ronde selesai, recordScore dipanggil"
  Tersimpan --> Tersimpan: "ronde baru, best = max, attempts + 1"
  Tersimpan --> Kosong: "parseScores menolak nilai rusak"
  classDef kosong fill:#dc2626,color:#fff
  classDef simpan fill:#2d7d46,color:#fff
  class Kosong kosong
  class Tersimpan simpan
%% fig: Siklus skor per kategori
%% desc: Kategori tanpa skor berubah menjadi tersimpan setelah ronde pertama, ronde berikutnya hanya menaikkan attempts, dan nilai rusak dibuang kembali ke kosong.
Fig. 3: Siklus skor per kategoriKategori tanpa skor berubah menjadi tersimpan setelah ronde pertama, ronde berikutnya hanya menaikkan attempts, dan nilai rusak dibuang kembali ke kosong.

QuizScoreBadge mencetak score.best * 10, score.last * 10, dan jumlah percobaan, lalu tidak merender apa pun bila kategori belum pernah diuji src/components/quiz/quiz-ui.tsx:22-33.

⚠️ Skor melekat pada localStorage peramban yang dipakai, bukan pada akun. Bila nilai tidak ada atau rusak, parseScores mengembalikan emptyScores, sehingga kartu tes tampil tanpa riwayat nilai src/lib/quiz.ts:46-48.

Yang dijaga test

Berkas ujiYang dijaga
tests/quiz.test.ts:15-24panjang bank minimal QUIZ_LENGTH * 5, soal unik, empat opsi unik, explanation tidak kosong, dan lesson ada di daftar lessons tajwid
tests/quiz.test.ts:28-34setiap pertemuan kelas Lanjutan punya soal, kecuali praktik-mengajar
tests/quiz.test.ts:36-50drawQuiz menghasilkan QUIZ_LENGTH soal berbeda, dan item.options[item.answer] sama dengan original.options[0]
tests/quiz.test.ts:52-65recordScore({}, "tajwid", 7) lalu recordScore(first, "tajwid", 5) menghasilkan { best: 7, last: 5, attempts: 2 }, dan parseScores menolak nilai rusak