169 lines
5.4 KiB
TypeScript
169 lines
5.4 KiB
TypeScript
'use client'
|
|
|
|
import { useRef } from 'react'
|
|
import { ChevronLeft, ChevronRight } from 'lucide-react'
|
|
import { strokesReceived, formatVsPar, scoreColorClass } from '@/lib/handicap'
|
|
import type { Hole, Player } from '@/app/(app)/rounds/[id]/scorecard/page'
|
|
|
|
type Props = {
|
|
hole: Hole
|
|
holeIndex: number
|
|
totalHoles: number
|
|
players: Player[]
|
|
scores: Record<string, number>
|
|
runningTotals: Record<string, { net: number; gross: number; holesPlayed: number }>
|
|
isActive: boolean
|
|
onPrev: () => void
|
|
onNext: () => void
|
|
onTapScore: (player: Player) => void
|
|
}
|
|
|
|
export function HoleView({
|
|
hole,
|
|
holeIndex,
|
|
totalHoles,
|
|
players,
|
|
scores,
|
|
runningTotals,
|
|
isActive,
|
|
onPrev,
|
|
onNext,
|
|
onTapScore,
|
|
}: Props) {
|
|
const touchStartX = useRef<number>(0)
|
|
|
|
function handleTouchStart(e: React.TouchEvent) {
|
|
touchStartX.current = e.touches[0].clientX
|
|
}
|
|
|
|
function handleTouchEnd(e: React.TouchEvent) {
|
|
const diff = touchStartX.current - e.changedTouches[0].clientX
|
|
if (diff > 50) onNext()
|
|
if (diff < -50) onPrev()
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className="flex flex-col flex-1"
|
|
onTouchStart={handleTouchStart}
|
|
onTouchEnd={handleTouchEnd}
|
|
>
|
|
{/* Hole header */}
|
|
<div className="flex items-center justify-between border-b bg-muted/30 px-4 py-3">
|
|
<button
|
|
onClick={onPrev}
|
|
disabled={holeIndex === 0}
|
|
className="flex h-10 w-10 items-center justify-center rounded-full hover:bg-muted disabled:opacity-30"
|
|
>
|
|
<ChevronLeft className="h-5 w-5" />
|
|
</button>
|
|
|
|
<div className="text-center">
|
|
<div className="text-lg font-bold">Hole {hole.hole_number}</div>
|
|
<div className="text-sm text-muted-foreground">
|
|
Par {hole.par} · SI {hole.stroke_index}
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
onClick={onNext}
|
|
disabled={holeIndex === totalHoles - 1}
|
|
className="flex h-10 w-10 items-center justify-center rounded-full hover:bg-muted disabled:opacity-30"
|
|
>
|
|
<ChevronRight className="h-5 w-5" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Players */}
|
|
<div className="flex-1 divide-y overflow-y-auto">
|
|
{players.map((player) => {
|
|
const gross = scores[`${player.user_id}-${hole.id}`] ?? null
|
|
const ch = player.course_handicap ?? 0
|
|
const received = strokesReceived(ch, hole.stroke_index, totalHoles)
|
|
const net = gross !== null ? gross - received : null
|
|
const grossVsPar = gross !== null ? gross - hole.par : null
|
|
const netVsPar = net !== null ? net - hole.par : null
|
|
|
|
return (
|
|
<button
|
|
key={player.user_id}
|
|
onClick={() => isActive && onTapScore(player)}
|
|
disabled={!isActive}
|
|
className="flex w-full items-center gap-3 px-4 py-4 text-left transition-colors hover:bg-muted/50 active:bg-muted disabled:cursor-default"
|
|
>
|
|
{/* Avatar */}
|
|
<PlayerAvatar
|
|
name={player.profile?.display_name ?? '?'}
|
|
url={player.profile?.avatar_url}
|
|
/>
|
|
|
|
{/* Name + running total */}
|
|
<div className="flex-1 min-w-0">
|
|
<div className="font-medium truncate">
|
|
{player.profile?.display_name}
|
|
</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
{runningTotals[player.user_id]
|
|
? `${runningTotals[player.user_id].holesPlayed} holes · ${formatVsPar(runningTotals[player.user_id].net)} net`
|
|
: 'No scores yet'}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Net score (primary, bold) */}
|
|
<div className="flex flex-col items-end gap-0.5">
|
|
{net !== null ? (
|
|
<>
|
|
<span
|
|
className={`flex h-9 w-9 items-center justify-center text-base font-bold ${scoreColorClass(netVsPar)}`}
|
|
>
|
|
{net}
|
|
</span>
|
|
<span className="text-xs text-muted-foreground">
|
|
{gross} gross
|
|
</span>
|
|
</>
|
|
) : (
|
|
<span className="flex h-9 w-9 items-center justify-center rounded-lg border-2 border-dashed text-muted-foreground text-sm">
|
|
—
|
|
</span>
|
|
)}
|
|
</div>
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
{/* Hole progress dots */}
|
|
<div className="flex justify-center gap-1 border-t py-3">
|
|
{Array.from({ length: totalHoles }, (_, i) => (
|
|
<div
|
|
key={i}
|
|
className={`h-1.5 rounded-full transition-all ${
|
|
i === holeIndex
|
|
? 'w-4 bg-primary'
|
|
: 'w-1.5 bg-muted-foreground/30'
|
|
}`}
|
|
/>
|
|
))}
|
|
</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>
|
|
)
|
|
}
|