Lompat ke konten
dokumentasi.id
Commit 120a248
v1
Star0

Ada commit baru di branch utama.

0792dcf

8

Progres Belajar di Perangkat

Semua progres belajar tinggal di browser pembaca, bukan di server. Aplikasi ini tidak memakai akun maupun tabel pengguna: tanda bagian lancar, materi yang selesai, dan halaman terakhir dibaca disimpan sebagai JSON di localStorage. Dua modul murni memegang aturan datanya tanpa dependensi, src/lib/practice-progress.ts untuk latihan dan src/lib/learning-progress.ts untuk materi serta halaman terakhir src/lib/practice-progress.ts:1-10 src/lib/learning-progress.ts:3-11.

Tiga kunci penyimpanan

Setiap kunci punya satu fungsi parse dan satu hook tipis di atasnya. usePracticeProgress hanya membungkus PRACTICE_PROGRESS_KEY, parseProgress, dan emptyProgress src/components/practice/use-practice-progress.ts:1-7. Dua kunci materi dibuat berdampingan di berkas UI-nya src/components/learning-progress-ui.tsx:18-19.

graph LR
  subgraph Browser
    K1["praktik-progress"]
    K2["materi-progress"]
    K3["terakhir-belajar"]
  end
  K1 --> P1["parseProgress"]
  K2 --> P2["parseLessonProgress"]
  K3 --> P3["parseLastLearned"]
  P1 --> H1["usePracticeProgress"]
  P2 --> H2["useLessonProgress"]
  P3 --> H3["useLastLearned"]
  H1 --> C1["PracticeSheet"]
  H1 --> C2["OverallProgress"]
  H1 --> C3["PracticePills"]
  H2 --> C4["LessonDoneToggle"]
  H3 --> C5["StartLearningMenu"]
  style K1 fill:#336791,color:#fff
  style K2 fill:#336791,color:#fff
  style K3 fill:#336791,color:#fff
  style P1 fill:#8b5cf6,color:#fff
  style P2 fill:#8b5cf6,color:#fff
  style P3 fill:#8b5cf6,color:#fff
  style H1 fill:#f59e0b,color:#000
  style H2 fill:#f59e0b,color:#000
  style H3 fill:#f59e0b,color:#000
%% fig: Dari kunci localStorage ke hook dan komponen
%% desc: Tiga kunci JSON dibaca oleh fungsi parse masing-masing, lalu dibagikan ke komponen lewat hook hasil createLocalStore.
Fig. 1: Dari kunci localStorage ke hook dan komponenTiga kunci JSON dibaca oleh fungsi parse masing-masing, lalu dibagikan ke komponen lewat hook hasil createLocalStore.
KunciKonstantaParseHookPemakai
praktik-progressPRACTICE_PROGRESS_KEYparseProgressusePracticeProgressPracticeSheet, PracticePills, OverallProgress, ContinuePractice
materi-progressLESSON_PROGRESS_KEYparseLessonProgressuseLessonProgressLessonDoneToggle, LessonCardStatus, LessonPills
terakhir-belajarLAST_LEARNED_KEYparseLastLearneduseLastLearnedStartLearningMenu

Nilai tes pemahaman memakai pola yang sama dengan kunci QUIZ_SCORES_KEY; rinciannya ada di Tes Pemahaman Tajwid src/components/quiz/quiz-ui.tsx:19.

Status praktik: new, started, dan done

practiceStatus mengembalikan "new" hanya ketika belum ada entry untuk latihan itu. Begitu sebuah entry ada, hasilnya "done" bila entry.total > 0 dan entry.done.length >= entry.total, selain itu "started" — termasuk saat total masih 0 src/lib/practice-progress.ts:61-64.

stateDiagram-v2
  state "new, belum ada entry" as New
  state "started, sebagian lancar" as Started
  state "done, semua bagian lancar" as Done
  [*] --> New
  New --> Started: visitPractice
  Started --> Done: done.length >= total
  Done --> Started: toggleSection melepas tanda
  Started --> Started: resetPractice
  Done --> Started: resetPractice
  classDef kosong fill:#f59e0b,color:#000
  classDef sebagian fill:#8b5cf6,color:#fff
  classDef selesai fill:#2d7d46,color:#fff
  class New kosong
  class Started sebagian
  class Done selesai
%% fig: Mesin keadaan progres satu latihan
%% desc: Entry yang belum ada berstatus new, penandaan bagian membawanya ke started, dan memenuhi seluruh bagian membuatnya done; resetPractice hanya kembali ke started.
Fig. 2: Mesin keadaan progres satu latihanEntry yang belum ada berstatus new, penandaan bagian membawanya ke started, dan memenuhi seluruh bagian membuatnya done; resetPractice hanya kembali ke started.

resetPractice mengosongkan done tetapi tetap menyimpan entry dengan total lamanya, jadi latihan yang diulang kembali ke "started", bukan "new" src/lib/practice-progress.ts:55-59. Hanya practiceStatus(undefined) yang menghasilkan "new" src/lib/practice-progress.ts:61-64.

Status itu yang dipakai pil nomor latihan untuk memasang centang dan PracticeCardStatus untuk menulis "Sudah lancar" atau i/total bagian lancar src/components/practice/practice-progress-ui.tsx:14-21 src/components/practice/practice-progress-ui.tsx:52-68. Uraian komponennya ada di Komponen UI Pelajaran.

