136 lines
4.0 KiB
TypeScript
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.userId}-${hole.id}`]
|
|
if (gross !== undefined) {
|
|
const ch = player.courseHandicap ?? 0
|
|
const net = gross - strokesReceived(ch, hole.strokeIndex, 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.userId}
|
|
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?.displayName ?? '?'}
|
|
url={player.profile?.avatarUrl}
|
|
/>
|
|
|
|
{/* Name + holes played */}
|
|
<div className="flex-1 min-w-0">
|
|
<div className="font-semibold truncate">
|
|
{player.profile?.displayName}
|
|
</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
{holesPlayed > 0
|
|
? `${holesPlayed} of ${totalHoles} holes`
|
|
: 'No scores yet'}
|
|
{player.courseHandicap !== null &&
|
|
` · HCP ${player.courseHandicap}`}
|
|
</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>
|
|
)
|
|
}
|