teecup/frontend/components/tournament-card.tsx
Erol Haagenrud 8445b137ec Klubb-landingssiden er live på https://teecup.teeoff.no/clubs/{slug} — dermed er ADR-018 sine to planlagte frontend-skjermer komplette.
Én reell komponentkonflikt løst, ikke duplisert bort: TournamentCard var bygget kun for den innloggede dashbord-konteksten. I stedet for å lage en egen kopi for den offentlige klubbsiden, gjorde jeg orgId valgfri — satt gir innlogget lenke, utelatt gir /t/{id} i stedet. Samme kort, to kontekster. Bekreftet dashbordet fortsatt fungerer uendret.

V0 laget selv en rute denne gangen, men kalte parameteren [id] selv om det faktisk er en slug — skrev en egen, riktig navngitt [slug]-rute i stedet.

Verifisert mot ekte scratch-data gjennom en kjørende frontend-dev-server: en org med to turneringer (én offentlig, én org-privat) — klubbsiden viste kun den offentlige, akkurat som filteret i API-et tilsier.

Live, teeoff.no upåvirket. Gjenstår av ADR-018: Open Graph-metadata for delingsforhåndsvisning, og MinIO/bilder som egen runde. Vil du ta Open Graph-metadataen nå, siden det er en liten, avgrenset bit?
2026-07-18 10:48:20 +02:00

75 lines
2.5 KiB
TypeScript
Raw 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.

import Link from "next/link"
import { Calendar, ChevronRight } from "lucide-react"
import { TournamentStatusBadge, type TournamentStatus } from "@/components/tournament-status-badge"
export type Tournament = {
id: string
name: string
status: TournamentStatus
startDate?: string
endDate?: string
}
const dateFormatter = new Intl.DateTimeFormat("no-NO", {
day: "numeric",
month: "short",
year: "numeric",
})
function formatDate(value?: string) {
if (!value) return null
const parsed = new Date(value)
if (Number.isNaN(parsed.getTime())) return null
return dateFormatter.format(parsed)
}
function formatDateRange(start?: string, end?: string) {
const from = formatDate(start)
const to = formatDate(end)
if (from && to) return from === to ? from : `${from} ${to}`
return from ?? to ?? null
}
export function TournamentCard({
tournament,
orgId,
}: {
tournament: Tournament
// Satt (dashbordet, innlogget) -> lenker til den innloggede detaljsiden.
// Utelatt (klubb-landingsside, offentlig) -> lenker til den offentlige
// /t/{id}-ruten i stedet. Samme kort, to kontekster.
orgId?: string
}) {
const dateRange = formatDateRange(tournament.startDate, tournament.endDate)
const href = orgId
? `/tournaments/${tournament.id}?org=${orgId}&name=${encodeURIComponent(tournament.name)}`
: `/t/${tournament.id}`
return (
<Link
href={href}
className="group flex w-full items-center gap-4 rounded-2xl border border-border bg-card p-4 text-left shadow-sm shadow-black/5 transition-colors hover:border-primary/50 hover:bg-accent/50 sm:p-5"
>
<div className="flex min-w-0 flex-1 flex-col gap-2">
<div className="flex flex-wrap items-center gap-x-3 gap-y-2">
<h3 className="truncate text-base font-bold text-foreground sm:text-lg">
{tournament.name}
</h3>
<TournamentStatusBadge status={tournament.status} />
</div>
{dateRange ? (
<div className="flex items-center gap-1.5 text-sm text-muted-foreground">
<Calendar aria-hidden="true" className="size-4 shrink-0" />
<span className="tabular-nums">{dateRange}</span>
</div>
) : (
<span className="text-sm text-muted-foreground">Ingen datoer satt</span>
)}
</div>
<ChevronRight
aria-hidden="true"
className="size-5 shrink-0 text-muted-foreground transition-transform group-hover:translate-x-0.5 group-hover:text-foreground"
/>
</Link>
)
}