Initial commit v1
This commit is contained in:
@@ -0,0 +1,366 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { createCourse } from '@/app/actions/courses'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { ChevronLeft } from 'lucide-react'
|
||||
|
||||
const TEE_COLORS = [
|
||||
{ label: 'Yellow', value: '#EAB308' },
|
||||
{ label: 'Red', value: '#DC2626' },
|
||||
{ label: 'White', value: '#F9FAFB' },
|
||||
{ label: 'Blue', value: '#2563EB' },
|
||||
{ label: 'Black', value: '#111827' },
|
||||
{ label: 'Green', value: '#16A34A' },
|
||||
{ label: 'Gold', value: '#D97706' },
|
||||
]
|
||||
|
||||
type Tee = { name: string; color: string; course_rating: string; slope_rating: string }
|
||||
type Hole = { hole_number: number; par: number; stroke_index: number }
|
||||
|
||||
function defaultHoles(count: number): Hole[] {
|
||||
return Array.from({ length: count }, (_, i) => ({
|
||||
hole_number: i + 1,
|
||||
par: 4,
|
||||
stroke_index: i + 1,
|
||||
}))
|
||||
}
|
||||
|
||||
export default function NewCoursePage() {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const returnTo = searchParams.get('returnTo') ?? '/rounds/new'
|
||||
|
||||
const [step, setStep] = useState(1)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
// Step 1
|
||||
const [name, setName] = useState('')
|
||||
const [par, setPar] = useState('72')
|
||||
const [holesCount, setHolesCount] = useState<9 | 18>(18)
|
||||
|
||||
// Step 2
|
||||
const [tees, setTees] = useState<Tee[]>([])
|
||||
const [teeForm, setTeeForm] = useState<Tee>({
|
||||
name: '',
|
||||
color: '#2563EB',
|
||||
course_rating: '',
|
||||
slope_rating: '',
|
||||
})
|
||||
|
||||
// Step 3
|
||||
const [holes, setHoles] = useState<Hole[]>(defaultHoles(18))
|
||||
|
||||
const parSum = holes.slice(0, holesCount).reduce((s, h) => s + h.par, 0)
|
||||
const parTarget = parseInt(par) || 0
|
||||
const parValid = parSum === parTarget
|
||||
|
||||
function addTee() {
|
||||
if (!teeForm.name || !teeForm.course_rating || !teeForm.slope_rating) return
|
||||
setTees([...tees, teeForm])
|
||||
setTeeForm({ name: '', color: '#2563EB', course_rating: '', slope_rating: '' })
|
||||
}
|
||||
|
||||
function removeTee(i: number) {
|
||||
setTees(tees.filter((_, idx) => idx !== i))
|
||||
}
|
||||
|
||||
function updateHole(index: number, field: 'par' | 'stroke_index', value: number) {
|
||||
setHoles(holes.map((h, i) => (i === index ? { ...h, [field]: value } : h)))
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
const result = await createCourse({
|
||||
name,
|
||||
par: parseInt(par),
|
||||
tees: tees.map((t) => ({
|
||||
name: t.name,
|
||||
color: t.color,
|
||||
course_rating: parseFloat(t.course_rating),
|
||||
slope_rating: parseInt(t.slope_rating),
|
||||
})),
|
||||
holes: holes.slice(0, holesCount).map((h) => ({
|
||||
hole_number: h.hole_number,
|
||||
par: h.par,
|
||||
stroke_index: h.stroke_index,
|
||||
})),
|
||||
})
|
||||
|
||||
if (result.error) {
|
||||
setError(result.error)
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
router.push(`${returnTo}?course_id=${result.courseId}`)
|
||||
}
|
||||
|
||||
const steps = ['Course', 'Tees', 'Holes']
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-3 border-b p-4">
|
||||
<button onClick={() => (step > 1 ? setStep(step - 1) : router.back())}>
|
||||
<ChevronLeft className="h-5 w-5" />
|
||||
</button>
|
||||
<h1 className="font-semibold">New course</h1>
|
||||
<div className="ml-auto flex gap-1">
|
||||
{steps.map((s, i) => (
|
||||
<div
|
||||
key={s}
|
||||
className={`h-1.5 w-8 rounded-full ${i + 1 <= step ? 'bg-primary' : 'bg-muted'}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
{/* Step 1: Course basics */}
|
||||
{step === 1 && (
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-lg font-semibold">Course details</h2>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="course-name">Course name</Label>
|
||||
<Input
|
||||
id="course-name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="e.g. Augusta National"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Number of holes</Label>
|
||||
<div className="flex gap-2">
|
||||
{([9, 18] as const).map((n) => (
|
||||
<button
|
||||
key={n}
|
||||
onClick={() => {
|
||||
setHolesCount(n)
|
||||
setHoles(defaultHoles(n))
|
||||
setPar(n === 9 ? '36' : '72')
|
||||
}}
|
||||
className={`flex-1 rounded-lg border py-3 text-sm font-medium transition-colors ${
|
||||
holesCount === n
|
||||
? 'border-primary bg-primary text-primary-foreground'
|
||||
: 'border-border hover:bg-muted'
|
||||
}`}
|
||||
>
|
||||
{n} holes
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="course-par">Total par</Label>
|
||||
<Input
|
||||
id="course-par"
|
||||
type="number"
|
||||
value={par}
|
||||
onChange={(e) => setPar(e.target.value)}
|
||||
min={27}
|
||||
max={90}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="w-full"
|
||||
disabled={!name || !par}
|
||||
onClick={() => setStep(2)}
|
||||
>
|
||||
Next: Add tees
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 2: Tees */}
|
||||
{step === 2 && (
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-lg font-semibold">Tees</h2>
|
||||
|
||||
{/* Existing tees */}
|
||||
{tees.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
{tees.map((t, i) => (
|
||||
<div key={i} className="flex items-center justify-between rounded-lg border p-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className="h-4 w-4 rounded-full border"
|
||||
style={{ backgroundColor: t.color }}
|
||||
/>
|
||||
<div>
|
||||
<div className="font-medium">{t.name}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Rating {t.course_rating} · Slope {t.slope_rating}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => removeTee(i)}
|
||||
className="text-xs text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add tee form */}
|
||||
<div className="space-y-3 rounded-lg border p-3">
|
||||
<p className="text-sm font-medium">Add tee</p>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Color</Label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{TEE_COLORS.map((c) => (
|
||||
<button
|
||||
key={c.value}
|
||||
title={c.label}
|
||||
onClick={() => setTeeForm({ ...teeForm, color: c.value })}
|
||||
className={`h-7 w-7 rounded-full border-2 transition-transform ${
|
||||
teeForm.color === c.value
|
||||
? 'scale-110 border-foreground'
|
||||
: 'border-transparent'
|
||||
}`}
|
||||
style={{ backgroundColor: c.value }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="tee-name">Name</Label>
|
||||
<Input
|
||||
id="tee-name"
|
||||
value={teeForm.name}
|
||||
onChange={(e) => setTeeForm({ ...teeForm, name: e.target.value })}
|
||||
placeholder="e.g. White"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="tee-rating">Course rating</Label>
|
||||
<Input
|
||||
id="tee-rating"
|
||||
type="number"
|
||||
value={teeForm.course_rating}
|
||||
onChange={(e) => setTeeForm({ ...teeForm, course_rating: e.target.value })}
|
||||
placeholder="71.5"
|
||||
step="0.1"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="tee-slope">Slope</Label>
|
||||
<Input
|
||||
id="tee-slope"
|
||||
type="number"
|
||||
value={teeForm.slope_rating}
|
||||
onChange={(e) => setTeeForm({ ...teeForm, slope_rating: e.target.value })}
|
||||
placeholder="125"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={addTee}
|
||||
disabled={!teeForm.name || !teeForm.course_rating || !teeForm.slope_rating}
|
||||
>
|
||||
Add tee
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="w-full"
|
||||
disabled={tees.length === 0}
|
||||
onClick={() => setStep(3)}
|
||||
>
|
||||
Next: Add holes
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 3: Holes */}
|
||||
{step === 3 && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold">Holes</h2>
|
||||
<span className={`text-sm font-medium ${parValid ? 'text-green-600' : 'text-destructive'}`}>
|
||||
Par sum: {parSum} / {parTarget}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b text-left text-muted-foreground">
|
||||
<th className="pb-2 font-medium">Hole</th>
|
||||
<th className="pb-2 font-medium">Par</th>
|
||||
<th className="pb-2 font-medium">SI</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{holes.slice(0, holesCount).map((hole, i) => (
|
||||
<tr key={hole.hole_number}>
|
||||
<td className="py-2 pr-4 font-medium">{hole.hole_number}</td>
|
||||
<td className="py-1 pr-4">
|
||||
<div className="flex gap-1">
|
||||
{[3, 4, 5].map((p) => (
|
||||
<button
|
||||
key={p}
|
||||
onClick={() => updateHole(i, 'par', p)}
|
||||
className={`h-8 w-8 rounded text-xs font-medium transition-colors ${
|
||||
hole.par === p
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'border border-border hover:bg-muted'
|
||||
}`}
|
||||
>
|
||||
{p}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-1">
|
||||
<input
|
||||
type="number"
|
||||
value={hole.stroke_index}
|
||||
onChange={(e) => updateHole(i, 'stroke_index', parseInt(e.target.value) || 1)}
|
||||
min={1}
|
||||
max={holesCount}
|
||||
className="h-8 w-14 rounded border border-border bg-background px-2 text-sm"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||
|
||||
<Button
|
||||
className="w-full"
|
||||
disabled={!parValid || loading}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{loading ? 'Creating…' : 'Create course'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { redirect } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { buttonVariants } from '@/components/ui/button'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export default async function DashboardPage() {
|
||||
const supabase = await createClient()
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
|
||||
if (!user) redirect('/login')
|
||||
|
||||
type RoundRow = {
|
||||
round: {
|
||||
id: string
|
||||
name: string | null
|
||||
date: string
|
||||
status: string
|
||||
holes_count: number
|
||||
course: { name: string } | null
|
||||
tee: { name: string; color: string } | null
|
||||
} | null
|
||||
}
|
||||
|
||||
// Fetch rounds the user is participating in
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const { data: rounds } = (await (supabase as any)
|
||||
.from('round_players')
|
||||
.select(
|
||||
`
|
||||
round:rounds (
|
||||
id, name, date, status, holes_count,
|
||||
course:courses (name),
|
||||
tee:tees (name, color)
|
||||
)
|
||||
`,
|
||||
)
|
||||
.eq('user_id', user.id)
|
||||
.in('round.status', ['lobby', 'active'])
|
||||
.order('round(date)', { ascending: false })) as { data: RoundRow[] | null }
|
||||
|
||||
return (
|
||||
<div className="p-4 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-bold">Rounds</h1>
|
||||
<Link href="/rounds/new" className={cn(buttonVariants({ size: 'sm' }))}>
|
||||
New round
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{!rounds?.length && (
|
||||
<p className="text-muted-foreground text-sm py-8 text-center">
|
||||
No active rounds. Start one!
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
{rounds?.map(({ round }) => {
|
||||
if (!round) return null
|
||||
return (
|
||||
<Link
|
||||
key={round.id}
|
||||
href={round.status === 'active' ? `/rounds/${round.id}/scorecard` : `/rounds/${round.id}`}
|
||||
className="block rounded-xl border p-4 space-y-1 hover:bg-muted transition-colors"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-semibold">{round.course?.name}</span>
|
||||
<StatusBadge status={round.status} />
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{round.name && <span>{round.name} · </span>}
|
||||
{round.tee?.name} tees · {round.holes_count} holes ·{' '}
|
||||
{new Date(round.date).toLocaleDateString()}
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function StatusBadge({ status }: { status: string }) {
|
||||
const styles = {
|
||||
lobby: 'bg-yellow-100 text-yellow-800',
|
||||
active: 'bg-green-100 text-green-800',
|
||||
completed: 'bg-gray-100 text-gray-600',
|
||||
}
|
||||
return (
|
||||
<span
|
||||
className={`text-xs font-medium px-2 py-0.5 rounded-full ${styles[status as keyof typeof styles] ?? styles.completed}`}
|
||||
>
|
||||
{status}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { redirect } from 'next/navigation'
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { BottomNav } from '@/components/bottom-nav'
|
||||
|
||||
export default async function AppLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const supabase = await createClient()
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
|
||||
if (!user) redirect('/login')
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col">
|
||||
<main className="flex-1 pb-20">{children}</main>
|
||||
<BottomNav />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { redirect, notFound } from 'next/navigation'
|
||||
import { JoinRound } from '@/components/round/join-round'
|
||||
|
||||
export default async function JoinRoundPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params
|
||||
const supabase = await createClient()
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const db = supabase as any
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
if (!user) redirect(`/login`)
|
||||
|
||||
const { data: round } = await db
|
||||
.from('rounds')
|
||||
.select(
|
||||
`
|
||||
id, name, date, holes_count, status, created_by,
|
||||
course:courses (name),
|
||||
tee:tees (name, color),
|
||||
round_players (user_id)
|
||||
`,
|
||||
)
|
||||
.eq('id', id)
|
||||
.single()
|
||||
|
||||
if (!round) notFound()
|
||||
|
||||
// Already a member — go straight to the round
|
||||
const alreadyJoined = round.round_players?.some(
|
||||
(p: { user_id: string }) => p.user_id === user.id,
|
||||
)
|
||||
if (alreadyJoined) redirect(`/rounds/${id}`)
|
||||
|
||||
// Round must be in lobby to join
|
||||
if (round.status !== 'lobby') {
|
||||
redirect(`/dashboard`)
|
||||
}
|
||||
|
||||
return <JoinRound round={round} userId={user.id} />
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { redirect, notFound } from 'next/navigation'
|
||||
import { RoundLobby } from '@/components/round/round-lobby'
|
||||
|
||||
export default async function RoundPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params
|
||||
const supabase = await createClient()
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const db = supabase as any
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
if (!user) redirect('/login')
|
||||
|
||||
const { data: round } = await db
|
||||
.from('rounds')
|
||||
.select(
|
||||
`
|
||||
id, name, date, status, holes_count, created_by,
|
||||
course:courses (id, name, par),
|
||||
tee:tees (id, name, color, course_rating, slope_rating)
|
||||
`,
|
||||
)
|
||||
.eq('id', id)
|
||||
.single()
|
||||
|
||||
if (!round) notFound()
|
||||
|
||||
const { data: players } = await db
|
||||
.from('round_players')
|
||||
.select(
|
||||
`
|
||||
id, role, handicap_index, course_handicap, joined_at,
|
||||
profile:profiles (id, display_name, avatar_url)
|
||||
`,
|
||||
)
|
||||
.eq('round_id', id)
|
||||
.order('joined_at')
|
||||
|
||||
const { data: invites } = await db
|
||||
.from('round_invites')
|
||||
.select('id, email, accepted_at, created_at')
|
||||
.eq('round_id', id)
|
||||
.is('accepted_at', null)
|
||||
.order('created_at')
|
||||
|
||||
const isCreator = round.created_by === user.id
|
||||
|
||||
if (round.status === 'active' || round.status === 'completed') {
|
||||
redirect(`/rounds/${id}/scorecard`)
|
||||
}
|
||||
|
||||
return (
|
||||
<RoundLobby
|
||||
round={round}
|
||||
players={players ?? []}
|
||||
pendingInvites={invites ?? []}
|
||||
currentUserId={user.id}
|
||||
isCreator={isCreator}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { redirect, notFound } from 'next/navigation'
|
||||
import { ScorecardView } from '@/components/scorecard/scorecard-view'
|
||||
|
||||
export type Hole = {
|
||||
id: string
|
||||
hole_number: number
|
||||
par: number
|
||||
stroke_index: number
|
||||
}
|
||||
|
||||
export type Player = {
|
||||
user_id: string
|
||||
course_handicap: number | null
|
||||
handicap_index: number | null
|
||||
profile: { id: string; display_name: string; avatar_url: string | null } | null
|
||||
}
|
||||
|
||||
export type ScoreRow = {
|
||||
hole_id: string
|
||||
player_id: string
|
||||
strokes: number
|
||||
}
|
||||
|
||||
export type RoundInfo = {
|
||||
id: string
|
||||
name: string | null
|
||||
date: string
|
||||
status: string
|
||||
holes_count: number
|
||||
created_by: string
|
||||
course: { name: string } | null
|
||||
tee: { name: string; color: string } | null
|
||||
}
|
||||
|
||||
export default async function ScorecardPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>
|
||||
}) {
|
||||
const { id } = await params
|
||||
const supabase = await createClient()
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const db = supabase as any
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
if (!user) redirect('/login')
|
||||
|
||||
const { data: round } = await db
|
||||
.from('rounds')
|
||||
.select(
|
||||
`id, name, date, status, holes_count, created_by,
|
||||
course:courses (name),
|
||||
tee:tees (name, color)`,
|
||||
)
|
||||
.eq('id', id)
|
||||
.single()
|
||||
|
||||
if (!round) notFound()
|
||||
if (round.status === 'lobby') redirect(`/rounds/${id}`)
|
||||
|
||||
// Holes ordered by hole_number, limited to holes_count
|
||||
const { data: holes } = await db
|
||||
.from('holes')
|
||||
.select('id, hole_number, par, stroke_index')
|
||||
.eq('course_id', await getCourseId(db, id))
|
||||
.order('hole_number')
|
||||
.limit(round.holes_count)
|
||||
|
||||
const { data: players } = await db
|
||||
.from('round_players')
|
||||
.select(
|
||||
`user_id, course_handicap, handicap_index,
|
||||
profile:profiles (id, display_name, avatar_url)`,
|
||||
)
|
||||
.eq('round_id', id)
|
||||
.order('joined_at')
|
||||
|
||||
const { data: scores } = await db
|
||||
.from('scores')
|
||||
.select('hole_id, player_id, strokes')
|
||||
.eq('round_id', id)
|
||||
|
||||
return (
|
||||
<ScorecardView
|
||||
round={round as RoundInfo}
|
||||
holes={(holes as Hole[]) ?? []}
|
||||
players={(players as Player[]) ?? []}
|
||||
initialScores={(scores as ScoreRow[]) ?? []}
|
||||
currentUserId={user.id}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
async function getCourseId(db: any, roundId: string): Promise<string> {
|
||||
const { data } = await db
|
||||
.from('rounds')
|
||||
.select('course_id')
|
||||
.eq('id', roundId)
|
||||
.single()
|
||||
return data?.course_id ?? ''
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { NewRoundWizard } from '@/components/round/new-round-wizard'
|
||||
|
||||
type Tee = {
|
||||
id: string
|
||||
name: string
|
||||
color: string
|
||||
course_rating: number
|
||||
slope_rating: number
|
||||
}
|
||||
|
||||
type Course = {
|
||||
id: string
|
||||
name: string
|
||||
par: number
|
||||
tees: Tee[]
|
||||
}
|
||||
|
||||
export default async function NewRoundPage() {
|
||||
const supabase = await createClient()
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const { data: courses } = await (supabase as any)
|
||||
.from('courses')
|
||||
.select('id, name, par, tees(id, name, color, course_rating, slope_rating)')
|
||||
.order('name')
|
||||
|
||||
return <NewRoundWizard courses={(courses as Course[]) ?? []} />
|
||||
}
|
||||
Reference in New Issue
Block a user