Proyek pencarian dan filter menutup satu era dalam course ini: seluruh datanya — 22 buku fiktif — sudah tinggal di dalam browser, tinggal disaring. Dashboard data membuka era berikutnya: angka yang ditampilkan tidak lagi berasal dari satu berkas, melainkan dari tiga sumber yang berbeda. Dashboard sebuah toko biasanya merangkum statistik kunjungan, daftar transaksi terbaru, dan alur aktivitas; ketiganya hidup di endpoint yang berbeda, datang dengan kecepatan yang tidak sama, dan bisa saja salah satunya gagal di tengah jalan. Ada pula tuntutan yang khas dashboard: angka mentah seperti 3420 dan 34500000 harus berubah menjadi 3,4 rb, 34,5 jt, dan bar yang bisa dibaca sekilas.

Empat tantangan itu dijawab bertahap dalam satu produk utuh: dashboard toko dengan tiga kartu ringkas, bar teks, tabel transaksi, dan daftar aktivitas. Pendekatan utamanya sama dengan keempat proyek sebelumnya — pemisahan logika dari tampilan. Seluruh keputusan data hidup di src/dashboard.mjs, module murni tanpa satu pun document: memuat data secara paralel, mengelola state dengan aliran satu arah, memformat angka, dan melaporkan kegagalan per sumber. Pemuat data disuntikkan dari luar — pola dependency injection — sehingga seluruh logika jaringan teruji lewat node --test tanpa satu pun permintaan internet. Berkas HTML hanya menampilkan state dan meneruskan peristiwa ke logika.

Proyek dikerjakan dalam empat bagian berurutan:

  • Bagian 1: Mengambil Data — menulis muatDashboard(pemuat) dengan Promise.all: tiga pemuat dipanggil sekaligus, hasilnya dikumpulkan sesuai urutan permintaan. Demo membandingkan pemuatan berurutan dan paralel lewat urutan peristiwa, bukan angka waktu, dan tiga uji pertama mengunci perilakunya — termasuk sifat Promise.all yang menolak keseluruhan muatan ketika satu sumber gagal. Kita belajar dependency injection: pemuat nyata boleh memanggil fetch, uji memasang pemuat tiruan.
  • Bagian 2: Mengelola State — menyimpan seluruh wujud dashboard dalam satu object yang dibuat oleh buatState, dengan tiga pintu perubahan murni: setState, patchState, dan terapkanData. Setiap perubahan menghasilkan state baru; state lama tidak pernah tersentuh — aliran satu arah sebagaimana dibahas pada chapter State Management Fundamentals. Uji kumulatif bertambah menjadi sembilan.
  • Bagian 3: Visualisasi Data — merapikan angka mentah dengan formatter murni: ringkasAngka berbasis Intl.NumberFormat compact untuk locale id-ID, formatPersen, dan buatBar yang menggambar bar teks ASCII. Antarmuka lengkapnya dirakit dari kartu ringkas, tabel transaksi, dan daftar aktivitas yang dirender sekali tempel lewat DocumentFragment. Uji kumulatif mencapai tujuh belas.
  • Bagian 4: Error Handling dan Testing — mengganti kegagalan “semua atau tidak sama sekali” dengan laporan per sumber: ApiError yang membawa nama sumbernya, muatDashboardToleran berbasis Promise.allSettled, dan terapkanLaporan yang memilih keadaan siap, sebagian, atau galat. Bagian ini juga meninjau seluruh strategi pengujian proyek dan menutupnya dengan keluaran node --test lengkap: dua puluh empat uji dalam sebelas suite.

Struktur Berkas Proyek #

Seluruh kode proyek tinggal di tiga folder pada content/javascript/proyek/dashboard-data/:

text
dashboard-data/
├── src/
│   ├── dashboard.mjs      # logika inti: muat paralel, state, formatter, laporan galat
│   └── index.html.txt     # contoh antarmuka (salin ulang sebagai index.html)
├── styles/
│   └── styles.css         # tampilan dashboard: kartu, bar, tabel, panel
└── tests/
    └── dashboard.test.mjs # uji otomatis untuk dashboard.mjs

Peran tiap berkas:

BerkasPeran
src/dashboard.mjsOtak dashboard: muatDashboard paralel, state satu pintu, formatter Intl, dan laporan kegagalan per sumber. Tidak menyentuh DOM.
src/index.html.txtWajah aplikasi: kartu ringkas dengan bar, tabel transaksi, daftar aktivitas, dan skrip penghubung ke logika.
styles/styles.cssPenampil: grid kartu, bar monospace, tabel, serta warna status dan daftar kegagalan.
tests/dashboard.test.mjsPenjaga: dua puluh empat uji otomatis dalam sebelas suite yang dijalankan test runner bawaan Node.

