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
+366
View File
@@ -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>
)
}