teecup/frontend/components/print-startlist.tsx
Erol Haagenrud 546900c2d6
Some checks failed
Backend-tester / test (push) Successful in 1m8s
Frontend-tester / test (push) Failing after 20s
Før endringer i turneringsoppsettet
2026-08-21 13:54:01 +02:00

615 lines
26 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import type React from "react"
import { useEffect, useId, useMemo, useRef, useState } from "react"
import { Download, Printer, Star } from "lucide-react"
import { cn } from "@/lib/utils"
import {
fetchMatches,
fetchPlayers,
fetchRoundGroups,
fetchRoundParticipants,
fetchRounds,
fetchSessions,
fetchTeams,
fetchTournament,
fetchTournamentParticipants,
formatDate,
formatTeeTime,
type ApiMatch,
type ApiPlayer,
type ApiRound,
type ApiRoundParticipant,
type ApiSession,
type ApiTeam,
type ApiTournamentParticipant,
} from "@/lib/print-data"
/**
* Print-friendly START-LISTE -- ekte data (ADR-097). Videreført fra
* V0-mockupen: Variant A (individuell, gruppevis) / Variant B (Cup,
* match-oppstilling m/ lagfarger). A3/A2-plakatformat kun for Cup.
*/
type SizeKey = "A4" | "Letter" | "A3" | "A2"
type Orientation = "portrait" | "landscape"
const PAPER: Record<SizeKey, { label: string; sub: string; w: number; h: number; large: boolean }> = {
A4: { label: "A4", sub: "210 × 297 mm", w: 210, h: 297, large: false },
Letter: { label: "Letter", sub: "216 × 279 mm", w: 216, h: 279, large: false },
A3: { label: "A3 / Tabloid", sub: "297 × 420 mm", w: 297, h: 420, large: true },
A2: { label: "A2 / ANSI C", sub: "420 × 594 mm", w: 420, h: 594, large: true },
}
const MM_TO_PX = 96 / 25.4
const INK = "#171717"
const INK_MUTED = "#5b5b5b"
const HAIRLINE = "#d9d9d9"
const HAIRLINE_STRONG = "#a3a3a3"
type GroupRow = { seq: number; time: string; players: { name: string; tee: string; hcp: string; klasse: string; gender: string; club: string }[] }
type MatchRow = {
no: number
time: string
points: string
nord: { name: string; tee: string; hcp: string; klasse: string; captain?: boolean }[]
sor: { name: string; tee: string; hcp: string; klasse: string; captain?: boolean }[]
}
function Segmented<T extends string>({
options,
value,
onChange,
ariaLabel,
}: {
options: { key: T; label: string; sub?: string; disabled?: boolean }[]
value: T
onChange: (v: T) => void
ariaLabel: string
}) {
return (
<div role="radiogroup" aria-label={ariaLabel} className="flex flex-wrap gap-1.5">
{options.map((o) => {
const active = o.key === value
return (
<button
key={o.key}
type="button"
role="radio"
aria-checked={active}
disabled={o.disabled}
onClick={() => onChange(o.key)}
className={cn(
"flex min-h-11 flex-1 flex-col items-start justify-center rounded-xl border px-3 py-2 text-left transition-colors",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
active ? "border-primary bg-primary/10 text-foreground" : "border-border bg-card text-muted-foreground hover:bg-accent/50 hover:text-foreground",
o.disabled && "cursor-not-allowed opacity-40 hover:bg-card hover:text-muted-foreground",
)}
>
<span className="text-sm font-bold">{o.label}</span>
{o.sub ? <span className="text-[11px] font-medium opacity-70">{o.sub}</span> : null}
</button>
)
})}
</div>
)
}
function ToggleRow({ label, hint, checked, onCheckedChange }: { label: string; hint?: string; checked: boolean; onCheckedChange: (v: boolean) => void }) {
return (
<label className="flex cursor-pointer items-center justify-between gap-3 rounded-xl px-1 py-1.5">
<span className="flex flex-col">
<span className="text-sm font-semibold text-foreground">{label}</span>
{hint ? <span className="text-[11px] text-muted-foreground">{hint}</span> : null}
</span>
<button
type="button"
role="switch"
aria-checked={checked}
onClick={() => onCheckedChange(!checked)}
className={cn("relative h-6 w-11 shrink-0 rounded-full transition-colors", checked ? "bg-primary" : "bg-muted-foreground/30")}
>
<span className={cn("absolute top-0.5 size-5 rounded-full bg-white shadow transition-transform", checked ? "translate-x-[22px]" : "translate-x-0.5")} />
</button>
</label>
)
}
function PanelSection({ title, children }: { title: string; children: React.ReactNode }) {
return (
<section className="flex flex-col gap-2">
<h3 className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">{title}</h3>
{children}
</section>
)
}
function TeamChip({ color, name }: { color: string; name: string }) {
return (
<span className="inline-flex items-center gap-2">
<span aria-hidden style={{ backgroundColor: color, borderColor: "rgba(0,0,0,0.25)" }} className="inline-block h-3 w-3 shrink-0 rounded-[3px] border" />
<span style={{ color: INK }} className="font-bold">
{name}
</span>
</span>
)
}
function SheetIndividual({
tournamentName,
round,
groups,
fields,
}: {
tournamentName: string
round: ApiRound
groups: GroupRow[]
fields: { hcp: boolean; klasse: boolean; gender: boolean; club: boolean }
}) {
return (
<div style={{ color: INK }} className="flex h-full flex-col">
<header style={{ borderColor: HAIRLINE_STRONG }} className="border-b-2 pb-3">
<div className="flex items-baseline justify-between gap-4">
<h1 className="text-[22px] font-extrabold leading-tight">{tournamentName}</h1>
<span style={{ color: INK_MUTED }} className="text-[12px] font-semibold">
{round.start_mode === "shotgun" ? "Startliste — shotgun" : "Startliste"}
</span>
</div>
<div style={{ color: INK_MUTED }} className="mt-1 flex flex-wrap gap-x-4 gap-y-0.5 text-[12px] font-medium">
<span style={{ color: INK }} className="font-bold">
{round.name}
</span>
<span>{round.course_name}</span>
<span>{formatDate(round.scheduled_at)}</span>
</div>
</header>
<div className="mt-3 flex flex-1 flex-col gap-3">
{groups.map((g) => (
<div key={g.seq} style={{ borderColor: HAIRLINE }} className="break-inside-avoid rounded-[2px] border">
<div style={{ backgroundColor: "#f2f2f0", borderColor: HAIRLINE }} className="flex items-center justify-between border-b px-2.5 py-1.5">
<span className="text-[13px] font-extrabold">Gruppe {g.seq}</span>
<span className="text-[13px] font-bold tabular-nums">
{round.start_mode === "shotgun" ? `Starthull ${g.time}` : `Utslag ${g.time}`}
</span>
</div>
<table className="w-full border-collapse text-[12px]">
<thead>
<tr style={{ color: INK_MUTED, borderColor: HAIRLINE }} className="border-b">
<th className="px-2.5 py-1 text-left font-semibold uppercase tracking-wide">Spiller</th>
<th className="px-2 py-1 text-left font-semibold uppercase tracking-wide">Tee</th>
{fields.hcp && <th className="px-2 py-1 text-right font-semibold uppercase tracking-wide">HCP</th>}
{fields.klasse && <th className="px-2 py-1 text-left font-semibold uppercase tracking-wide">Klasse</th>}
{fields.gender && <th className="px-2 py-1 text-center font-semibold uppercase tracking-wide">Kjønn</th>}
{fields.club && <th className="px-2 py-1 text-left font-semibold uppercase tracking-wide">Hjemmeklubb</th>}
</tr>
</thead>
<tbody>
{g.players.map((p, i) => (
<tr key={p.name + i} style={{ borderColor: HAIRLINE }} className={cn(i < g.players.length - 1 && "border-b")}>
<td className="px-2.5 py-1 font-bold">{p.name}</td>
<td className="px-2 py-1">{p.tee}</td>
{fields.hcp && <td className="px-2 py-1 text-right tabular-nums">{p.hcp}</td>}
{fields.klasse && <td className="px-2 py-1">{p.klasse}</td>}
{fields.gender && <td className="px-2 py-1 text-center">{p.gender}</td>}
{fields.club && <td className="px-2 py-1">{p.club}</td>}
</tr>
))}
</tbody>
</table>
</div>
))}
</div>
<SheetFooter />
</div>
)
}
function SheetCup({
tournamentName,
session,
teamNord,
teamSor,
matches,
fields,
poster,
}: {
tournamentName: string
session: ApiSession
teamNord: ApiTeam
teamSor: ApiTeam
matches: MatchRow[]
fields: { tee: boolean; hcp: boolean; klasse: boolean; points: boolean; captain: boolean }
poster: boolean
}) {
const meta = [`${session.name}${session.format}`, formatDate(session.scheduled_at)]
return (
<div style={{ color: INK }} className="flex h-full flex-col">
<header style={{ borderColor: HAIRLINE_STRONG }} className="border-b-2 pb-3">
<div className="flex items-baseline justify-between gap-4">
<h1 className={cn("font-extrabold leading-tight", poster ? "text-[30px]" : "text-[22px]")}>{tournamentName}</h1>
<span style={{ color: INK_MUTED }} className="text-[12px] font-semibold">
Oppstilling
</span>
</div>
<div style={{ color: INK_MUTED }} className="mt-1 flex flex-wrap gap-x-4 gap-y-0.5 text-[12px] font-medium">
{meta.map((m, i) => (
<span key={m} style={i === 0 ? { color: INK } : undefined} className={i === 0 ? "font-bold" : undefined}>
{m}
</span>
))}
</div>
<div className="mt-2 flex flex-wrap items-center gap-x-6 gap-y-1 text-[13px]">
<TeamChip color={teamNord.color ?? "#1f5fbf"} name={teamNord.name} />
<span style={{ color: INK_MUTED }} className="font-bold">
vs
</span>
<TeamChip color={teamSor.color ?? "#d1561b"} name={teamSor.name} />
</div>
</header>
<div className={cn("mt-3 flex flex-1 flex-col", poster ? "gap-4" : "gap-2.5")}>
{matches.map((m) => (
<div key={m.no} style={{ borderColor: HAIRLINE_STRONG }} className="grid break-inside-avoid grid-cols-[auto_1fr_auto_1fr] items-stretch overflow-hidden rounded-[3px] border">
<div style={{ backgroundColor: "#f2f2f0", borderColor: HAIRLINE }} className={cn("flex flex-col items-center justify-center border-r px-3", poster ? "min-w-[92px]" : "min-w-[64px]")}>
<span style={{ color: INK_MUTED }} className="text-[10px] font-bold uppercase tracking-wide">
Match
</span>
<span className={cn("font-extrabold leading-none", poster ? "text-[34px]" : "text-[22px]")}>{m.no}</span>
<span className="mt-0.5 text-[12px] font-bold tabular-nums">{m.time}</span>
{fields.points && (
<span style={{ color: INK_MUTED }} className="mt-0.5 text-[10px] font-semibold">
{m.points}
</span>
)}
</div>
<MatchSide accent={teamNord.color ?? "#1f5fbf"} players={m.nord} fields={fields} poster={poster} align="left" />
<div style={{ borderColor: HAIRLINE }} className="flex items-center justify-center border-x px-2">
<span style={{ color: INK_MUTED }} className={cn("font-extrabold", poster ? "text-[16px]" : "text-[13px]")}>
vs
</span>
</div>
<MatchSide accent={teamSor.color ?? "#d1561b"} players={m.sor} fields={fields} poster={poster} align="right" />
</div>
))}
</div>
<SheetFooter />
</div>
)
}
function MatchSide({
accent,
players,
fields,
poster,
align,
}: {
accent: string
players: MatchRow["nord"]
fields: { tee: boolean; hcp: boolean; klasse: boolean; points: boolean; captain: boolean }
poster: boolean
align: "left" | "right"
}) {
return (
<div className={cn("flex flex-col justify-center gap-1 px-3 py-2", align === "right" && "items-end text-right")}>
<span aria-hidden style={{ backgroundColor: accent }} className="h-1 w-8 rounded-full" />
{players.map((p, i) => (
<div key={p.name + i} className={cn("flex flex-col", align === "right" && "items-end")}>
<span className={cn("font-bold leading-tight", poster ? "text-[16px]" : "text-[13px]")}>
{p.name}
{fields.captain && p.captain && (
<span style={{ color: INK_MUTED }} className="ml-1 inline-flex items-center align-middle text-[11px] font-semibold">
<Star aria-hidden className="mr-0.5 size-3" fill="currentColor" />
<span>Kaptein</span>
</span>
)}
</span>
{(fields.tee || fields.hcp || fields.klasse) && (
<span style={{ color: INK_MUTED }} className="text-[11px] font-medium">
{[fields.tee && p.tee, fields.hcp && `HCP ${p.hcp}`, fields.klasse && `Kl. ${p.klasse}`].filter(Boolean).join(" · ")}
</span>
)}
</div>
))}
</div>
)
}
function SheetFooter() {
return (
<footer style={{ borderColor: HAIRLINE, color: INK_MUTED }} className="mt-3 flex items-center justify-between border-t pt-2 text-[10px] font-medium">
<span>Side 1 av 1</span>
<span>Generert {new Date().toLocaleString("no-NO")}</span>
<span className="font-bold" style={{ color: INK }}>
TeeCup
</span>
</footer>
)
}
export function PrintStartlist({
organizationId,
tournamentId,
roundId,
sessionId,
initialSize = "A4",
initialOrientation = "portrait",
}: {
organizationId: string
tournamentId: string
roundId?: string
sessionId?: string
initialSize?: SizeKey
initialOrientation?: Orientation
}) {
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [tournamentName, setTournamentName] = useState("Turnering")
const [round, setRound] = useState<ApiRound | null>(null)
const [groups, setGroups] = useState<GroupRow[]>([])
const [session, setSession] = useState<ApiSession | null>(null)
const [matches, setMatches] = useState<MatchRow[]>([])
const [teamNord, setTeamNord] = useState<ApiTeam | null>(null)
const [teamSor, setTeamSor] = useState<ApiTeam | null>(null)
const [downloading, setDownloading] = useState(false)
const [size, setSize] = useState<SizeKey>(initialSize)
const [orientation, setOrientation] = useState<Orientation>(initialOrientation)
const [aFields, setAFields] = useState({ hcp: true, klasse: true, gender: false, club: true })
const [bFields, setBFields] = useState({ tee: true, hcp: false, klasse: false, points: true, captain: true })
useEffect(() => {
let cancelled = false
async function load() {
setLoading(true)
setError(null)
try {
const t = await fetchTournament(organizationId, tournamentId)
if (cancelled) return
setTournamentName(t?.name ?? "Turnering")
if (roundId) {
const [rounds, groupsRes, roundParticipants, tournamentParticipants, players] = await Promise.all([
fetchRounds(organizationId, tournamentId),
fetchRoundGroups(organizationId, tournamentId, roundId),
fetchRoundParticipants(organizationId, tournamentId, roundId),
fetchTournamentParticipants(organizationId, tournamentId),
fetchPlayers(organizationId),
])
const r = rounds.find((x: ApiRound) => x.id === roundId) ?? null
if (!r) throw new Error("Fant ikke runden.")
const rpById = new Map<string, ApiRoundParticipant>(roundParticipants.map((rp) => [rp.id, rp]))
const tpById = new Map<string, ApiTournamentParticipant>(tournamentParticipants.map((tp) => [tp.id, tp]))
const playerById = new Map<string, ApiPlayer>(players.map((p) => [p.id, p]))
const genderLabel = (g: ApiPlayer["gender"]) => (g === "m" ? "M" : g === "f" ? "K" : g === "x" ? "A" : "")
const built: GroupRow[] = groupsRes.groups.map((g, i) => ({
seq: g.sequence ?? i + 1,
time: r.start_mode === "shotgun" ? String(g.start_hole ?? "") : formatTeeTime(g.tee_time),
players: g.participants.map((gp) => {
const rp = rpById.get(gp.round_participant_id)
const tp = rp ? tpById.get(rp.tournament_participant_id) : undefined
const player = tp ? playerById.get(tp.player_id) : undefined
return {
name: gp.player_name,
tee: rp?.tee_name ?? "—",
hcp: tp?.handicap_index_snapshot != null ? tp.handicap_index_snapshot.toFixed(1).replace(".", ",") : "",
klasse: tp?.class_name ?? "—",
gender: genderLabel(player?.gender ?? null),
club: player?.club ?? "—",
}
}),
}))
if (cancelled) return
setRound(r)
setGroups(built)
} else if (sessionId) {
const [sessions, matchesRaw, teams] = await Promise.all([
fetchSessions(organizationId, tournamentId),
fetchMatches(organizationId, sessionId),
fetchTeams(organizationId, tournamentId),
])
const s = sessions.find((x: ApiSession) => x.id === sessionId) ?? null
if (!s) throw new Error("Fant ikke økten.")
const [teamA, teamB] = teams
const built: MatchRow[] = matchesRaw.map((m: ApiMatch) => {
const side = (sideKey: "a" | "b") =>
m.participants
.filter((p) => p.team_side === sideKey)
.map((p) => ({ name: p.player_name, tee: p.tee_name ?? "—", hcp: p.playing_handicap != null ? String(p.playing_handicap) : "", klasse: "—" }))
return {
no: m.sequence,
time: s.start_mode === "shotgun" ? String(m.start_hole ?? "") : formatTeeTime(m.tee_time),
points: `${s.points_per_match.toLocaleString("nb-NO", { maximumFractionDigits: 1 })} poeng`,
nord: side("a"),
sor: side("b"),
}
})
if (cancelled) return
setSession(s)
setTeamNord(teamA ?? { id: "a", name: "Lag A", color: "#1f5fbf" })
setTeamSor(teamB ?? { id: "b", name: "Lag B", color: "#d1561b" })
setMatches(built)
} else {
throw new Error("Mangler runde- eller økt-ID.")
}
} catch {
if (!cancelled) setError("Klarte ikke å laste data for startlisten. Prøv igjen.")
} finally {
if (!cancelled) setLoading(false)
}
}
void load()
return () => {
cancelled = true
}
}, [organizationId, tournamentId, roundId, sessionId])
const paper = PAPER[size]
const natW = (orientation === "portrait" ? paper.w : paper.h) * MM_TO_PX
const natH = (orientation === "portrait" ? paper.h : paper.w) * MM_TO_PX
const areaRef = useRef<HTMLDivElement>(null)
const [scale, setScale] = useState(0.4)
useEffect(() => {
const el = areaRef.current
if (!el) return
const compute = () => {
const pad = 56
const availW = el.clientWidth - pad
const availH = el.clientHeight - pad
if (availW <= 0 || availH <= 0) return
setScale(Math.max(0.12, Math.min(availW / natW, availH / natH, 1.6)))
}
compute()
const ro = new ResizeObserver(compute)
ro.observe(el)
return () => ro.disconnect()
}, [natW, natH])
const poster = sessionId != null && paper.large
const pageCss = useMemo(
() => `@page { size: ${orientation === "portrait" ? `${paper.w}mm ${paper.h}mm` : `${paper.h}mm ${paper.w}mm`}; margin: 0; }`,
[paper.w, paper.h, orientation],
)
const sizeOptions = (Object.keys(PAPER) as SizeKey[]).map((k) => ({
key: k,
label: PAPER[k].label,
sub: PAPER[k].sub,
disabled: PAPER[k].large && roundId != null,
}))
async function downloadPdf() {
setDownloading(true)
try {
const res = await fetch(`/orgs/${organizationId}/print/pdf`, {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({
kind: "startlist",
tournament_id: tournamentId,
round_id: roundId ?? null,
session_id: sessionId ?? null,
paper: size === "A3" || size === "A2" ? "A4" : size,
orientation,
}),
})
if (!res.ok) throw new Error()
const blob = await res.blob()
const url = URL.createObjectURL(blob)
const a = document.createElement("a")
a.href = url
a.download = "teecup-startliste.pdf"
document.body.appendChild(a)
a.click()
a.remove()
URL.revokeObjectURL(url)
} catch {
setError("Klarte ikke å generere PDF-en. Prøv igjen.")
} finally {
setDownloading(false)
}
}
const ready = !loading
const styleId = useId()
return (
<div className="flex min-h-screen flex-col bg-background text-foreground" data-print-ready={ready ? "true" : undefined}>
<style key={styleId}>{pageCss}</style>
<header className="flex flex-wrap items-center justify-between gap-3 border-b border-border px-4 py-3 lg:px-6 print:hidden">
<div className="flex items-center gap-3">
<div className="flex size-9 items-center justify-center rounded-xl bg-primary/15 text-primary">
<Printer className="size-5" aria-hidden />
</div>
<div>
<h1 className="text-base font-extrabold leading-tight">Skriv ut startliste</h1>
<p className="text-[12px] text-muted-foreground">{error ?? (loading ? "Laster …" : `${groups.length + matches.length} rad(er) klare`)}</p>
</div>
</div>
<button
type="button"
onClick={downloadPdf}
disabled={downloading || loading}
className="inline-flex min-h-10 items-center gap-2 rounded-xl bg-primary px-4 text-sm font-extrabold text-primary-foreground shadow-sm transition-transform hover:brightness-105 active:scale-[0.99] disabled:opacity-60"
>
<Download className="size-4" aria-hidden />
{downloading ? "Genererer …" : "Last ned PDF"}
</button>
</header>
<div className="flex flex-1 flex-col lg:flex-row">
<aside className="w-full shrink-0 border-b border-border bg-card p-4 lg:w-80 lg:border-b-0 lg:border-r lg:p-5 print:hidden">
<div className="flex flex-col gap-6">
<PanelSection title="Papirstørrelse">
<Segmented ariaLabel="Papirstørrelse" options={sizeOptions} value={size} onChange={setSize} />
{sessionId && <p className="text-[11px] leading-relaxed text-muted-foreground">A3 og A2 gir en plakat for oppslagstavla.</p>}
</PanelSection>
<PanelSection title="Orientering">
<Segmented
ariaLabel="Orientering"
options={[
{ key: "portrait" as Orientation, label: "Portrett" },
{ key: "landscape" as Orientation, label: "Landskap" },
]}
value={orientation}
onChange={setOrientation}
/>
</PanelSection>
<PanelSection title="Valgfrie felt">
{roundId ? (
<div className="flex flex-col divide-y divide-border/70">
<ToggleRow label="HCP" checked={aFields.hcp} onCheckedChange={(v) => setAFields((s) => ({ ...s, hcp: v }))} />
<ToggleRow label="Klasse" checked={aFields.klasse} onCheckedChange={(v) => setAFields((s) => ({ ...s, klasse: v }))} />
<ToggleRow label="Kjønn" checked={aFields.gender} onCheckedChange={(v) => setAFields((s) => ({ ...s, gender: v }))} />
<ToggleRow label="Hjemmeklubb" checked={aFields.club} onCheckedChange={(v) => setAFields((s) => ({ ...s, club: v }))} />
</div>
) : (
<div className="flex flex-col divide-y divide-border/70">
<ToggleRow label="Utslag / tee" checked={bFields.tee} onCheckedChange={(v) => setBFields((s) => ({ ...s, tee: v }))} />
<ToggleRow label="HCP" checked={bFields.hcp} onCheckedChange={(v) => setBFields((s) => ({ ...s, hcp: v }))} />
<ToggleRow label="Poeng for matchen" checked={bFields.points} onCheckedChange={(v) => setBFields((s) => ({ ...s, points: v }))} />
<ToggleRow label="Kapteinmerking" checked={bFields.captain} onCheckedChange={(v) => setBFields((s) => ({ ...s, captain: v }))} />
</div>
)}
</PanelSection>
</div>
</aside>
<main ref={areaRef} className="grid flex-1 place-items-center overflow-auto bg-[oklch(0.9_0.01_130)] p-7 dark:bg-[oklch(0.15_0.01_150)] print:block print:bg-transparent print:p-0">
{loading ? (
<p className="text-sm text-muted-foreground">Laster startliste </p>
) : round ? (
<div style={{ width: natW * scale, height: natH * scale }} className="shrink-0 print:h-auto print:w-auto">
<div
style={{ width: natW, height: natH, transform: `scale(${scale})`, transformOrigin: "top left", backgroundColor: "#fff", boxShadow: "0 10px 30px rgba(0,0,0,0.28)", padding: `${12 * MM_TO_PX}px` }}
className="overflow-hidden print:!scale-100 print:shadow-none"
>
<SheetIndividual tournamentName={tournamentName} round={round} groups={groups} fields={aFields} />
</div>
</div>
) : session && teamNord && teamSor ? (
<div style={{ width: natW * scale, height: natH * scale }} className="shrink-0 print:h-auto print:w-auto">
<div
style={{ width: natW, height: natH, transform: `scale(${scale})`, transformOrigin: "top left", backgroundColor: "#fff", boxShadow: "0 10px 30px rgba(0,0,0,0.28)", padding: `${12 * MM_TO_PX}px` }}
className="overflow-hidden print:!scale-100 print:shadow-none"
>
<SheetCup tournamentName={tournamentName} session={session} teamNord={teamNord} teamSor={teamSor} matches={matches} fields={bFields} poster={poster} />
</div>
</div>
) : (
<p className="text-sm text-muted-foreground">{error ?? "Ingen data funnet."}</p>
)}
</main>
</div>
</div>
)
}