Componentticket-stub1.14 KB gzipZero dependencies

Medal Display

Medal board with gold/silver/bronze tiers and shine sweep.

Preview

Live Preview
Champion×3

Installation

Tokens guide →

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

npx bigbullui add medal-display

Or install the full npm package:

npm install bigbullui
import { MedalDisplay } from "@/components/ui/medal-display";

<MedalDisplay />

Usage guidance

Use Medal Display when a ticket stub surface needs Medal board with gold/silver/bronze tiers and shine sweep. Keep props minimal and prefer composition with sibling stubs.

Avoid Medal Display for long form flows or dense data grids where a dedicated layout block fits better.

Source code

medal-display.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 MedalDisplayEntry {
  id: string;
  label: string;
  tier: "gold" | "silver" | "bronze";
  count?: number;
}

export interface MedalDisplayProps extends React.HTMLAttributes<HTMLDivElement> {
  medals: MedalDisplayEntry[];
}

const TIER = {
  gold: { bg: "bg-amber-400/20", border: "border-amber-400/60", text: "text-amber-600", rank: "1" },
  silver: { bg: "bg-slate-400/15", border: "border-slate-400/50", text: "text-slate-500", rank: "2" },
  bronze: { bg: "bg-orange-400/15", border: "border-orange-400/50", text: "text-orange-600", rank: "3" },
};

/** Medal showcase: gold/silver/bronze tiers + shine overlay. */
export function MedalDisplay({ medals, className, ...props }: MedalDisplayProps) {
  return (
    <div className={cn("w-full max-w-sm space-y-2", className)} {...props}>
      <style>{`@keyframes mdShine { 0% { transform: translateX(-120%); } 100% { transform: translateX(220%); } }`}</style>
      {medals.map((m, idx) => {
        const t = TIER[m.tier];
        return (
          <div
            key={m.id}
            className={cn(
              "relative flex items-center gap-3 overflow-hidden rounded-lg border p-3",
              t.bg, t.border
            )}
            style={{ animation: `fade-in-up 0.35s ease-out both`, animationDelay: `${idx * 90}ms` }}
          >
            <span className="relative text-2xl" aria-hidden="true">
              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="size-6">
                <path d="M8.5 2.5 12 8l3.5-5.5" />
                <circle cx="12" cy="14.5" r="6" />
                <circle cx="12" cy="14.5" r="4" strokeDasharray="2 1.6" />
                <text x="12" y="17" textAnchor="middle" fontSize="7" fontWeight="700" fill="currentColor" stroke="none" fontFamily="monospace">{t.rank}</text>
              </svg>
              <span className="absolute inset-0 overflow-hidden">
                <span className="absolute inset-y-0 w-4 bg-white/30" style={{ animation: "mdShine 2.8s ease-in-out infinite", animationDelay: `${idx * 300}ms` }} />
              </span>
            </span>
            <span className="min-w-0 flex-1">
              <span className={cn("block truncate text-sm font-semibold", t.text)}>{m.label}</span>
            </span>
            {m.count !== undefined && (
              <span className={cn("shrink-0 font-mono text-lg font-bold tabular-nums", t.text)}>×{m.count}</span>
            )}
          </div>
        );
      })}
    </div>
  );
}

Props

PropTypeDescription
medalsMedalDisplayEntry[]