'use client' import { useEffect, useMemo, useState } from 'react' import Link from 'next/link' import { upsertScore } from '@/app/actions/scores' import { strokesReceived } from '@/lib/handicap' import { HoleView } from './hole-view' import { FullGrid } from './full-grid' import { LeaderboardView } from './leaderboard-view' import { ScoreEntrySheet } from './score-entry-sheet' import { ChevronLeft } from 'lucide-react' import type { Hole, Player, ScoreRow, RoundInfo } from '@/app/(app)/rounds/[id]/scorecard/page' type View = 'hole' | 'grid' | 'leaderboard' type EntryState = { player: Player hole: Hole } function buildScoreMap(rows: ScoreRow[]): Record { return Object.fromEntries(rows.map((r) => [`${r.playerId}-${r.holeId}`, r.strokes])) } export function ScorecardView({ round, holes, players, initialScores, currentUserId, }: { round: RoundInfo holes: Hole[] players: Player[] initialScores: ScoreRow[] currentUserId: string }) { const [scores, setScores] = useState>(buildScoreMap(initialScores)) const [activeHoleIdx, setActiveHoleIdx] = useState(0) const [view, setView] = useState('hole') const [entry, setEntry] = useState(null) const [saving, setSaving] = useState(false) const isActive = round.status === 'active' const activeHole = holes[activeHoleIdx] // ── SSE real-time subscription ───────────────────────────── useEffect(() => { const es = new EventSource(`/api/rounds/${round.id}/stream`) es.onmessage = (event) => { try { const rows: ScoreRow[] = JSON.parse(event.data) setScores(buildScoreMap(rows)) } catch { // ignore parse errors } } return () => es.close() }, [round.id]) // ── Running totals per player ────────────────────────────── const runningTotals = useMemo(() => { const totals: Record = {} for (const player of players) { let net = 0 let gross = 0 let played = 0 const ch = player.courseHandicap ?? 0 for (const hole of holes) { const g = scores[`${player.userId}-${hole.id}`] if (g !== undefined) { gross += g net += g - strokesReceived(ch, hole.strokeIndex, holes.length) played++ } } totals[player.userId] = { net, gross, holesPlayed: played } } return totals }, [scores, players, holes]) // ── Score save with optimistic update ───────────────────── async function handleSave(strokes: number) { if (!entry) return setSaving(true) const key = `${entry.player.userId}-${entry.hole.id}` const prev = scores[key] // Optimistic update setScores((s) => ({ ...s, [key]: strokes })) setEntry(null) const result = await upsertScore(round.id, entry.hole.id, entry.player.userId, strokes) if (result.error) { // Revert setScores((s) => { const next = { ...s } if (prev === undefined) delete next[key] else next[key] = prev return next }) } setSaving(false) } function openEntry(player: Player, hole?: Hole) { if (!isActive) return setEntry({ player, hole: hole ?? activeHole }) } return (
{/* Header */}
{round.name ?? round.course?.name ?? 'Scorecard'}
{round.tee?.name} tees · {round.holesCount} holes {!isActive && ( (completed) )}
{saving && ( Saving… )}
{/* View tabs */}
{(['hole', 'grid', 'leaderboard'] as View[]).map((v) => ( ))}
{/* Main content */}
{view === 'hole' && activeHole && ( setActiveHoleIdx((i) => Math.max(0, i - 1))} onNext={() => setActiveHoleIdx((i) => Math.min(holes.length - 1, i + 1))} onTapScore={(player) => openEntry(player)} /> )} {view === 'grid' && (
openEntry(player, hole)} />
)} {view === 'leaderboard' && (
)}
{/* Score entry sheet */} setEntry(null)} />
) }