Turneringsoppsett som steg-wizard (ADR-092)
Oppsett-fanen var én lang stablet side med alle kort etter hverandre. Omgjort til 6 klikkbare steg med fri navigasjon, strukturelt inspirert av Golf GameBooks steg-navigasjon (ikke visuelt -- egne clubhouse- tokens). Grupper og startliste er det eneste genuint nye steget, løfter Utslagsgrupper ut av å være gjemt inni hver enkelt runde. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
6f25b57f90
commit
1867c65143
5 changed files with 425 additions and 128 deletions
|
|
@ -9319,6 +9319,89 @@ allerede fungerer andre steder i appen).
|
|||
ekte `teecup_db`, deretter `docker compose build teecup_api
|
||||
teecup_frontend && up -d` for begge. Rene logger, 200 OK.
|
||||
|
||||
## ADR-092: Turneringsoppsett som steg-wizard, strukturelt inspirert av Golf GameBook (2026-08-18)
|
||||
|
||||
Bruker viste skjermdumper av Golf GameBooks turneringsoppsett (steg-
|
||||
navigasjon: Tournament Setup → Round Setup → Tournament Site → Branding
|
||||
→ Entries → Players → Groups and Start List → Results and Options) og
|
||||
spurte: "Jeg synes denne delen av TeeCup er veldig lite brukervennlig.
|
||||
Kan du hjelpe meg å tenke gjennom dette?" -- TeeCups "Oppsett"-fane
|
||||
(`SetupTab` i `individual-tournament-detail.tsx`) var én lang stablet
|
||||
side med alle kort etter hverandre (`TournamentSettingsCard` →
|
||||
Scoringsmetode → `RoundsCard` → `CutCard`/`TiebreakCard` →
|
||||
`ClassesCard` → `ParticipantsCard`), i byggerekkefølge, ikke i en
|
||||
rekkefølge en arrangør faktisk tenker gjennom oppsettet i. Bruker
|
||||
svarte "Full wizard. Bring gjerne V0 inn", med ett eksplisitt forbehold
|
||||
underveis: "Yes, men husk at vi ikke skal ende opp med et plagiat av
|
||||
Golf GameBook."
|
||||
|
||||
**Struktur, ikke visuell kopi.** GameBook-skjermdumpene ble brukt
|
||||
UTELUKKENDE som referanse for selve IA-mønsteret (steg i stedet for én
|
||||
lang side), ikke som visuell mal -- V0-prompten instruerte eksplisitt
|
||||
om å bruke TeeCups egne clubhouse-fargetoken/`rounded-2xl`-kortstil fra
|
||||
`DESIGN_SYSTEM.md`, ingen grønnfarge/ikonsett/spacing hentet fra
|
||||
GameBook. Dette var et rent IA-/navigasjonsproblem, ikke et manglende-
|
||||
funksjonalitet-problem -- praktisk talt alle felt GameBook viser fantes
|
||||
allerede og fungerte. Ingen backend-endring, ingen migrasjon.
|
||||
|
||||
**6 steg** (ikke GameBooks 8 -- "Tournament Site"/"Branding" dekkes
|
||||
allerede av TeeCups egen frittstående Presentasjon-fane, "Entries" er
|
||||
allerede en velfungerende inne-i-kortet-seksjon i `TournamentSettings
|
||||
Card` som ikke var verdt å splitte ut av et nylig ferdigstilt kort),
|
||||
hver drevet av et kort som allerede fantes og fungerte -- ingen av dem
|
||||
trengte V0-rework: Turneringsoppsett (`TournamentSettingsCard`), Runder
|
||||
og baner (`RoundsCard`), Klasser (`ClassesCard`), Spillere
|
||||
(`ParticipantsCard`), Resultat og opsjoner (Scoringsmetode-seksjonen,
|
||||
flyttet hit fra å stå løst øverst siden det er et resultatvalg ikke
|
||||
identitet, + `CutCard` + `TiebreakCard`, samme betingede visning som
|
||||
før).
|
||||
|
||||
**Grupper og startliste er det eneste genuint nye steget.**
|
||||
`RoundGroupsPanel` var tidligere gjemt bak en "Utslagsgrupper"-knapp
|
||||
inni hver enkelt `RoundCard` (måtte åpne runden først) -- direkte i
|
||||
ånden til brukerens opprinnelige klage om at rundene har mye å ta tak i
|
||||
mens org-oppsettet har lite. Løftet til et eget toppnivå-steg: ny
|
||||
`RoundGroupsStepList`-komponent (V0-generert) lister alle runder
|
||||
(sammenhengende liste-mønster, `divide-y`, ikke separate kort) med én
|
||||
"Rediger grupper"-knapp per runde som åpner samme `RoundGroupsPanel`
|
||||
uendret. Den gamle knappen inni `RoundCard` ble FJERNET (ikke bare
|
||||
duplisert) -- to veier til samme handling ville vært akkurat den typen
|
||||
duplisering wizarden var ment å rydde opp i.
|
||||
|
||||
**Fri navigasjon, ingen låsing.** GameBooks egne skjermdumper viser
|
||||
faktisk grønne haker på ALLE 8 steg samtidig som "Tournament name"
|
||||
sto tomt i opprettelses-flyten -- hakene er altså ikke ekte fullført-
|
||||
validering, bare statisk wizard-pynt. Ny `TournamentSetupNav`
|
||||
(V0-generert): alle 6 steg alltid klikkbare i vilkårlig rekkefølge,
|
||||
ingen fullført-per-steg-logikk bygget (TeeCup er allerede fleksibelt på
|
||||
oppsett-rekkefølge, og en slik validering ble aldri etterspurt). Steget
|
||||
er ren `useState` inni `SetupTab`, ikke URL-drevet -- samme mønster som
|
||||
de ytre fanene (Oppsett/Scorekort/Leaderboard/Presentasjon) allerede
|
||||
bruker.
|
||||
|
||||
**Bredde-regresjon funnet og rettet under verifisering:** Oppsett-fanen
|
||||
delte tidligere en `max-w-4xl` (896px) sidewrapper med Scorekort/
|
||||
Presentasjon-fanene. Ved en lokal scratch-visning på 1440px viste det
|
||||
seg at 6 steg-piller IKKE fikk plass side ved side i 896px -- akkurat
|
||||
den "stor skjerm først"-egenskapen wizarden skulle innfri (jf. samme
|
||||
mønster som leaderboardets `max-w-[110rem]`-unntak fra tidligere denne
|
||||
dagen). Fikset med en egen `max-w-6xl`-unntaksbredde for `tab ===
|
||||
"setup"` i den delte ternæren i `individual-tournament-detail.tsx`
|
||||
sin `<main>`.
|
||||
|
||||
**Verifisert:** `tsc --noEmit` rent, `vitest run` 55/55. Lokal scratch-
|
||||
visning (`next dev` på port 3100, midlertidig `/setupwizardpreviewtmp`-
|
||||
side, ALDRI committet, fjernet etter bruk): alle 6 steg klikkbare og
|
||||
viser riktig innhold, Grupper-steget bekreftet å sende riktig runde-id
|
||||
videre til panelet (tre varianter testet: dato+bane, kun bane, ingen av
|
||||
delene), lys+mørk tema, smal skjerm (390px -- steg-raden scroller
|
||||
sidelengs i stedet for å wrappe, som spesifisert, i stedet for feilende
|
||||
layout).
|
||||
|
||||
**Rullet ut 2026-08-18** -- bruker bekreftet. Ingen migrasjon (ren
|
||||
frontend). `docker compose build teecup_frontend && up -d
|
||||
teecup_frontend`, rene logger, 200 OK.
|
||||
|
||||
---
|
||||
|
||||
## Utviklingsplan (rekkefølge)
|
||||
|
|
|
|||
36
CHANGELOG.md
36
CHANGELOG.md
|
|
@ -12878,3 +12878,39 @@ Neste steg:
|
|||
**Rullet ut 2026-08-18** -- bruker bekreftet. Migrasjon 087 kjørt mot
|
||||
ekte `teecup_db`, deretter `docker compose build teecup_api
|
||||
teecup_frontend && up -d` for begge, rene logger, 200 OK.
|
||||
|
||||
123. **Turneringsoppsett som steg-wizard -- 2026-08-18 (ADR-092).**
|
||||
Bruker viste GameBook-skjermdumper: "Jeg synes denne delen av
|
||||
TeeCup er veldig lite brukervennlig ... Full wizard. Bring gjerne
|
||||
V0 inn", med presisering underveis: "husk at vi ikke skal ende opp
|
||||
med et plagiat av Golf GameBook." Oppsett-fanen (`SetupTab`) var én
|
||||
lang stablet side -- omgjort til 6 klikkbare steg (Turnerings-
|
||||
oppsett/Runder og baner/Klasser/Spillere/Grupper og startliste/
|
||||
Resultat og opsjoner), fri navigasjon uten låsing eller fullført-
|
||||
haker (GameBooks egne skjermdumper viste haker på alle steg selv
|
||||
med tomt navnefelt -- ikke ekte validering). Kun to nye V0-
|
||||
komponenter, GameBook brukt UTELUKKENDE som strukturelt forbilde
|
||||
(steg-mønsteret), ikke visuelt -- begge bruker TeeCups egne
|
||||
clubhouse-tokens fra `DESIGN_SYSTEM.md`: `tournament-setup-nav.tsx`
|
||||
(steg-raden) og `round-groups-step-list.tsx` (ny topplassering for
|
||||
"Utslagsgrupper", tidligere gjemt bak en knapp inni hver enkelt
|
||||
runde -- den gamle knappen fjernet, ikke duplisert). Alle 6 stegs
|
||||
faktiske innhold er allerede-eksisterende kort (TournamentSettings
|
||||
Card/RoundsCard/ClassesCard/ParticipantsCard/CutCard/TiebreakCard),
|
||||
kun flyttet til riktig steg -- ingen ny logikk, ingen backend-
|
||||
endring, ingen migrasjon. Fant og fikset en bredde-regresjon
|
||||
underveis: Oppsett delte en `max-w-4xl`-wrapper med Scorekort/
|
||||
Presentasjon som gjorde at 6 steg-piller ikke fikk plass ved siden
|
||||
av hverandre selv på en bred skjerm -- egen `max-w-6xl`-unntaksbredde
|
||||
lagt til (samme mønster som leaderboardets `max-w-[110rem]`-unntak).
|
||||
|
||||
**Verifisert:** `tsc --noEmit` rent, `vitest run` 55/55. Lokal
|
||||
scratch-visning (`next dev`, midlertidig previewside, ALDRI
|
||||
committet, fjernet etter bruk): alle 6 steg klikkbare med riktig
|
||||
innhold, Grupper-steget sender riktig runde-id videre (tre
|
||||
varianter: dato+bane/kun bane/ingen av delene), lys+mørk, smal
|
||||
skjerm (steg-raden scroller sidelengs i stedet for å wrappe/feile).
|
||||
|
||||
**Rullet ut 2026-08-18** -- bruker bekreftet. Ingen migrasjon (ren
|
||||
frontend). `docker compose build teecup_frontend && up -d
|
||||
teecup_frontend`, rene logger, 200 OK.
|
||||
|
|
|
|||
|
|
@ -29,9 +29,11 @@ import {
|
|||
Crosshair,
|
||||
Flag,
|
||||
KeyRound,
|
||||
Layers,
|
||||
MapPin,
|
||||
Medal,
|
||||
Plus,
|
||||
Settings2,
|
||||
Trash2,
|
||||
Trophy,
|
||||
Upload,
|
||||
|
|
@ -44,7 +46,9 @@ import { Input } from "@/components/ui/input"
|
|||
import { Label } from "@/components/ui/label"
|
||||
import { PlayerImportPanel } from "@/components/player-import-panel"
|
||||
import { RoundGroupsPanel } from "@/components/round-groups-panel"
|
||||
import { RoundGroupsStepList, type RoundSummary } from "@/components/round-groups-step-list"
|
||||
import { TournamentSettingsCard, type TournamentSettingsValues } from "@/components/tournament-settings-card"
|
||||
import { TournamentSetupNav, type SetupStep } from "@/components/tournament-setup-nav"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
|
|
@ -991,11 +995,17 @@ export function IndividualTournamentDetail({
|
|||
egen kommentar om "storskjerm uten skrolling"), men satt inni denne
|
||||
delte 4xl-wrapperen (896px, samme wrapper som skjema-tunge Oppsett/
|
||||
Presentasjon-fanene bruker) fikk den aldri sjansen til det -- felles
|
||||
med resten av siden siden komponenten ble bygget/testet isolert. */}
|
||||
med resten av siden siden komponenten ble bygget/testet isolert.
|
||||
|
||||
Oppsett-fanen fikk samme problem 2026-08-18 (ADR-092): 6 steg i
|
||||
TournamentSetupNav får ikke plass side ved side i 896px, selv på en
|
||||
bred skjerm -- akkurat den "stor skjerm først"-regelen wizarden
|
||||
skulle innfri. Egen, litt smalere unntaksbredde enn leaderboardets
|
||||
110rem (dette er skjemainnhold, ikke en datatabell). */}
|
||||
<main
|
||||
className={cn(
|
||||
"mx-auto w-full flex-1 px-5 py-6 sm:py-8",
|
||||
tab === "leaderboard" ? "max-w-[110rem]" : "max-w-4xl",
|
||||
tab === "leaderboard" ? "max-w-[110rem]" : tab === "setup" ? "max-w-6xl" : "max-w-4xl",
|
||||
)}
|
||||
>
|
||||
{error && (
|
||||
|
|
@ -1104,6 +1114,21 @@ function fromDatetimeLocalValue(value: string | null): string | null {
|
|||
return Number.isNaN(date.getTime()) ? null : date.toISOString()
|
||||
}
|
||||
|
||||
// Wizard-steg (ADR-092) -- GameBook-skjermdumper var strukturelt forbilde
|
||||
// (steg i stedet for én lang stablet side), IKKE visuelt forbilde.
|
||||
// Fri navigasjon -- ingen låsing/fullført-haker, se ADR-092: GameBooks
|
||||
// egne skjermdumper viser haker på alle steg selv med tomt navnefelt,
|
||||
// altså ikke ekte validering, og TeeCup er allerede fleksibelt på
|
||||
// oppsett-rekkefølge.
|
||||
const SETUP_STEPS: SetupStep[] = [
|
||||
{ key: "basics", label: "Turneringsoppsett", icon: Settings2 },
|
||||
{ key: "rounds", label: "Runder og baner", icon: Flag },
|
||||
{ key: "classes", label: "Klasser", icon: Layers },
|
||||
{ key: "players", label: "Spillere", icon: Users },
|
||||
{ key: "groups", label: "Grupper og startliste", icon: Clock },
|
||||
{ key: "results", label: "Resultat og opsjoner", icon: Trophy },
|
||||
]
|
||||
|
||||
function SetupTab({
|
||||
base,
|
||||
organizationId,
|
||||
|
|
@ -1171,6 +1196,8 @@ function SetupTab({
|
|||
) => Promise<void>
|
||||
onError: (message: string) => void
|
||||
}) {
|
||||
const [step, setStep] = useState<string>("basics")
|
||||
const [groupsOpenFor, setGroupsOpenFor] = useState<string | null>(null)
|
||||
const [pool, setPool] = useState<ApiPlayer[]>([])
|
||||
|
||||
async function refreshPool() {
|
||||
|
|
@ -1202,116 +1229,151 @@ function SetupTab({
|
|||
registrationRequiresApproval: tournament.registration_requires_approval,
|
||||
}
|
||||
|
||||
const roundSummaries: RoundSummary[] = rounds.map((r) => ({
|
||||
id: r.id,
|
||||
name: r.name || `Runde ${r.sequence}`,
|
||||
sequence: r.sequence,
|
||||
courseName: r.course_name || null,
|
||||
dateLabel: r.scheduled_at
|
||||
? new Date(r.scheduled_at).toLocaleDateString("nb-NO", { day: "numeric", month: "short", year: "numeric" })
|
||||
: null,
|
||||
}))
|
||||
const groupsOpenRound = rounds.find((r) => r.id === groupsOpenFor) ?? null
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<TournamentSettingsCard values={settingsValues} onSave={onUpdateSettings} />
|
||||
<TournamentSetupNav steps={SETUP_STEPS} activeStep={step} onChange={setStep}>
|
||||
{step === "basics" && <TournamentSettingsCard values={settingsValues} onSave={onUpdateSettings} />}
|
||||
|
||||
<section className="flex flex-col gap-3 rounded-2xl border border-border bg-card p-4 shadow-md shadow-black/8">
|
||||
<h2 className="text-base font-bold text-foreground">Scoringsmetode</h2>
|
||||
<p className="text-sm leading-relaxed text-muted-foreground text-pretty">
|
||||
Avgjør hvordan totalen på leaderboardet regnes ut, og om deltakere trenger
|
||||
registrert handicap og kjønn.
|
||||
</p>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="flex min-h-11 w-fit items-center gap-2 rounded-xl border border-border bg-background px-4 text-sm font-bold text-foreground transition-colors hover:bg-accent/50">
|
||||
{tournament.scoring_method ? SCORING_METHOD_LABELS[tournament.scoring_method] : "Ikke satt"}
|
||||
<ChevronDown aria-hidden="true" className="size-4 text-muted-foreground" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-56 rounded-2xl p-1.5">
|
||||
{Object.entries(SCORING_METHOD_LABELS).map(([value, label]) => (
|
||||
<DropdownMenuItem
|
||||
key={value}
|
||||
onClick={() => onUpdateScoringMethod(value)}
|
||||
className="cursor-pointer rounded-xl px-3 py-2.5 text-sm font-semibold"
|
||||
>
|
||||
{label}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{tournament.scoring_method === "bingo_bango_bongo" && (
|
||||
<label className="flex min-h-11 items-start gap-3 rounded-xl border border-border bg-background px-4 py-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={tournament.bbb_sweep_bonus_enabled}
|
||||
onChange={(e) => onUpdateBbbSweepBonus(e.target.checked)}
|
||||
className="mt-1 size-5 shrink-0 accent-primary"
|
||||
{step === "rounds" && (
|
||||
<RoundsCard
|
||||
base={base}
|
||||
organizationId={organizationId}
|
||||
rounds={rounds}
|
||||
courses={courses}
|
||||
participants={participants}
|
||||
classes={classes}
|
||||
scoringMethod={tournament.scoring_method}
|
||||
onAddRound={onAddRound}
|
||||
onDeleteRound={onDeleteRound}
|
||||
onAddCourse={onAddCourse}
|
||||
onCourseImported={onCourseImported}
|
||||
onError={onError}
|
||||
/>
|
||||
)}
|
||||
|
||||
{step === "classes" && (
|
||||
<ClassesCard
|
||||
classes={classes}
|
||||
courses={courses}
|
||||
organizationId={organizationId}
|
||||
onCreate={onCreateClass}
|
||||
onDelete={onDeleteClass}
|
||||
/>
|
||||
)}
|
||||
|
||||
{step === "players" && (
|
||||
<ParticipantsCard
|
||||
organizationId={organizationId}
|
||||
tournamentId={tournament.id}
|
||||
participants={participants}
|
||||
pool={pool}
|
||||
classes={classes}
|
||||
onAddParticipant={onAddParticipant}
|
||||
onAddNewPlayer={onAddNewPlayer}
|
||||
onImportComplete={async () => {
|
||||
await Promise.all([refreshPool(), onImportComplete()])
|
||||
}}
|
||||
onRemoveParticipant={onRemoveParticipant}
|
||||
onSetClass={onSetParticipantClass}
|
||||
onSetStatLevel={onSetParticipantStatLevel}
|
||||
onSetHandicap={onSetParticipantHandicap}
|
||||
onSetStatus={onSetParticipantStatus}
|
||||
/>
|
||||
)}
|
||||
|
||||
{step === "groups" && (
|
||||
<div className="flex flex-col gap-6">
|
||||
<RoundGroupsStepList rounds={roundSummaries} onManageGroups={setGroupsOpenFor} />
|
||||
{groupsOpenRound && (
|
||||
<RoundGroupsPanel
|
||||
base={base}
|
||||
roundId={groupsOpenRound.id}
|
||||
roundLabel={groupsOpenRound.name || `Runde ${groupsOpenRound.sequence}`}
|
||||
onClose={() => setGroupsOpenFor(null)}
|
||||
/>
|
||||
<span className="flex flex-col gap-0.5">
|
||||
<span className="text-sm font-bold text-foreground">Bonuspoeng ved "sveip"</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Ekstra poeng til en spiller som vinner alle tre kategoriene på samme hull.
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
)}
|
||||
{tournament.scoring_method === "flag" && (
|
||||
<label className="flex min-h-11 items-start gap-3 rounded-xl border border-border bg-background px-4 py-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={tournament.flag_map_visible}
|
||||
onChange={(e) => onUpdateFlagMapVisible(e.target.checked)}
|
||||
className="mt-1 size-5 shrink-0 accent-primary"
|
||||
/>
|
||||
<span className="flex flex-col gap-0.5">
|
||||
<span className="text-sm font-bold text-foreground">Kartoversikt over flagg</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Når dette er PÅ, kan alle deltakere se hvor alle andre har plantet flagget. Av som standard.
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<RoundsCard
|
||||
base={base}
|
||||
organizationId={organizationId}
|
||||
rounds={rounds}
|
||||
courses={courses}
|
||||
participants={participants}
|
||||
classes={classes}
|
||||
scoringMethod={tournament.scoring_method}
|
||||
onAddRound={onAddRound}
|
||||
onDeleteRound={onDeleteRound}
|
||||
onAddCourse={onAddCourse}
|
||||
onCourseImported={onCourseImported}
|
||||
onError={onError}
|
||||
/>
|
||||
{step === "results" && (
|
||||
<div className="flex flex-col gap-6">
|
||||
<section className="flex flex-col gap-3 rounded-2xl border border-border bg-card p-4 shadow-md shadow-black/8">
|
||||
<h2 className="text-base font-bold text-foreground">Scoringsmetode</h2>
|
||||
<p className="text-sm leading-relaxed text-muted-foreground text-pretty">
|
||||
Avgjør hvordan totalen på leaderboardet regnes ut, og om deltakere trenger
|
||||
registrert handicap og kjønn.
|
||||
</p>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="flex min-h-11 w-fit items-center gap-2 rounded-xl border border-border bg-background px-4 text-sm font-bold text-foreground transition-colors hover:bg-accent/50">
|
||||
{tournament.scoring_method ? SCORING_METHOD_LABELS[tournament.scoring_method] : "Ikke satt"}
|
||||
<ChevronDown aria-hidden="true" className="size-4 text-muted-foreground" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-56 rounded-2xl p-1.5">
|
||||
{Object.entries(SCORING_METHOD_LABELS).map(([value, label]) => (
|
||||
<DropdownMenuItem
|
||||
key={value}
|
||||
onClick={() => onUpdateScoringMethod(value)}
|
||||
className="cursor-pointer rounded-xl px-3 py-2.5 text-sm font-semibold"
|
||||
>
|
||||
{label}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{tournament.scoring_method === "bingo_bango_bongo" && (
|
||||
<label className="flex min-h-11 items-start gap-3 rounded-xl border border-border bg-background px-4 py-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={tournament.bbb_sweep_bonus_enabled}
|
||||
onChange={(e) => onUpdateBbbSweepBonus(e.target.checked)}
|
||||
className="mt-1 size-5 shrink-0 accent-primary"
|
||||
/>
|
||||
<span className="flex flex-col gap-0.5">
|
||||
<span className="text-sm font-bold text-foreground">Bonuspoeng ved "sveip"</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Ekstra poeng til en spiller som vinner alle tre kategoriene på samme hull.
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
)}
|
||||
{tournament.scoring_method === "flag" && (
|
||||
<label className="flex min-h-11 items-start gap-3 rounded-xl border border-border bg-background px-4 py-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={tournament.flag_map_visible}
|
||||
onChange={(e) => onUpdateFlagMapVisible(e.target.checked)}
|
||||
className="mt-1 size-5 shrink-0 accent-primary"
|
||||
/>
|
||||
<span className="flex flex-col gap-0.5">
|
||||
<span className="text-sm font-bold text-foreground">Kartoversikt over flagg</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Når dette er PÅ, kan alle deltakere se hvor alle andre har plantet flagget. Av som standard.
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{tournament.scoring_method != null &&
|
||||
["stroke_gross", "stroke_net", "stableford"].includes(tournament.scoring_method) && (
|
||||
<>
|
||||
<CutCard tournament={tournament} rounds={rounds} onUpdateConfig={onUpdateCutConfig} onApplyCut={onApplyCut} />
|
||||
<TiebreakCard tournament={tournament} onUpdateConfig={onUpdateTiebreakConfig} />
|
||||
</>
|
||||
)}
|
||||
|
||||
<ClassesCard
|
||||
classes={classes}
|
||||
courses={courses}
|
||||
organizationId={organizationId}
|
||||
onCreate={onCreateClass}
|
||||
onDelete={onDeleteClass}
|
||||
/>
|
||||
|
||||
<ParticipantsCard
|
||||
organizationId={organizationId}
|
||||
tournamentId={tournament.id}
|
||||
participants={participants}
|
||||
pool={pool}
|
||||
classes={classes}
|
||||
onAddParticipant={onAddParticipant}
|
||||
onAddNewPlayer={onAddNewPlayer}
|
||||
onImportComplete={async () => {
|
||||
await Promise.all([refreshPool(), onImportComplete()])
|
||||
}}
|
||||
onRemoveParticipant={onRemoveParticipant}
|
||||
onSetClass={onSetParticipantClass}
|
||||
onSetStatLevel={onSetParticipantStatLevel}
|
||||
onSetHandicap={onSetParticipantHandicap}
|
||||
onSetStatus={onSetParticipantStatus}
|
||||
/>
|
||||
</div>
|
||||
{tournament.scoring_method != null &&
|
||||
["stroke_gross", "stroke_net", "stableford"].includes(tournament.scoring_method) && (
|
||||
<>
|
||||
<CutCard tournament={tournament} rounds={rounds} onUpdateConfig={onUpdateCutConfig} onApplyCut={onApplyCut} />
|
||||
<TiebreakCard tournament={tournament} onUpdateConfig={onUpdateTiebreakConfig} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</TournamentSetupNav>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -2603,7 +2665,6 @@ function RoundCard({
|
|||
const [roundParticipants, setRoundParticipants] = useState<ApiRoundParticipant[] | null>(null)
|
||||
const [tees, setTees] = useState<ApiTee[]>([])
|
||||
const [confirmingDelete, setConfirmingDelete] = useState(false)
|
||||
const [groupsOpen, setGroupsOpen] = useState(false)
|
||||
|
||||
async function loadRoundParticipants() {
|
||||
const [rpData, teeData] = await Promise.all([
|
||||
|
|
@ -2758,30 +2819,10 @@ function RoundCard({
|
|||
deltakere før det er på plass.
|
||||
</p>
|
||||
)}
|
||||
{roundParticipants.length > 0 && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={() => setGroupsOpen(true)}
|
||||
className="h-10 w-full rounded-xl text-sm font-bold"
|
||||
>
|
||||
<Clock aria-hidden="true" className="size-4" />
|
||||
Utslagsgrupper -- hvem spiller med hvem, klokka når
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{groupsOpen && (
|
||||
<RoundGroupsPanel
|
||||
base={base}
|
||||
roundId={round.id}
|
||||
roundLabel={round.name || `Runde ${round.sequence}`}
|
||||
onClose={() => setGroupsOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
66
frontend/components/round-groups-step-list.tsx
Normal file
66
frontend/components/round-groups-step-list.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
"use client"
|
||||
|
||||
import { Clock } from "lucide-react"
|
||||
|
||||
export type RoundSummary = {
|
||||
id: string
|
||||
name: string
|
||||
sequence: number
|
||||
courseName: string | null
|
||||
dateLabel: string | null // already formatted, e.g. "20. sep 2026", or null if unscheduled
|
||||
}
|
||||
|
||||
export type RoundGroupsStepListProps = {
|
||||
rounds: RoundSummary[]
|
||||
onManageGroups: (roundId: string) => void
|
||||
}
|
||||
|
||||
export function RoundGroupsStepList({ rounds, onManageGroups }: RoundGroupsStepListProps) {
|
||||
if (rounds.length === 0) {
|
||||
return (
|
||||
<div className="rounded-2xl border border-border bg-card p-6 shadow-md shadow-black/8">
|
||||
<p className="text-pretty text-center text-sm leading-relaxed text-muted-foreground">
|
||||
Ingen runder er lagt til ennå — legg til runder under «Runder og baner» først.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
// Connected list: one container, rows divided by hairlines — not separate
|
||||
// shadowed cards with gaps between them.
|
||||
<div className="overflow-hidden rounded-2xl border border-border bg-card shadow-md shadow-black/8">
|
||||
<ul className="divide-y divide-border">
|
||||
{rounds.map((round) => {
|
||||
// Build the secondary "course · date" line from whatever is set.
|
||||
const meta = [round.courseName, round.dateLabel].filter(Boolean).join(" · ")
|
||||
return (
|
||||
<li
|
||||
key={round.id}
|
||||
className="flex flex-wrap items-center justify-between gap-3 p-4 sm:p-5"
|
||||
>
|
||||
<div className="flex min-w-0 flex-col gap-0.5">
|
||||
<span className="text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
||||
Runde {round.sequence}
|
||||
</span>
|
||||
<span className="truncate text-base font-bold text-foreground">{round.name}</span>
|
||||
<span className="truncate text-sm text-muted-foreground tabular-nums">
|
||||
{meta === "" ? "Ingen bane eller dato satt ennå" : meta}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onManageGroups(round.id)}
|
||||
className="inline-flex min-h-11 shrink-0 items-center gap-2 rounded-xl border border-border bg-card px-4 py-2.5 text-sm font-bold text-foreground transition-all duration-200 ease-in-out hover:bg-accent/50 active:scale-[0.98] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
|
||||
>
|
||||
<Clock aria-hidden="true" className="size-5 shrink-0" />
|
||||
<span>Rediger grupper</span>
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
71
frontend/components/tournament-setup-nav.tsx
Normal file
71
frontend/components/tournament-setup-nav.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
"use client"
|
||||
|
||||
import type { LucideIcon } from "lucide-react"
|
||||
import type { ReactNode } from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export type SetupStep = {
|
||||
key: string
|
||||
label: string
|
||||
icon: LucideIcon
|
||||
}
|
||||
|
||||
export type TournamentSetupNavProps = {
|
||||
steps: SetupStep[]
|
||||
activeStep: string
|
||||
onChange: (key: string) => void
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
export function TournamentSetupNav({
|
||||
steps,
|
||||
activeStep,
|
||||
onChange,
|
||||
children,
|
||||
}: TournamentSetupNavProps) {
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<nav aria-label="Turneringsoppsett-steg">
|
||||
{/*
|
||||
Every step is always selectable in any order — no gating, no completion
|
||||
checkmarks, no step numbers. On wide screens all 6 pills share the row
|
||||
equally; on narrow screens the row scrolls sideways rather than wrapping
|
||||
or collapsing, so every labelled step stays reachable.
|
||||
*/}
|
||||
<div
|
||||
role="tablist"
|
||||
aria-orientation="horizontal"
|
||||
className="flex snap-x snap-mandatory gap-2 overflow-x-auto pb-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||
>
|
||||
{steps.map((step) => {
|
||||
const Icon = step.icon
|
||||
const isActive = step.key === activeStep
|
||||
return (
|
||||
<button
|
||||
key={step.key}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={isActive}
|
||||
onClick={() => onChange(step.key)}
|
||||
className={cn(
|
||||
"flex min-h-11 flex-1 shrink-0 snap-start items-center justify-center gap-2 rounded-2xl px-4 py-2.5",
|
||||
"whitespace-nowrap text-sm font-bold transition-all duration-200 ease-in-out",
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
||||
"active:scale-[0.98]",
|
||||
isActive
|
||||
? "bg-primary text-primary-foreground shadow-md shadow-black/8"
|
||||
: "border border-border bg-card text-foreground hover:bg-accent/50",
|
||||
)}
|
||||
>
|
||||
<Icon aria-hidden="true" className="size-5 shrink-0" />
|
||||
<span>{step.label}</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in a new issue