Setiap bagian menampilkan kondisi berkas pada akhir bagian tersebut: blok kode yang membahas sebuah berkas menyediakan tautan unduhan salinannya di header blok. Versi lengkap dan terakhir dari seluruh berkas selalu ada di folder src/, styles/, dan tests/ di atas — jika Anda mengikuti proyek dari Bagian 1, berkas-berkas itu tumbuh bertahap mengikuti pembahasan.

Menjalankan Pengujian #

Uji otomatis dijalankan dari folder proyek dengan test runner bawaan Node.js:

bash
cd dashboard-data
node --test

Node mencari berkas uji dengan pola nama seperti *.test.mjs di seluruh pohon folder proyek. Folder course ini memuat berkas uji yang sama persis di dua tempat — tests/dashboard.test.mjs dan salinan unduhannya di folder Bagian 4 — sehingga rangkaian dua puluh empat uji dijalankan dua kali. Ringkasan di ujung keluarannya berbunyi:

text
ℹ tests 48
ℹ suites 22
ℹ pass 48
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 316.662852

Angka pass 48 adalah dua kali dua puluh empat uji dari dua salinan berkas uji yang identik — keduanya lulus semua. Bila folder Anda hanya berisi src/, styles/, dan tests/ seperti struktur di atas, perintah yang sama melaporkan tests 24, suites 11, pass 24; selisihnya murni berapa banyak salinan berkas uji yang ditemukan, bukan perbedaan perilaku. duration_ms akan berbeda-beda di mesin Anda karena mengukur waktu eksekusi. Bila ingin menyasar satu berkas uji secara eksplisit, tulis nama berkasnya utuh: node --test tests/dashboard.test.mjs.

Mulailah dari Bagian 1 dan ikuti urutannya — setiap bagian dibangun di atas bagian sebelumnya. Pada bagian akhir, dashboard kita berdiri utuh: datanya dimuat paralel, keadaannya bergerak lewat satu pintu, angkanya siap dibaca, dan kegagalannya dilaporkan per sumber, bukan ditelan bersama semuanya.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

styles/styles.css

Unduh berkas
css
/* styles.css — tampilan dashboard data */

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 24px 16px;
  background-color: #f3f4f6;
  font-family: system-ui, sans-serif;
  color: #111827;
}

.dashboard {
  max-width: 880px;
  margin: 0 auto;
}

.dashboard-atas {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 16px;
}

.dashboard-atas h1 {
  margin: 0;
  font-size: 1.5rem;
}

.tombol-muat {
  padding: 8px 16px;
  border: none;
  border-radius: 8px;
  background-color: #2563eb;
  color: #ffffff;
  font-size: 0.95rem;
  cursor: pointer;
}

.tombol-muat:hover {
  background-color: #1d4ed8;
}

.status {
  margin: 0 0 16px;
  padding: 10px 12px;
  background-color: #e5e7eb;
  border-radius: 8px;
}

.daftar-gagal {
  margin: 0 0 16px;
  padding: 10px 12px 10px 28px;
  background-color: #fee2e2;
  color: #991b1b;
  border-radius: 8px;
}

.daftar-gagal:empty {
  display: none;
}

.kartu {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 12px;
  margin-bottom: 16px;
}

.kartu-item {
  padding: 14px;
  background-color: #ffffff;
  border-radius: 12px;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
}

.kartu-item h2 {
  margin: 0 0 8px;
  font-size: 0.85rem;
  font-weight: 600;
  color: #6b7280;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.kartu-angka {
  margin: 0 0 8px;
  font-size: 1.6rem;
  font-weight: 700;
}

.kartu-bar {
  margin: 0;
  font-family: monospace;
  font-size: 0.9rem;
  color: #2563eb;
  letter-spacing: 1px;
}

.konversi {
  margin: 0 0 16px;
  color: #6b7280;
}

.panel {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
}

.panel-item {
  padding: 14px;
  background-color: #ffffff;
  border-radius: 12px;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
}

.panel-item h2 {
  margin: 0 0 10px;
  font-size: 1rem;
}

.tabel-transaksi {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.9rem;
}

.tabel-transaksi th,
.tabel-transaksi td {
  padding: 6px 8px;
  text-align: left;
  border-bottom: 1px solid #e5e7eb;
}

.tabel-transaksi th {
  color: #6b7280;
  font-weight: 600;
}

.daftar-aktivitas {
  margin: 0;
  padding-left: 18px;
  font-size: 0.9rem;
}

.daftar-aktivitas li {
  margin-bottom: 6px;
  color: #374151;
}