Kopierbar offentlig lenke + trygg fallback for ikke-innlogget besøkende

Organisator delte ved et uhell sin egen admin-URL (/tournaments/{id}?org=...)
i stedet for den ekte offentlige (/t/{id}) -- lenken fantes fra før, men var
kun ren, ikke-kopierbar tekst. tournament-presentation.tsx fikk en ekte
kopier-knapp.

Reell, uavhengig bug: tournament-router.tsx falt ubetinget tilbake til
Cup-format-adminskjermen ved ENHVER feil i formatoppslaget, inkl. 401 for en
ikke-innlogget besøkende -- viste et ødelagt lag-oppsett-skjermbilde for en
individuell turnering. Skiller nå 401 fra andre feil og sender videre til
den ekte offentlige siden i stedet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Erol Haagenrud 2026-08-21 19:44:02 +02:00
parent 6644447b3e
commit f9ae32f4e1
2 changed files with 65 additions and 2 deletions

View file

@ -15,6 +15,7 @@ import {
ArrowLeft, ArrowLeft,
Camera, Camera,
Check, Check,
Copy,
ExternalLink, ExternalLink,
Globe, Globe,
Lock, Lock,
@ -105,6 +106,21 @@ export function TournamentPresentationPanel({
const [sponsorName, setSponsorName] = useState("") const [sponsorName, setSponsorName] = useState("")
const [sponsorUrl, setSponsorUrl] = useState("") const [sponsorUrl, setSponsorUrl] = useState("")
const [addingSponsor, setAddingSponsor] = useState(false) const [addingSponsor, setAddingSponsor] = useState(false)
// Kopier-lenke (2026-08-21) -- fant reell forvirring: organisator delte
// sin egen administrator-URL (/tournaments/{id}?org=...) i stedet for
// denne, siden lenken under kun var ren, ikke-kopierbar tekst. Samme
// kopier-mønster som JoinCodeChip i individual-tournament-detail.tsx.
const [linkCopied, setLinkCopied] = useState(false)
async function copyPublicLink() {
try {
await navigator.clipboard.writeText(`${window.location.origin}/t/${tournamentId}`)
setLinkCopied(true)
setTimeout(() => setLinkCopied(false), 1500)
} catch {
// stille -- ikke kritisk
}
}
const [sponsorError, setSponsorError] = useState<string | null>(null) const [sponsorError, setSponsorError] = useState<string | null>(null)
const [logoTargetId, setLogoTargetId] = useState<string | null>(null) const [logoTargetId, setLogoTargetId] = useState<string | null>(null)
const [uploadingLogoFor, setUploadingLogoFor] = useState<string | null>(null) const [uploadingLogoFor, setUploadingLogoFor] = useState<string | null>(null)
@ -282,7 +298,7 @@ export function TournamentPresentationPanel({
return ( return (
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<div className="mb-4 flex flex-col gap-1"> <div className="mb-4 flex flex-col gap-2">
<p className="text-sm leading-relaxed text-muted-foreground text-pretty"> <p className="text-sm leading-relaxed text-muted-foreground text-pretty">
Hvordan turneringen vises sin offentlige side ( Hvordan turneringen vises sin offentlige side (
<Link <Link
@ -295,6 +311,18 @@ export function TournamentPresentationPanel({
</Link> </Link>
) og klubbsiden. ) og klubbsiden.
</p> </p>
<button
type="button"
onClick={copyPublicLink}
className="inline-flex min-h-11 w-fit items-center gap-1.5 rounded-xl border border-border bg-card px-3 py-2 text-sm font-bold text-foreground transition-colors hover:bg-accent/50"
>
{linkCopied ? (
<Check aria-hidden="true" className="size-4 text-primary" />
) : (
<Copy aria-hidden="true" className="size-4 text-muted-foreground" />
)}
{linkCopied ? "Kopiert!" : "Kopier offentlig lenke"}
</button>
</div> </div>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">

View file

@ -8,6 +8,7 @@
// egen rad"-mønster som tournament-detail.tsx allerede bruker for join_code. // egen rad"-mønster som tournament-detail.tsx allerede bruker for join_code.
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { useRouter } from "next/navigation"
import { TournamentDetail } from "@/components/tournament-detail" import { TournamentDetail } from "@/components/tournament-detail"
import { IndividualTournamentDetail } from "@/components/individual-tournament-detail" import { IndividualTournamentDetail } from "@/components/individual-tournament-detail"
@ -20,13 +21,30 @@ export function TournamentRouter({
tournamentId: string tournamentId: string
tournamentName: string tournamentName: string
}) { }) {
const router = useRouter()
const [formatType, setFormatType] = useState<"team" | "individual" | null>(null) const [formatType, setFormatType] = useState<"team" | "individual" | null>(null)
const [error, setError] = useState(false) const [error, setError] = useState(false)
// Ikke-innlogget besøkende på denne organisator-URL-en (f.eks. en delt
// /tournaments/{id}?org=...-lenke) -- funnet 2026-08-21: den generiske
// feil-fallback-en under viste tidligere ALLTID Cup/lag-format-
// administrasjonsskjermen (samme "trygt mot krasj"-begrunnelse som før),
// helt uavhengig av turneringens faktiske format og helt uavhengig av
// ÅRSAKEN til feilen -- en anonym besøkende på en individuell turnering
// fikk et forvirrende, ødelagt Ryder Cup-oppsett-skjermbilde i stedet for
// en ren "logg inn"-beskjed. Skiller nå eksplisitt 401 (ikke innlogget)
// fra andre feil, og sender i stedet videre til den ekte offentlige siden.
const [notAuthenticated, setNotAuthenticated] = useState(false)
useEffect(() => { useEffect(() => {
let cancelled = false let cancelled = false
fetch(`/orgs/${organizationId}/tournaments`, { credentials: "include" }) fetch(`/orgs/${organizationId}/tournaments`, { credentials: "include" })
.then((res) => (res.ok ? res.json() : Promise.reject())) .then((res) => {
if (res.status === 401) {
if (!cancelled) setNotAuthenticated(true)
return Promise.reject()
}
return res.ok ? res.json() : Promise.reject()
})
.then((list: { id: string; format_type: string }[]) => { .then((list: { id: string; format_type: string }[]) => {
if (cancelled) return if (cancelled) return
const mine = list.find((t) => t.id === tournamentId) const mine = list.find((t) => t.id === tournamentId)
@ -40,10 +58,27 @@ export function TournamentRouter({
} }
}, [organizationId, tournamentId]) }, [organizationId, tournamentId])
useEffect(() => {
if (notAuthenticated) router.replace(`/t/${tournamentId}`)
}, [notAuthenticated, tournamentId, router])
if (notAuthenticated) {
return (
<div className="flex min-h-[100dvh] flex-col items-center justify-center gap-4 bg-background">
<div
aria-hidden="true"
className="size-10 animate-spin rounded-full border-4 border-primary/20 border-t-primary"
/>
</div>
)
}
if (error) { if (error) {
// Samme trygge fallback som resten av appen: en ukjent/util­gjengelig // Samme trygge fallback som resten av appen: en ukjent/util­gjengelig
// turnering skal ikke krasje -- TournamentDetail sin egen feilhåndtering // turnering skal ikke krasje -- TournamentDetail sin egen feilhåndtering
// (join_code/teams/players) tar over og viser en tydelig feilmelding. // (join_code/teams/players) tar over og viser en tydelig feilmelding.
// (Denne grenen er nå kun for EKTE feil hos en innlogget bruker --
// 401/ikke-innlogget håndteres over, IKKE lenger her.)
return ( return (
<TournamentDetail organizationId={organizationId} tournamentId={tournamentId} tournamentName={tournamentName} /> <TournamentDetail organizationId={organizationId} tournamentId={tournamentId} tournamentName={tournamentName} />
) )