Membaca data yang rusak atau diedit tangan

parseProgress tidak pernah melempar galat. null, JSON yang tidak sah, dan nilai yang bukan objek semuanya jatuh ke emptyProgress, sedangkan tanda yang tidak valid dibuang, dideduplikasi, lalu diurutkan src/lib/practice-progress.ts:15-38.

Isi localStorageHasil parseProgress
null, {not json, [1,2]{ last: null, practices: {} }
{"last":"a","practices":{"a":{"done":[2,0,0,9,-1,"x"],"total":3},"b":{"done":"no","total":2},"c":null}}{ last: "a", practices: { a: { done: [0, 2], total: 3 } } }

Dua baris tabel itu adalah kasus yang dijaga tests/practice-progress.test.ts: storage yang hilang atau rusak tests/practice-progress.test.ts:5-9, dan tanda hasil edit tangan yang menyisakan [0, 2] saja tests/practice-progress.test.ts:11-18.

Pada materi, parseLessonProgress menyaring slug non-string lalu mendeduplikasinya, dan parseLastLearned hanya menerima href yang lolos pola /^\/(?!\/)/ supaya nilai hasil edit tangan tidak bisa mengarahkan tombol ke situs lain src/lib/learning-progress.ts:25-41. visitLesson tidak menulis ulang apa pun bila slug sama, sedangkan toggleLessonDone menambah atau melepas slug dari done sekaligus menandainya sebagai last src/lib/learning-progress.ts:43-51.

Menjaga tanda saat jumlah bagian berubah

Tanda disimpan sebagai posisi bagian, bukan label, sehingga menyunting judul bagian tidak menghapus progres. PracticeSheet menomori bagian yang punya baris latihan secara berurutan lalu memakai jumlahnya sebagai total src/components/practice/practice-sheet.tsx:98-112.

visitPractice menyaring tanda yang kini melewati total baru: entry { done: [0, 4], total: 5 } yang dibuka sebagai latihan tiga bagian menjadi { done: [0], total: 3 } src/lib/practice-progress.ts:41-45 tests/practice-progress.test.ts:34-46. overallProgress melakukan penyaringan yang sama, sehingga latihan yang jumlah bagiannya menyusut tidak lagi menyumbang tanda usang src/lib/practice-progress.ts:70-91.

toggleSection menambah atau melepas satu indeks lalu mengurutkannya kembali, dan resetPractice mengembalikan progres apa adanya bila slug tidak dikenal src/lib/practice-progress.ts:47-53. Tombol "Tandai lancar" pada tiap bagian memanggil toggleSection dengan aria-pressed, sedangkan tombol "Ulangi dari awal" memanggil resetPractice hanya ketika sudah ada tanda src/components/practice/practice-sheet.tsx:289-307 src/components/practice/practice-sheet.tsx:335-343.

Uji yang sama memastikan dua tanda membuat status "done" dan satu tanda yang dilepas mengembalikannya ke "started" tests/practice-progress.test.ts:20-32.

Satu nilai untuk semua komponen dan semua tab

createLocalStore menyimpan satu nilai current pada lingkup modul, jadi semua komponen di halaman membaca nilai yang sama lewat useSyncExternalStore. getServerSnapshot mengembalikan empty, sehingga halaman dirender kosong di server lalu terisi setelah hydration src/components/use-local-store.ts:13-22.

subscribe juga memasang listener storage: tab lain yang menulis kunci yang sama memicu load() dan satu render ulang src/components/use-local-store.ts:24-38. Saat update dipanggil, nilai baru dihitung dari snapshot terakhir, disimpan ke localStorage, lalu semua listener diberi tahu src/components/use-local-store.ts:40-54.

⚠️ Bila localStorage.setItem gagal — kuota penuh atau storage diblokir — galatnya ditelan dan progres hanya bertahan sampai halaman ditutup src/components/use-local-store.ts:40-54. Progres juga tidak berpindah perangkat atau browser, dan menghapus data situs berarti menghapus seluruh riwayat belajar.

Ringkasan seri dan tombol lanjut

overallProgress menjumlahkan semua latihan yang terbit, bukan hanya yang pernah dibuka: totals menentukan totalSections dan totalPractices, dan percent dibulatkan dengan Math.round src/lib/practice-progress.ts:70-91. Dengan { a: 2, b: 4, c: 4 } dan progres yang hanya menyentuh a dan b, hasilnya doneSections: 3, donePractices: 1, totalPractices: 3, percent: 30 tests/practice-progress.test.ts:48-62.

OverallProgress menampilkan angka itu sebagai Keseluruhan {percent}% dan menghitung ulang persentase per bagian bila prop part diberikan src/components/practice/practice-progress-ui.tsx:91-98. ContinuePractice menautkan latihan yang tersimpan di progress.last src/components/practice/practice-progress-ui.tsx:70-85, dan StartLearningMenu memakai terakhir-belajar lebih dulu, lalu jatuh ke practiceProgress.last dan lessonProgress.last agar progres yang tersimpan sebelum halaman terakhir dicatat tetap punya tujuan src/components/start-learning-menu.tsx:28-36.