teecup/frontend/components/ny-runde/step4-teams.tsx

310 lines
13 KiB
TypeScript
Raw Permalink Normal View History

"use client"
import { isScrambleVsSolo, isTwoSided } from "@/lib/ny-runde/formats"
import type { Player } from "@/lib/ny-runde/types"
import { cn } from "@/lib/utils"
import { ArrowLeftRight, Check, UserMinus } from "lucide-react"
import { Avatar, NativeSelect, Panel, Pill, TextInput } from "./primitives"
import { useWizard } from "./wizard-context"
function fullName(p: Player) {
return `${p.firstName}${p.lastName ? ` ${p.lastName}` : ""}`.trim() || "Spiller"
}
export function Step4Teams() {
const { state } = useWizard()
const f = state.format
return (
<div className="flex flex-col gap-4">
<div>
<h2 className="text-lg font-semibold tracking-tight text-[var(--nr-ink)]">Lag og rekkefølge</h2>
<p className="text-sm text-[var(--nr-muted)]">Sett opp lagene for denne spilleformen.</p>
</div>
{isTwoSided(f) ? <TwoSided /> : f === "moneyball" ? <MoneyBall /> : isScrambleVsSolo(f) ? <ScrambleVsSolo /> : null}
</div>
)
}
/* ------------------------------------------------------------------ */
/* Two-sided A/B */
/* ------------------------------------------------------------------ */
function TwoSided() {
const { state, patch } = useWizard()
const assign = state.sideAssign
const unassigned = state.players.filter((p) => !assign[p.key])
const sideA = state.players.filter((p) => assign[p.key] === "A")
const sideB = state.players.filter((p) => assign[p.key] === "B")
function set(key: string, side: "A" | "B" | undefined) {
patch({ sideAssign: { ...assign, [key]: side } })
}
return (
<div className="flex flex-col gap-4">
{unassigned.length > 0 ? (
<Panel className="bg-[var(--nr-surface-2)]">
<p className="mb-2 text-[13px] font-medium text-[var(--nr-muted)]">Ikke tildelt</p>
<ul className="flex flex-col gap-2">
{unassigned.map((p) => (
<li key={p.key} className="flex items-center gap-3 rounded-lg bg-[var(--nr-surface)] p-2 pr-3">
<Avatar first={p.firstName} last={p.lastName} className="h-8 w-8" />
<span className="flex-1 text-sm text-[var(--nr-ink)]">{fullName(p)}</span>
<div className="flex gap-1.5">
<AssignBtn onClick={() => set(p.key, "A")}>{state.sideALabel || "Side A"}</AssignBtn>
<AssignBtn onClick={() => set(p.key, "B")}>{state.sideBLabel || "Side B"}</AssignBtn>
</div>
</li>
))}
</ul>
</Panel>
) : null}
<div className="grid gap-3 sm:grid-cols-2">
<SideColumn
label={state.sideALabel}
placeholder="Side A"
suggestion="Rødt lag"
onLabel={(v) => patch({ sideALabel: v })}
players={sideA}
otherSideLabel={state.sideBLabel || "Side B"}
onMoveOther={(k) => set(k, "B")}
onRemove={(k) => set(k, undefined)}
/>
<SideColumn
label={state.sideBLabel}
placeholder="Side B"
suggestion="Blått lag"
onLabel={(v) => patch({ sideBLabel: v })}
players={sideB}
otherSideLabel={state.sideALabel || "Side A"}
onMoveOther={(k) => set(k, "A")}
onRemove={(k) => set(k, undefined)}
/>
</div>
</div>
)
}
function SideColumn({
label,
placeholder,
suggestion,
onLabel,
players,
otherSideLabel,
onMoveOther,
onRemove,
}: {
label: string
placeholder: string
suggestion: string
onLabel: (v: string) => void
players: Player[]
otherSideLabel: string
onMoveOther: (key: string) => void
onRemove: (key: string) => void
}) {
return (
<div className="flex flex-col gap-2 rounded-xl border border-[var(--nr-border)] bg-[var(--nr-surface)] p-3">
<TextInput value={label} onChange={(e) => onLabel(e.target.value)} placeholder={placeholder} className="h-10 font-medium" />
{!label ? (
<button
type="button"
onClick={() => onLabel(suggestion)}
className="self-start text-xs font-medium text-[var(--nr-accent)] underline-offset-2 hover:underline"
>
Foreslå «{suggestion}»
</button>
) : null}
{players.length === 0 ? (
<p className="rounded-lg border border-dashed border-[var(--nr-border)] px-3 py-6 text-center text-xs text-[var(--nr-muted)]">
Ingen spillere ennå.
</p>
) : (
<ul className="flex flex-col gap-2">
{players.map((p) => (
<li key={p.key} className="flex items-center gap-2 rounded-lg bg-[var(--nr-surface-2)] p-2 pr-2">
<Avatar first={p.firstName} last={p.lastName} className="h-8 w-8" />
<span className="flex-1 truncate text-sm text-[var(--nr-ink)]">{fullName(p)}</span>
<button
type="button"
onClick={() => onMoveOther(p.key)}
aria-label={`Flytt ${fullName(p)} til ${otherSideLabel}`}
className="flex h-9 w-9 items-center justify-center rounded-lg text-[var(--nr-muted)] hover:bg-[var(--nr-surface)] hover:text-[var(--nr-accent)]"
>
<ArrowLeftRight className="h-4 w-4" aria-hidden="true" />
</button>
<button
type="button"
onClick={() => onRemove(p.key)}
aria-label={`Fjern ${fullName(p)} fra laget`}
className="flex h-9 w-9 items-center justify-center rounded-lg text-[var(--nr-muted)] hover:bg-[var(--nr-surface)] hover:text-[var(--nr-danger)]"
>
<UserMinus className="h-4 w-4" aria-hidden="true" />
</button>
</li>
))}
</ul>
)}
</div>
)
}
function AssignBtn({ onClick, children }: { onClick: () => void; children: React.ReactNode }) {
return (
<button
type="button"
onClick={onClick}
className="inline-flex min-h-9 items-center rounded-lg border border-[var(--nr-border)] bg-[var(--nr-surface-2)] px-3 text-xs font-medium text-[var(--nr-ink)] transition-colors hover:border-[var(--nr-accent)] hover:text-[var(--nr-accent)]"
>
{children}
</button>
)
}
/* ------------------------------------------------------------------ */
/* Money Ball */
/* ------------------------------------------------------------------ */
function MoneyBall() {
const { state, patch } = useWizard()
return (
<Panel>
<p className="mb-3 text-sm text-[var(--nr-muted)]">Gi hver spiller en plass i rotasjonen (03).</p>
<ul className="flex flex-col gap-2">
{state.players.map((p) => (
<li key={p.key} className="flex items-center gap-3 rounded-lg bg-[var(--nr-surface-2)] p-2 pr-3">
<Avatar first={p.firstName} last={p.lastName} className="h-8 w-8" />
<span className="flex-1 truncate text-sm text-[var(--nr-ink)]">{fullName(p)}</span>
<NativeSelect
aria-label={`Plass for ${fullName(p)}`}
value={state.moneyballSlots[p.key] ?? 0}
onChange={(e) => patch({ moneyballSlots: { ...state.moneyballSlots, [p.key]: Number.parseInt(e.target.value, 10) } })}
className="h-10 w-24 bg-[var(--nr-surface)]"
>
{[0, 1, 2, 3].map((n) => (
<option key={n} value={n}>
Plass {n}
</option>
))}
</NativeSelect>
</li>
))}
</ul>
</Panel>
)
}
/* ------------------------------------------------------------------ */
/* Scramble vs solo (asymmetric) */
/* ------------------------------------------------------------------ */
function ScrambleVsSolo() {
const { state, patch } = useWizard()
const assign = state.scrambleAssign
const team = state.players.filter((p) => assign[p.key] === "team")
const opponent = state.players.find((p) => assign[p.key] === "opponent")
const unplaced = state.players.filter((p) => !assign[p.key])
const teamReady = team.length >= 2
const opponentFilled = !!opponent
function set(key: string, slot: "team" | "opponent" | undefined) {
patch({ scrambleAssign: { ...assign, [key]: slot } })
}
return (
<div className="flex flex-col gap-4">
{/* The team — open-ended, emphasized */}
<div className="rounded-xl border-2 border-[var(--nr-accent-ring)] bg-[var(--nr-surface)] p-4">
<div className="mb-2 flex items-center justify-between">
<h3 className="text-base font-semibold text-[var(--nr-ink)]">Laget</h3>
<span
role="status"
aria-live="polite"
className={cn(
"inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[11px] font-medium",
teamReady
? "border-[var(--nr-ok)]/30 bg-[var(--nr-ok-soft)] text-[var(--nr-ok)]"
: "border-[var(--nr-border)] bg-[var(--nr-surface-2)] text-[var(--nr-muted)]",
)}
>
{teamReady ? <Check className="h-3 w-3" strokeWidth={3} /> : null}
{teamReady ? "Minst 2 spillere lagt til" : "Minst 2 spillere"}
</span>
</div>
{team.length === 0 ? (
<p className="rounded-lg border border-dashed border-[var(--nr-border)] px-3 py-6 text-center text-xs text-[var(--nr-muted)]">
Legg til spillere fra listen under.
</p>
) : (
<ul className="flex flex-col gap-2">
{team.map((p) => (
<li key={p.key} className="flex items-center gap-2 rounded-lg bg-[var(--nr-surface-2)] p-2 pr-2">
<Avatar first={p.firstName} last={p.lastName} className="h-8 w-8" />
<span className="flex-1 truncate text-sm text-[var(--nr-ink)]">{fullName(p)}</span>
<button
type="button"
onClick={() => set(p.key, undefined)}
aria-label={`Fjern ${fullName(p)} fra laget`}
className="flex h-9 w-9 items-center justify-center rounded-lg text-[var(--nr-muted)] hover:bg-[var(--nr-surface)] hover:text-[var(--nr-danger)]"
>
<UserMinus className="h-4 w-4" aria-hidden="true" />
</button>
</li>
))}
</ul>
)}
</div>
{/* Individual opponent — capped at 1, visually distinct single slot */}
<div className="rounded-xl border border-[var(--nr-border)] bg-[var(--nr-surface-2)] p-4">
<h3 className="mb-2 text-sm font-semibold text-[var(--nr-ink)]">Individuell motstander</h3>
{opponent ? (
<div className="flex items-center gap-3 rounded-lg bg-[var(--nr-surface)] p-2 pr-3 ring-1 ring-inset ring-[var(--nr-border)]">
<Avatar first={opponent.firstName} last={opponent.lastName} className="h-10 w-10" />
<span className="flex-1 truncate text-sm font-medium text-[var(--nr-ink)]">{fullName(opponent)}</span>
<Pill tone="neutral">1 av 1</Pill>
<button
type="button"
onClick={() => set(opponent.key, undefined)}
aria-label={`Fjern ${fullName(opponent)} som motstander`}
className="flex h-9 w-9 items-center justify-center rounded-lg text-[var(--nr-muted)] hover:bg-[var(--nr-surface-2)] hover:text-[var(--nr-danger)]"
>
<UserMinus className="h-4 w-4" aria-hidden="true" />
</button>
</div>
) : (
<p className="rounded-lg border border-dashed border-[var(--nr-border)] px-3 py-5 text-center text-xs text-[var(--nr-muted)]">
Én spiller. Velg «Som motstander» en spiller under.
</p>
)}
</div>
{/* Unplaced */}
{unplaced.length > 0 ? (
<Panel>
<p className="mb-2 text-[13px] font-medium text-[var(--nr-muted)]">Ikke plassert</p>
<ul className="flex flex-col gap-2">
{unplaced.map((p) => (
<li key={p.key} className="flex items-center gap-2 rounded-lg bg-[var(--nr-surface-2)] p-2 pr-2">
<Avatar first={p.firstName} last={p.lastName} className="h-8 w-8" />
<span className="flex-1 truncate text-sm text-[var(--nr-ink)]">{fullName(p)}</span>
<AssignBtn onClick={() => set(p.key, "team")}>Til laget</AssignBtn>
<button
type="button"
onClick={() => set(p.key, "opponent")}
disabled={opponentFilled}
className="inline-flex min-h-9 items-center rounded-lg border border-[var(--nr-border)] bg-[var(--nr-surface-2)] px-3 text-xs font-medium text-[var(--nr-ink)] transition-colors hover:border-[var(--nr-accent)] hover:text-[var(--nr-accent)] disabled:cursor-not-allowed disabled:opacity-40"
>
Som motstander
</button>
</li>
))}
</ul>
</Panel>
) : null}
</div>
)
}