Initial commit v1.1
This commit is contained in:
+45
-36
@@ -1,26 +1,29 @@
|
||||
'use server'
|
||||
|
||||
import { z } from 'zod'
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { db } from '@/lib/db'
|
||||
import { courses, tees, holes } from '@/lib/db/schema'
|
||||
import { auth } from '@/lib/auth'
|
||||
import { headers } from 'next/headers'
|
||||
import { revalidatePath } from 'next/cache'
|
||||
|
||||
const TeeInput = z.object({
|
||||
name: z.string().min(1, 'Tee name required'),
|
||||
color: z.string().regex(/^#[0-9a-fA-F]{6}$/),
|
||||
course_rating: z.coerce.number().min(50).max(80),
|
||||
slope_rating: z.coerce.number().int().min(55).max(155),
|
||||
name: z.string().min(1, 'Tee name required'),
|
||||
color: z.string().regex(/^#[0-9a-fA-F]{6}$/),
|
||||
courseRating: z.coerce.number().min(50).max(80),
|
||||
slopeRating: z.coerce.number().int().min(55).max(155),
|
||||
})
|
||||
|
||||
const HoleInput = z.object({
|
||||
hole_number: z.number().int().min(1).max(18),
|
||||
par: z.number().int().min(3).max(5),
|
||||
stroke_index: z.number().int().min(1).max(18),
|
||||
holeNumber: z.number().int().min(1).max(18),
|
||||
par: z.number().int().min(3).max(5),
|
||||
strokeIndex: z.number().int().min(1).max(18),
|
||||
})
|
||||
|
||||
export const CreateCourseSchema = z.object({
|
||||
name: z.string().min(1, 'Course name required').max(100),
|
||||
par: z.number().int().min(27).max(90),
|
||||
tees: z.array(TeeInput).min(1, 'At least one tee required'),
|
||||
name: z.string().min(1, 'Course name required').max(100),
|
||||
par: z.number().int().min(27).max(90),
|
||||
tees: z.array(TeeInput).min(1, 'At least one tee required'),
|
||||
holes: z.array(HoleInput).min(9),
|
||||
})
|
||||
|
||||
@@ -28,35 +31,41 @@ export type CreateCourseInput = z.infer<typeof CreateCourseSchema>
|
||||
|
||||
export async function createCourse(data: CreateCourseInput) {
|
||||
const parsed = CreateCourseSchema.safeParse(data)
|
||||
if (!parsed.success) {
|
||||
return { error: parsed.error.issues[0]?.message ?? 'Invalid data' }
|
||||
if (!parsed.success) return { error: parsed.error.issues[0]?.message ?? 'Invalid data' }
|
||||
|
||||
const session = await auth.api.getSession({ headers: await headers() })
|
||||
if (!session) return { error: 'Not authenticated' }
|
||||
|
||||
// Validate par sum equals declared course par
|
||||
const parSum = parsed.data.holes.reduce((s, h) => s + h.par, 0)
|
||||
if (parSum !== parsed.data.par) {
|
||||
return { error: `Sum of hole pars (${parSum}) does not equal course par (${parsed.data.par})` }
|
||||
}
|
||||
|
||||
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) return { error: 'Not authenticated' }
|
||||
const [course] = await db
|
||||
.insert(courses)
|
||||
.values({ name: parsed.data.name, par: parsed.data.par, createdBy: session.user.id })
|
||||
.returning({ id: courses.id })
|
||||
|
||||
const { data: course, error: courseError } = await db
|
||||
.from('courses')
|
||||
.insert({ name: parsed.data.name, par: parsed.data.par, created_by: user.id })
|
||||
.select('id')
|
||||
.single()
|
||||
if (courseError) return { error: courseError.message }
|
||||
await db.insert(tees).values(
|
||||
parsed.data.tees.map((t) => ({
|
||||
courseId: course.id,
|
||||
name: t.name,
|
||||
color: t.color,
|
||||
courseRating: String(t.courseRating),
|
||||
slopeRating: t.slopeRating,
|
||||
})),
|
||||
)
|
||||
|
||||
const { error: teesError } = await db
|
||||
.from('tees')
|
||||
.insert(parsed.data.tees.map((t) => ({ ...t, course_id: course.id })))
|
||||
if (teesError) return { error: teesError.message }
|
||||
|
||||
const { error: holesError } = await db
|
||||
.from('holes')
|
||||
.insert(parsed.data.holes.map((h) => ({ ...h, course_id: course.id })))
|
||||
if (holesError) return { error: holesError.message }
|
||||
await db.insert(holes).values(
|
||||
parsed.data.holes.map((h) => ({
|
||||
courseId: course.id,
|
||||
holeNumber: h.holeNumber,
|
||||
par: h.par,
|
||||
strokeIndex: h.strokeIndex,
|
||||
})),
|
||||
)
|
||||
|
||||
revalidatePath('/rounds/new')
|
||||
return { courseId: course.id as string }
|
||||
return { courseId: course.id }
|
||||
}
|
||||
|
||||
+17
-37
@@ -1,50 +1,30 @@
|
||||
'use server'
|
||||
|
||||
import { z } from 'zod'
|
||||
import { createClient, createAdminClient } from '@/lib/supabase/server'
|
||||
|
||||
const InviteSchema = z.object({
|
||||
roundId: z.string().uuid(),
|
||||
email: z.string().email(),
|
||||
})
|
||||
import { db } from '@/lib/db'
|
||||
import { rounds } from '@/lib/db/schema'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { auth } from '@/lib/auth'
|
||||
import { headers } from 'next/headers'
|
||||
import { inviteToRound } from './rounds'
|
||||
|
||||
export async function inviteFromLobby(roundId: string, email: string) {
|
||||
const parsed = InviteSchema.safeParse({ roundId, email })
|
||||
const parsed = z.object({ roundId: z.string().uuid(), email: z.string().email() })
|
||||
.safeParse({ roundId, email })
|
||||
if (!parsed.success) return { error: 'Invalid email address' }
|
||||
|
||||
const supabase = await createClient()
|
||||
const adminClient = await createAdminClient()
|
||||
// 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) return { error: 'Not authenticated' }
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
if (!user) return { error: 'Not authenticated' }
|
||||
|
||||
const { data: round } = await db
|
||||
.from('rounds')
|
||||
.select('status, created_by')
|
||||
.eq('id', parsed.data.roundId)
|
||||
.single()
|
||||
const [round] = await db
|
||||
.select({ status: rounds.status, createdBy: rounds.createdBy })
|
||||
.from(rounds)
|
||||
.where(eq(rounds.id, roundId))
|
||||
.limit(1)
|
||||
|
||||
if (!round) return { error: 'Round not found' }
|
||||
if (round.created_by !== user.id) return { error: 'Only the round creator can invite players' }
|
||||
if (round.createdBy !== session.user.id) return { error: 'Only the round creator can invite players' }
|
||||
if (round.status !== 'lobby') return { error: 'Cannot invite after the round has started' }
|
||||
|
||||
const redirectTo = `${process.env.SITE_URL}/auth/confirm?next=${encodeURIComponent(`/rounds/${parsed.data.roundId}/join`)}`
|
||||
|
||||
const { error: inviteError } = await (adminClient as any).auth.admin.inviteUserByEmail(
|
||||
parsed.data.email,
|
||||
{ redirectTo },
|
||||
)
|
||||
if (inviteError) return { error: inviteError.message }
|
||||
|
||||
await db.from('round_invites').insert({
|
||||
round_id: parsed.data.roundId,
|
||||
email: parsed.data.email,
|
||||
invited_by: user.id,
|
||||
})
|
||||
|
||||
return { success: true }
|
||||
return inviteToRound(roundId, email, session.user.id)
|
||||
}
|
||||
|
||||
+199
-89
@@ -1,129 +1,239 @@
|
||||
'use server'
|
||||
|
||||
import { z } from 'zod'
|
||||
import { createClient, createAdminClient } from '@/lib/supabase/server'
|
||||
import { db } from '@/lib/db'
|
||||
import { rounds, roundPlayers, roundInvites, tees, courses, user } from '@/lib/db/schema'
|
||||
import { eq, and } from 'drizzle-orm'
|
||||
import { auth } from '@/lib/auth'
|
||||
import { headers } from 'next/headers'
|
||||
import { sendEmail } from '@/lib/email'
|
||||
import { revalidatePath } from 'next/cache'
|
||||
|
||||
const CreateRoundSchema = z.object({
|
||||
course_id: z.string().uuid(),
|
||||
tee_id: z.string().uuid(),
|
||||
name: z.string().max(100).optional(),
|
||||
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
||||
course_id: z.string().uuid(),
|
||||
tee_id: z.string().uuid(),
|
||||
name: z.string().max(100).optional(),
|
||||
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
||||
holes_count: z.union([z.literal(9), z.literal(18)]),
|
||||
emails: z.array(z.string().email()).default([]),
|
||||
emails: z.array(z.string().email()).default([]),
|
||||
})
|
||||
|
||||
export type CreateRoundInput = z.infer<typeof CreateRoundSchema>
|
||||
|
||||
function computeCourseHandicap(
|
||||
handicapIndex: number,
|
||||
slopeRating: number,
|
||||
courseRating: number,
|
||||
par: number,
|
||||
): number {
|
||||
return Math.round(handicapIndex * (slopeRating / 113) + (courseRating - par))
|
||||
}
|
||||
|
||||
export async function createRoundWithInvites(data: CreateRoundInput) {
|
||||
const parsed = CreateRoundSchema.safeParse(data)
|
||||
if (!parsed.success) {
|
||||
return { error: parsed.error.issues[0]?.message ?? 'Invalid data' }
|
||||
}
|
||||
if (!parsed.success) return { error: parsed.error.issues[0]?.message ?? 'Invalid data' }
|
||||
|
||||
const supabase = await createClient()
|
||||
const adminClient = await createAdminClient()
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const db = supabase as any
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const adminDb = adminClient as any
|
||||
const session = await auth.api.getSession({ headers: await headers() })
|
||||
if (!session) return { error: 'Not authenticated' }
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
if (!user) return { error: 'Not authenticated' }
|
||||
// Snapshot handicap
|
||||
const [creator] = await db
|
||||
.select({ handicapIndex: user.handicapIndex })
|
||||
.from(user)
|
||||
.where(eq(user.id, session.user.id))
|
||||
.limit(1)
|
||||
|
||||
// Snapshot handicap and compute course handicap
|
||||
const { data: profile } = await db
|
||||
.from('profiles')
|
||||
.select('handicap_index')
|
||||
.eq('id', user.id)
|
||||
.single()
|
||||
const [tee] = await db
|
||||
.select({ courseRating: tees.courseRating, slopeRating: tees.slopeRating })
|
||||
.from(tees)
|
||||
.where(eq(tees.id, parsed.data.tee_id))
|
||||
.limit(1)
|
||||
|
||||
const { data: tee } = await db
|
||||
.from('tees')
|
||||
.select('course_rating, slope_rating')
|
||||
.eq('id', parsed.data.tee_id)
|
||||
.single()
|
||||
const [course] = await db
|
||||
.select({ par: courses.par })
|
||||
.from(courses)
|
||||
.where(eq(courses.id, parsed.data.course_id))
|
||||
.limit(1)
|
||||
|
||||
const { data: course } = await db
|
||||
.from('courses')
|
||||
.select('par')
|
||||
.eq('id', parsed.data.course_id)
|
||||
.single()
|
||||
|
||||
const handicapIndex: number | null = profile?.handicap_index ?? null
|
||||
const handicapIndex = creator?.handicapIndex ? parseFloat(creator.handicapIndex) : null
|
||||
const courseHandicap =
|
||||
handicapIndex !== null && tee && course
|
||||
? Math.round(handicapIndex * (tee.slope_rating / 113) + (tee.course_rating - course.par))
|
||||
? computeCourseHandicap(
|
||||
handicapIndex,
|
||||
tee.slopeRating,
|
||||
parseFloat(tee.courseRating),
|
||||
course.par,
|
||||
)
|
||||
: null
|
||||
|
||||
// Create round
|
||||
const { data: round, error: roundError } = await db
|
||||
.from('rounds')
|
||||
.insert({
|
||||
course_id: parsed.data.course_id,
|
||||
tee_id: parsed.data.tee_id,
|
||||
name: parsed.data.name || null,
|
||||
date: parsed.data.date,
|
||||
holes_count: parsed.data.holes_count,
|
||||
created_by: user.id,
|
||||
status: 'lobby',
|
||||
const [round] = await db
|
||||
.insert(rounds)
|
||||
.values({
|
||||
courseId: parsed.data.course_id,
|
||||
teeId: parsed.data.tee_id,
|
||||
name: parsed.data.name ?? null,
|
||||
date: parsed.data.date,
|
||||
holesCount: parsed.data.holes_count,
|
||||
createdBy: session.user.id,
|
||||
status: 'lobby',
|
||||
})
|
||||
.select('id')
|
||||
.single()
|
||||
if (roundError) return { error: roundError.message }
|
||||
.returning({ id: rounds.id })
|
||||
|
||||
// Add creator as admin player
|
||||
await db.from('round_players').insert({
|
||||
round_id: round.id,
|
||||
user_id: user.id,
|
||||
role: 'admin',
|
||||
handicap_index: handicapIndex,
|
||||
course_handicap: courseHandicap,
|
||||
await db.insert(roundPlayers).values({
|
||||
roundId: round.id,
|
||||
userId: session.user.id,
|
||||
role: 'admin',
|
||||
handicapIndex: handicapIndex !== null ? String(handicapIndex) : null,
|
||||
courseHandicap: courseHandicap,
|
||||
})
|
||||
|
||||
// Send invites
|
||||
const failedInvites: string[] = []
|
||||
for (const email of parsed.data.emails) {
|
||||
const redirectTo = `${process.env.SITE_URL}/auth/confirm?next=${encodeURIComponent(`/rounds/${round.id}/join`)}`
|
||||
const { error } = await adminDb.auth.admin.inviteUserByEmail(email, { redirectTo })
|
||||
if (error) {
|
||||
failedInvites.push(email)
|
||||
} else {
|
||||
await db.from('round_invites').insert({
|
||||
round_id: round.id,
|
||||
email,
|
||||
invited_by: user.id,
|
||||
})
|
||||
}
|
||||
const result = await inviteToRound(round.id, email, session.user.id)
|
||||
if (result.error) failedInvites.push(email)
|
||||
}
|
||||
|
||||
return {
|
||||
roundId: round.id as string,
|
||||
failedInvites,
|
||||
}
|
||||
return { roundId: round.id, failedInvites }
|
||||
}
|
||||
|
||||
export async function startRound(roundId: string) {
|
||||
const parsed = z.string().uuid().safeParse(roundId)
|
||||
if (!parsed.success) return { error: 'Invalid round ID' }
|
||||
const session = await auth.api.getSession({ headers: await headers() })
|
||||
if (!session) return { error: 'Not authenticated' }
|
||||
|
||||
const supabase = await createClient()
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
if (!user) return { error: 'Not authenticated' }
|
||||
const [round] = await db
|
||||
.select({ createdBy: rounds.createdBy, status: rounds.status })
|
||||
.from(rounds)
|
||||
.where(eq(rounds.id, roundId))
|
||||
.limit(1)
|
||||
|
||||
const { error } = await (supabase as any)
|
||||
.from('rounds')
|
||||
.update({ status: 'active' })
|
||||
.eq('id', parsed.data)
|
||||
.eq('created_by', user.id)
|
||||
.eq('status', 'lobby')
|
||||
if (!round) return { error: 'Round not found' }
|
||||
if (round.createdBy !== session.user.id) return { error: 'Only the round creator can start it' }
|
||||
if (round.status !== 'lobby') return { error: 'Round is not in lobby' }
|
||||
|
||||
if (error) return { error: error.message }
|
||||
await db.update(rounds).set({ status: 'active' }).where(eq(rounds.id, roundId))
|
||||
revalidatePath(`/rounds/${roundId}`)
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export async function completeRound(roundId: string) {
|
||||
const session = await auth.api.getSession({ headers: await headers() })
|
||||
if (!session) return { error: 'Not authenticated' }
|
||||
|
||||
const [round] = await db
|
||||
.select({ createdBy: rounds.createdBy, status: rounds.status })
|
||||
.from(rounds)
|
||||
.where(eq(rounds.id, roundId))
|
||||
.limit(1)
|
||||
|
||||
if (!round) return { error: 'Round not found' }
|
||||
if (round.createdBy !== session.user.id) return { error: 'Only the round creator can complete it' }
|
||||
if (round.status !== 'active') return { error: 'Round is not active' }
|
||||
|
||||
await db.update(rounds).set({ status: 'completed' }).where(eq(rounds.id, roundId))
|
||||
revalidatePath(`/rounds/${roundId}`)
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export async function joinRound(roundId: string) {
|
||||
const session = await auth.api.getSession({ headers: await headers() })
|
||||
if (!session) return { error: 'Not authenticated' }
|
||||
|
||||
const [roundData] = await db
|
||||
.select({ teeId: rounds.teeId, courseId: rounds.courseId, status: rounds.status })
|
||||
.from(rounds)
|
||||
.where(eq(rounds.id, roundId))
|
||||
.limit(1)
|
||||
|
||||
if (!roundData) return { error: 'Round not found' }
|
||||
if (roundData.status !== 'lobby') return { error: 'Round is not accepting new players' }
|
||||
|
||||
const [me] = await db
|
||||
.select({ handicapIndex: user.handicapIndex })
|
||||
.from(user)
|
||||
.where(eq(user.id, session.user.id))
|
||||
.limit(1)
|
||||
|
||||
const [tee] = await db
|
||||
.select({ courseRating: tees.courseRating, slopeRating: tees.slopeRating })
|
||||
.from(tees)
|
||||
.where(eq(tees.id, roundData.teeId))
|
||||
.limit(1)
|
||||
|
||||
const [course] = await db
|
||||
.select({ par: courses.par })
|
||||
.from(courses)
|
||||
.where(eq(courses.id, roundData.courseId))
|
||||
.limit(1)
|
||||
|
||||
const handicapIndex = me?.handicapIndex ? parseFloat(me.handicapIndex) : null
|
||||
const courseHandicap =
|
||||
handicapIndex !== null && tee && course
|
||||
? computeCourseHandicap(
|
||||
handicapIndex,
|
||||
tee.slopeRating,
|
||||
parseFloat(tee.courseRating),
|
||||
course.par,
|
||||
)
|
||||
: null
|
||||
|
||||
try {
|
||||
await db.insert(roundPlayers).values({
|
||||
roundId,
|
||||
userId: session.user.id,
|
||||
role: 'player',
|
||||
handicapIndex: handicapIndex !== null ? String(handicapIndex) : null,
|
||||
courseHandicap: courseHandicap,
|
||||
})
|
||||
} catch {
|
||||
return { error: 'Already a member of this round' }
|
||||
}
|
||||
|
||||
// Mark invite as accepted
|
||||
await db
|
||||
.update(roundInvites)
|
||||
.set({ acceptedAt: new Date() })
|
||||
.where(and(eq(roundInvites.roundId, roundId), eq(roundInvites.email, session.user.email!)))
|
||||
|
||||
revalidatePath(`/rounds/${roundId}`)
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
// Shared helper used by both createRoundWithInvites and inviteFromLobby
|
||||
export async function inviteToRound(roundId: string, email: string, invitedByUserId: string) {
|
||||
// Create user if they don't exist yet
|
||||
const [existing] = await db
|
||||
.select({ id: user.id })
|
||||
.from(user)
|
||||
.where(eq(user.email, email.toLowerCase()))
|
||||
.limit(1)
|
||||
|
||||
if (!existing) {
|
||||
await db.insert(user).values({
|
||||
id: crypto.randomUUID(),
|
||||
email: email.toLowerCase(),
|
||||
name: email.split('@')[0],
|
||||
emailVerified: false,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
}
|
||||
|
||||
// Send magic link via better-auth
|
||||
const callbackURL = `${process.env.SITE_URL}/rounds/${roundId}/join`
|
||||
try {
|
||||
await auth.api.signInMagicLink({
|
||||
body: { email: email.toLowerCase(), callbackURL },
|
||||
headers: await headers(),
|
||||
})
|
||||
} catch (err: any) {
|
||||
return { error: err?.message ?? 'Failed to send invite email' }
|
||||
}
|
||||
|
||||
// Record invite
|
||||
await db
|
||||
.insert(roundInvites)
|
||||
.values({ roundId, email: email.toLowerCase(), invitedBy: invitedByUserId })
|
||||
.onConflictDoNothing()
|
||||
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
+41
-25
@@ -1,13 +1,17 @@
|
||||
'use server'
|
||||
|
||||
import { z } from 'zod'
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { db } from '@/lib/db'
|
||||
import { scores, roundPlayers, rounds } from '@/lib/db/schema'
|
||||
import { eq, and } from 'drizzle-orm'
|
||||
import { auth } from '@/lib/auth'
|
||||
import { headers } from 'next/headers'
|
||||
|
||||
const UpsertScoreSchema = z.object({
|
||||
roundId: z.string().uuid(),
|
||||
holeId: z.string().uuid(),
|
||||
playerId: z.string().uuid(),
|
||||
strokes: z.number().int().min(1).max(20),
|
||||
roundId: z.string().uuid(),
|
||||
holeId: z.string().uuid(),
|
||||
playerId: z.string(),
|
||||
strokes: z.number().int().min(1).max(20),
|
||||
})
|
||||
|
||||
export async function upsertScore(
|
||||
@@ -19,27 +23,39 @@ export async function upsertScore(
|
||||
const parsed = UpsertScoreSchema.safeParse({ roundId, holeId, playerId, strokes })
|
||||
if (!parsed.success) return { error: parsed.error.issues[0]?.message ?? 'Invalid data' }
|
||||
|
||||
const supabase = await createClient()
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
if (!user) return { error: 'Not authenticated' }
|
||||
const session = await auth.api.getSession({ headers: await headers() })
|
||||
if (!session) return { error: 'Not authenticated' }
|
||||
|
||||
// Verify round is active and user is a member (RLS also enforces this)
|
||||
const { error } = await (supabase as any)
|
||||
.from('scores')
|
||||
.upsert(
|
||||
{
|
||||
round_id: parsed.data.roundId,
|
||||
hole_id: parsed.data.holeId,
|
||||
player_id: parsed.data.playerId,
|
||||
strokes: parsed.data.strokes,
|
||||
updated_by: user.id,
|
||||
updated_at: new Date().toISOString(),
|
||||
},
|
||||
{ onConflict: 'round_id,hole_id,player_id' },
|
||||
)
|
||||
// Caller must be a member of the round
|
||||
const [membership] = await db
|
||||
.select()
|
||||
.from(roundPlayers)
|
||||
.where(and(eq(roundPlayers.roundId, roundId), eq(roundPlayers.userId, session.user.id)))
|
||||
.limit(1)
|
||||
if (!membership) return { error: 'Not a member of this round' }
|
||||
|
||||
// Round must be active
|
||||
const [round] = await db
|
||||
.select({ status: rounds.status })
|
||||
.from(rounds)
|
||||
.where(eq(rounds.id, roundId))
|
||||
.limit(1)
|
||||
if (round?.status !== 'active') return { error: 'Round is not active' }
|
||||
|
||||
await db
|
||||
.insert(scores)
|
||||
.values({
|
||||
roundId,
|
||||
holeId,
|
||||
playerId,
|
||||
strokes,
|
||||
updatedBy: session.user.id,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [scores.roundId, scores.holeId, scores.playerId],
|
||||
set: { strokes, updatedBy: session.user.id, updatedAt: new Date() },
|
||||
})
|
||||
|
||||
if (error) return { error: error.message }
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user