Cara membuat scoreboard sepak bola live dengan Next.js

Table of Contents
Timnas putri Indonesia bermain imbang 0-0 melawan Yordania di Stadion Aqaba Development, Aqaba, pada 13 Oktober 2026, seperti dilaporkan ANTARA. Kalau laga seperti itu ditayangkan di nonton bareng, turnamen kampung, atau acara kantor, skor di layar sebaiknya langsung berubah begitu ada gol, tanpa ada yang menekan tombol reload.
Hasil akhir: scoreboard yang update tanpa reload
Di akhir tulisan ini, kamu punya dua halaman. Halaman utama menampilkan skor besar untuk layar TV atau proyektor. Halaman admin berisi tombol untuk menambah atau mengurangi gol, dan perubahan dari admin muncul di layar dalam hitungan detik.
Data contoh di tulisan ini adalah final FIFA ASEAN Cup 2026 pada 5 Oktober 2026: Indonesia vs Thailand 2-2 (4-2 adu penalti), seperti dilaporkan tirto.id. Skor ini hanya data contoh, jadi kamu bisa menggantinya dengan pertandingan apa pun.
Kapan cukup Excel atau OBS, kapan perlu web app
Tidak semua acara butuh aplikasi web. Pilih alat yang paling sederhana untuk kebutuhan kamu.

