Initial commit v1.1
This commit is contained in:
@@ -1,9 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { createClient } from '@/lib/supabase/client'
|
||||
import { upsertScore } from '@/app/actions/scores'
|
||||
import { strokesReceived } from '@/lib/handicap'
|
||||
import { HoleView } from './hole-view'
|
||||
@@ -21,7 +19,7 @@ type EntryState = {
|
||||
}
|
||||
|
||||
function buildScoreMap(rows: ScoreRow[]): Record<string, number> {
|
||||
return Object.fromEntries(rows.map((r) => [`${r.player_id}-${r.hole_id}`, r.strokes]))
|
||||
return Object.fromEntries(rows.map((r) => [`${r.playerId}-${r.holeId}`, r.strokes]))
|
||||
}
|
||||
|
||||
export function ScorecardView({
|
||||
@@ -37,7 +35,6 @@ export function ScorecardView({
|
||||
initialScores: ScoreRow[]
|
||||
currentUserId: string
|
||||
}) {
|
||||
const router = useRouter()
|
||||
const [scores, setScores] = useState<Record<string, number>>(buildScoreMap(initialScores))
|
||||
const [activeHoleIdx, setActiveHoleIdx] = useState(0)
|
||||
const [view, setView] = useState<View>('hole')
|
||||
@@ -47,33 +44,20 @@ export function ScorecardView({
|
||||
const isActive = round.status === 'active'
|
||||
const activeHole = holes[activeHoleIdx]
|
||||
|
||||
// ── Realtime subscription ──────────────────────────────────
|
||||
// ── SSE real-time subscription ─────────────────────────────
|
||||
useEffect(() => {
|
||||
const supabase = createClient()
|
||||
const channel = supabase
|
||||
.channel(`scores:${round.id}`)
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{
|
||||
event: '*',
|
||||
schema: 'public',
|
||||
table: 'scores',
|
||||
filter: `round_id=eq.${round.id}`,
|
||||
},
|
||||
(payload) => {
|
||||
if (payload.eventType === 'DELETE') return
|
||||
const row = payload.new as ScoreRow
|
||||
setScores((prev) => ({
|
||||
...prev,
|
||||
[`${row.player_id}-${row.hole_id}`]: row.strokes,
|
||||
}))
|
||||
},
|
||||
)
|
||||
.subscribe()
|
||||
const es = new EventSource(`/api/rounds/${round.id}/stream`)
|
||||
|
||||
return () => {
|
||||
supabase.removeChannel(channel)
|
||||
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 ──────────────────────────────
|
||||
@@ -83,16 +67,16 @@ export function ScorecardView({
|
||||
let net = 0
|
||||
let gross = 0
|
||||
let played = 0
|
||||
const ch = player.course_handicap ?? 0
|
||||
const ch = player.courseHandicap ?? 0
|
||||
for (const hole of holes) {
|
||||
const g = scores[`${player.user_id}-${hole.id}`]
|
||||
const g = scores[`${player.userId}-${hole.id}`]
|
||||
if (g !== undefined) {
|
||||
gross += g
|
||||
net += g - strokesReceived(ch, hole.stroke_index, holes.length)
|
||||
net += g - strokesReceived(ch, hole.strokeIndex, holes.length)
|
||||
played++
|
||||
}
|
||||
}
|
||||
totals[player.user_id] = { net, gross, holesPlayed: played }
|
||||
totals[player.userId] = { net, gross, holesPlayed: played }
|
||||
}
|
||||
return totals
|
||||
}, [scores, players, holes])
|
||||
@@ -102,14 +86,14 @@ export function ScorecardView({
|
||||
if (!entry) return
|
||||
setSaving(true)
|
||||
|
||||
const key = `${entry.player.user_id}-${entry.hole.id}`
|
||||
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.user_id, strokes)
|
||||
const result = await upsertScore(round.id, entry.hole.id, entry.player.userId, strokes)
|
||||
|
||||
if (result.error) {
|
||||
// Revert
|
||||
@@ -140,7 +124,7 @@ export function ScorecardView({
|
||||
{round.name ?? round.course?.name ?? 'Scorecard'}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{round.tee?.name} tees · {round.holes_count} holes
|
||||
{round.tee?.name} tees · {round.holesCount} holes
|
||||
{!isActive && (
|
||||
<span className="ml-1 text-muted-foreground">(completed)</span>
|
||||
)}
|
||||
@@ -210,7 +194,7 @@ export function ScorecardView({
|
||||
hole={entry?.hole ?? null}
|
||||
player={entry?.player ?? null}
|
||||
currentStrokes={
|
||||
entry ? (scores[`${entry.player.user_id}-${entry.hole.id}`] ?? null) : null
|
||||
entry ? (scores[`${entry.player.userId}-${entry.hole.id}`] ?? null) : null
|
||||
}
|
||||
onSave={handleSave}
|
||||
onClose={() => setEntry(null)}
|
||||
|
||||
Reference in New Issue
Block a user