Files
MulliganMates/components/scorecard/leaderboard-view.tsx
T
2026-03-18 12:37:17 +01:00

136 lines
4.0 KiB
TypeScript

'use client'
import { strokesReceived, formatVsPar } from '@/lib/handicap'
import type { Hole, Player } from '@/app/(app)/rounds/[id]/scorecard/page'
type Props = {
holes: Hole[]
players: Player[]
scores: Record<string, number>
}
export function LeaderboardView({ holes, players, scores }: Props) {
const totalHoles = holes.length
const standings = players
.map((player) => {
let grossTotal = 0
let netTotal = 0
let holesPlayed = 0
let parPlayed = 0
for (const hole of holes) {
const gross = scores[`${player.user_id}-${hole.id}`]
if (gross !== undefined) {
const ch = player.course_handicap ?? 0
const net = gross - strokesReceived(ch, hole.stroke_index, totalHoles)
grossTotal += gross
netTotal += net
parPlayed += hole.par
holesPlayed++
}
}
const netVsPar = holesPlayed > 0 ? netTotal - parPlayed : null
const grossVsPar = holesPlayed > 0 ? grossTotal - parPlayed : null
return {
player,
grossTotal,
netTotal,
holesPlayed,
netVsPar,
grossVsPar,
}
})
.sort((a, b) => {
if (a.netVsPar === null && b.netVsPar === null) return 0
if (a.netVsPar === null) return 1
if (b.netVsPar === null) return -1
return a.netVsPar - b.netVsPar || a.grossVsPar! - b.grossVsPar!
})
return (
<div className="divide-y">
{standings.map(({ player, grossTotal, netVsPar, grossVsPar, holesPlayed }, i) => {
const isLead = i === 0 && netVsPar !== null
return (
<div
key={player.user_id}
className={`flex items-center gap-4 px-4 py-4 ${isLead ? 'bg-primary/5' : ''}`}
>
{/* Position */}
<div
className={`w-7 text-center text-sm font-bold ${
i === 0 ? 'text-primary' : 'text-muted-foreground'
}`}
>
{holesPlayed > 0 ? i + 1 : '—'}
</div>
{/* Avatar */}
<PlayerAvatar
name={player.profile?.display_name ?? '?'}
url={player.profile?.avatar_url}
/>
{/* Name + holes played */}
<div className="flex-1 min-w-0">
<div className="font-semibold truncate">
{player.profile?.display_name}
</div>
<div className="text-xs text-muted-foreground">
{holesPlayed > 0
? `${holesPlayed} of ${totalHoles} holes`
: 'No scores yet'}
{player.course_handicap !== null &&
` · HCP ${player.course_handicap}`}
</div>
</div>
{/* Net score (primary, bold) */}
<div className="flex flex-col items-end gap-0.5">
<span
className={`text-lg font-bold ${
netVsPar === null
? 'text-muted-foreground'
: netVsPar < 0
? 'text-red-500'
: netVsPar === 0
? 'text-foreground'
: 'text-muted-foreground'
}`}
>
{netVsPar !== null ? formatVsPar(netVsPar) : '—'}
</span>
{grossVsPar !== null && (
<span className="text-xs text-muted-foreground">
{grossTotal} gross
</span>
)}
</div>
</div>
)
})}
</div>
)
}
function PlayerAvatar({ name, url }: { name: string; url?: string | null }) {
const initials = name
.split(' ')
.map((n) => n[0])
.join('')
.toUpperCase()
.slice(0, 2)
if (url) {
return <img src={url} alt={name} className="h-10 w-10 rounded-full object-cover shrink-0" />
}
return (
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-primary/10 text-sm font-semibold text-primary">
{initials}
</div>
)
}