teecup/frontend/components/ny-runde/step2-format.tsx

316 lines
9.9 KiB
TypeScript
Raw Normal View History

"use client"
import { FORMATS, FORMAT_MAP, isScrambleVsSolo, isTwoSided } from "@/lib/ny-runde/formats"
import type { StatLevel } from "@/lib/ny-runde/types"
import { cn } from "@/lib/utils"
import { ChevronDown } from "lucide-react"
import { useState } from "react"
import { ChoiceCard, Field, Panel, Segmented, Stepper, TextInput, Toggle, ToggleButton } from "./primitives"
import { useWizard } from "./wizard-context"
const STAT_OPTIONS: { value: StatLevel; title: string; description: string }[] = [
{ value: "score", title: "Kun slag", description: "Registrer bare score per hull." },
{ value: "score_putts", title: "Slag og putter", description: "Score pluss antall putter." },
{ value: "full", title: "All statistikk", description: "Kølle, retning, chip, bunker, straff m.m." },
]
export function Step2Format() {
const { state, patch } = useWizard()
const format = state.format
const selectedDef = FORMAT_MAP[format]
const svs = isScrambleVsSolo(format)
return (
<div className="flex flex-col gap-6">
<div>
<h2 className="text-lg font-semibold tracking-tight text-[var(--nr-ink)]">Spilleform</h2>
<p className="text-sm text-[var(--nr-muted)]">Velg statistikknivå og hvordan runden spilles.</p>
</div>
{/* Stat level for owner */}
<section className="flex flex-col gap-2">
<h3 className="text-sm font-semibold text-[var(--nr-ink)]">Statistikk for deg selv</h3>
<div className="grid gap-2 sm:grid-cols-3">
{STAT_OPTIONS.map((o) => (
<ChoiceCard
key={o.value}
selected={state.statLevel === o.value}
onSelect={() => patch({ statLevel: o.value })}
title={o.title}
description={o.description}
/>
))}
</div>
</section>
{/* Format grid */}
<section className="flex flex-col gap-2">
<h3 className="text-sm font-semibold text-[var(--nr-ink)]">Spilleform</h3>
<div
role="radiogroup"
aria-label="Spilleform"
className="grid grid-cols-2 gap-2 sm:grid-cols-3"
>
{FORMATS.map((f) => (
<ToggleButton key={f.id} active={format === f.id} onClick={() => patch({ format: f.id })}>
{f.label}
</ToggleButton>
))}
</div>
<Panel className="mt-1 bg-[var(--nr-surface-2)] py-3">
<p className="text-sm text-[var(--nr-ink)]">
<span className="font-semibold">{selectedDef.label}.</span>{" "}
<span className="text-[var(--nr-muted)]">{selectedDef.description}</span>
</p>
</Panel>
</section>
{/* Format-specific config */}
<FormatConfig />
{/* Solo allowance (scramble vs solo only) */}
{svs ? (
<section className="flex flex-col gap-2">
<h3 className="text-sm font-semibold text-[var(--nr-ink)]">Individualspillerens handicap-andel</h3>
<div className="flex items-center gap-3">
<Stepper
value={state.soloAllowance}
onChange={(v) => patch({ soloAllowance: v })}
min={0}
max={100}
step={5}
snap
suffix="%"
/>
<p className="text-xs text-[var(--nr-muted)]">Andel av enkeltspillerens handicap som teller (0100 %).</p>
</div>
</section>
) : null}
{/* Advanced HCP (hidden for scramble vs solo) */}
{!svs ? <AdvancedHcp /> : null}
{/* Number of holes */}
<section className="flex flex-col gap-2">
<h3 className="text-sm font-semibold text-[var(--nr-ink)]">Antall hull</h3>
<div className="max-w-xs">
<Segmented
label="Antall hull"
value={String(state.numHoles) as "9" | "18"}
onChange={(v) => patch({ numHoles: v === "9" ? 9 : 18 })}
options={[
{ value: "9", label: "9 hull" },
{ value: "18", label: "18 hull" },
]}
/>
</div>
</section>
</div>
)
}
function FormatConfig() {
const { state, patch } = useWizard()
const f = state.format
if (f === "match") {
return (
<ConfigCard>
<CheckboxRow
checked={state.matchExcludeHcp}
onChange={(v) => patch({ matchExcludeHcp: v })}
label="Ekskluder denne runden fra mitt faktiske HCP"
/>
</ConfigCard>
)
}
if (f === "skins") {
return (
<ConfigCard>
<Field label="Poengberegning">
<Segmented
value={state.skinsScoring}
onChange={(v) => patch({ skinsScoring: v })}
options={[
{ value: "net", label: "Netto (m/HCP)" },
{ value: "gross", label: "Brutto" },
]}
/>
</Field>
<Field label="Ved uavgjort hull">
<Segmented
value={state.skinsTie}
onChange={(v) => patch({ skinsTie: v })}
options={[
{ value: "carry", label: "Rullerer over" },
{ value: "split", label: "Deles likt" },
]}
/>
</Field>
<p className="text-xs text-[var(--nr-muted)]">Skins krever minst 3 spillere.</p>
</ConfigCard>
)
}
if (f === "bbb") {
return (
<ConfigCard>
<CheckboxRow
checked={state.bbbSweep}
onChange={(v) => patch({ bbbSweep: v })}
label="Bonuspoeng ved «sveip»"
/>
</ConfigCard>
)
}
if (f === "shamble") {
return (
<ConfigCard>
<Field label="Beste N av laget teller per hull">
<div className="flex gap-2">
{[1, 2, 3, 4].map((n) => (
<ToggleButton
key={n}
active={state.shambleBestN === n}
onClick={() => patch({ shambleBestN: n })}
className="flex-1"
>
{n}
</ToggleButton>
))}
</div>
</Field>
</ConfigCard>
)
}
if (f === "moneyball") {
return (
<ConfigCard>
<p className="text-sm text-[var(--nr-muted)]">Rotasjonen for «money ball» settes opp i neste steg.</p>
</ConfigCard>
)
}
if (f === "copenhagen") {
return (
<ConfigCard>
<p className="text-sm text-[var(--nr-muted)]">Nøyaktig 3 spillere.</p>
</ConfigCard>
)
}
if (isScrambleVsSolo(f)) {
return null // handled by the dedicated allowance stepper
}
if (isTwoSided(f)) {
return (
<ConfigCard>
<p className="text-sm text-[var(--nr-muted)]">Du setter opp lagene i neste steg.</p>
</ConfigCard>
)
}
return null
}
function AdvancedHcp() {
const { state, patch } = useWizard()
const [open, setOpen] = useState(false)
const f = state.format
const isSlagOrStable = f === "slagspill" || f === "stableford"
return (
<section className="rounded-xl border border-[var(--nr-border)] bg-[var(--nr-surface)]">
<button
type="button"
onClick={() => setOpen((o) => !o)}
aria-expanded={open}
className="flex min-h-11 w-full items-center justify-between gap-2 px-4 py-3 text-left focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-[var(--nr-accent-ring)]/40"
>
<span className="text-sm font-semibold text-[var(--nr-ink)]">Avanserte handicap-innstillinger</span>
<ChevronDown className={cn("h-4 w-4 text-[var(--nr-muted)] transition-transform", open && "rotate-180")} aria-hidden="true" />
</button>
{open ? (
<div className="flex flex-col gap-4 border-t border-[var(--nr-border)] p-4">
<Toggle checked={state.useHandicap} onChange={(v) => patch({ useHandicap: v })} label="Bruk handicap" />
{state.useHandicap && !isSlagOrStable ? (
<Toggle
checked={state.useCourseHcpAdj}
onChange={(v) => patch({ useCourseHcpAdj: v })}
label="Bruk course handicap-justering"
/>
) : null}
{state.useHandicap && isTwoSided(f) ? (
<Toggle
checked={state.useMatchplayHcp}
onChange={(v) => patch({ useMatchplayHcp: v })}
label="Bruk matchplay-handicap"
/>
) : null}
{state.useHandicap ? (
<Field label="HCP-prosent" optional htmlFor="hcp-pct">
<TextInput
id="hcp-pct"
inputMode="decimal"
value={state.hcpPercent}
onChange={(e) => patch({ hcpPercent: e.target.value })}
placeholder="F.eks. 95"
className="max-w-[10rem]"
/>
</Field>
) : null}
</div>
) : null}
</section>
)
}
function ConfigCard({ children }: { children: React.ReactNode }) {
return (
<section className="flex flex-col gap-4 rounded-xl border border-[var(--nr-border)] bg-[var(--nr-surface)] p-4">
{children}
</section>
)
}
function CheckboxRow({
checked,
onChange,
label,
}: {
checked: boolean
onChange: (v: boolean) => void
label: string
}) {
return (
<button
type="button"
role="checkbox"
aria-checked={checked}
onClick={() => onChange(!checked)}
className="flex min-h-11 w-full items-center gap-3 text-left focus-visible:outline-none"
>
<span
className={cn(
"flex h-5 w-5 shrink-0 items-center justify-center rounded-[6px] border transition-colors",
checked ? "border-[var(--nr-accent)] bg-[var(--nr-accent)] text-[var(--nr-accent-ink)]" : "border-[var(--nr-border-strong)] bg-[var(--nr-surface)]",
)}
>
{checked ? (
<svg viewBox="0 0 16 16" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2.5">
<path d="M3 8.5l3.5 3.5L13 5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
) : null}
</span>
<span className="text-sm text-[var(--nr-ink)]">{label}</span>
</button>
)
}