teecup/frontend/components/own-rounds.tsx

163 lines
5.4 KiB
TypeScript
Raw Normal View History

V0-integrasjonen er ferdig. Kort om hva som skjedde: Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig. Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet: Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet. La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon. Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang). Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder. Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk. Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
"use client"
// Frittstående rundeføring (ADR-033) -- runder eid direkte av en BRUKER
// (app_user), ikke en organisasjon. Presentasjon fra V0, datalag skrevet om
// fra mock til ekte fetch mot /rounds (samme mønster som resten av appen).
import { useEffect, useState } from "react"
import { useRouter } from "next/navigation"
import Link from "next/link"
import { Loader2, Plus, ClipboardList } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Wordmark } from "@/components/wordmark"
import { RoundCard, type Round } from "@/components/round-card"
import { ArrowLeft } from "lucide-react"
type ApiRoundParticipant = {
id: string
is_owner: boolean
guest_name: string | null
}
type ApiRound = {
id: string
course_name_snapshot: string
tee_name_snapshot: string
played_at: string
holes_planned: number
completed_at: string | null
participants: ApiRoundParticipant[]
}
function toRound(r: ApiRound): Round {
return {
id: r.id,
courseName: r.course_name_snapshot,
status: r.completed_at ? "completed" : "active",
teeName: r.tee_name_snapshot,
holes: r.holes_planned === 9 ? 9 : 18,
date: r.played_at,
playerCount: r.participants.length,
}
}
export function OwnRounds() {
const router = useRouter()
const [rounds, setRounds] = useState<Round[] | null>(null)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
let cancelled = false
async function load() {
try {
const res = await fetch("/rounds", { credentials: "include" })
if (res.status === 401) {
router.replace("/")
return
}
if (!res.ok) throw new Error(`rounds: ${res.status}`)
const data: ApiRound[] = await res.json()
if (!cancelled) setRounds(data.map(toRound))
} catch {
if (!cancelled) setError("Klarte ikke å hente rundene dine. Prøv igjen om litt.")
}
}
void load()
return () => {
cancelled = true
}
}, [router])
return (
<div className="flex min-h-[100dvh] flex-col bg-background">
<header className="sticky top-0 z-10 border-b border-border bg-background/80 backdrop-blur">
<div className="mx-auto flex w-full max-w-3xl items-center justify-between gap-4 px-5 py-4">
<Link
href="/dashboard"
className="inline-flex min-h-[44px] items-center gap-2 rounded-xl px-2 py-2 text-base font-semibold text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
>
<ArrowLeft aria-hidden="true" className="size-5" />
Til dashbord
</Link>
<Wordmark compact />
</div>
</header>
<main className="mx-auto w-full max-w-3xl flex-1 px-5 py-8 sm:py-10">
<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between sm:gap-6">
<div className="flex flex-col gap-2">
<h1 className="text-3xl font-extrabold tracking-tight text-foreground text-balance">
Egne runder
</h1>
<p className="max-w-prose text-lg leading-relaxed text-muted-foreground text-pretty">
Frittstående golfrunder du har registrert selv, uavhengig av turnering og klubb.
</p>
</div>
<Button
Update Todos Diagnostisere de to feilmeldingene fra skjermbildene Flytte frontend-sidene fra /rounds/* til /my-rounds/* (kollisjon med API-proxyen) Oppdatere alle interne lenker/router-kall til nye stier Oppdatere next.config.mjs-kommentaren med denne lærdommen Typesjekket produksjonsbuild Rulle ut live etter bekreftelse Oppdatere statusdokumentasjon Fant og fikset rot-årsaken til begge feilmeldingene — en reell, alvorlig bug, ikke bare kosmetikk: /rounds var samtidig navnet på både frontend-listesiden og backend-APIets ressursprefiks. Next.js sin rewrite-presedens ("afterFiles") gjorde at: GET/POST /rounds fra klienten aldri nådde backend — den statiske siden vant, så fetch() fikk Next sin egen HTML tilbake i stedet for JSON. Det ga akkurat de to feilmeldingene i skjermbildene. /rounds/[id] (rundedetaljsiden) hadde det motsatte problemet — der vant rewriten, så selve siden var fullstendig uoppnåelig (ville vist rå backend-JSON i stedet for UI-et om du hadde kommet så langt). Dette er nøyaktig samme klasse feil som medlemsside-hendelsen under ADR-016, bare i begge retninger samtidig siden det denne gangen var en eksakt navnekollisjon, ikke bare nøsting. Fikset: flyttet alle tre frontend-sidene til /my-rounds/* (list, new, [id]) — API-et beholder /rounds uendret. Oppdatert alle interne lenker (round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx, dashboard.tsx), lagt inn en tydelig advarsel i next.config.mjs for å hindre at dette gjentar seg. Bekreftet: «55/50/44/32» i det andre skjermbildet er ikke en bug — det er Tjøme Golfklubb sine faktiske utslagsnavn i teeoff (lengde i hundremeter). Typesjekket produksjonsbuild kompilerer rent med de nye rutene.
2026-07-23 12:07:40 +02:00
render={<Link href="/my-rounds/new" />}
V0-integrasjonen er ferdig. Kort om hva som skjedde: Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig. Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet: Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet. La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon. Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang). Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder. Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk. Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
size="lg"
className="h-12 shrink-0 gap-2 rounded-2xl px-6 text-base font-bold shadow-sm"
>
<Plus aria-hidden="true" className="size-5" />
Ny runde
</Button>
</div>
{error && (
<p role="alert" className="mt-4 text-base font-medium text-destructive">
{error}
</p>
)}
<div className="mt-8">
{rounds === null ? (
<LoadingState />
) : rounds.length === 0 ? (
<EmptyState />
) : (
<ul className="flex flex-col gap-4">
{rounds.map((round) => (
<li key={round.id}>
<RoundCard round={round} />
</li>
))}
</ul>
)}
</div>
</main>
</div>
)
}
function LoadingState() {
return (
<div
role="status"
aria-live="polite"
className="flex flex-col items-center justify-center gap-4 py-20 text-center"
>
<Loader2 aria-hidden="true" className="size-9 animate-spin text-primary" />
<span className="text-base font-medium text-muted-foreground">Laster runder</span>
</div>
)
}
function EmptyState() {
return (
<div className="flex flex-col items-center gap-5 rounded-3xl border border-dashed border-border bg-card/50 px-6 py-16 text-center">
<div className="flex size-16 items-center justify-center rounded-2xl bg-primary/15">
<ClipboardList aria-hidden="true" className="size-8 text-primary" />
</div>
<div className="flex max-w-md flex-col gap-2">
<h2 className="text-xl font-bold text-foreground">Ingen runder ennå</h2>
<p className="text-lg leading-relaxed text-muted-foreground text-pretty">
Her dukker rundene dine opp etter hvert som du registrerer dem. Trykk {" "}
<span className="font-semibold text-foreground">«Ny runde»</span> øverst for å komme i
gang.
</p>
</div>
</div>
)
}