trophy-ui
uileaderboard-cardsource: ok · hit
Leaderboard Card
React leaderboard component that composes title, date range, podium, and rankings—one card for competitive gamification UIs. Fits Next.js and React apps. Built on shadcn/ui + Tailwind. Open source.
npx shadcn add "https://ui.trophy.so/r/leaderboard-card.json"free
Where it goes
kanban
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lightRegistry dependencies
leaderboard-podiumleaderboard-rankings
Files1 · source cached 10/15/2026, 4:15:43 AM
registry/trophy/ui/leaderboard-card.tsx
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
import {
LeaderboardPodium,
type LeaderboardRanking as LeaderboardPodiumRanking,
} from "@/components/ui/leaderboard-podium"
import {
LeaderboardRankings,
type LeaderboardRankingItem,
} from "@/components/ui/leaderboard-rankings"
interface LeaderboardRunOption {
id: string
label: string
}
interface LeaderboardCardProps extends React.HTMLAttributes<HTMLDivElement> {
title?: string
fromDate: string | Date
toDate: string | Date
podiumRankings: LeaderboardPodiumRanking[]
rankings: LeaderboardRankingItem[]
currentUserId?: string
runOptions?: LeaderboardRunOption[]
selectedRunId?: string
onRunChange?: (runId: string) => void
}
function formatRangeDate(date: string | Date) {
const parsed = date instanceof Date ? date : new Date(date)
if (Number.isNaN(parsed.getTime())) return ""
return parsed.toLocaleDateString(undefined, {
month: "short",
day: "numeric",
year: "numeric",
})
}
const LeaderboardCard = React.forwardRef<HTMLDivElement, LeaderboardCardProps>(
(
{
className,
title = "Leaderboard",
fromDate,
toDate,
podiumRankings,
rankings,
currentUserId,
runOptions,
selectedRunId,
onRunChange,
...props
},
ref
) => {
const fromLabel = formatRangeDate(fromDate)
const toLabel = formatRangeDate(toDate)
const resolvedRunId = selectedRunId ?? runOptions?.[0]?.id ?? ""
const hasOnRunChange = Boolean(onRunChange)
const [localRunId, setLocalRunId] = React.useState(resolvedRunId)
React.useEffect(() => {
if (hasOnRunChange) return
setLocalRunId(resolvedRunId)
}, [hasOnRunChange, resolvedRunId])
const activeRunId = hasOnRunChange ? resolvedRunId : localRunId
return (
<div
ref={ref}
className={cn("bg-card rounded-2xl border p-6 shadow-sm", className)}
{...props}
>
<div className="mb-6 flex items-start justify-between gap-4">
<div className="space-y-1">
<h3 className="text-xl font-semibold">{title}</h3>
<p className="text-muted-foreground text-sm">
{fromLabel} - {toLabel}
</p>
</div>
{runOptions && runOptions.length > 0 ? (
<select
aria-label="Select leaderboard run"
value={activeRunId}
onChange={(e) => {
if (onRunChange) {
onRunChange(e.target.value)
return
}
setLocalRunId(e.target.value)
}}
className="bg-background text-foreground rounded-md border px-3 py-1.5 text-sm"
>
{runOptions.map((option) => (
<option key={option.id} value={option.id}>
{option.label}
</option>
))}
</select>
) : null}
</div>
<LeaderboardPodium rankings={podiumRankings} className="mb-6" />
<LeaderboardRankings
rankings={rankings}
currentUserId={currentUserId}
showPagination
defaultPageSize={10}
/>
</div>
)
}
)
LeaderboardCard.displayName = "LeaderboardCard"
export { LeaderboardCard }
export type { LeaderboardCardProps, LeaderboardRunOption }