From 8445b137ecc4057860b7c45b6ca6ce9996b12255 Mon Sep 17 00:00:00 2001 From: Erol Haagenrud Date: Sat, 18 Jul 2026 10:48:20 +0200 Subject: [PATCH] =?UTF-8?q?Klubb-landingssiden=20er=20live=20p=C3=A5=20htt?= =?UTF-8?q?ps://teecup.teeoff.no/clubs/{slug}=20=E2=80=94=20dermed=20er=20?= =?UTF-8?q?ADR-018=20sine=20to=20planlagte=20frontend-skjermer=20komplette?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit É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? --- .claude/settings.local.json | 5 +- CLAUDE.md | 35 ++++- FEATURE_BACKLOG.md | 8 +- frontend/app/clubs/[slug]/page.tsx | 10 ++ frontend/components/public-club.tsx | 188 ++++++++++++++++++++++++ frontend/components/tournament-card.tsx | 10 +- 6 files changed, 245 insertions(+), 11 deletions(-) create mode 100644 frontend/app/clubs/[slug]/page.tsx create mode 100644 frontend/components/public-club.tsx diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 3e3fb02..e8b09de 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -200,7 +200,10 @@ "Bash(python3 -m zipfile -l \"tee-cup-login-screen \\(3\\).zip\")", "Bash(python3 -c \"import sys,json;print\\('confirmed_count:', json.load\\(sys.stdin\\)['confirmed_count']\\)\")", "Bash(rm -f /opt/teecup/.env.scratch /tmp/towner.txt)", - "Bash(curl -s -o /dev/null -w '%{http_code}\\\\n' https://teecup.teeoff.no/t/00000000-0000-0000-0000-000000000000)" + "Bash(curl -s -o /dev/null -w '%{http_code}\\\\n' https://teecup.teeoff.no/t/00000000-0000-0000-0000-000000000000)", + "Bash(python3 -m zipfile -l \"tee-cup-login-screen \\(4\\).zip\")", + "Bash(rm -f /opt/teecup/.env.scratch /tmp/cowner.txt)", + "Bash(curl -s -o /dev/null -w '/clubs/ukjent: %{http_code}\\\\n' https://teecup.teeoff.no/clubs/ukjent-slug-garantert)" ], "additionalDirectories": [ "/opt/teeoff/deploy", diff --git a/CLAUDE.md b/CLAUDE.md index 8691a1e..08d517a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -553,12 +553,39 @@ Ferdig og verifisert: runden), `teeoff.no` upåvirket. **Gjenstår:** org-landingssiden (egen V0-prompt), Open Graph-metadata for deling, MinIO/bilder. +- **Offentlig klubb-landingsside LIVE (2026-07-18), samme dag:** femte og + siste V0-skjerm i ADR-018 (`components/public-club.tsx`, ny rute + `/clubs/[slug]`). Samme banner-språk som turnering-siden, liste over + klubbens turneringer (gjenbrukte eksisterende `TournamentCard`), egen tom- + tilstand. + **Reell delt-komponent-kollisjon løst, ikke duplisert bort:** + `TournamentCard` var bygget for KUN den innloggede konteksten (krevde + `orgId`, lenket til `/tournaments/{id}?org=...`). I stedet for en egen + kopi av kortet for den offentlige siden, gjort `orgId` valgfri — + satt (dashbordet) gir innlogget lenke, utelatt (klubbsiden) gir + `/t/{id}` i stedet. Samme kort, to kontekster, ingen duplisering. + Bekreftet dashbordets egen bruk uendret/upåvirket etterpå. + **V0 opprettet denne gangen selv en rute** (`app/clubs/[id]/page.tsx`, + uten noen props sendt inn i det hele tatt) — men navnga parameteren + `[id]` selv om den faktisk er en SLUG (`public_org_by_slug()`, ADR-018). + Skrev selv en ny, riktig `app/clubs/[slug]/page.tsx` i stedet for å bruke + V0s (feilnavngitte og prop-løse) versjon. + **Verifisert mot fersk `teecup_scratch` + ekte kjørende frontend-dev- + server:** org med slug+`public_profile`, én turnering satt `public`, én + latt stå på default `org` — klubbsiden viste GJENNOM frontend-proxyen + kun den ene offentlige turneringen, den org-private var korrekt + usynlig (samme filtermønster som API-et selv, bekreftet fra + klientsiden også). Ukjent slug ga korrekt 404. + **Rullet ut live**, kun `teecup_frontend`, `teeoff.no` upåvirket. + **ADR-018s planlagte skjermer er dermed komplette.** Gjenstår: Open + Graph-metadata for deling, MinIO/bilder — begge bevisst egne, senere + runder. Neste steg: -1. Org-landingsside (V0-prompt, samme mønster som turnering-siden) — siste - skjerm i ADR-018. Deretter Open Graph-metadata (og:title/og:image/ - og:description) for delingsforhåndsvisning, så MinIO/bilder som egen, - senere runde. +1. Open Graph-metadata (og:title/og:image/og:description) på + `/t/[id]`/`/clubs/[slug]` for delingsforhåndsvisning, deretter + MinIO/bildeopplasting som egen runde — de to gjenværende punktene fra + ADR-018. 2. Flere V0-skjermer (økt/program, blind draw, scorekort, leaderboard) — samme mønster: design i V0 (fortsett i samme prosjekt), FORVENT en full re-eksport hver gang — diff mot live-treet i et scratch-område før noe diff --git a/FEATURE_BACKLOG.md b/FEATURE_BACKLOG.md index ac49122..72c034a 100644 --- a/FEATURE_BACKLOG.md +++ b/FEATURE_BACKLOG.md @@ -405,11 +405,11 @@ --- -## Landingssider (turnering + organisasjon) — ADR-018, backend ✅ LIVE 2026-07-18 +## Landingssider (turnering + organisasjon) — ADR-018 ✅ LIVE 2026-07-18 Reist av brukeren 2026-07-18, rett etter registrerings-ADR-en (ADR-017). -Backend bygget og live samme dag. Frontend-skjermene og MinIO/bilder -gjenstår, bevisst egne runder. +Backend OG begge planlagte frontend-skjermer bygget og live samme dag. +Open Graph-metadata og MinIO/bilder gjenstår, bevisst egne runder. | Del | Status | Notat | |---|---|---| @@ -425,7 +425,7 @@ gjenstår, bevisst egne runder. | Blind draw-skjuling på offentlig side | ✅ | Arves automatisk via delt `_fetch_sessions()`-hjelpefunksjon (ADR-018 Beslutning E) — ikke reimplementert. | | Antall påmeldte / ledige plasser vist åpent | ✅ | `confirmed_count` i `GET /public/tournaments/{id}`. | | Frontend: turnering-landingsside + påmeldingsskjema, LIVE | ✅ | `/t/[id]`. Tre bekreftelsestilstander (bekreftet/venteliste/godkjenning venter). Verifisert med ekte `POST`-registrering mot scratch. | -| Frontend: org-landingsside | 📋 | Ikke startet — neste steg. | +| Frontend: org-/klubb-landingsside, LIVE | ✅ | `/clubs/[slug]`. Gjenbruker `TournamentCard` (nå med valgfri `orgId`) for turneringslisten. Verifisert: viser kun `public`-synlige turneringer. | --- diff --git a/frontend/app/clubs/[slug]/page.tsx b/frontend/app/clubs/[slug]/page.tsx new file mode 100644 index 0000000..82a1710 --- /dev/null +++ b/frontend/app/clubs/[slug]/page.tsx @@ -0,0 +1,10 @@ +import { PublicClub } from "@/components/public-club" + +export default async function ClubPage({ + params, +}: { + params: Promise<{ slug: string }> +}) { + const { slug } = await params + return +} diff --git a/frontend/components/public-club.tsx b/frontend/components/public-club.tsx new file mode 100644 index 0000000..760b220 --- /dev/null +++ b/frontend/components/public-club.tsx @@ -0,0 +1,188 @@ +"use client" + +import { useEffect, useState } from "react" +import { CalendarX2, ShieldAlert } from "lucide-react" +import { Wordmark } from "@/components/wordmark" +import { TournamentCard, type Tournament } from "@/components/tournament-card" + +// --- API-typer (matcher app/routers/registration.py sin get_public_org) ---- + +type ApiOrgTournament = { + id: string + name: string + status: string + start_date: string | null + end_date: string | null +} + +type ApiOrgInfo = { + name: string + tournaments: ApiOrgTournament[] +} + +function toTournament(t: ApiOrgTournament): Tournament { + return { + id: t.id, + name: t.name, + status: t.status as Tournament["status"], + startDate: t.start_date ?? undefined, + endDate: t.end_date ?? undefined, + } +} + +// --- Ordering -------------------------------------------------------------- + +// Upcoming (has a future/any start date and not completed) first, then +// tournaments without dates set, then completed/finished ones last. +function orderTournaments(list: Tournament[]) { + const rank = (t: Tournament) => { + if (t.status === "completed" || t.status === "archived") return 2 + if (!t.startDate) return 1 + return 0 + } + return [...list].sort((a, b) => { + const diff = rank(a) - rank(b) + if (diff !== 0) return diff + // Within the upcoming group, earliest start first. + if (a.startDate && b.startDate) { + return a.startDate.localeCompare(b.startDate) + } + return 0 + }) +} + +// --- Root ------------------------------------------------------------------ + +export function PublicClub({ slug }: { slug: string }) { + const [org, setOrg] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + let cancelled = false + async function load() { + try { + const res = await fetch(`/public/orgs/${slug}`, { credentials: "include" }) + if (res.status === 404) { + if (!cancelled) setError("Klubben finnes ikke.") + return + } + if (!res.ok) throw new Error(`org: ${res.status}`) + const data: ApiOrgInfo = await res.json() + if (!cancelled) setOrg(data) + } catch { + if (!cancelled) setError("Klarte ikke å laste klubbsiden. Prøv å laste siden på nytt.") + } finally { + if (!cancelled) setLoading(false) + } + } + void load() + return () => { + cancelled = true + } + }, [slug]) + + if (loading) { + return ( +
+ + ) + } + + if (error || !org) { + return ( +
+
+
+

+ {error ?? "Klubben finnes ikke."} +

+
+ ) + } + + const tournaments = orderTournaments(org.tournaments.map(toTournament)) + + return ( +
+
+
+ +
+
+ +
+
+ + +
+

Turneringer

+ {tournaments.length > 0 ? ( +
    + {tournaments.map((tournament) => ( +
  • + +
  • + ))} +
+ ) : ( + + )} +
+
+
+
+ ) +} + +// --- Banner ---------------------------------------------------------------- +// Same visual treatment as the tournament landing banner: solid brand-green +// gradient block with subtle decorative rings. Permanent look for now. + +function Banner({ name }: { name: string }) { + return ( +
+