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
+8
View File
@@ -0,0 +1,8 @@
/**
* Returns true if the given email matches the configured admin email.
* The admin email is set via ADMIN_EMAIL environment variable.
*/
export function isAdminEmail(email: string | undefined): boolean {
if (!email) return false
return email.toLowerCase() === process.env.ADMIN_EMAIL?.toLowerCase()
}
+47
View File
@@ -0,0 +1,47 @@
/**
* Returns the number of handicap strokes a player receives on a given hole.
* WHS allocation: distribute course_handicap strokes across holes by stroke index.
*/
export function strokesReceived(
courseHandicap: number,
strokeIndex: number,
holesCount: number,
): number {
if (courseHandicap <= 0) return 0
const fullStrokes = Math.floor(courseHandicap / holesCount)
const extraStrokes = courseHandicap % holesCount
return fullStrokes + (strokeIndex <= extraStrokes ? 1 : 0)
}
/**
* Net score on a hole (gross strokes minus strokes received).
*/
export function netStrokes(
gross: number,
courseHandicap: number,
strokeIndex: number,
holesCount: number,
): number {
return gross - strokesReceived(courseHandicap, strokeIndex, holesCount)
}
/**
* Score vs par as a display string: "2", "E", "+1", "+3"
*/
export function formatVsPar(diff: number): string {
if (diff === 0) return 'E'
return diff > 0 ? `+${diff}` : `${diff}`
}
/**
* CSS class for a gross score vs par (standard golf scorecard conventions).
*/
export function scoreColorClass(vsPar: number | null): string {
if (vsPar === null) return ''
if (vsPar <= -2) return 'bg-yellow-300 text-yellow-900 rounded-full' // Eagle or better
if (vsPar === -1) return 'bg-red-500 text-white rounded-full' // Birdie
if (vsPar === 0) return '' // Par
if (vsPar === 1) return 'border border-border rounded-sm' // Bogey
if (vsPar === 2) return 'border-2 border-border rounded-sm' // Double
return 'border-2 border-destructive rounded-sm text-destructive' // Triple+
}
+9
View File
@@ -0,0 +1,9 @@
import { createBrowserClient } from '@supabase/ssr'
import type { Database } from './types'
export function createClient() {
return createBrowserClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
)
}
+51
View File
@@ -0,0 +1,51 @@
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
import type { Database } from './types'
export async function createClient() {
const cookieStore = await cookies()
return createServerClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll()
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options),
)
} catch {
// Server Component — cookie setting ignored (middleware handles refresh)
}
},
},
},
)
}
export async function createAdminClient() {
const cookieStore = await cookies()
return createServerClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll()
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options),
)
} catch {}
},
},
},
)
}
+10
View File
@@ -0,0 +1,10 @@
// Auto-generated types will go here after running: supabase gen types typescript
// For now, a placeholder that will be replaced once the DB schema is applied.
export type Database = {
public: {
Tables: Record<string, never>
Views: Record<string, never>
Functions: Record<string, never>
Enums: Record<string, never>
}
}
+6
View File
@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}