| Kebutuhan | Alat yang cukup |
|---|---|
| Satu layar, satu operator di laptop yang sama | Excel, PowerPoint, atau papan skor fisik |
| Skor tampil di siaran live streaming | Overlay di software siaran seperti OBS |
| Skor diubah dari HP, tampil di layar lain | Web app seperti di tulisan ini |
| Banyak pertandingan, akun panitia, riwayat skor | Web app dengan database dan login |
Di sini saya fokus ke baris ketiga. Baris keempat butuh database dan login, dan dibahas di bagian akhir.
Yang kamu perlukan
- Node.js terpasang di komputer kamu.
- Dasar React: komponen, state, dan useEffect.
- Editor kode dan terminal.
Kode di tulisan ini dites pada 7 Oktober 2026 memakai Next.js 16.4.0 dan Node.js 20.19.2.
Langkah 1: buat project Next.js
Jalankan perintah berikut di terminal. Flag-nya menjawab pertanyaan setup: TypeScript, App Router, tanpa folder src.
npx create-next-app@latest scoreboard --ts --app --eslint --no-tailwind --no-src-dir --import-alias "@/*" --use-npm --yes
cd scoreboard
Biarkan app/page.tsx bawaan dulu. Isinya kamu ganti di langkah 4. Di app/layout.tsx, ganti lang="en" menjadi lang="id", supaya browser dan pembaca layar tahu bahasa halamannya.
Langkah 2: simpan skor di server
Buat file lib/score-store.ts. File ini menyimpan skor di memori server dan menyediakan fungsi untuk membaca dan mengubahnya.
export type Score = {
home: string;
away: string;
homeGoals: number;
awayGoals: number;
homePens: number | null;
awayPens: number | null;
status: string;
updatedAt: string;
};
// Data contoh: final FIFA ASEAN Cup 2026 menurut tirto.id.
const initialScore: Score = {
home: "Indonesia",
away: "Thailand",
homeGoals: 2,
awayGoals: 2,
homePens: 4,
awayPens: 2,
status: "Selesai (adu penalti)",
updatedAt: new Date().toISOString(),
};
// Disimpan di memori server. Cukup untuk belajar di komputer sendiri.
const globalStore = globalThis as unknown as { score?: Score };
export function getScore(): Score {
if (!globalStore.score) globalStore.score = { ...initialScore };
return globalStore.score;
}
export function updateScore(changes: Partial<Score>): Score {
const next = { ...getScore(), ...changes, updatedAt: new Date().toISOString() };
globalStore.score = next;
return next;
}
Penyimpanan di memori cukup untuk belajar di komputer sendiri. Skor kembali ke data awal setiap kali server dinyalakan ulang.
Langkah 3: buat API skor
Buat file app/api/score/route.ts. GET mengirim skor terbaru. POST mengubah skor, tetapi hanya kalau token admin cocok dan datanya valid.
import { getScore, updateScore, type Score } from "@/lib/score-store";
export async function GET() {
return Response.json(getScore(), {
headers: { "Cache-Control": "no-store" },
});
}
const numberFields = ["homeGoals", "awayGoals", "homePens", "awayPens"] as const;
function isCount(value: unknown) {
return Number.isInteger(value) && (value as number) >= 0;
}
export async function POST(request: Request) {
const token = request.headers.get("x-admin-token");
if (!process.env.ADMIN_TOKEN || token !== process.env.ADMIN_TOKEN) {
return Response.json({ error: "Tidak diizinkan" }, { status: 401 });
}
let body: Record<string, unknown>;
try {
body = await request.json();
} catch {
return Response.json({ error: "Data tidak valid" }, { status: 400 });
}
if (typeof body !== "object" || body === null || Array.isArray(body)) {
return Response.json({ error: "Data tidak valid" }, { status: 400 });
}
const changes: Partial<Score> = {};
for (const field of numberFields) {
if (!(field in body)) continue;
const value = body[field];
const pensField = field === "homePens" || field === "awayPens";
if (isCount(value) || (pensField && value === null)) {
changes[field] = value as number;
} else {
return Response.json({ error: `${field} harus angka 0 atau lebih` }, { status: 400 });
}
}
if ("status" in body) {
if (typeof body.status !== "string") {
return Response.json({ error: "status harus teks" }, { status: 400 });
}
changes.status = body.status.slice(0, 60);
}
return Response.json(updateScore(changes));
}
Simpan token di file .env.local, misalnya ADMIN_TOKEN=ganti-dengan-token-panjang. Jangan masukkan file ini ke Git. API hanya menerima gol dan penalti berupa angka 0 atau lebih, plus status berupa teks. Data lain ditolak dengan kode 400, dan nama tim tidak bisa diganti lewat API.
Langkah 4: halaman skor yang update sendiri
Buat file app/scoreboard.tsx. Komponen ini mengambil skor dari API saat halaman dibuka, lalu mengulanginya setiap 2 detik.
"use client";
import { useEffect, useState } from "react";
import type { Score } from "@/lib/score-store";
export default function Scoreboard() {
const [score, setScore] = useState<Score | null>(null);
useEffect(() => {
async function load() {
const res = await fetch("/api/score", { cache: "no-store" });
if (res.ok) setScore(await res.json());
}
load();
const timer = setInterval(load, 2000);
return () => clearInterval(timer);
}, []);
if (!score) return <main className="board">Memuat skor...</main>;
const hasPens = score.homePens !== null && score.awayPens !== null;
return (
<main className="board">
<p className="status">{score.status}</p>
<div className="teams">
<span>{score.home}</span>
<strong>
{score.homeGoals} - {score.awayGoals}
</strong>
<span>{score.away}</span>
</div>
{hasPens && (
<p className="pens">
Adu penalti: {score.homePens} - {score.awayPens}
</p>
)}
</main>
);
}
Lalu ganti isi app/page.tsx dengan kode berikut.
import Scoreboard from "./scoreboard";
export default function Home() {
return <Scoreboard />;
}
Halaman utama hanya berisi kerangka. Skor diambil oleh browser, sehingga halaman bisa disiapkan sekali dan isinya tetap baru.
Langkah 5: halaman admin
Buat file app/admin/page.tsx. Operator membuka halaman ini di HP, memasukkan token, lalu menekan tombol +1 atau -1.
"use client";
import { useState } from "react";
type Field = "homeGoals" | "awayGoals" | "homePens" | "awayPens";
export default function Admin() {
const [token, setToken] = useState("");
const [message, setMessage] = useState("");
async function change(field: Field, delta: number) {
const current = await fetch("/api/score", { cache: "no-store" }).then((r) => r.json());
const value = Math.max(0, (current[field] ?? 0) + delta);
const res = await fetch("/api/score", {
method: "POST",
headers: { "Content-Type": "application/json", "x-admin-token": token },
body: JSON.stringify({ [field]: value }),
});
setMessage(res.ok ? `Tersimpan: ${field} = ${value}` : "Token salah");
}
const fields: [Field, string][] = [
["homeGoals", "Gol tuan rumah"],
["awayGoals", "Gol tamu"],
["homePens", "Penalti tuan rumah"],
["awayPens", "Penalti tamu"],
];
return (
<main className="admin">
<h1>Admin skor</h1>
<input
type="password"
placeholder="Token admin"
value={token}
onChange={(e) => setToken(e.target.value)}
/>
{fields.map(([field, label]) => (
<div key={field} className="row">
<span>{label}</span>
<button onClick={() => change(field, -1)}>-1</button>
<button onClick={() => change(field, 1)}>+1</button>
</div>
))}
<p>{message}</p>
</main>
);
}
Halaman admin ini sengaja sederhana. Siapa pun yang tahu token bisa mengubah skor, jadi pakai token yang panjang dan ganti setelah acara.
Langkah 6: tampilan untuk layar besar
Ganti isi app/globals.css dengan gaya berikut. Ukuran huruf memakai clamp, jadi skor tetap terbaca di HP maupun TV.
body { margin: 0; font-family: system-ui, sans-serif; background: #0b1220; color: #fff; }
.board { min-height: 100vh; display: grid; place-content: center; text-align: center; gap: 1rem; }
.teams { display: flex; gap: 2rem; align-items: center; font-size: clamp(1.5rem, 5vw, 3rem); }
.teams strong { font-size: clamp(3rem, 12vw, 8rem); }
.status, .pens { font-size: clamp(1rem, 3vw, 1.75rem); opacity: 0.85; }
.admin { max-width: 420px; margin: 2rem auto; display: grid; gap: 0.75rem; }
.admin .row { display: flex; gap: 0.5rem; align-items: center; justify-content: space-between; }
.admin button, .admin input { font-size: 1rem; padding: 0.5rem 1rem; }
Jalankan npm run dev, buka http://localhost:3000 di satu tab dan http://localhost:3000/admin di tab lain. Ubah skor di admin, lalu lihat layar skor berubah sendiri. Untuk acara, jalankan npm run build lalu npm start, supaya yang berjalan adalah versi produksi.
Sebelum online: skor perlu tempat simpan bersama
Di komputer sendiri, satu server menyimpan skor di memori. Di hosting serverless seperti Vercel, permintaan bisa dilayani oleh beberapa instance berbeda, dan memori tidak dibagi antar-instance. Akibatnya, layar skor bisa membaca angka lama.
Untuk versi online, ganti isi getScore dan updateScore dengan penyimpanan bersama, misalnya database atau key-value store. Bagian lain dari kode tidak perlu berubah. Di sini saya berhenti di versi lokal, jadi langkah itu tidak dibahas.
Masalah umum dan solusinya
| Masalah | Sebab | Solusi |
|---|---|---|
| Skor terlambat sampai 2 detik | Interval polling | Perkecil interval, atau pakai server-sent events |
| Dua admin mengubah skor bersamaan | Admin membaca lalu menulis angka | Untuk acara kecil, tunjuk satu operator |
| Skor kembali ke awal | Server dinyalakan ulang | Pakai penyimpanan bersama seperti di bagian sebelumnya |
| Token salah terus | .env.local belum dibaca | Nyalakan ulang npm run dev setelah mengubah file |
Kapan perlu developer
Scoreboard ini cukup untuk satu pertandingan dengan satu operator. Kalau kamu butuh banyak pertandingan sekaligus, akun panitia, riwayat skor, atau tampilan untuk sponsor, kamu butuh aplikasi web yang dirancang dari awal.
Panitia nonton bareng, misalnya, bisa saja menayangkan beberapa laga timnas dalam sepekan seperti laga di awal tulisan, dan di situ kebutuhan seperti riwayat skor mulai terasa.
Pertanyaan soal membuat scoreboard
Bisakah membuat scoreboard tanpa coding?
Bisa, karena Excel, PowerPoint, papan skor fisik, atau overlay di software siaran sudah cukup untuk banyak acara. Pilihan ini cocok kalau skor diubah dan ditampilkan dari satu komputer yang sama. Kamu baru perlu kode seperti di tulisan ini kalau skor harus diubah dari HP dan tampil di layar lain tanpa reload.
Bagaimana menampilkan scoreboard ini di TV?
Buka alamat halaman skor di browser pada laptop yang tersambung ke TV atau proyektor, lalu pakai mode layar penuh. Ukuran huruf di langkah 6 menyesuaikan lebar layar, jadi skor tetap besar di layar lebar. Kalau server berjalan di laptop, HP admin harus memakai Wi-Fi yang sama dan membuka alamat “Network” yang dicetak di terminal.
Kenapa skor di layar terlambat beberapa detik?
Layar skor mengambil data baru setiap 2 detik, jadi perubahan dari admin bisa muncul sampai 2 detik kemudian. Kamu bisa memperkecil interval di setInterval, tapi permintaan ke server jadi lebih banyak. Untuk acara besar dengan banyak layar yang terbuka bersamaan, server-sent events atau layanan real-time lebih cocok daripada polling setiap beberapa detik.
Langkah berikutnya
Alat yang paling sederhana biasanya sudah cukup, dan enam langkah di atas baru diperlukan kalau skor harus diubah dari HP lalu tampil di layar lain. Sebelum hari pertandingan, jalankan scoreboard ini di komputer sendiri dan coba ubah skornya dari HP lewat Wi-Fi yang sama.
Untuk mengenal framework yang dipakai, baca penjelasan Next.js untuk pemilik bisnis . Kalau kamu butuh aplikasi web untuk turnamen atau komunitas, lihat jasa pembuatan website Next.js saya, atau baca dulu tentang layanan Next.js development.
Kirim kebutuhan acara kamu lewat halaman kontak. Saya balas dalam 24 jam.
Terakhir dicek: 14 Oktober 2026.
Need this built instead of read about?
Send me your project. I reply within 24 hours and tell you plainly whether I’m the right fit.
Send me your project

