Initial commit v1.1

This commit is contained in:
Rolf
2026-03-18 13:34:41 +01:00
parent cb04bf2ab8
commit b982584244
38 changed files with 3196 additions and 1103 deletions
+43 -29
View File
@@ -1,43 +1,57 @@
import { createClient } from '@/lib/supabase/server'
import { auth } from '@/lib/auth'
import { db } from '@/lib/db'
import { rounds, roundPlayers, courses, tees } from '@/lib/db/schema'
import { eq } from 'drizzle-orm'
import { redirect, notFound } from 'next/navigation'
import { headers } from 'next/headers'
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 session = await auth.api.getSession({ headers: await headers() })
if (!session) redirect(`/login`)
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()
const [round] = await db
.select({
id: rounds.id,
name: rounds.name,
date: rounds.date,
holesCount: rounds.holesCount,
status: rounds.status,
courseName: courses.name,
teeName: tees.name,
teeColor: tees.color,
})
.from(rounds)
.innerJoin(courses, eq(rounds.courseId, courses.id))
.innerJoin(tees, eq(rounds.teeId, tees.id))
.where(eq(rounds.id, id))
.limit(1)
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}`)
const [membership] = await db
.select({ id: roundPlayers.id })
.from(roundPlayers)
.where(eq(roundPlayers.roundId, id))
.limit(1)
if (membership) redirect(`/rounds/${id}`)
// Round must be in lobby to join
if (round.status !== 'lobby') {
redirect(`/dashboard`)
}
if (round.status !== 'lobby') redirect('/dashboard')
return <JoinRound round={round} userId={user.id} />
return (
<JoinRound
roundId={id}
round={{
name: round.name,
date: round.date,
holesCount: round.holesCount,
course: { name: round.courseName },
tee: { name: round.teeName, color: round.teeColor },
}}
/>
)
}
+67 -46
View File
@@ -1,63 +1,84 @@
import { createClient } from '@/lib/supabase/server'
import { auth } from '@/lib/auth'
import { db } from '@/lib/db'
import { rounds, roundPlayers, roundInvites, courses, tees, user } from '@/lib/db/schema'
import { eq } from 'drizzle-orm'
import { redirect, notFound } from 'next/navigation'
import { headers } from 'next/headers'
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 session = await auth.api.getSession({ headers: await headers() })
if (!session) redirect('/login')
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()
const [round] = await db
.select({
id: rounds.id,
name: rounds.name,
date: rounds.date,
status: rounds.status,
holesCount: rounds.holesCount,
createdBy: rounds.createdBy,
courseName: courses.name,
teeName: tees.name,
teeColor: tees.color,
courseRating: tees.courseRating,
slopeRating: tees.slopeRating,
})
.from(rounds)
.innerJoin(courses, eq(rounds.courseId, courses.id))
.innerJoin(tees, eq(rounds.teeId, tees.id))
.where(eq(rounds.id, id))
.limit(1)
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`)
}
const players = await db
.select({
id: roundPlayers.id,
userId: roundPlayers.userId,
role: roundPlayers.role,
handicapIndex: roundPlayers.handicapIndex,
courseHandicap: roundPlayers.courseHandicap,
joinedAt: roundPlayers.joinedAt,
displayName: user.name,
avatarUrl: user.image,
})
.from(roundPlayers)
.innerJoin(user, eq(roundPlayers.userId, user.id))
.where(eq(roundPlayers.roundId, id))
.orderBy(roundPlayers.joinedAt)
const pendingInvites = await db
.select({ id: roundInvites.id, email: roundInvites.email, createdAt: roundInvites.createdAt })
.from(roundInvites)
.where(eq(roundInvites.roundId, id))
return (
<RoundLobby
round={round}
players={players ?? []}
pendingInvites={invites ?? []}
currentUserId={user.id}
isCreator={isCreator}
round={{
id: round.id,
name: round.name,
date: round.date,
status: round.status,
holesCount: round.holesCount,
createdBy: round.createdBy,
course: { name: round.courseName },
tee: { name: round.teeName, color: round.teeColor },
}}
players={players.map((p) => ({
id: p.id,
role: p.role,
handicapIndex: p.handicapIndex,
courseHandicap: p.courseHandicap,
profile: { id: p.userId, displayName: p.displayName, avatarUrl: p.avatarUrl },
}))}
pendingInvites={pendingInvites.map((i) => ({ id: i.id, email: i.email, createdAt: i.createdAt, acceptedAt: null }))}
currentUserId={session.user.id}
isCreator={round.createdBy === session.user.id}
/>
)
}
+89 -61
View File
@@ -1,24 +1,28 @@
import { createClient } from '@/lib/supabase/server'
import { auth } from '@/lib/auth'
import { db } from '@/lib/db'
import { rounds, roundPlayers, holes, scores, courses, tees, user } from '@/lib/db/schema'
import { eq, asc } from 'drizzle-orm'
import { redirect, notFound } from 'next/navigation'
import { headers } from 'next/headers'
import { ScorecardView } from '@/components/scorecard/scorecard-view'
export type Hole = {
id: string
hole_number: number
holeNumber: number
par: number
stroke_index: number
strokeIndex: 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
userId: string
courseHandicap: number | null
handicapIndex: string | null
profile: { id: string; displayName: string; avatarUrl: string | null } | null
}
export type ScoreRow = {
hole_id: string
player_id: string
holeId: string
playerId: string
strokes: number
}
@@ -27,8 +31,8 @@ export type RoundInfo = {
name: string | null
date: string
status: string
holes_count: number
created_by: string
holesCount: number
createdBy: string
course: { name: string } | null
tee: { name: string; color: string } | null
}
@@ -39,66 +43,90 @@ export default async function ScorecardPage({
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 session = await auth.api.getSession({ headers: await headers() })
if (!session) redirect('/login')
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()
const [round] = await db
.select({
id: rounds.id,
name: rounds.name,
date: rounds.date,
status: rounds.status,
holesCount: rounds.holesCount,
createdBy: rounds.createdBy,
courseId: rounds.courseId,
courseName: courses.name,
teeName: tees.name,
teeColor: tees.color,
})
.from(rounds)
.innerJoin(courses, eq(rounds.courseId, courses.id))
.innerJoin(tees, eq(rounds.teeId, tees.id))
.where(eq(rounds.id, id))
.limit(1)
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 holeRows = await db
.select({
id: holes.id,
holeNumber: holes.holeNumber,
par: holes.par,
strokeIndex: holes.strokeIndex,
})
.from(holes)
.where(eq(holes.courseId, round.courseId))
.orderBy(asc(holes.holeNumber))
.limit(round.holesCount)
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 playerRows = await db
.select({
userId: roundPlayers.userId,
courseHandicap: roundPlayers.courseHandicap,
handicapIndex: roundPlayers.handicapIndex,
displayName: user.name,
avatarUrl: user.image,
})
.from(roundPlayers)
.innerJoin(user, eq(roundPlayers.userId, user.id))
.where(eq(roundPlayers.roundId, id))
.orderBy(roundPlayers.joinedAt)
const { data: scores } = await db
.from('scores')
.select('hole_id, player_id, strokes')
.eq('round_id', id)
const scoreRows = await db
.select({ holeId: scores.holeId, playerId: scores.playerId, strokes: scores.strokes })
.from(scores)
.where(eq(scores.roundId, id))
const roundInfo: RoundInfo = {
id: round.id,
name: round.name,
date: round.date,
status: round.status,
holesCount: round.holesCount,
createdBy: round.createdBy,
course: { name: round.courseName },
tee: { name: round.teeName, color: round.teeColor },
}
const players: Player[] = playerRows.map((p) => ({
userId: p.userId,
courseHandicap: p.courseHandicap,
handicapIndex: p.handicapIndex,
profile: {
id: p.userId,
displayName: p.displayName,
avatarUrl: p.avatarUrl,
},
}))
return (
<ScorecardView
round={round as RoundInfo}
holes={(holes as Hole[]) ?? []}
players={(players as Player[]) ?? []}
initialScores={(scores as ScoreRow[]) ?? []}
currentUserId={user.id}
round={roundInfo}
holes={holeRows}
players={players}
initialScores={scoreRows}
currentUserId={session.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 ?? ''
}
+30 -23
View File
@@ -1,28 +1,35 @@
import { createClient } from '@/lib/supabase/server'
import { auth } from '@/lib/auth'
import { db } from '@/lib/db'
import { courses, tees } from '@/lib/db/schema'
import { eq } from 'drizzle-orm'
import { headers } from 'next/headers'
import { redirect } from 'next/navigation'
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')
const session = await auth.api.getSession({ headers: await headers() })
if (!session) redirect('/login')
return <NewRoundWizard courses={(courses as Course[]) ?? []} />
const courseList = await db.select().from(courses).orderBy(courses.name)
const teesData = await db.select().from(tees)
const teesById = teesData.reduce<Record<string, typeof teesData>>((acc, tee) => {
;(acc[tee.courseId] ??= []).push(tee)
return acc
}, {})
const coursesWithTees = courseList.map((c) => ({
id: c.id,
name: c.name,
par: c.par,
tees: (teesById[c.id] ?? []).map((t) => ({
id: t.id,
name: t.name,
color: t.color,
course_rating: parseFloat(t.courseRating),
slope_rating: t.slopeRating,
})),
}))
return <NewRoundWizard courses={coursesWithTees} />
}