Initial commit v1

This commit is contained in:
Rolf
2026-03-18 12:37:17 +01:00
commit 4897d3003f
55 changed files with 14241 additions and 0 deletions
+168
View File
@@ -0,0 +1,168 @@
'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>
)
}