Initial commit v1
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
'use client'
|
||||
|
||||
import { strokesReceived, scoreColorClass, 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>
|
||||
onTapScore: (player: Player, hole: Hole) => void
|
||||
isActive: boolean
|
||||
}
|
||||
|
||||
export function FullGrid({ holes, players, scores, onTapScore, isActive }: Props) {
|
||||
const totalHoles = holes.length
|
||||
const front = holes.filter((h) => h.hole_number <= 9)
|
||||
const back = holes.filter((h) => h.hole_number > 9)
|
||||
const hasBoth = front.length > 0 && back.length > 0
|
||||
|
||||
function getGross(playerId: string, hole: Hole) {
|
||||
return scores[`${playerId}-${hole.id}`] ?? null
|
||||
}
|
||||
|
||||
function getNet(playerId: string, hole: Hole) {
|
||||
const gross = getGross(playerId, hole)
|
||||
if (gross === null) return null
|
||||
const ch = players.find((p) => p.user_id === playerId)?.course_handicap ?? 0
|
||||
return gross - strokesReceived(ch, hole.stroke_index, totalHoles)
|
||||
}
|
||||
|
||||
function subtotal(playerId: string, holeSet: Hole[], fn: (p: string, h: Hole) => number | null) {
|
||||
const values = holeSet.map((h) => fn(playerId, h)).filter((v): v is number => v !== null)
|
||||
return values.length === holeSet.length ? values.reduce((a, b) => a + b, 0) : null
|
||||
}
|
||||
|
||||
const parFront = front.reduce((s, h) => s + h.par, 0)
|
||||
const parBack = back.reduce((s, h) => s + h.par, 0)
|
||||
const parTotal = parFront + parBack
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full text-xs border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-muted/50 text-muted-foreground">
|
||||
<th className="sticky left-0 z-10 bg-muted/50 px-3 py-2 text-left font-medium min-w-[100px]">
|
||||
Player
|
||||
</th>
|
||||
{front.map((h) => (
|
||||
<th key={h.id} className="px-2 py-2 text-center font-medium w-8">
|
||||
{h.hole_number}
|
||||
</th>
|
||||
))}
|
||||
{hasBoth && (
|
||||
<th className="px-2 py-2 text-center font-semibold w-10 bg-muted">Out</th>
|
||||
)}
|
||||
{back.map((h) => (
|
||||
<th key={h.id} className="px-2 py-2 text-center font-medium w-8">
|
||||
{h.hole_number}
|
||||
</th>
|
||||
))}
|
||||
{hasBoth && (
|
||||
<th className="px-2 py-2 text-center font-semibold w-10 bg-muted">In</th>
|
||||
)}
|
||||
<th className="px-2 py-2 text-center font-semibold w-12 bg-muted">Tot</th>
|
||||
<th className="px-2 py-2 text-center font-bold w-12 bg-muted">Net</th>
|
||||
</tr>
|
||||
{/* Par row */}
|
||||
<tr className="bg-muted/20 text-muted-foreground border-b">
|
||||
<td className="sticky left-0 z-10 bg-muted/20 px-3 py-1 text-xs italic">Par</td>
|
||||
{front.map((h) => (
|
||||
<td key={h.id} className="px-2 py-1 text-center">
|
||||
{h.par}
|
||||
</td>
|
||||
))}
|
||||
{hasBoth && (
|
||||
<td className="px-2 py-1 text-center font-medium bg-muted">{parFront}</td>
|
||||
)}
|
||||
{back.map((h) => (
|
||||
<td key={h.id} className="px-2 py-1 text-center">
|
||||
{h.par}
|
||||
</td>
|
||||
))}
|
||||
{hasBoth && (
|
||||
<td className="px-2 py-1 text-center font-medium bg-muted">{parBack}</td>
|
||||
)}
|
||||
<td className="px-2 py-1 text-center font-medium bg-muted">{parTotal}</td>
|
||||
<td className="px-2 py-1 text-center bg-muted" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{players.map((player, pi) => {
|
||||
const frontOut = subtotal(player.user_id, front, getGross)
|
||||
const backIn = subtotal(player.user_id, back, getGross)
|
||||
const total =
|
||||
frontOut !== null && backIn !== null
|
||||
? frontOut + backIn
|
||||
: hasBoth
|
||||
? null
|
||||
: subtotal(player.user_id, holes, getGross)
|
||||
const netTotal = subtotal(player.user_id, holes, getNet)
|
||||
const netVsPar = netTotal !== null ? netTotal - parTotal : null
|
||||
|
||||
return (
|
||||
<tr key={player.user_id} className={pi % 2 === 0 ? '' : 'bg-muted/20'}>
|
||||
<td className="sticky left-0 z-10 bg-background px-3 py-2 font-medium truncate max-w-[100px]">
|
||||
{player.profile?.display_name}
|
||||
</td>
|
||||
|
||||
{front.map((h) => {
|
||||
const gross = getGross(player.user_id, h)
|
||||
const grossVsPar = gross !== null ? gross - h.par : null
|
||||
return (
|
||||
<td
|
||||
key={h.id}
|
||||
className="px-1 py-1.5 text-center"
|
||||
onClick={() => isActive && onTapScore(player, h)}
|
||||
>
|
||||
<span
|
||||
className={`flex h-6 w-6 mx-auto items-center justify-center text-xs cursor-pointer ${
|
||||
gross !== null ? scoreColorClass(grossVsPar) : 'text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
{gross ?? '·'}
|
||||
</span>
|
||||
</td>
|
||||
)
|
||||
})}
|
||||
|
||||
{hasBoth && (
|
||||
<td className="px-2 py-1.5 text-center font-semibold bg-muted/30">
|
||||
{frontOut ?? '—'}
|
||||
</td>
|
||||
)}
|
||||
|
||||
{back.map((h) => {
|
||||
const gross = getGross(player.user_id, h)
|
||||
const grossVsPar = gross !== null ? gross - h.par : null
|
||||
return (
|
||||
<td
|
||||
key={h.id}
|
||||
className="px-1 py-1.5 text-center"
|
||||
onClick={() => isActive && onTapScore(player, h)}
|
||||
>
|
||||
<span
|
||||
className={`flex h-6 w-6 mx-auto items-center justify-center text-xs cursor-pointer ${
|
||||
gross !== null ? scoreColorClass(grossVsPar) : 'text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
{gross ?? '·'}
|
||||
</span>
|
||||
</td>
|
||||
)
|
||||
})}
|
||||
|
||||
{hasBoth && (
|
||||
<td className="px-2 py-1.5 text-center font-semibold bg-muted/30">
|
||||
{backIn ?? '—'}
|
||||
</td>
|
||||
)}
|
||||
|
||||
<td className="px-2 py-1.5 text-center font-semibold bg-muted/30">
|
||||
{total ?? '—'}
|
||||
</td>
|
||||
<td className="px-2 py-1.5 text-center font-bold bg-muted/30">
|
||||
{netVsPar !== null ? formatVsPar(netVsPar) : '—'}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user