Situs ini dikemudikan dua tombol, seperti mesin pemiliknya. Membaca: j dan k menggulir, d dan u setengah layar, gg ke atas, G ke bawah, H dan L pindah jendela, ? membuka panduan. Spasi adalah kunci leader Neovim dan mengurus isi: Spasi lalu h beranda, r riset, p proyek, g gear, a tentang, / tag, atau Spasi lalu angka untuk melompat ke jendela bernomor itu. Home adalah prefix tmux dan mengurus jendela: Home lalu c membuka terminal, Home lalu & menutup jendela, Home lalu spasi ke jendela berikutnya. Saat fokus ada di papan ketik, h j k l memindahkan pilihan antar tombol dan Enter membukanya.

RisetR TentangA Gear — G untuk gear — papan ketik, terminal, editor, homelab.G
    ProyekP Tag — / seperti di vim: cari./
    ×
    Menu

    Marcom: Dashboard Materi Komunikasi Pemasaran

    Dashboard internal untuk melacak materi komunikasi pemasaran per brand, cluster, fitur, dan jenis — lengkap dengan pengingat masa berlaku dan update realtime. Next.js 15 + React 19 di depan, Bun + Elysia di belakang.

    Marcom itu dashboard internal buat mengelola materi komunikasi pemasaran: satu materi punya brand, cluster, fitur, jenis, tanggal mulai, dan tanggal berakhir. Masalah yang mau diselesaikan sederhana tapi menyebalkan kalau dikerjakan manual — materi yang masa berlakunya habis harus ketahuan sebelum habis, bukan sesudah.

    Repo ini bagian depan (Next.js). Bagian belakangnya ada di marcom_services, Bun + Elysia + MySQL, dan keduanya dinyalakan bareng lewat repo orkestrator pakai git submodule + Docker Compose.

    Empat sumbu, satu tabel

    Semua yang bisa disaring di dashboard turun dari empat dimensi master data yang sama:

    // constants/filter-options.ts
    export type FilterKey = "brand" | "cluster" | "fitur" | "status" | "jenis";

    Konsekuensinya di kode: ada master-data.store.ts yang memegang keempat daftar itu, dan filter-materi.store.ts yang memegang pilihan yang sedang aktif. Tabel materi tidak pernah memfilter sendiri — dia baca hasil dari useFilteredMateri. Waktu ada permintaan tambah dimensi baru, yang berubah cuma dua tempat itu.

    Rentang tanggal punya preset sendiri, karena 90% pemakaian cuma butuh empat pilihan itu:

    export enum PresetDate {
      ALL_TIME = "All time",
      THIS_MONTH = "Bulan ini",
      LAST_MONTH = "Bulan lalu",
      THIS_YEAR = "Tahun ini",
      CUSTOM = "Pilih tanggal tertentu",
    }

    Pengingat kedaluwarsa

    Ini bagian yang paling banyak dipikir ulang. Backend punya expiryService yang menarik materi yang akan habis, dan kondisinya ditulis langsung di SQL:

    WHERE COALESCE(m.notification_count, 0) < 1
      AND m.end_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL ? DAY)

    notification_count yang menjaga supaya orang tidak dikirimi email yang sama tiap hari sampai materinya benar-benar kedaluwarsa. Cron-nya bukan proses terpisah — dia endpoint HTTP di cronController yang dijaga cronAuth, jadi penjadwalannya bisa dititipkan ke apa pun yang sudah ada dan servicenya tetap satu binary.

    Realtime, tapi seperlunya

    Socket.IO jalan di port sendiri (SOCKET_IO, default 5002) terpisah dari HTTP API (PORT, default 5001). Di frontend cuma ada satu useSocket, dipakai supaya daftar materi yang sedang dibuka ikut berubah kalau ada orang lain yang menyimpan perubahan. Sisanya tetap request biasa lewat TanStack Query — tidak semua hal perlu jadi stream.

    Susunan frontend

    BagianIsi
    app/dashboardtabel materi, stats, chart, filter (materi + tanggal), form materi [id]
    app/master-dataCRUD brand / cluster / fitur / jenis
    app/usersmanajemen pengguna
    stores/Zustand: auth, materi, filter, master data, activity log
    hooks/useFilteredMateri, useStatsData, useDateRange, useDebounce, useAutoLogout, useSocket

    useAutoLogout ada karena dashboard ini sering ditinggal terbuka di layar bersama. Sesi yang menganggur terlalu lama dilepas sendiri, tidak menunggu token kedaluwarsa.

    Yang dipelajari

    Memecah frontend dan backend jadi dua repo lalu menyatukannya lewat submodule bikin masing-masing punya siklus rilis sendiri, tapi harganya adalah setiap orang baru harus ingat --recurse-submodules. Kalau lupa, foldernya kosong dan Compose gagal dengan pesan yang tidak menjelaskan apa-apa. Itu satu-satunya langkah yang sampai sekarang masih ditulis tebal di README.

    id en
    rss gh in