bigbullui logobigbullui
Componentdata

Score Keeper

Two-team score keeper with pulse on score change.

Preview

Live Preview

Team A

2

Team B

1

Installation

Tokens guide →

Add to your project via CLI (zero dependencies, code you own):

npx bigbullui add score-keeper

Or install the full npm package:

npm install bigbullui

Usage

import { ScoreKeeper } from "@/components/ui/score-keeper";

<ScoreKeeper />

Source code

score-keeper.tsx

Zero dependencies (only React + utils). Copy and paste directly into your project:

"use client";

import * as React from "react";
import { cn } from "./lib/utils";

export interface ScoreboardTeam {
  id: string;
  name: string;
  score: number;
}

export interface ScoreKeeperProps extends React.HTMLAttributes<HTMLDivElement> {
  teams: [ScoreboardTeam, ScoreboardTeam];
  onScoreChange?: (teamId: string, delta: number) => void;
}

/** Skor tutucu: iki takım + +1/-1 butonları + rakam değişim animasyonu. */
export function ScoreKeeper({ teams, onScoreChange, className, ...props }: ScoreKeeperProps) {
  const [scores, setScores] = React.useState<[number, number]>([teams[0].score, teams[1].score]);
  const [pulse, setPulse] = React.useState<string | null>(null);

  const change = (teamIdx: 0 | 1, delta: number) => {
    const id = teams[teamIdx].id;
    setScores((prev) => {
      const next: [number, number] = [...prev];
      next[teamIdx] = Math.max(0, next[teamIdx] + delta);
      return next;
    });
    setPulse(`${id}-${delta}-${Date.now()}`);
    onScoreChange?.(id, delta);
  };

  return (
    <div className={cn("grid w-64 grid-cols-2 overflow-hidden rounded-lg border-2 border-dashed border-border bg-card divide-x-2 divide-dashed divide-border", className)} {...props}>
      <style>{`@keyframes skPulse { 0% { transform: scale(1.25); color: var(--accent); } 100% { transform: scale(1); } }`}</style>
      {teams.map((team, teamIdx) => (
        <div key={team.id} className="flex flex-col items-center gap-1 p-3">
          <p className="font-mono text-[9px] uppercase tracking-widest text-muted-foreground">{team.name}</p>
          <span
            key={`${team.id}-${scores[teamIdx]}`}
            className={cn("font-mono text-3xl font-black tabular-nums", pulse?.startsWith(team.id) && "animate-[skPulse_0.25s_ease-out] motion-reduce:animate-none")}
          >
            {scores[teamIdx]}
          </span>
          <div className="flex gap-1">
            <button
              onClick={() => change(teamIdx as 0 | 1, -1)}
              className="flex size-6 items-center justify-center rounded-sm border border-border font-mono text-xs text-muted-foreground transition-colors hover:border-destructive hover:text-destructive focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring motion-reduce:transition-none"
              aria-label={`${team.name} skoru azalt`}
            >
              −
            </button>
            <button
              onClick={() => change(teamIdx as 0 | 1, 1)}
              className="flex size-6 items-center justify-center rounded-sm border border-border font-mono text-xs text-muted-foreground transition-colors hover:border-accent hover:text-accent focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring motion-reduce:transition-none"
              aria-label={`${team.name} skoru artır`}
            >
              +
            </button>
          </div>
        </div>
      ))}
    </div>
  );
}

Props

PropTypeDescription
teams[ScoreboardTeam, ScoreboardTeam]
onScoreChange(teamId: string, delta: number) => void