2026-07-18 06:46:09 +02:00
|
|
|
|
"use client"
|
|
|
|
|
|
|
2026-07-25 07:51:32 +02:00
|
|
|
|
// Dashbord-redesign (ADR-035, 2026-07-25): organisasjon er ikke lenger det
|
|
|
|
|
|
// første/viktigste en bruker møter -- en bruker er først og fremst en
|
|
|
|
|
|
// GOLFSPILLER. Presentasjon fra V0 (zip 18), datalag skrevet om fra mock
|
|
|
|
|
|
// til ekte fetch. Organisasjon-opprettelse er nå USYNLIG/automatisk ("Ny
|
|
|
|
|
|
// turnering" oppretter/gjenbruker en organisasjon i bakgrunnen, ingen eget
|
|
|
|
|
|
// "opprett organisasjon"-steg for det vanlige tilfellet) -- se
|
|
|
|
|
|
// ARCHITECTURE_DECISIONS.md ADR-035 Beslutning A.
|
2026-08-01 21:15:07 +02:00
|
|
|
|
//
|
|
|
|
|
|
// "Forest Green"-visuell redesign (2026-08-01, V0 zip 23, samme retning som
|
|
|
|
|
|
// login-form.tsx): kun JSX/farger endret nedenfor -- all ekte datahenting,
|
|
|
|
|
|
// alle ekte handlers og alle delte komponenter (RoundCard/TournamentCard/
|
|
|
|
|
|
// InstallPrompt/NewTournamentInline/JoinByCodeInline) er UENDRET, kun
|
|
|
|
|
|
// re-stylet der de selv definerer sin egen visuelle ramme.
|
|
|
|
|
|
//
|
|
|
|
|
|
// "Venner på banen" (ny seksjon, plassert rett under hurtighandlingene per
|
|
|
|
|
|
// brukerens eksplisitte ønske fra en tidligere Stitch-designsamtale) er en
|
|
|
|
|
|
// HELT NY funksjon -- viser venner med en AKTIV runde nå, eller en runde
|
|
|
|
|
|
// fullført innen de siste 24 timene, hentet fra det nye
|
|
|
|
|
|
// GET /friends/on-course-endepunktet (app/routers/rounds.py). Synlighet
|
|
|
|
|
|
// respekterer nøyaktig samme regel som all annen rundedeling (ADR-036
|
|
|
|
|
|
// fase 2, `_can_view_round`s 'friends'-gren) -- håndhevet server-side, ikke
|
|
|
|
|
|
// duplisert her.
|
2026-08-07 22:02:56 +02:00
|
|
|
|
//
|
|
|
|
|
|
// "Clubhouse"-visuell redesign (2026-08-07, V0, egen parallell utforskning
|
|
|
|
|
|
// -- IKKE samme retning som "Forest Green" over). Brukeren: "full
|
|
|
|
|
|
// overtagelse, også bakgrunnen" -- eksplisitt bekreftet å gå imot
|
|
|
|
|
|
// 2026-08-02-fiksen som fjernet en utilsiktet grønn hue fra de nøytrale
|
|
|
|
|
|
// tokenene (DESIGN_SYSTEM.md), siden clubhouse-bakgrunnen er en BEVISST
|
|
|
|
|
|
// varm/turf-tonet off-white, ikke en utilsiktet lekkasje. Gjenbruker
|
|
|
|
|
|
// `--tee`/`--tee-strong`/`--cup`/`--cup-strong`/`--clubhouse-*`-tokenene og
|
|
|
|
|
|
// `TeeCupWordmark` som ALLEREDE fantes i `globals.css`/`components/teecup/`
|
|
|
|
|
|
// fra en tidligere, ennå ikke integrert `/logg-inn`-utforskning -- samme
|
|
|
|
|
|
// palett, ikke funnet opp på nytt. Bevisst IKKE lagt til Bricolage-
|
|
|
|
|
|
// visningsfonten (samme utelatelse som `/logg-inn` selv gjorde) -- Nunito
|
|
|
|
|
|
// beholdt for å unngå en ekstra font-avhengighet for kun to skjermer.
|
|
|
|
|
|
//
|
2026-08-08 12:34:03 +02:00
|
|
|
|
// Omfangsgrense (delt komponent IKKE re-stylet her, se CHANGELOG.md):
|
|
|
|
|
|
// `RoundCard`/`TournamentCard` (også brukt av /my-rounds) beholder sin
|
|
|
|
|
|
// eksisterende styling inntil videre -- kun selve dashbord-skjermens
|
|
|
|
|
|
// EGNE, ikke-delte JSX er re-stylet i denne runden.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Bunn-navigasjonen (2026-08-08, egen V0-prompt, se CHANGELOG.md): den
|
|
|
|
|
|
// gamle `BottomTabBar` (definert her, `active`-prop-styrt) er ERSTATTET av
|
|
|
|
|
|
// `BottomNav` (`components/teecup/bottom-nav.tsx`) -- en selvstendig,
|
|
|
|
|
|
// bevisst palett-nøytral "flytende hvit flate" som leser pathname selv
|
|
|
|
|
|
// via `usePathname()` i stedet for å få en `active`-prop utenfra, siden
|
|
|
|
|
|
// den deles mellom clubhouse- og Forest Green-sider. hrefs rettet til de
|
|
|
|
|
|
// faktiske rutene under integrering (V0 kjente dem ikke).
|
2026-07-25 07:51:32 +02:00
|
|
|
|
|
2026-07-18 06:46:09 +02:00
|
|
|
|
import type React from "react"
|
2026-07-25 07:51:32 +02:00
|
|
|
|
import { useCallback, useEffect, useState } from "react"
|
2026-07-18 06:46:09 +02:00
|
|
|
|
import { useRouter } from "next/navigation"
|
Update Todos
Legge til argon2-cffi, pyotp, qrcode i requirements.txt
Skrive migrasjon 012 (passord, 2FA, superadmin, org-invitasjoner)
app/auth.py: sesjonsstadier, passord-hashing, TOTP-hjelpere
app/routers/auth.py: passord-innlogging, 2FA-oppsett/verifisering
app/email.py: 2FA-kode og invitasjons-maler
app/routers/organizations.py: invitasjoner, medlemskapsstyring, superadmin-sti
Frontend: login-form passord-modus + 2FA-skjermer
Frontend: kontoinnstillinger + org-medlemsstyring-skjerm
Ekte typesjekket frontend-build
Scratch-verifisere hele auth-løpet grundig (backend)
Deploy mot ekte teecup_db/containere + oppdatere .md-filer
Backend og frontend er grundig scratch-verifisert — inkludert tre reelle bugs jeg fant og fikset underveis (en UUID-serialiseringsfeil i magic-link-innlogging, og to tilfeller av en uendelig 2FA-løkke der en nettopp bekreftet kode ble sjekket på nytt). Alle sikkerhetsvern testet eksplisitt: siste-eier-vern, blokkert selv-forfremmelse, admin kan ikke gi eierskap, superadmin fungerer/avvises riktig, tvungen 2FA for nye eiere, passord med spesialtegn/mellomrom, og bakoverkompatibilitet med eksisterende magic-link-flyt.
Klar til utrulling mot ekte systemer:
Migrasjon: 012_password_2fa_and_org_invitations.sql mot ekte teecup_db
Redeploy: både teecup_api og teecup_frontend
AskUserQuestion
2026-07-19 10:36:50 +02:00
|
|
|
|
import Link from "next/link"
|
Update Todos
Flytte is_participant-logikk til team_authz.py (unngå sirkulær import)
Widen scoring.py: get_scorecard/submit_hole_score/submit_hole_result/concede_match
Widen matches.py: list_matches/add_participant/remove_participant/lock_lineup
Widen tournaments.py: list_sessions/list_teams/concede_tournament
Widen courses.py: list_holes
Widen messaging.py: team chat REST-endepunkter (list/send/delete)
Legge til my_session_id/my_match_id i /auth/me sin my_tournaments
Frontend: legge til lenker til lag-chat/scorekort fra «Mine runder»
Scratch-verifisere hele deltaker-tilgang-runden (positive + negative kontroller)
Ekte typesjekket frontend-produksjonsbuild + backend py_compile på nytt før deploy-forslag
Presentere plan og be om bekreftelse før utrulling mot ekte teecup_db/containere
Bygge sekundær e-postadresse (enkelt tilfelle, ADR-032-mønster)
Bygge HCP-historikk over tid
Deltaker-tilgang til lag-chat/scorekort er bygget og grundig scratch-verifisert (43/43 sjekker, test_isolation.sql fortsatt 12/12). Kort oppsummert hva som er gjort:
Backend — den blanke org-medlemskaps-sperren (get_authorized_org) er fjernet fra ni endepunkter (lag-chat lese/skrive/slette, scorekort-lesing, slag/hull-resultat-innsending, walkover på match- og turneringsnivå, match-/lag-/økt-listing, bane-hull) og erstattet med de faktiske, allerede eksisterende autorisasjonssjekkene (user_is_rostered_on_team, user_is_match_participant, user_is_team_captain — alle uendret) — pluss et nytt eksplisitt org-medlem ELLER turnering-deltaker-OR på de endepunktene som tidligere ikke hadde noen finkornet sjekk i det hele tatt (der en ren fjerning ville åpnet dem for enhver innlogget bruker). /auth/me viser nå også my_session_id/my_match_id per rad i "Mine runder".
Frontend — "Mine runder"-kortet har fått to nye handlingslenker: "Lag-chat" (alltid) og "Scorekort" (når spilleren har en aktiv match).
Verifisert grundig: en rostret spiller UTEN organisasjonsmedlemskap får nå riktig tilgang til alt dette; en helt fremmed bruker avvises fortsatt overalt; org-eier beholder full tilgang som før; lag-chatten er fortsatt ekte privat (org-eier fortsatt avvist der, med vilje); kryss-org-isolasjon holder; og en rostret-men-ikke-kaptein spiller ble korrekt avvist fra walkover (fant samtidig en eksisterende, dokumentert fallback-oppførsel — et lag uten utpekt kaptein godtar enhver rostret spiller — som viste seg å fungere akkurat som tiltenkt).
2026-07-22 05:46:47 +02:00
|
|
|
|
import {
|
2026-07-25 08:37:07 +02:00
|
|
|
|
ArrowRight,
|
Varsler (fra zip 20): in-app varslingssenter er live — bjelle med uleste-tall i dashbord-headeren, /my-notifications-side. Trigges i dag ved venneforespørsel sendt/akseptert; flere hendelser kan kobles på senere.
Rundeleaderboard: ny GET /rounds/{id}/leaderboard-backend er live (rangering, thru-tall, brutto+netto til par, håndterer 1 til 15+ deltakere). V0-prompten for selve visningen ligger i FEATURE_BACKLOG.md, klar til å limes inn i v0.app — send meg zip-en når du har den, så kobler jeg den på (foreslått rute /my-rounds/[id]/leaderboard, lenket fra rundesiden).
Begge deler scratch-verifisert (39/39 sjekker, inkl. en uavhengig kryssjekk av netto-beregningen mot handicap_engine direkte), rullet ut mot ekte teecup_db/containere, teeoff.no upåvirket.
2026-07-26 06:53:58 +02:00
|
|
|
|
Bell,
|
Update Todos
Flytte is_participant-logikk til team_authz.py (unngå sirkulær import)
Widen scoring.py: get_scorecard/submit_hole_score/submit_hole_result/concede_match
Widen matches.py: list_matches/add_participant/remove_participant/lock_lineup
Widen tournaments.py: list_sessions/list_teams/concede_tournament
Widen courses.py: list_holes
Widen messaging.py: team chat REST-endepunkter (list/send/delete)
Legge til my_session_id/my_match_id i /auth/me sin my_tournaments
Frontend: legge til lenker til lag-chat/scorekort fra «Mine runder»
Scratch-verifisere hele deltaker-tilgang-runden (positive + negative kontroller)
Ekte typesjekket frontend-produksjonsbuild + backend py_compile på nytt før deploy-forslag
Presentere plan og be om bekreftelse før utrulling mot ekte teecup_db/containere
Bygge sekundær e-postadresse (enkelt tilfelle, ADR-032-mønster)
Bygge HCP-historikk over tid
Deltaker-tilgang til lag-chat/scorekort er bygget og grundig scratch-verifisert (43/43 sjekker, test_isolation.sql fortsatt 12/12). Kort oppsummert hva som er gjort:
Backend — den blanke org-medlemskaps-sperren (get_authorized_org) er fjernet fra ni endepunkter (lag-chat lese/skrive/slette, scorekort-lesing, slag/hull-resultat-innsending, walkover på match- og turneringsnivå, match-/lag-/økt-listing, bane-hull) og erstattet med de faktiske, allerede eksisterende autorisasjonssjekkene (user_is_rostered_on_team, user_is_match_participant, user_is_team_captain — alle uendret) — pluss et nytt eksplisitt org-medlem ELLER turnering-deltaker-OR på de endepunktene som tidligere ikke hadde noen finkornet sjekk i det hele tatt (der en ren fjerning ville åpnet dem for enhver innlogget bruker). /auth/me viser nå også my_session_id/my_match_id per rad i "Mine runder".
Frontend — "Mine runder"-kortet har fått to nye handlingslenker: "Lag-chat" (alltid) og "Scorekort" (når spilleren har en aktiv match).
Verifisert grundig: en rostret spiller UTEN organisasjonsmedlemskap får nå riktig tilgang til alt dette; en helt fremmed bruker avvises fortsatt overalt; org-eier beholder full tilgang som før; lag-chatten er fortsatt ekte privat (org-eier fortsatt avvist der, med vilje); kryss-org-isolasjon holder; og en rostret-men-ikke-kaptein spiller ble korrekt avvist fra walkover (fant samtidig en eksisterende, dokumentert fallback-oppførsel — et lag uten utpekt kaptein godtar enhver rostret spiller — som viste seg å fungere akkurat som tiltenkt).
2026-07-22 05:46:47 +02:00
|
|
|
|
Building2,
|
|
|
|
|
|
ChevronRight,
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
Flag,
|
2026-07-25 07:51:32 +02:00
|
|
|
|
KeyRound,
|
Update Todos
Flytte is_participant-logikk til team_authz.py (unngå sirkulær import)
Widen scoring.py: get_scorecard/submit_hole_score/submit_hole_result/concede_match
Widen matches.py: list_matches/add_participant/remove_participant/lock_lineup
Widen tournaments.py: list_sessions/list_teams/concede_tournament
Widen courses.py: list_holes
Widen messaging.py: team chat REST-endepunkter (list/send/delete)
Legge til my_session_id/my_match_id i /auth/me sin my_tournaments
Frontend: legge til lenker til lag-chat/scorekort fra «Mine runder»
Scratch-verifisere hele deltaker-tilgang-runden (positive + negative kontroller)
Ekte typesjekket frontend-produksjonsbuild + backend py_compile på nytt før deploy-forslag
Presentere plan og be om bekreftelse før utrulling mot ekte teecup_db/containere
Bygge sekundær e-postadresse (enkelt tilfelle, ADR-032-mønster)
Bygge HCP-historikk over tid
Deltaker-tilgang til lag-chat/scorekort er bygget og grundig scratch-verifisert (43/43 sjekker, test_isolation.sql fortsatt 12/12). Kort oppsummert hva som er gjort:
Backend — den blanke org-medlemskaps-sperren (get_authorized_org) er fjernet fra ni endepunkter (lag-chat lese/skrive/slette, scorekort-lesing, slag/hull-resultat-innsending, walkover på match- og turneringsnivå, match-/lag-/økt-listing, bane-hull) og erstattet med de faktiske, allerede eksisterende autorisasjonssjekkene (user_is_rostered_on_team, user_is_match_participant, user_is_team_captain — alle uendret) — pluss et nytt eksplisitt org-medlem ELLER turnering-deltaker-OR på de endepunktene som tidligere ikke hadde noen finkornet sjekk i det hele tatt (der en ren fjerning ville åpnet dem for enhver innlogget bruker). /auth/me viser nå også my_session_id/my_match_id per rad i "Mine runder".
Frontend — "Mine runder"-kortet har fått to nye handlingslenker: "Lag-chat" (alltid) og "Scorekort" (når spilleren har en aktiv match).
Verifisert grundig: en rostret spiller UTEN organisasjonsmedlemskap får nå riktig tilgang til alt dette; en helt fremmed bruker avvises fortsatt overalt; org-eier beholder full tilgang som før; lag-chatten er fortsatt ekte privat (org-eier fortsatt avvist der, med vilje); kryss-org-isolasjon holder; og en rostret-men-ikke-kaptein spiller ble korrekt avvist fra walkover (fant samtidig en eksisterende, dokumentert fallback-oppførsel — et lag uten utpekt kaptein godtar enhver rostret spiller — som viste seg å fungere akkurat som tiltenkt).
2026-07-22 05:46:47 +02:00
|
|
|
|
LogOut,
|
2026-07-25 07:51:32 +02:00
|
|
|
|
MapPin,
|
2026-08-01 21:15:07 +02:00
|
|
|
|
Trophy,
|
2026-07-25 07:51:32 +02:00
|
|
|
|
TrendingDown,
|
|
|
|
|
|
TrendingUp,
|
Update Todos
Flytte is_participant-logikk til team_authz.py (unngå sirkulær import)
Widen scoring.py: get_scorecard/submit_hole_score/submit_hole_result/concede_match
Widen matches.py: list_matches/add_participant/remove_participant/lock_lineup
Widen tournaments.py: list_sessions/list_teams/concede_tournament
Widen courses.py: list_holes
Widen messaging.py: team chat REST-endepunkter (list/send/delete)
Legge til my_session_id/my_match_id i /auth/me sin my_tournaments
Frontend: legge til lenker til lag-chat/scorekort fra «Mine runder»
Scratch-verifisere hele deltaker-tilgang-runden (positive + negative kontroller)
Ekte typesjekket frontend-produksjonsbuild + backend py_compile på nytt før deploy-forslag
Presentere plan og be om bekreftelse før utrulling mot ekte teecup_db/containere
Bygge sekundær e-postadresse (enkelt tilfelle, ADR-032-mønster)
Bygge HCP-historikk over tid
Deltaker-tilgang til lag-chat/scorekort er bygget og grundig scratch-verifisert (43/43 sjekker, test_isolation.sql fortsatt 12/12). Kort oppsummert hva som er gjort:
Backend — den blanke org-medlemskaps-sperren (get_authorized_org) er fjernet fra ni endepunkter (lag-chat lese/skrive/slette, scorekort-lesing, slag/hull-resultat-innsending, walkover på match- og turneringsnivå, match-/lag-/økt-listing, bane-hull) og erstattet med de faktiske, allerede eksisterende autorisasjonssjekkene (user_is_rostered_on_team, user_is_match_participant, user_is_team_captain — alle uendret) — pluss et nytt eksplisitt org-medlem ELLER turnering-deltaker-OR på de endepunktene som tidligere ikke hadde noen finkornet sjekk i det hele tatt (der en ren fjerning ville åpnet dem for enhver innlogget bruker). /auth/me viser nå også my_session_id/my_match_id per rad i "Mine runder".
Frontend — "Mine runder"-kortet har fått to nye handlingslenker: "Lag-chat" (alltid) og "Scorekort" (når spilleren har en aktiv match).
Verifisert grundig: en rostret spiller UTEN organisasjonsmedlemskap får nå riktig tilgang til alt dette; en helt fremmed bruker avvises fortsatt overalt; org-eier beholder full tilgang som før; lag-chatten er fortsatt ekte privat (org-eier fortsatt avvist der, med vilje); kryss-org-isolasjon holder; og en rostret-men-ikke-kaptein spiller ble korrekt avvist fra walkover (fant samtidig en eksisterende, dokumentert fallback-oppførsel — et lag uten utpekt kaptein godtar enhver rostret spiller — som viste seg å fungere akkurat som tiltenkt).
2026-07-22 05:46:47 +02:00
|
|
|
|
UserCircle,
|
2026-07-25 07:51:32 +02:00
|
|
|
|
UserPlus,
|
Update Todos
Flytte is_participant-logikk til team_authz.py (unngå sirkulær import)
Widen scoring.py: get_scorecard/submit_hole_score/submit_hole_result/concede_match
Widen matches.py: list_matches/add_participant/remove_participant/lock_lineup
Widen tournaments.py: list_sessions/list_teams/concede_tournament
Widen courses.py: list_holes
Widen messaging.py: team chat REST-endepunkter (list/send/delete)
Legge til my_session_id/my_match_id i /auth/me sin my_tournaments
Frontend: legge til lenker til lag-chat/scorekort fra «Mine runder»
Scratch-verifisere hele deltaker-tilgang-runden (positive + negative kontroller)
Ekte typesjekket frontend-produksjonsbuild + backend py_compile på nytt før deploy-forslag
Presentere plan og be om bekreftelse før utrulling mot ekte teecup_db/containere
Bygge sekundær e-postadresse (enkelt tilfelle, ADR-032-mønster)
Bygge HCP-historikk over tid
Deltaker-tilgang til lag-chat/scorekort er bygget og grundig scratch-verifisert (43/43 sjekker, test_isolation.sql fortsatt 12/12). Kort oppsummert hva som er gjort:
Backend — den blanke org-medlemskaps-sperren (get_authorized_org) er fjernet fra ni endepunkter (lag-chat lese/skrive/slette, scorekort-lesing, slag/hull-resultat-innsending, walkover på match- og turneringsnivå, match-/lag-/økt-listing, bane-hull) og erstattet med de faktiske, allerede eksisterende autorisasjonssjekkene (user_is_rostered_on_team, user_is_match_participant, user_is_team_captain — alle uendret) — pluss et nytt eksplisitt org-medlem ELLER turnering-deltaker-OR på de endepunktene som tidligere ikke hadde noen finkornet sjekk i det hele tatt (der en ren fjerning ville åpnet dem for enhver innlogget bruker). /auth/me viser nå også my_session_id/my_match_id per rad i "Mine runder".
Frontend — "Mine runder"-kortet har fått to nye handlingslenker: "Lag-chat" (alltid) og "Scorekort" (når spilleren har en aktiv match).
Verifisert grundig: en rostret spiller UTEN organisasjonsmedlemskap får nå riktig tilgang til alt dette; en helt fremmed bruker avvises fortsatt overalt; org-eier beholder full tilgang som før; lag-chatten er fortsatt ekte privat (org-eier fortsatt avvist der, med vilje); kryss-org-isolasjon holder; og en rostret-men-ikke-kaptein spiller ble korrekt avvist fra walkover (fant samtidig en eksisterende, dokumentert fallback-oppførsel — et lag uten utpekt kaptein godtar enhver rostret spiller — som viste seg å fungere akkurat som tiltenkt).
2026-07-22 05:46:47 +02:00
|
|
|
|
X,
|
|
|
|
|
|
} from "lucide-react"
|
2026-07-18 06:46:09 +02:00
|
|
|
|
import { Button } from "@/components/ui/button"
|
|
|
|
|
|
import { Input } from "@/components/ui/input"
|
|
|
|
|
|
import { Label } from "@/components/ui/label"
|
|
|
|
|
|
import {
|
|
|
|
|
|
DropdownMenu,
|
|
|
|
|
|
DropdownMenuContent,
|
|
|
|
|
|
DropdownMenuItem,
|
|
|
|
|
|
DropdownMenuTrigger,
|
|
|
|
|
|
} from "@/components/ui/dropdown-menu"
|
Update Todos
Bygg PWA-installasjonsoppfordring (Android beforeinstallprompt + iOS instruksjonsbanner)
Design og bygg flere flighter i én frittstående runde (løs gruppering, retning 1)
Design og bygg scramble/greensome-statistikk (valgt utslag per spiller)
Bygg push-varsler til telefonens OS (VAPID, push_subscription, service worker)
Scratch-verifiser backend for alle fire (27/27 sjekker gjort)
Browserverifiser alle fire i nettleser mot scratch-backend
Rull ut mot ekte systemer (migrasjoner + containere), med bekreftelse
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Alle fire er ferdigstilt, verifisert og live. Kort oppsummert:
PWA-installasjonsoppfordring — ekte beforeinstallprompt-fanging, Android-knapp + iOS-instruksjonsbanner, vist på dashbordet. Bekreftet at Chrome faktisk fyrte eventet i testøkten.
Flere flighter i én frittstående runde — løs gruppering av separate runder (delt UUID), "+ Legg til en flight til" gjenbruker hele opprett-flyten forhåndsutfylt, nytt kombinert leaderboard på tvers av flighter. Testet fra knappetrykk til ferdig runde, bekreftet i database.
Scramble/greensome: valgt utslag per spiller — nytt felt i hull-registreringen (helt valgfritt), "Utslag brukt"-oppsummering på statistikksiden. Bygget for frittstående runder; org-turneringer bevisst utenfor omfang denne runden (notert i backloggen).
Push-varsler til telefonens OS — Web Push/VAPID, ny av/på-bryter i kontoinnstillinger, dekker alle fire varseltyper automatisk. VAPID-nøkler generert og lagt i .env uten å vises i chatten. Én ærlig begrensning: ekte OS-varsel-levering med innvilget tillatelse kunne ikke testes i denne automatiserte nettleserøkten (tillatelsen var forhåndsblokkert av miljøet) — verdt å teste selv på en ekte enhet.
27/27 scratch-sjekker, full nettleser-gjennomgang uten konsollfeil, migrasjoner 035–037 kjørt mot ekte teecup_db, begge containere redeployet og verifisert, teeoff.no upåvirket. Alle tre .md-filene er oppdatert med full detalj.
2026-07-28 22:34:16 +02:00
|
|
|
|
import { InstallPrompt } from "@/components/install-prompt"
|
2026-08-07 22:02:56 +02:00
|
|
|
|
import { TeeCupWordmark } from "@/components/teecup/wordmark"
|
2026-08-08 12:34:03 +02:00
|
|
|
|
import { BottomNav } from "@/components/teecup/bottom-nav"
|
2026-07-25 07:51:32 +02:00
|
|
|
|
import { RoundCard, type Round } from "@/components/round-card"
|
2026-07-18 06:46:09 +02:00
|
|
|
|
import { TournamentCard, type Tournament } from "@/components/tournament-card"
|
2026-07-25 07:51:32 +02:00
|
|
|
|
import { type TournamentStatus } from "@/components/tournament-status-badge"
|
Update Todos
Migrasjon 015: app_user-profilfelt + player_organizations_for_user()-bro
Backend: utvid Me + PATCH /auth/profile + avatar-opplasting/sletting
Backend: 'mine runder'-data + check_visibility-utvidelse for deltakere
Frontend: profil-seksjon i /account
Frontend: 'Mine runder'-seksjon + betinget tom-tilstand i dashboard.tsx
Scratch-verifisere alt (15 sjekker bestått)
Typesjekket frontend-build
ADR-031 + .md-oppdatering
Bygget og scratch-verifisert (15 automatiserte sjekker). Oppsummering:
Personlig profil — nye felt på selve kontoen (ikke på org-ens spillerdata, det er bevisst holdt atskilt siden en person kan ha ulike spiller-rader i ulike klubber): profilbilde, fornavn, etternavn, fødselsdato, kjønn, HCP, hjemmeklubb. Redigeres i en ny seksjon på /account. Sletting av enkeltfelt fungerer (send tomt/null), profilbilde kan lastes opp og fjernes.
"Mine runder" — ny seksjon øverst på dashbordet, viser turneringer du er rostret i på tvers av alle organisasjoner, uavhengig av om du er medlem noe sted.
Et reelt sikkerhetshull jeg fant underveis, ikke antatt på forhånd: da jeg testet "Mine runder" mot en faktisk ren spiller (ingen organisasjonsmedlemskap), oppdaget jeg at synlighetsregelen kun ga deltakere tilgang for det strengeste synlighetsnivået — ikke for standard-nivået («org»), som er det ALLE nye turneringer får automatisk. En ren spiller ville altså vært låst ute av sin egen, helt normale turnering. Fikset og verifisert grundig at det er en ren utvidelse: en fremmed innlogget bruker og en anonym leser blir fortsatt korrekt avvist som før.
Bevisst utenfor omfang, tydelig flagget: "Mine runder" lenker til den offentlige turnering-siden, ikke til lagets private chat eller scorekortet ennå — de krever fortsatt ekte organisasjonsmedlemskap, en strengere sperre brukt bredt i hele appen som jeg ikke ville endre uten en egen, forsiktig runde. Notert som naturlig neste steg.
Ingen kode for punkt 2 (midlertidige spillere) i denne runden, som avtalt.
2026-07-20 10:41:30 +02:00
|
|
|
|
import { cn } from "@/lib/utils"
|
2026-07-18 06:46:09 +02:00
|
|
|
|
|
Ja, jeg forsto rekkefølgen — steg 1 og 2 er nå ferdig og scratch-verifisert:
Steg 1 (backend, HCP-prosent + Match-HCP): ny migrasjon 051_round_allowance_override.sql (round.allowance_override jsonb), koblet inn i _recompute_side_handicaps/_relative_strokes_for_round + POST/PATCH /rounds. Verifisert presist i scratch: 100%→50%-prosent ga playing_handicap 24/10 → 12/5 nøyaktig som beregnet for hånd, og use_matchplay_handicap slått av ga rå 12/5 i stedet for differensial 7/0. Slagspill upåvirket (regresjon bekreftet).
Steg 2 (kode-fiks, lag-gruppering): ScorecardGrid (live-scoring) og MatchScorecardGrid (post-runde/fremtidig sammenslått side) sorterer nå stabilt på round_side_id før rendering. Bygget en fersk scratch-fourball-runde med akkurat ditt bug-scenario (A→Rødt, B→Blått, C→Rødt, D→Blått, lagt til i den rekkefølgen) og bekreftet visuelt i nettleseren i begge visningene: Rødt lag vises samlet, deretter Blått — ikke lenger interleaved.
Typesjekk + full produksjonsbuild kompilerte rent gjennom hele runden.
Gjenstår før jeg går videre til V0-promptene: migrasjon 051 er kun kjørt i scratch, ikke mot ekte teecup_db. Planen er:
docker exec -i teeoff_db psql -U teeoff_admin -d teecup_db -v ON_ERROR_STOP=1 < 051_round_allowance_override.sql
— ett rent additivt ALTER TABLE round ADD COLUMN allowance_override jsonb, ingen eksisterende rader berøres. Deretter docker compose up -d --build teecup_api teecup_frontend (backend for allowance-koblingen, frontend for gruppering-fiksen).
Bekreft så kjører jeg dette, og går videre til V0-prompt 1 (rundeoppsett-veiviseren).
2026-08-02 08:24:01 +02:00
|
|
|
|
// Kategoriske pastellfarger for "Venner på banen" -- én tone per person, i
|
|
|
|
|
|
// stedet for én ensfarget tone. Hver par en lys bakgrunn med en mørk,
|
|
|
|
|
|
// kontrastrik tekst-tone for initialene.
|
|
|
|
|
|
const AVATAR_HUES = [
|
|
|
|
|
|
{ bg: "#dbeafe", ink: "#1e40af" }, // lys blå
|
|
|
|
|
|
{ bg: "#ffedd5", ink: "#9a3412" }, // lys oransje
|
|
|
|
|
|
{ bg: "#dcfce7", ink: "#166534" }, // lys grønn
|
|
|
|
|
|
{ bg: "#fce7f3", ink: "#9d174d" }, // lys rosa
|
|
|
|
|
|
] as const
|
|
|
|
|
|
|
2026-07-25 07:51:32 +02:00
|
|
|
|
// --- API-typer ---------------------------------------------------------------
|
|
|
|
|
|
|
2026-07-18 06:46:09 +02:00
|
|
|
|
type MyOrg = { organization_id: string; name: string; role: string }
|
|
|
|
|
|
|
2026-07-25 07:51:32 +02:00
|
|
|
|
// ADR-031: turneringer brukeren er SPILLER i (rostret på et lag), på tvers
|
|
|
|
|
|
// av organisasjoner, uavhengig av organisasjonsmedlemskap.
|
Update Todos
Migrasjon 015: app_user-profilfelt + player_organizations_for_user()-bro
Backend: utvid Me + PATCH /auth/profile + avatar-opplasting/sletting
Backend: 'mine runder'-data + check_visibility-utvidelse for deltakere
Frontend: profil-seksjon i /account
Frontend: 'Mine runder'-seksjon + betinget tom-tilstand i dashboard.tsx
Scratch-verifisere alt (15 sjekker bestått)
Typesjekket frontend-build
ADR-031 + .md-oppdatering
Bygget og scratch-verifisert (15 automatiserte sjekker). Oppsummering:
Personlig profil — nye felt på selve kontoen (ikke på org-ens spillerdata, det er bevisst holdt atskilt siden en person kan ha ulike spiller-rader i ulike klubber): profilbilde, fornavn, etternavn, fødselsdato, kjønn, HCP, hjemmeklubb. Redigeres i en ny seksjon på /account. Sletting av enkeltfelt fungerer (send tomt/null), profilbilde kan lastes opp og fjernes.
"Mine runder" — ny seksjon øverst på dashbordet, viser turneringer du er rostret i på tvers av alle organisasjoner, uavhengig av om du er medlem noe sted.
Et reelt sikkerhetshull jeg fant underveis, ikke antatt på forhånd: da jeg testet "Mine runder" mot en faktisk ren spiller (ingen organisasjonsmedlemskap), oppdaget jeg at synlighetsregelen kun ga deltakere tilgang for det strengeste synlighetsnivået — ikke for standard-nivået («org»), som er det ALLE nye turneringer får automatisk. En ren spiller ville altså vært låst ute av sin egen, helt normale turnering. Fikset og verifisert grundig at det er en ren utvidelse: en fremmed innlogget bruker og en anonym leser blir fortsatt korrekt avvist som før.
Bevisst utenfor omfang, tydelig flagget: "Mine runder" lenker til den offentlige turnering-siden, ikke til lagets private chat eller scorekortet ennå — de krever fortsatt ekte organisasjonsmedlemskap, en strengere sperre brukt bredt i hele appen som jeg ikke ville endre uten en egen, forsiktig runde. Notert som naturlig neste steg.
Ingen kode for punkt 2 (midlertidige spillere) i denne runden, som avtalt.
2026-07-20 10:41:30 +02:00
|
|
|
|
type MyTournament = {
|
|
|
|
|
|
organization_id: string
|
|
|
|
|
|
organization_name: string
|
|
|
|
|
|
tournament_id: string
|
|
|
|
|
|
tournament_name: string
|
|
|
|
|
|
status: TournamentStatus
|
|
|
|
|
|
team_id: string
|
|
|
|
|
|
team_name: string
|
|
|
|
|
|
team_color: string | null
|
|
|
|
|
|
next_session_at: string | null
|
Update Todos
Flytte is_participant-logikk til team_authz.py (unngå sirkulær import)
Widen scoring.py: get_scorecard/submit_hole_score/submit_hole_result/concede_match
Widen matches.py: list_matches/add_participant/remove_participant/lock_lineup
Widen tournaments.py: list_sessions/list_teams/concede_tournament
Widen courses.py: list_holes
Widen messaging.py: team chat REST-endepunkter (list/send/delete)
Legge til my_session_id/my_match_id i /auth/me sin my_tournaments
Frontend: legge til lenker til lag-chat/scorekort fra «Mine runder»
Scratch-verifisere hele deltaker-tilgang-runden (positive + negative kontroller)
Ekte typesjekket frontend-produksjonsbuild + backend py_compile på nytt før deploy-forslag
Presentere plan og be om bekreftelse før utrulling mot ekte teecup_db/containere
Bygge sekundær e-postadresse (enkelt tilfelle, ADR-032-mønster)
Bygge HCP-historikk over tid
Deltaker-tilgang til lag-chat/scorekort er bygget og grundig scratch-verifisert (43/43 sjekker, test_isolation.sql fortsatt 12/12). Kort oppsummert hva som er gjort:
Backend — den blanke org-medlemskaps-sperren (get_authorized_org) er fjernet fra ni endepunkter (lag-chat lese/skrive/slette, scorekort-lesing, slag/hull-resultat-innsending, walkover på match- og turneringsnivå, match-/lag-/økt-listing, bane-hull) og erstattet med de faktiske, allerede eksisterende autorisasjonssjekkene (user_is_rostered_on_team, user_is_match_participant, user_is_team_captain — alle uendret) — pluss et nytt eksplisitt org-medlem ELLER turnering-deltaker-OR på de endepunktene som tidligere ikke hadde noen finkornet sjekk i det hele tatt (der en ren fjerning ville åpnet dem for enhver innlogget bruker). /auth/me viser nå også my_session_id/my_match_id per rad i "Mine runder".
Frontend — "Mine runder"-kortet har fått to nye handlingslenker: "Lag-chat" (alltid) og "Scorekort" (når spilleren har en aktiv match).
Verifisert grundig: en rostret spiller UTEN organisasjonsmedlemskap får nå riktig tilgang til alt dette; en helt fremmed bruker avvises fortsatt overalt; org-eier beholder full tilgang som før; lag-chatten er fortsatt ekte privat (org-eier fortsatt avvist der, med vilje); kryss-org-isolasjon holder; og en rostret-men-ikke-kaptein spiller ble korrekt avvist fra walkover (fant samtidig en eksisterende, dokumentert fallback-oppførsel — et lag uten utpekt kaptein godtar enhver rostret spiller — som viste seg å fungere akkurat som tiltenkt).
2026-07-22 05:46:47 +02:00
|
|
|
|
my_session_id: string | null
|
|
|
|
|
|
my_match_id: string | null
|
Update Todos
Migrasjon 015: app_user-profilfelt + player_organizations_for_user()-bro
Backend: utvid Me + PATCH /auth/profile + avatar-opplasting/sletting
Backend: 'mine runder'-data + check_visibility-utvidelse for deltakere
Frontend: profil-seksjon i /account
Frontend: 'Mine runder'-seksjon + betinget tom-tilstand i dashboard.tsx
Scratch-verifisere alt (15 sjekker bestått)
Typesjekket frontend-build
ADR-031 + .md-oppdatering
Bygget og scratch-verifisert (15 automatiserte sjekker). Oppsummering:
Personlig profil — nye felt på selve kontoen (ikke på org-ens spillerdata, det er bevisst holdt atskilt siden en person kan ha ulike spiller-rader i ulike klubber): profilbilde, fornavn, etternavn, fødselsdato, kjønn, HCP, hjemmeklubb. Redigeres i en ny seksjon på /account. Sletting av enkeltfelt fungerer (send tomt/null), profilbilde kan lastes opp og fjernes.
"Mine runder" — ny seksjon øverst på dashbordet, viser turneringer du er rostret i på tvers av alle organisasjoner, uavhengig av om du er medlem noe sted.
Et reelt sikkerhetshull jeg fant underveis, ikke antatt på forhånd: da jeg testet "Mine runder" mot en faktisk ren spiller (ingen organisasjonsmedlemskap), oppdaget jeg at synlighetsregelen kun ga deltakere tilgang for det strengeste synlighetsnivået — ikke for standard-nivået («org»), som er det ALLE nye turneringer får automatisk. En ren spiller ville altså vært låst ute av sin egen, helt normale turnering. Fikset og verifisert grundig at det er en ren utvidelse: en fremmed innlogget bruker og en anonym leser blir fortsatt korrekt avvist som før.
Bevisst utenfor omfang, tydelig flagget: "Mine runder" lenker til den offentlige turnering-siden, ikke til lagets private chat eller scorekortet ennå — de krever fortsatt ekte organisasjonsmedlemskap, en strengere sperre brukt bredt i hele appen som jeg ikke ville endre uten en egen, forsiktig runde. Notert som naturlig neste steg.
Ingen kode for punkt 2 (midlertidige spillere) i denne runden, som avtalt.
2026-07-20 10:41:30 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-18 06:46:09 +02:00
|
|
|
|
type Me = {
|
|
|
|
|
|
id: string
|
|
|
|
|
|
email: string
|
|
|
|
|
|
display_name: string
|
2026-07-26 07:54:00 +02:00
|
|
|
|
first_name: string | null
|
2026-07-18 06:46:09 +02:00
|
|
|
|
preferred_locale: string
|
Update Todos
Skriv migrasjon 019 (app_user.country + bio)
Backend: profile_complete-felt, ProfileUpdate country/bio/HCP-cap
Frontend: onboarding-visning i /account, gate i page.tsx + dashboard.tsx
Scratch-verifiser hele flyten
Be om bekreftelse og rull ut live
Scratch-verifisert, alt grønt:
Migrasjon 019 kjørte rent (001→019), test_isolation.sql 12/12 uendret.
16/16 backend-sjekker: fersk konto starter profile_complete: false, delvis utfylling forblir ufullstendig, HCP >54 avvist (422), full utfylling (inkl. HCP=54) gir profile_complete: true, beskrivelse er reelt valgfri, å nullstille et obligatorisk felt i etterkant slår profile_complete tilbake til false, full isolasjon mellom to kontoer.
Ekte produksjonsbuild av frontend kompilerte rent, alle 16 ruter listet.
Ekte HTTP-nivå-bevis mot en kjørende produksjonscontainer: anonym mot / → 200 (innloggingsskjema), en ekte innlogget-men-ufullstendig sesjonscookie mot / → 307 → /account.
Klar for ekte teecup_db. Dette krever:
Kjøre 019_profile_country_bio.sql mot ekte teecup_db (kun to nye nullable kolonner på app_user — additivt, ingen eksisterende rader berørt).
docker compose up -d --build teecup_api teecup_frontend.
Verdt å merke: med dette rullet ut vil både hei@erol.no og erol.haagenrud@envide.no bli sendt til profil-fullføring ved neste innlogging — ingen av kontoene har disse feltene utfylt i dag (bekreftet med spørringen tidligere).
2026-07-22 07:53:29 +02:00
|
|
|
|
profile_complete: boolean
|
2026-07-25 07:51:32 +02:00
|
|
|
|
handicap_index: number | null
|
Statistikk: ekspandert trekkspill, spilt-til-HCP, omstrukturert /my-rounds/stats
Tre brukertilbakemeldinger etter gjennomgang av statistikksidene:
1. round-stats.tsx: trekkspillseksjonene starter nå ekspandert (reverserer
en tidligere beslutning fra 2026-07-25 om å vise dem sammenslått).
2. Dashbordets "HCP nå" viser nå computed_handicap_index ("spilt til"-HCP,
beregnet fra faktiske runder) i stedet for det manuelt satte tallet i
profilen -- historikk-sparklinen filtreres til samme kilde (source=
"computed"), så den ikke lenger blander manuelle og beregnede punkter.
3. /my-rounds/stats:
- Alle "vs. forrige periode"-deltaer (pil+tall+pp) er nå synlig
selvforklarende -- teksten fantes tidligere kun som skjermleser-tekst,
seende brukere så bare et ikon og et tall uten kontekst.
- Greentreff (GIR) og Innspill splittet i to kort (GIR er en beregnet
andel, Innspill er en retningsbeskrivelse -- samme prinsipp som
round-stats.tsx sin eksisterende splitt). Utslag flyttet først.
Redning og "Til par: med vs. uten" flyttet ned under Annet.
- Ny stolpegraf med glidende snitt-trendlinje (snitt til par per runde,
eldste til venstre) -- tidligere bevisst utsatt pga. for lite
rundehistorikk (se CHANGELOG 2026-07-28), bygget nå. Backend:
RoundStatsSummary fikk et nytt round_series-felt (kronologisk
snitt-til-par per runde i valgt vindu).
Verifisert: tsc --noEmit rent, 45/45 vitest, 31/31 backend-tester (inkl.
det nye round_series-feltet via en scratch-DB-kjøring). Full scratch-
stack med seks syntetiske runder (varierende dato/resultat) browser-
verifisert lys+mørk: HCP-bytte bekreftet (computed 5,3 vist i stedet for
manuelt satt 15,0), ny kortrekkefølge, splittet GIR/Innspill, synlige
delta-bildetekster, graf med trendlinje. Ingen konsollfeil. teecup_db
urørt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 09:12:49 +02:00
|
|
|
|
computed_handicap_index: number | null
|
2026-07-18 06:46:09 +02:00
|
|
|
|
organizations: MyOrg[]
|
Update Todos
Migrasjon 015: app_user-profilfelt + player_organizations_for_user()-bro
Backend: utvid Me + PATCH /auth/profile + avatar-opplasting/sletting
Backend: 'mine runder'-data + check_visibility-utvidelse for deltakere
Frontend: profil-seksjon i /account
Frontend: 'Mine runder'-seksjon + betinget tom-tilstand i dashboard.tsx
Scratch-verifisere alt (15 sjekker bestått)
Typesjekket frontend-build
ADR-031 + .md-oppdatering
Bygget og scratch-verifisert (15 automatiserte sjekker). Oppsummering:
Personlig profil — nye felt på selve kontoen (ikke på org-ens spillerdata, det er bevisst holdt atskilt siden en person kan ha ulike spiller-rader i ulike klubber): profilbilde, fornavn, etternavn, fødselsdato, kjønn, HCP, hjemmeklubb. Redigeres i en ny seksjon på /account. Sletting av enkeltfelt fungerer (send tomt/null), profilbilde kan lastes opp og fjernes.
"Mine runder" — ny seksjon øverst på dashbordet, viser turneringer du er rostret i på tvers av alle organisasjoner, uavhengig av om du er medlem noe sted.
Et reelt sikkerhetshull jeg fant underveis, ikke antatt på forhånd: da jeg testet "Mine runder" mot en faktisk ren spiller (ingen organisasjonsmedlemskap), oppdaget jeg at synlighetsregelen kun ga deltakere tilgang for det strengeste synlighetsnivået — ikke for standard-nivået («org»), som er det ALLE nye turneringer får automatisk. En ren spiller ville altså vært låst ute av sin egen, helt normale turnering. Fikset og verifisert grundig at det er en ren utvidelse: en fremmed innlogget bruker og en anonym leser blir fortsatt korrekt avvist som før.
Bevisst utenfor omfang, tydelig flagget: "Mine runder" lenker til den offentlige turnering-siden, ikke til lagets private chat eller scorekortet ennå — de krever fortsatt ekte organisasjonsmedlemskap, en strengere sperre brukt bredt i hele appen som jeg ikke ville endre uten en egen, forsiktig runde. Notert som naturlig neste steg.
Ingen kode for punkt 2 (midlertidige spillere) i denne runden, som avtalt.
2026-07-20 10:41:30 +02:00
|
|
|
|
my_tournaments: MyTournament[]
|
2026-07-18 06:46:09 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type ApiTournament = {
|
|
|
|
|
|
id: string
|
|
|
|
|
|
name: string
|
|
|
|
|
|
status: TournamentStatus
|
|
|
|
|
|
start_date: string | null
|
|
|
|
|
|
end_date: string | null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-26 08:22:41 +02:00
|
|
|
|
type ApiRoundParticipant = {
|
|
|
|
|
|
user_id: string | null
|
|
|
|
|
|
is_owner: boolean
|
|
|
|
|
|
counts_for_handicap: boolean
|
|
|
|
|
|
score_differential: number | null
|
|
|
|
|
|
}
|
2026-07-25 07:51:32 +02:00
|
|
|
|
type ApiRound = {
|
|
|
|
|
|
id: string
|
|
|
|
|
|
name: string | null
|
|
|
|
|
|
course_name_snapshot: string
|
|
|
|
|
|
tee_name_snapshot: string
|
|
|
|
|
|
played_at: string
|
|
|
|
|
|
holes_planned: number
|
|
|
|
|
|
completed_at: string | null
|
|
|
|
|
|
participants: ApiRoundParticipant[]
|
2026-07-26 08:22:41 +02:00
|
|
|
|
my_holes_played: number
|
|
|
|
|
|
my_total_score: number | null
|
|
|
|
|
|
my_score_to_par: number | null
|
2026-07-29 15:46:49 +02:00
|
|
|
|
my_net_score_to_par: number | null
|
|
|
|
|
|
play_format: string
|
|
|
|
|
|
my_match_status: string | null
|
|
|
|
|
|
my_match_lead: number | null
|
2026-07-25 07:51:32 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
Statistikk: ekspandert trekkspill, spilt-til-HCP, omstrukturert /my-rounds/stats
Tre brukertilbakemeldinger etter gjennomgang av statistikksidene:
1. round-stats.tsx: trekkspillseksjonene starter nå ekspandert (reverserer
en tidligere beslutning fra 2026-07-25 om å vise dem sammenslått).
2. Dashbordets "HCP nå" viser nå computed_handicap_index ("spilt til"-HCP,
beregnet fra faktiske runder) i stedet for det manuelt satte tallet i
profilen -- historikk-sparklinen filtreres til samme kilde (source=
"computed"), så den ikke lenger blander manuelle og beregnede punkter.
3. /my-rounds/stats:
- Alle "vs. forrige periode"-deltaer (pil+tall+pp) er nå synlig
selvforklarende -- teksten fantes tidligere kun som skjermleser-tekst,
seende brukere så bare et ikon og et tall uten kontekst.
- Greentreff (GIR) og Innspill splittet i to kort (GIR er en beregnet
andel, Innspill er en retningsbeskrivelse -- samme prinsipp som
round-stats.tsx sin eksisterende splitt). Utslag flyttet først.
Redning og "Til par: med vs. uten" flyttet ned under Annet.
- Ny stolpegraf med glidende snitt-trendlinje (snitt til par per runde,
eldste til venstre) -- tidligere bevisst utsatt pga. for lite
rundehistorikk (se CHANGELOG 2026-07-28), bygget nå. Backend:
RoundStatsSummary fikk et nytt round_series-felt (kronologisk
snitt-til-par per runde i valgt vindu).
Verifisert: tsc --noEmit rent, 45/45 vitest, 31/31 backend-tester (inkl.
det nye round_series-feltet via en scratch-DB-kjøring). Full scratch-
stack med seks syntetiske runder (varierende dato/resultat) browser-
verifisert lys+mørk: HCP-bytte bekreftet (computed 5,3 vist i stedet for
manuelt satt 15,0), ny kortrekkefølge, splittet GIR/Innspill, synlige
delta-bildetekster, graf med trendlinje. Ingen konsollfeil. teecup_db
urørt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 09:12:49 +02:00
|
|
|
|
type ApiHandicapPoint = { handicap_index: number; recorded_at: string; source: "manual" | "computed" }
|
2026-07-25 07:51:32 +02:00
|
|
|
|
|
2026-07-25 08:37:07 +02:00
|
|
|
|
// ADR-036 fase 1 -- kun det dashbordets kompakte kort trenger (ikke hele
|
|
|
|
|
|
// GET /friends-responsen, som også bærer utgående forespørsler/kategorier).
|
|
|
|
|
|
type ApiFriendsSummary = {
|
|
|
|
|
|
friends: { first_name: string | null; last_name: string | null }[]
|
|
|
|
|
|
incoming_requests: unknown[]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-01 21:15:07 +02:00
|
|
|
|
// "Venner på banen" -- speiler FriendOnCourseEntry i app/routers/rounds.py.
|
|
|
|
|
|
type ApiFriendOnCourse = {
|
|
|
|
|
|
round_id: string
|
|
|
|
|
|
friend_user_id: string
|
|
|
|
|
|
friend_name: string
|
|
|
|
|
|
course_name: string
|
|
|
|
|
|
play_format: string
|
|
|
|
|
|
status: "playing" | "recently_finished"
|
|
|
|
|
|
holes_played: number
|
|
|
|
|
|
holes_planned: number
|
|
|
|
|
|
score_to_par: number | null
|
|
|
|
|
|
started_at: string | null
|
|
|
|
|
|
completed_at: string | null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-18 06:46:09 +02:00
|
|
|
|
function toTournament(t: ApiTournament): Tournament {
|
2026-07-25 07:51:32 +02:00
|
|
|
|
return { id: t.id, name: t.name, status: t.status, startDate: t.start_date ?? undefined, endDate: t.end_date ?? undefined }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
All done and verified live in the browser. Summary of this round:
"Kommende runder" → "Runder" on the dashboard.
Tjøme course-name duplicate fixed in the real database (one round's stale course_name_snapshot corrected) — all three of your rounds now group under one "Tjøme Golfklubb" entry.
Two new colors, per your request for two: lifted the already-validated --chart-3 (blue → --info) and --chart-4 (gold → --gold) into core design tokens. First real uses: a blue "Hcp spilt til X" badge on round cards, and a gold "Personlig rekord" badge on your best completed round.
Clicking a course under "Spilte baner" now opens a new page listing every round played there (/my-rounds/course/[name]) — caught and fixed a real double-encoding bug here via an actual browser screenshot before shipping.
Aggregated statistics, clickable from the dashboard's "Statistikk" section (/my-rounds/stats): rounds completed, avg to-par, putts/18 holes (implementing your padding rule exactly — unplayed holes count as 2 putts, only for rounds where putt-tracking was on), fairway%, GIR%, one-putt%, scrambling%, sand save%, and chip/bunker/penalty/anywayslag averages per round.
Everything is typechecked, the putts-padding logic is verified against a hand-computed synthetic dataset, and all new screens were checked live in the browser with no console errors.
2026-07-28 07:04:56 +02:00
|
|
|
|
function toRound(r: ApiRound, viewerId: string | null, personalBestRoundId: string | null): Round {
|
2026-07-26 08:22:41 +02:00
|
|
|
|
const me = (viewerId && r.participants.find((p) => p.user_id === viewerId)) || r.participants.find((p) => p.is_owner)
|
2026-07-18 06:46:09 +02:00
|
|
|
|
return {
|
2026-07-25 07:51:32 +02:00
|
|
|
|
id: r.id,
|
|
|
|
|
|
name: r.name,
|
|
|
|
|
|
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,
|
2026-07-26 08:22:41 +02:00
|
|
|
|
holesPlayed: r.my_holes_played,
|
|
|
|
|
|
totalScore: r.my_total_score ?? undefined,
|
|
|
|
|
|
toPar: r.my_score_to_par ?? undefined,
|
2026-07-29 15:46:49 +02:00
|
|
|
|
netToPar: r.my_net_score_to_par ?? undefined,
|
|
|
|
|
|
playFormat: r.play_format,
|
|
|
|
|
|
matchStatus: r.my_match_status,
|
|
|
|
|
|
matchLead: r.my_match_lead,
|
2026-07-26 08:22:41 +02:00
|
|
|
|
differential: me?.counts_for_handicap ? me.score_differential : null,
|
All done and verified live in the browser. Summary of this round:
"Kommende runder" → "Runder" on the dashboard.
Tjøme course-name duplicate fixed in the real database (one round's stale course_name_snapshot corrected) — all three of your rounds now group under one "Tjøme Golfklubb" entry.
Two new colors, per your request for two: lifted the already-validated --chart-3 (blue → --info) and --chart-4 (gold → --gold) into core design tokens. First real uses: a blue "Hcp spilt til X" badge on round cards, and a gold "Personlig rekord" badge on your best completed round.
Clicking a course under "Spilte baner" now opens a new page listing every round played there (/my-rounds/course/[name]) — caught and fixed a real double-encoding bug here via an actual browser screenshot before shipping.
Aggregated statistics, clickable from the dashboard's "Statistikk" section (/my-rounds/stats): rounds completed, avg to-par, putts/18 holes (implementing your padding rule exactly — unplayed holes count as 2 putts, only for rounds where putt-tracking was on), fairway%, GIR%, one-putt%, scrambling%, sand save%, and chip/bunker/penalty/anywayslag averages per round.
Everything is typechecked, the putts-padding logic is verified against a hand-computed synthetic dataset, and all new screens were checked live in the browser with no console errors.
2026-07-28 07:04:56 +02:00
|
|
|
|
isPersonalBest: personalBestRoundId !== null && r.id === personalBestRoundId,
|
2026-07-18 06:46:09 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
All done and verified live in the browser. Summary of this round:
"Kommende runder" → "Runder" on the dashboard.
Tjøme course-name duplicate fixed in the real database (one round's stale course_name_snapshot corrected) — all three of your rounds now group under one "Tjøme Golfklubb" entry.
Two new colors, per your request for two: lifted the already-validated --chart-3 (blue → --info) and --chart-4 (gold → --gold) into core design tokens. First real uses: a blue "Hcp spilt til X" badge on round cards, and a gold "Personlig rekord" badge on your best completed round.
Clicking a course under "Spilte baner" now opens a new page listing every round played there (/my-rounds/course/[name]) — caught and fixed a real double-encoding bug here via an actual browser screenshot before shipping.
Aggregated statistics, clickable from the dashboard's "Statistikk" section (/my-rounds/stats): rounds completed, avg to-par, putts/18 holes (implementing your padding rule exactly — unplayed holes count as 2 putts, only for rounds where putt-tracking was on), fairway%, GIR%, one-putt%, scrambling%, sand save%, and chip/bunker/penalty/anywayslag averages per round.
Everything is typechecked, the putts-padding logic is verified against a hand-computed synthetic dataset, and all new screens were checked live in the browser with no console errors.
2026-07-28 07:04:56 +02:00
|
|
|
|
// Laveste egne til-par blant minst to fullførte runder -- én eneste
|
|
|
|
|
|
// fullført runde er ikke ennå meningsfullt en "rekord".
|
|
|
|
|
|
function findPersonalBestRoundId(completedRounds: ApiRound[]): string | null {
|
|
|
|
|
|
if (completedRounds.length < 2) return null
|
|
|
|
|
|
let best: { id: string; toPar: number } | null = null
|
|
|
|
|
|
for (const r of completedRounds) {
|
|
|
|
|
|
if (r.my_score_to_par === null) continue
|
|
|
|
|
|
if (!best || r.my_score_to_par < best.toPar) best = { id: r.id, toPar: r.my_score_to_par }
|
|
|
|
|
|
}
|
|
|
|
|
|
return best?.id ?? null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-25 07:51:32 +02:00
|
|
|
|
// --- Kombinert turnering-oppføring (deltaker OG/ELLER arrangør) ------------
|
|
|
|
|
|
|
|
|
|
|
|
type CombinedTournament = { tournament: Tournament; organizer: boolean; orgId: string; sortKey: string | null }
|
|
|
|
|
|
|
|
|
|
|
|
function combineTournaments(myTournaments: MyTournament[], organizerLists: { orgId: string; tournaments: ApiTournament[] }[]): CombinedTournament[] {
|
|
|
|
|
|
const byId = new Map<string, CombinedTournament>()
|
|
|
|
|
|
|
|
|
|
|
|
for (const t of myTournaments) {
|
|
|
|
|
|
byId.set(t.tournament_id, {
|
|
|
|
|
|
tournament: { id: t.tournament_id, name: t.tournament_name, status: t.status, startDate: t.next_session_at ?? undefined },
|
|
|
|
|
|
organizer: false,
|
|
|
|
|
|
orgId: t.organization_id,
|
|
|
|
|
|
sortKey: t.next_session_at,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for (const { orgId, tournaments } of organizerLists) {
|
|
|
|
|
|
for (const t of tournaments) {
|
|
|
|
|
|
const existing = byId.get(t.id)
|
|
|
|
|
|
byId.set(t.id, {
|
|
|
|
|
|
tournament: existing?.tournament ?? toTournament(t),
|
|
|
|
|
|
organizer: true,
|
|
|
|
|
|
orgId: existing?.orgId ?? orgId,
|
|
|
|
|
|
sortKey: existing?.sortKey ?? t.start_date,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Kun "kommende" -- fullførte/arkiverte turneringer skal ikke fylle opp
|
|
|
|
|
|
// en "kommende"-liste, uansett dato.
|
|
|
|
|
|
const upcoming = [...byId.values()].filter((c) => c.tournament.status === "draft" || c.tournament.status === "active")
|
|
|
|
|
|
upcoming.sort((a, b) => {
|
|
|
|
|
|
if (a.sortKey && b.sortKey) return a.sortKey < b.sortKey ? -1 : 1
|
|
|
|
|
|
if (a.sortKey) return -1
|
|
|
|
|
|
if (b.sortKey) return 1
|
|
|
|
|
|
return a.tournament.name.localeCompare(b.tournament.name)
|
|
|
|
|
|
})
|
|
|
|
|
|
return upcoming
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Formattering ------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
function formatSigned(value: number, decimals = 1): string {
|
|
|
|
|
|
if (value === 0) return decimals === 0 ? "0" : `0,${"0".repeat(decimals)}`
|
|
|
|
|
|
const abs = Math.abs(value).toFixed(decimals).replace(".", ",")
|
|
|
|
|
|
return value > 0 ? `+${abs}` : `−${abs}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const dateFormatter = new Intl.DateTimeFormat("no-NO", { day: "numeric", month: "short", year: "numeric" })
|
|
|
|
|
|
function formatDate(value: string) {
|
|
|
|
|
|
const parsed = new Date(value)
|
|
|
|
|
|
if (Number.isNaN(parsed.getTime())) return value
|
|
|
|
|
|
return dateFormatter.format(parsed)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-01 21:15:07 +02:00
|
|
|
|
// "for N timer/minutter siden" -- kun brukt av "Venner på banen" sin
|
|
|
|
|
|
// "nylig fullført"-status.
|
|
|
|
|
|
function timeAgoLabel(iso: string): string {
|
|
|
|
|
|
const then = new Date(iso).getTime()
|
|
|
|
|
|
if (Number.isNaN(then)) return ""
|
|
|
|
|
|
const minutes = Math.max(1, Math.round((Date.now() - then) / 60000))
|
|
|
|
|
|
if (minutes < 60) return `${minutes} min siden`
|
|
|
|
|
|
const hours = Math.round(minutes / 60)
|
|
|
|
|
|
return `${hours} ${hours === 1 ? "time" : "timer"} siden`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-25 07:51:32 +02:00
|
|
|
|
// --- Root component ----------------------------------------------------------
|
2026-07-18 06:46:09 +02:00
|
|
|
|
|
|
|
|
|
|
export function Dashboard() {
|
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
|
const [me, setMe] = useState<Me | null>(null)
|
|
|
|
|
|
const [loadingMe, setLoadingMe] = useState(true)
|
2026-07-25 07:51:32 +02:00
|
|
|
|
const [rounds, setRounds] = useState<ApiRound[] | null>(null)
|
|
|
|
|
|
const [combinedTournaments, setCombinedTournaments] = useState<CombinedTournament[]>([])
|
|
|
|
|
|
const [hcpHistory, setHcpHistory] = useState<ApiHandicapPoint[]>([])
|
2026-07-25 08:37:07 +02:00
|
|
|
|
const [friendsSummary, setFriendsSummary] = useState<ApiFriendsSummary | null>(null)
|
2026-08-01 21:15:07 +02:00
|
|
|
|
const [friendsOnCourse, setFriendsOnCourse] = useState<ApiFriendOnCourse[]>([])
|
Varsler (fra zip 20): in-app varslingssenter er live — bjelle med uleste-tall i dashbord-headeren, /my-notifications-side. Trigges i dag ved venneforespørsel sendt/akseptert; flere hendelser kan kobles på senere.
Rundeleaderboard: ny GET /rounds/{id}/leaderboard-backend er live (rangering, thru-tall, brutto+netto til par, håndterer 1 til 15+ deltakere). V0-prompten for selve visningen ligger i FEATURE_BACKLOG.md, klar til å limes inn i v0.app — send meg zip-en når du har den, så kobler jeg den på (foreslått rute /my-rounds/[id]/leaderboard, lenket fra rundesiden).
Begge deler scratch-verifisert (39/39 sjekker, inkl. en uavhengig kryssjekk av netto-beregningen mot handicap_engine direkte), rullet ut mot ekte teecup_db/containere, teeoff.no upåvirket.
2026-07-26 06:53:58 +02:00
|
|
|
|
const [unreadNotifications, setUnreadNotifications] = useState(0)
|
2026-07-18 06:46:09 +02:00
|
|
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
|
|
|
2026-07-25 07:51:32 +02:00
|
|
|
|
const loadMe = useCallback(async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await fetch("/auth/me", { credentials: "include" })
|
|
|
|
|
|
if (!res.ok) {
|
2026-08-08 12:34:03 +02:00
|
|
|
|
router.replace("/logg-inn")
|
2026-07-25 07:51:32 +02:00
|
|
|
|
return
|
2026-07-18 06:46:09 +02:00
|
|
|
|
}
|
2026-07-25 07:51:32 +02:00
|
|
|
|
const data: Me = await res.json()
|
2026-08-10 07:00:11 +02:00
|
|
|
|
// Ikke rett til /account -- se ARCHITECTURE_DECISIONS.md-tillegg
|
|
|
|
|
|
// 2026-08-09: /velkommen viser kontekst før skjemaet.
|
2026-07-25 07:51:32 +02:00
|
|
|
|
if (!data.profile_complete) {
|
2026-08-10 07:00:11 +02:00
|
|
|
|
router.replace("/velkommen")
|
2026-07-25 07:51:32 +02:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
setMe(data)
|
|
|
|
|
|
} catch {
|
2026-08-08 12:34:03 +02:00
|
|
|
|
router.replace("/logg-inn")
|
2026-07-25 07:51:32 +02:00
|
|
|
|
} finally {
|
|
|
|
|
|
setLoadingMe(false)
|
2026-07-18 06:46:09 +02:00
|
|
|
|
}
|
|
|
|
|
|
}, [router])
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-07-25 07:51:32 +02:00
|
|
|
|
void loadMe()
|
|
|
|
|
|
}, [loadMe])
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
fetch("/rounds", { credentials: "include" })
|
|
|
|
|
|
.then((res) => (res.ok ? res.json() : []))
|
|
|
|
|
|
.then((data: ApiRound[]) => setRounds(data))
|
|
|
|
|
|
.catch(() => setRounds([]))
|
|
|
|
|
|
fetch("/auth/profile/handicap-history", { credentials: "include" })
|
|
|
|
|
|
.then((res) => (res.ok ? res.json() : []))
|
|
|
|
|
|
.then((data: ApiHandicapPoint[]) => setHcpHistory(data))
|
|
|
|
|
|
.catch(() => {})
|
2026-07-25 08:37:07 +02:00
|
|
|
|
fetch("/friends", { credentials: "include" })
|
|
|
|
|
|
.then((res) => (res.ok ? res.json() : null))
|
|
|
|
|
|
.then((data: ApiFriendsSummary | null) => setFriendsSummary(data))
|
|
|
|
|
|
.catch(() => {})
|
2026-08-01 21:15:07 +02:00
|
|
|
|
fetch("/friends/on-course", { credentials: "include" })
|
|
|
|
|
|
.then((res) => (res.ok ? res.json() : []))
|
|
|
|
|
|
.then((data: ApiFriendOnCourse[]) => setFriendsOnCourse(data))
|
|
|
|
|
|
.catch(() => {})
|
Varsler (fra zip 20): in-app varslingssenter er live — bjelle med uleste-tall i dashbord-headeren, /my-notifications-side. Trigges i dag ved venneforespørsel sendt/akseptert; flere hendelser kan kobles på senere.
Rundeleaderboard: ny GET /rounds/{id}/leaderboard-backend er live (rangering, thru-tall, brutto+netto til par, håndterer 1 til 15+ deltakere). V0-prompten for selve visningen ligger i FEATURE_BACKLOG.md, klar til å limes inn i v0.app — send meg zip-en når du har den, så kobler jeg den på (foreslått rute /my-rounds/[id]/leaderboard, lenket fra rundesiden).
Begge deler scratch-verifisert (39/39 sjekker, inkl. en uavhengig kryssjekk av netto-beregningen mot handicap_engine direkte), rullet ut mot ekte teecup_db/containere, teeoff.no upåvirket.
2026-07-26 06:53:58 +02:00
|
|
|
|
fetch("/notifications/unread-count", { credentials: "include" })
|
|
|
|
|
|
.then((res) => (res.ok ? res.json() : { count: 0 }))
|
|
|
|
|
|
.then((data: { count: number }) => setUnreadNotifications(data.count))
|
|
|
|
|
|
.catch(() => {})
|
2026-07-25 07:51:32 +02:00
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!me) return
|
2026-07-18 06:46:09 +02:00
|
|
|
|
let cancelled = false
|
2026-07-25 07:51:32 +02:00
|
|
|
|
async function loadOrganizerTournaments() {
|
2026-07-18 06:46:09 +02:00
|
|
|
|
try {
|
2026-07-25 07:51:32 +02:00
|
|
|
|
const lists = await Promise.all(
|
|
|
|
|
|
me!.organizations.map(async (o) => {
|
|
|
|
|
|
const res = await fetch(`/orgs/${o.organization_id}/tournaments`, { credentials: "include" })
|
|
|
|
|
|
const tournaments: ApiTournament[] = res.ok ? await res.json() : []
|
|
|
|
|
|
return { orgId: o.organization_id, tournaments }
|
|
|
|
|
|
}),
|
|
|
|
|
|
)
|
|
|
|
|
|
if (!cancelled) setCombinedTournaments(combineTournaments(me!.my_tournaments, lists))
|
2026-07-18 06:46:09 +02:00
|
|
|
|
} catch {
|
2026-07-25 07:51:32 +02:00
|
|
|
|
if (!cancelled) setCombinedTournaments(combineTournaments(me!.my_tournaments, []))
|
2026-07-18 06:46:09 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-25 07:51:32 +02:00
|
|
|
|
void loadOrganizerTournaments()
|
2026-07-18 06:46:09 +02:00
|
|
|
|
return () => {
|
|
|
|
|
|
cancelled = true
|
|
|
|
|
|
}
|
2026-07-25 07:51:32 +02:00
|
|
|
|
}, [me])
|
|
|
|
|
|
|
2026-07-29 12:44:51 +02:00
|
|
|
|
// Delt åpen/lukk-tilstand for hurtighandling-skjemaene (turnering/kode) --
|
|
|
|
|
|
// løftet til roten (2026-07-29, "alternativ designinstruks") slik at BÅDE
|
|
|
|
|
|
// Hurtighandlinger-kortene OG tomtilstands-knappene i "Kommende
|
|
|
|
|
|
// turneringer" kan åpne samme inline-skjema, i stedet for at sistnevnte
|
|
|
|
|
|
// var uten href/onClick og ikke gjorde noe ved trykk.
|
|
|
|
|
|
const [quickActionOpen, setQuickActionOpen] = useState<"tournament" | "code" | null>(null)
|
|
|
|
|
|
const openQuickAction = useCallback((which: "tournament" | "code") => {
|
|
|
|
|
|
setQuickActionOpen(which)
|
|
|
|
|
|
document.getElementById("hurtighandlinger")?.scrollIntoView({ behavior: "smooth", block: "start" })
|
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
2026-07-25 07:51:32 +02:00
|
|
|
|
// "Ny turnering": organisasjon opprettes/gjenbrukes USYNLIG (ADR-035) --
|
|
|
|
|
|
// ingen egen "opprett organisasjon"-skjerm for det vanlige tilfellet.
|
|
|
|
|
|
// Har brukeren INGEN organisasjon, genereres et navn og POST /orgs kalles
|
|
|
|
|
|
// FØRST ved selve innsendingen (ikke når skjemaet bare åpnes -- unngår en
|
|
|
|
|
|
// foreldreløs tom org hvis brukeren avbryter).
|
2026-07-30 07:55:35 +02:00
|
|
|
|
async function handleCreateTournament(name: string, chosenOrgId: string | null, format: "team" | "individual") {
|
2026-07-25 07:51:32 +02:00
|
|
|
|
if (!me) return
|
2026-07-18 06:46:09 +02:00
|
|
|
|
setError(null)
|
|
|
|
|
|
try {
|
2026-07-25 07:51:32 +02:00
|
|
|
|
let orgId = chosenOrgId ?? (me.organizations.length === 1 ? me.organizations[0].organization_id : null)
|
|
|
|
|
|
if (!orgId) {
|
|
|
|
|
|
const orgName = me.display_name ? `${me.display_name}s turneringer` : "Mine turneringer"
|
|
|
|
|
|
const orgRes = await fetch("/orgs", {
|
|
|
|
|
|
method: "POST",
|
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
|
credentials: "include",
|
|
|
|
|
|
body: JSON.stringify({ name: orgName }),
|
|
|
|
|
|
})
|
|
|
|
|
|
if (!orgRes.ok) throw new Error("orgs")
|
|
|
|
|
|
const org: { id: string; name: string; role: string } = await orgRes.json()
|
|
|
|
|
|
orgId = org.id
|
|
|
|
|
|
setMe((prev) => (prev ? { ...prev, organizations: [...prev.organizations, { organization_id: org.id, name: org.name, role: org.role }] } : prev))
|
|
|
|
|
|
}
|
|
|
|
|
|
const res = await fetch(`/orgs/${orgId}/tournaments`, {
|
2026-07-18 06:46:09 +02:00
|
|
|
|
method: "POST",
|
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
|
credentials: "include",
|
2026-07-30 07:55:35 +02:00
|
|
|
|
body: JSON.stringify({ name, format_type: format }),
|
2026-07-18 06:46:09 +02:00
|
|
|
|
})
|
2026-07-25 07:51:32 +02:00
|
|
|
|
if (!res.ok) throw new Error("create tournament")
|
2026-07-18 06:46:09 +02:00
|
|
|
|
const created: ApiTournament = await res.json()
|
2026-07-25 07:51:32 +02:00
|
|
|
|
router.push(`/tournaments/${created.id}?org=${orgId}&name=${encodeURIComponent(created.name)}`)
|
2026-07-18 06:46:09 +02:00
|
|
|
|
} catch {
|
|
|
|
|
|
setError("Klarte ikke å opprette turneringen. Prøv igjen.")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function handleLogout() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await fetch("/auth/logout", { method: "POST", credentials: "include" })
|
|
|
|
|
|
} finally {
|
2026-08-08 12:34:03 +02:00
|
|
|
|
router.replace("/logg-inn")
|
2026-07-18 06:46:09 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (loadingMe) {
|
|
|
|
|
|
return (
|
2026-08-07 22:02:56 +02:00
|
|
|
|
<div className="flex min-h-[100dvh] flex-col items-center justify-center gap-4 bg-clubhouse-bg">
|
|
|
|
|
|
<div aria-hidden="true" className="size-10 animate-spin rounded-full border-4 border-tee-strong/20 border-t-tee-strong" />
|
2026-07-18 06:46:09 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-08 12:34:03 +02:00
|
|
|
|
if (!me) return null // router.replace("/logg-inn") er allerede utløst
|
2026-07-18 06:46:09 +02:00
|
|
|
|
|
2026-07-25 07:51:32 +02:00
|
|
|
|
const rows = rounds ?? []
|
|
|
|
|
|
const activeRounds = rows.filter((r) => !r.completed_at)
|
|
|
|
|
|
const completedRounds = rows.filter((r) => r.completed_at)
|
|
|
|
|
|
const roundsForCards = activeRounds.length > 0 ? activeRounds : rows.slice(0, 3)
|
All done and verified live in the browser. Summary of this round:
"Kommende runder" → "Runder" on the dashboard.
Tjøme course-name duplicate fixed in the real database (one round's stale course_name_snapshot corrected) — all three of your rounds now group under one "Tjøme Golfklubb" entry.
Two new colors, per your request for two: lifted the already-validated --chart-3 (blue → --info) and --chart-4 (gold → --gold) into core design tokens. First real uses: a blue "Hcp spilt til X" badge on round cards, and a gold "Personlig rekord" badge on your best completed round.
Clicking a course under "Spilte baner" now opens a new page listing every round played there (/my-rounds/course/[name]) — caught and fixed a real double-encoding bug here via an actual browser screenshot before shipping.
Aggregated statistics, clickable from the dashboard's "Statistikk" section (/my-rounds/stats): rounds completed, avg to-par, putts/18 holes (implementing your padding rule exactly — unplayed holes count as 2 putts, only for rounds where putt-tracking was on), fairway%, GIR%, one-putt%, scrambling%, sand save%, and chip/bunker/penalty/anywayslag averages per round.
Everything is typechecked, the putts-padding logic is verified against a hand-computed synthetic dataset, and all new screens were checked live in the browser with no console errors.
2026-07-28 07:04:56 +02:00
|
|
|
|
const personalBestRoundId = findPersonalBestRoundId(completedRounds)
|
2026-07-25 07:51:32 +02:00
|
|
|
|
|
|
|
|
|
|
const avgToParSample = completedRounds
|
2026-07-26 08:22:41 +02:00
|
|
|
|
.filter((r) => r.my_score_to_par !== null)
|
2026-07-25 07:51:32 +02:00
|
|
|
|
.slice(0, 5)
|
2026-07-26 08:22:41 +02:00
|
|
|
|
.map((r) => r.my_score_to_par as number)
|
2026-07-25 07:51:32 +02:00
|
|
|
|
const avgToPar = avgToParSample.length > 0 ? formatSigned(avgToParSample.reduce((s, v) => s + v, 0) / avgToParSample.length, 1) : "—"
|
|
|
|
|
|
|
Statistikk: ekspandert trekkspill, spilt-til-HCP, omstrukturert /my-rounds/stats
Tre brukertilbakemeldinger etter gjennomgang av statistikksidene:
1. round-stats.tsx: trekkspillseksjonene starter nå ekspandert (reverserer
en tidligere beslutning fra 2026-07-25 om å vise dem sammenslått).
2. Dashbordets "HCP nå" viser nå computed_handicap_index ("spilt til"-HCP,
beregnet fra faktiske runder) i stedet for det manuelt satte tallet i
profilen -- historikk-sparklinen filtreres til samme kilde (source=
"computed"), så den ikke lenger blander manuelle og beregnede punkter.
3. /my-rounds/stats:
- Alle "vs. forrige periode"-deltaer (pil+tall+pp) er nå synlig
selvforklarende -- teksten fantes tidligere kun som skjermleser-tekst,
seende brukere så bare et ikon og et tall uten kontekst.
- Greentreff (GIR) og Innspill splittet i to kort (GIR er en beregnet
andel, Innspill er en retningsbeskrivelse -- samme prinsipp som
round-stats.tsx sin eksisterende splitt). Utslag flyttet først.
Redning og "Til par: med vs. uten" flyttet ned under Annet.
- Ny stolpegraf med glidende snitt-trendlinje (snitt til par per runde,
eldste til venstre) -- tidligere bevisst utsatt pga. for lite
rundehistorikk (se CHANGELOG 2026-07-28), bygget nå. Backend:
RoundStatsSummary fikk et nytt round_series-felt (kronologisk
snitt-til-par per runde i valgt vindu).
Verifisert: tsc --noEmit rent, 45/45 vitest, 31/31 backend-tester (inkl.
det nye round_series-feltet via en scratch-DB-kjøring). Full scratch-
stack med seks syntetiske runder (varierende dato/resultat) browser-
verifisert lys+mørk: HCP-bytte bekreftet (computed 5,3 vist i stedet for
manuelt satt 15,0), ny kortrekkefølge, splittet GIR/Innspill, synlige
delta-bildetekster, graf med trendlinje. Ingen konsollfeil. teecup_db
urørt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 09:12:49 +02:00
|
|
|
|
// Dashbordet skal vise "spilt til"-HCP (beregnet fra faktiske runder),
|
|
|
|
|
|
// ikke det manuelt satte tallet i profilen -- historikk-grafen filtreres
|
|
|
|
|
|
// til samme kilde, ellers ville sparklinen blande to ulike signaler.
|
|
|
|
|
|
const hcpValues = hcpHistory.filter((h) => h.source === "computed").map((h) => h.handicap_index)
|
2026-07-25 07:51:32 +02:00
|
|
|
|
const hcpTrend = hcpValues.length >= 2 ? hcpValues[hcpValues.length - 1] - hcpValues[hcpValues.length - 2] : null
|
Statistikk: ekspandert trekkspill, spilt-til-HCP, omstrukturert /my-rounds/stats
Tre brukertilbakemeldinger etter gjennomgang av statistikksidene:
1. round-stats.tsx: trekkspillseksjonene starter nå ekspandert (reverserer
en tidligere beslutning fra 2026-07-25 om å vise dem sammenslått).
2. Dashbordets "HCP nå" viser nå computed_handicap_index ("spilt til"-HCP,
beregnet fra faktiske runder) i stedet for det manuelt satte tallet i
profilen -- historikk-sparklinen filtreres til samme kilde (source=
"computed"), så den ikke lenger blander manuelle og beregnede punkter.
3. /my-rounds/stats:
- Alle "vs. forrige periode"-deltaer (pil+tall+pp) er nå synlig
selvforklarende -- teksten fantes tidligere kun som skjermleser-tekst,
seende brukere så bare et ikon og et tall uten kontekst.
- Greentreff (GIR) og Innspill splittet i to kort (GIR er en beregnet
andel, Innspill er en retningsbeskrivelse -- samme prinsipp som
round-stats.tsx sin eksisterende splitt). Utslag flyttet først.
Redning og "Til par: med vs. uten" flyttet ned under Annet.
- Ny stolpegraf med glidende snitt-trendlinje (snitt til par per runde,
eldste til venstre) -- tidligere bevisst utsatt pga. for lite
rundehistorikk (se CHANGELOG 2026-07-28), bygget nå. Backend:
RoundStatsSummary fikk et nytt round_series-felt (kronologisk
snitt-til-par per runde i valgt vindu).
Verifisert: tsc --noEmit rent, 45/45 vitest, 31/31 backend-tester (inkl.
det nye round_series-feltet via en scratch-DB-kjøring). Full scratch-
stack med seks syntetiske runder (varierende dato/resultat) browser-
verifisert lys+mørk: HCP-bytte bekreftet (computed 5,3 vist i stedet for
manuelt satt 15,0), ny kortrekkefølge, splittet GIR/Innspill, synlige
delta-bildetekster, graf med trendlinje. Ingen konsollfeil. teecup_db
urørt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 09:12:49 +02:00
|
|
|
|
const hcpNow = me.computed_handicap_index !== null ? me.computed_handicap_index.toFixed(1).replace(".", ",") : "—"
|
2026-07-25 07:51:32 +02:00
|
|
|
|
|
|
|
|
|
|
const coursesMap = new Map<string, { name: string; visits: number; lastPlayed: string }>()
|
|
|
|
|
|
for (const r of rows) {
|
|
|
|
|
|
const existing = coursesMap.get(r.course_name_snapshot)
|
|
|
|
|
|
if (existing) {
|
|
|
|
|
|
existing.visits += 1
|
|
|
|
|
|
if (r.played_at > existing.lastPlayed) existing.lastPlayed = r.played_at
|
|
|
|
|
|
} else {
|
|
|
|
|
|
coursesMap.set(r.course_name_snapshot, { name: r.course_name_snapshot, visits: 1, lastPlayed: r.played_at })
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
const playedCourses = [...coursesMap.values()].sort((a, b) => (a.lastPlayed < b.lastPlayed ? 1 : -1))
|
2026-07-18 06:46:09 +02:00
|
|
|
|
|
|
|
|
|
|
return (
|
2026-08-07 22:02:56 +02:00
|
|
|
|
<div className="flex min-h-[100dvh] flex-col bg-clubhouse-bg">
|
|
|
|
|
|
<header className="sticky top-0 z-10 border-b border-clubhouse-border bg-clubhouse-card/95 backdrop-blur pt-[env(safe-area-inset-top)] supports-[backdrop-filter]:bg-clubhouse-card/80">
|
2026-07-18 06:46:09 +02:00
|
|
|
|
<div className="mx-auto flex w-full max-w-3xl items-center justify-between gap-4 px-5 py-4">
|
2026-08-07 22:02:56 +02:00
|
|
|
|
<Link
|
|
|
|
|
|
href="/dashboard"
|
|
|
|
|
|
aria-label="TeeCup – til forsiden"
|
|
|
|
|
|
className="flex items-center rounded-xl py-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tee-strong"
|
|
|
|
|
|
>
|
|
|
|
|
|
<TeeCupWordmark className="h-7" />
|
2026-08-01 21:15:07 +02:00
|
|
|
|
</Link>
|
Update Todos
Legge til argon2-cffi, pyotp, qrcode i requirements.txt
Skrive migrasjon 012 (passord, 2FA, superadmin, org-invitasjoner)
app/auth.py: sesjonsstadier, passord-hashing, TOTP-hjelpere
app/routers/auth.py: passord-innlogging, 2FA-oppsett/verifisering
app/email.py: 2FA-kode og invitasjons-maler
app/routers/organizations.py: invitasjoner, medlemskapsstyring, superadmin-sti
Frontend: login-form passord-modus + 2FA-skjermer
Frontend: kontoinnstillinger + org-medlemsstyring-skjerm
Ekte typesjekket frontend-build
Scratch-verifisere hele auth-løpet grundig (backend)
Deploy mot ekte teecup_db/containere + oppdatere .md-filer
Backend og frontend er grundig scratch-verifisert — inkludert tre reelle bugs jeg fant og fikset underveis (en UUID-serialiseringsfeil i magic-link-innlogging, og to tilfeller av en uendelig 2FA-løkke der en nettopp bekreftet kode ble sjekket på nytt). Alle sikkerhetsvern testet eksplisitt: siste-eier-vern, blokkert selv-forfremmelse, admin kan ikke gi eierskap, superadmin fungerer/avvises riktig, tvungen 2FA for nye eiere, passord med spesialtegn/mellomrom, og bakoverkompatibilitet med eksisterende magic-link-flyt.
Klar til utrulling mot ekte systemer:
Migrasjon: 012_password_2fa_and_org_invitations.sql mot ekte teecup_db
Redeploy: både teecup_api og teecup_frontend
AskUserQuestion
2026-07-19 10:36:50 +02:00
|
|
|
|
<div className="flex items-center gap-1">
|
Varsler (fra zip 20): in-app varslingssenter er live — bjelle med uleste-tall i dashbord-headeren, /my-notifications-side. Trigges i dag ved venneforespørsel sendt/akseptert; flere hendelser kan kobles på senere.
Rundeleaderboard: ny GET /rounds/{id}/leaderboard-backend er live (rangering, thru-tall, brutto+netto til par, håndterer 1 til 15+ deltakere). V0-prompten for selve visningen ligger i FEATURE_BACKLOG.md, klar til å limes inn i v0.app — send meg zip-en når du har den, så kobler jeg den på (foreslått rute /my-rounds/[id]/leaderboard, lenket fra rundesiden).
Begge deler scratch-verifisert (39/39 sjekker, inkl. en uavhengig kryssjekk av netto-beregningen mot handicap_engine direkte), rullet ut mot ekte teecup_db/containere, teeoff.no upåvirket.
2026-07-26 06:53:58 +02:00
|
|
|
|
<NotificationBell unreadCount={unreadNotifications} />
|
Update Todos
Legge til argon2-cffi, pyotp, qrcode i requirements.txt
Skrive migrasjon 012 (passord, 2FA, superadmin, org-invitasjoner)
app/auth.py: sesjonsstadier, passord-hashing, TOTP-hjelpere
app/routers/auth.py: passord-innlogging, 2FA-oppsett/verifisering
app/email.py: 2FA-kode og invitasjons-maler
app/routers/organizations.py: invitasjoner, medlemskapsstyring, superadmin-sti
Frontend: login-form passord-modus + 2FA-skjermer
Frontend: kontoinnstillinger + org-medlemsstyring-skjerm
Ekte typesjekket frontend-build
Scratch-verifisere hele auth-løpet grundig (backend)
Deploy mot ekte teecup_db/containere + oppdatere .md-filer
Backend og frontend er grundig scratch-verifisert — inkludert tre reelle bugs jeg fant og fikset underveis (en UUID-serialiseringsfeil i magic-link-innlogging, og to tilfeller av en uendelig 2FA-løkke der en nettopp bekreftet kode ble sjekket på nytt). Alle sikkerhetsvern testet eksplisitt: siste-eier-vern, blokkert selv-forfremmelse, admin kan ikke gi eierskap, superadmin fungerer/avvises riktig, tvungen 2FA for nye eiere, passord med spesialtegn/mellomrom, og bakoverkompatibilitet med eksisterende magic-link-flyt.
Klar til utrulling mot ekte systemer:
Migrasjon: 012_password_2fa_and_org_invitations.sql mot ekte teecup_db
Redeploy: både teecup_api og teecup_frontend
AskUserQuestion
2026-07-19 10:36:50 +02:00
|
|
|
|
<Link
|
|
|
|
|
|
href="/account"
|
2026-08-07 22:02:56 +02:00
|
|
|
|
className="inline-flex min-h-[44px] items-center gap-2 rounded-lg px-2 py-2 text-sm font-semibold text-clubhouse-muted transition-all duration-200 ease-in-out hover:bg-clubhouse-field hover:text-clubhouse-ink active:opacity-70"
|
Update Todos
Legge til argon2-cffi, pyotp, qrcode i requirements.txt
Skrive migrasjon 012 (passord, 2FA, superadmin, org-invitasjoner)
app/auth.py: sesjonsstadier, passord-hashing, TOTP-hjelpere
app/routers/auth.py: passord-innlogging, 2FA-oppsett/verifisering
app/email.py: 2FA-kode og invitasjons-maler
app/routers/organizations.py: invitasjoner, medlemskapsstyring, superadmin-sti
Frontend: login-form passord-modus + 2FA-skjermer
Frontend: kontoinnstillinger + org-medlemsstyring-skjerm
Ekte typesjekket frontend-build
Scratch-verifisere hele auth-løpet grundig (backend)
Deploy mot ekte teecup_db/containere + oppdatere .md-filer
Backend og frontend er grundig scratch-verifisert — inkludert tre reelle bugs jeg fant og fikset underveis (en UUID-serialiseringsfeil i magic-link-innlogging, og to tilfeller av en uendelig 2FA-løkke der en nettopp bekreftet kode ble sjekket på nytt). Alle sikkerhetsvern testet eksplisitt: siste-eier-vern, blokkert selv-forfremmelse, admin kan ikke gi eierskap, superadmin fungerer/avvises riktig, tvungen 2FA for nye eiere, passord med spesialtegn/mellomrom, og bakoverkompatibilitet med eksisterende magic-link-flyt.
Klar til utrulling mot ekte systemer:
Migrasjon: 012_password_2fa_and_org_invitations.sql mot ekte teecup_db
Redeploy: både teecup_api og teecup_frontend
AskUserQuestion
2026-07-19 10:36:50 +02:00
|
|
|
|
>
|
|
|
|
|
|
<UserCircle aria-hidden="true" className="size-4" />
|
|
|
|
|
|
Konto
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={handleLogout}
|
2026-08-07 22:02:56 +02:00
|
|
|
|
className="inline-flex min-h-[44px] items-center gap-2 rounded-lg px-2 py-2 text-sm font-semibold text-clubhouse-muted transition-all duration-200 ease-in-out hover:bg-clubhouse-field hover:text-clubhouse-ink active:opacity-70"
|
Update Todos
Legge til argon2-cffi, pyotp, qrcode i requirements.txt
Skrive migrasjon 012 (passord, 2FA, superadmin, org-invitasjoner)
app/auth.py: sesjonsstadier, passord-hashing, TOTP-hjelpere
app/routers/auth.py: passord-innlogging, 2FA-oppsett/verifisering
app/email.py: 2FA-kode og invitasjons-maler
app/routers/organizations.py: invitasjoner, medlemskapsstyring, superadmin-sti
Frontend: login-form passord-modus + 2FA-skjermer
Frontend: kontoinnstillinger + org-medlemsstyring-skjerm
Ekte typesjekket frontend-build
Scratch-verifisere hele auth-løpet grundig (backend)
Deploy mot ekte teecup_db/containere + oppdatere .md-filer
Backend og frontend er grundig scratch-verifisert — inkludert tre reelle bugs jeg fant og fikset underveis (en UUID-serialiseringsfeil i magic-link-innlogging, og to tilfeller av en uendelig 2FA-løkke der en nettopp bekreftet kode ble sjekket på nytt). Alle sikkerhetsvern testet eksplisitt: siste-eier-vern, blokkert selv-forfremmelse, admin kan ikke gi eierskap, superadmin fungerer/avvises riktig, tvungen 2FA for nye eiere, passord med spesialtegn/mellomrom, og bakoverkompatibilitet med eksisterende magic-link-flyt.
Klar til utrulling mot ekte systemer:
Migrasjon: 012_password_2fa_and_org_invitations.sql mot ekte teecup_db
Redeploy: både teecup_api og teecup_frontend
AskUserQuestion
2026-07-19 10:36:50 +02:00
|
|
|
|
>
|
|
|
|
|
|
<LogOut aria-hidden="true" className="size-4" />
|
|
|
|
|
|
Logg ut
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2026-07-18 06:46:09 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
Ja, jeg forsto rekkefølgen — steg 1 og 2 er nå ferdig og scratch-verifisert:
Steg 1 (backend, HCP-prosent + Match-HCP): ny migrasjon 051_round_allowance_override.sql (round.allowance_override jsonb), koblet inn i _recompute_side_handicaps/_relative_strokes_for_round + POST/PATCH /rounds. Verifisert presist i scratch: 100%→50%-prosent ga playing_handicap 24/10 → 12/5 nøyaktig som beregnet for hånd, og use_matchplay_handicap slått av ga rå 12/5 i stedet for differensial 7/0. Slagspill upåvirket (regresjon bekreftet).
Steg 2 (kode-fiks, lag-gruppering): ScorecardGrid (live-scoring) og MatchScorecardGrid (post-runde/fremtidig sammenslått side) sorterer nå stabilt på round_side_id før rendering. Bygget en fersk scratch-fourball-runde med akkurat ditt bug-scenario (A→Rødt, B→Blått, C→Rødt, D→Blått, lagt til i den rekkefølgen) og bekreftet visuelt i nettleseren i begge visningene: Rødt lag vises samlet, deretter Blått — ikke lenger interleaved.
Typesjekk + full produksjonsbuild kompilerte rent gjennom hele runden.
Gjenstår før jeg går videre til V0-promptene: migrasjon 051 er kun kjørt i scratch, ikke mot ekte teecup_db. Planen er:
docker exec -i teeoff_db psql -U teeoff_admin -d teecup_db -v ON_ERROR_STOP=1 < 051_round_allowance_override.sql
— ett rent additivt ALTER TABLE round ADD COLUMN allowance_override jsonb, ingen eksisterende rader berøres. Deretter docker compose up -d --build teecup_api teecup_frontend (backend for allowance-koblingen, frontend for gruppering-fiksen).
Bekreft så kjører jeg dette, og går videre til V0-prompt 1 (rundeoppsett-veiviseren).
2026-08-02 08:24:01 +02:00
|
|
|
|
{/* Bunn-fanerad (2026-08-01) er fixed/56px + safe-area -- pb-24 gir
|
|
|
|
|
|
<main> nok bunnplass til at "Statistikk" ikke skjules bak den
|
|
|
|
|
|
(reelt overlapp funnet under browserverifisering av zip 24, samme
|
|
|
|
|
|
klasse feil som å glemme at en fixed bar tar plass fra layouten). */}
|
|
|
|
|
|
<main className="mx-auto w-full max-w-3xl flex-1 px-5 pb-24 pt-6 sm:pt-8">
|
2026-07-25 07:51:32 +02:00
|
|
|
|
<div className="flex flex-col gap-2">
|
2026-08-07 22:02:56 +02:00
|
|
|
|
<h1 className="text-2xl font-extrabold tracking-tight text-balance text-clubhouse-ink">
|
2026-07-26 07:54:00 +02:00
|
|
|
|
God dag, {me.first_name ?? me.display_name}
|
2026-07-25 07:51:32 +02:00
|
|
|
|
</h1>
|
2026-08-07 22:02:56 +02:00
|
|
|
|
<p className="text-base leading-relaxed text-pretty text-clubhouse-muted">
|
2026-07-25 07:51:32 +02:00
|
|
|
|
Klar for en ny runde? Her er golfen din på ett sted.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-07-18 06:46:09 +02:00
|
|
|
|
{error && (
|
2026-08-07 22:02:56 +02:00
|
|
|
|
<p role="alert" className="mt-4 text-sm font-medium text-cup-strong">
|
2026-07-18 06:46:09 +02:00
|
|
|
|
{error}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
Update Todos
Bygg PWA-installasjonsoppfordring (Android beforeinstallprompt + iOS instruksjonsbanner)
Design og bygg flere flighter i én frittstående runde (løs gruppering, retning 1)
Design og bygg scramble/greensome-statistikk (valgt utslag per spiller)
Bygg push-varsler til telefonens OS (VAPID, push_subscription, service worker)
Scratch-verifiser backend for alle fire (27/27 sjekker gjort)
Browserverifiser alle fire i nettleser mot scratch-backend
Rull ut mot ekte systemer (migrasjoner + containere), med bekreftelse
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Alle fire er ferdigstilt, verifisert og live. Kort oppsummert:
PWA-installasjonsoppfordring — ekte beforeinstallprompt-fanging, Android-knapp + iOS-instruksjonsbanner, vist på dashbordet. Bekreftet at Chrome faktisk fyrte eventet i testøkten.
Flere flighter i én frittstående runde — løs gruppering av separate runder (delt UUID), "+ Legg til en flight til" gjenbruker hele opprett-flyten forhåndsutfylt, nytt kombinert leaderboard på tvers av flighter. Testet fra knappetrykk til ferdig runde, bekreftet i database.
Scramble/greensome: valgt utslag per spiller — nytt felt i hull-registreringen (helt valgfritt), "Utslag brukt"-oppsummering på statistikksiden. Bygget for frittstående runder; org-turneringer bevisst utenfor omfang denne runden (notert i backloggen).
Push-varsler til telefonens OS — Web Push/VAPID, ny av/på-bryter i kontoinnstillinger, dekker alle fire varseltyper automatisk. VAPID-nøkler generert og lagt i .env uten å vises i chatten. Én ærlig begrensning: ekte OS-varsel-levering med innvilget tillatelse kunne ikke testes i denne automatiserte nettleserøkten (tillatelsen var forhåndsblokkert av miljøet) — verdt å teste selv på en ekte enhet.
27/27 scratch-sjekker, full nettleser-gjennomgang uten konsollfeil, migrasjoner 035–037 kjørt mot ekte teecup_db, begge containere redeployet og verifisert, teeoff.no upåvirket. Alle tre .md-filene er oppdatert med full detalj.
2026-07-28 22:34:16 +02:00
|
|
|
|
<div className="mt-6">
|
|
|
|
|
|
<InstallPrompt />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-07-25 07:51:32 +02:00
|
|
|
|
<div className="mt-6 flex flex-col gap-8">
|
2026-07-29 12:44:51 +02:00
|
|
|
|
<QuickActions
|
|
|
|
|
|
organizations={me.organizations}
|
|
|
|
|
|
onCreateTournament={handleCreateTournament}
|
|
|
|
|
|
open={quickActionOpen}
|
|
|
|
|
|
onOpenChange={setQuickActionOpen}
|
|
|
|
|
|
/>
|
2026-08-01 21:15:07 +02:00
|
|
|
|
<LiveFriends friends={friendsOnCourse} />
|
All done and verified live in the browser. Summary of this round:
"Kommende runder" → "Runder" on the dashboard.
Tjøme course-name duplicate fixed in the real database (one round's stale course_name_snapshot corrected) — all three of your rounds now group under one "Tjøme Golfklubb" entry.
Two new colors, per your request for two: lifted the already-validated --chart-3 (blue → --info) and --chart-4 (gold → --gold) into core design tokens. First real uses: a blue "Hcp spilt til X" badge on round cards, and a gold "Personlig rekord" badge on your best completed round.
Clicking a course under "Spilte baner" now opens a new page listing every round played there (/my-rounds/course/[name]) — caught and fixed a real double-encoding bug here via an actual browser screenshot before shipping.
Aggregated statistics, clickable from the dashboard's "Statistikk" section (/my-rounds/stats): rounds completed, avg to-par, putts/18 holes (implementing your padding rule exactly — unplayed holes count as 2 putts, only for rounds where putt-tracking was on), fairway%, GIR%, one-putt%, scrambling%, sand save%, and chip/bunker/penalty/anywayslag averages per round.
Everything is typechecked, the putts-padding logic is verified against a hand-computed synthetic dataset, and all new screens were checked live in the browser with no console errors.
2026-07-28 07:04:56 +02:00
|
|
|
|
<UpcomingRounds rounds={roundsForCards.map((r) => toRound(r, me.id, personalBestRoundId))} loading={rounds === null} />
|
2026-07-29 12:44:51 +02:00
|
|
|
|
<UpcomingTournaments
|
|
|
|
|
|
entries={combinedTournaments}
|
|
|
|
|
|
onNewTournament={() => openQuickAction("tournament")}
|
|
|
|
|
|
onJoinWithCode={() => openQuickAction("code")}
|
|
|
|
|
|
/>
|
2026-07-25 07:51:32 +02:00
|
|
|
|
<StatsSection roundsCount={rows.length} hcpNow={hcpNow} hcpTrend={hcpTrend} hcpHistory={hcpValues} avgToPar={avgToPar} />
|
|
|
|
|
|
<PlayedCourses courses={playedCourses} />
|
2026-07-25 08:37:07 +02:00
|
|
|
|
<FriendsSection summary={friendsSummary} />
|
2026-07-25 07:51:32 +02:00
|
|
|
|
<OrganizationsFooter organizations={me.organizations} />
|
Update Todos
Migrasjon 015: app_user-profilfelt + player_organizations_for_user()-bro
Backend: utvid Me + PATCH /auth/profile + avatar-opplasting/sletting
Backend: 'mine runder'-data + check_visibility-utvidelse for deltakere
Frontend: profil-seksjon i /account
Frontend: 'Mine runder'-seksjon + betinget tom-tilstand i dashboard.tsx
Scratch-verifisere alt (15 sjekker bestått)
Typesjekket frontend-build
ADR-031 + .md-oppdatering
Bygget og scratch-verifisert (15 automatiserte sjekker). Oppsummering:
Personlig profil — nye felt på selve kontoen (ikke på org-ens spillerdata, det er bevisst holdt atskilt siden en person kan ha ulike spiller-rader i ulike klubber): profilbilde, fornavn, etternavn, fødselsdato, kjønn, HCP, hjemmeklubb. Redigeres i en ny seksjon på /account. Sletting av enkeltfelt fungerer (send tomt/null), profilbilde kan lastes opp og fjernes.
"Mine runder" — ny seksjon øverst på dashbordet, viser turneringer du er rostret i på tvers av alle organisasjoner, uavhengig av om du er medlem noe sted.
Et reelt sikkerhetshull jeg fant underveis, ikke antatt på forhånd: da jeg testet "Mine runder" mot en faktisk ren spiller (ingen organisasjonsmedlemskap), oppdaget jeg at synlighetsregelen kun ga deltakere tilgang for det strengeste synlighetsnivået — ikke for standard-nivået («org»), som er det ALLE nye turneringer får automatisk. En ren spiller ville altså vært låst ute av sin egen, helt normale turnering. Fikset og verifisert grundig at det er en ren utvidelse: en fremmed innlogget bruker og en anonym leser blir fortsatt korrekt avvist som før.
Bevisst utenfor omfang, tydelig flagget: "Mine runder" lenker til den offentlige turnering-siden, ikke til lagets private chat eller scorekortet ennå — de krever fortsatt ekte organisasjonsmedlemskap, en strengere sperre brukt bredt i hele appen som jeg ikke ville endre uten en egen, forsiktig runde. Notert som naturlig neste steg.
Ingen kode for punkt 2 (midlertidige spillere) i denne runden, som avtalt.
2026-07-20 10:41:30 +02:00
|
|
|
|
</div>
|
2026-07-18 06:46:09 +02:00
|
|
|
|
</main>
|
Ja, jeg forsto rekkefølgen — steg 1 og 2 er nå ferdig og scratch-verifisert:
Steg 1 (backend, HCP-prosent + Match-HCP): ny migrasjon 051_round_allowance_override.sql (round.allowance_override jsonb), koblet inn i _recompute_side_handicaps/_relative_strokes_for_round + POST/PATCH /rounds. Verifisert presist i scratch: 100%→50%-prosent ga playing_handicap 24/10 → 12/5 nøyaktig som beregnet for hånd, og use_matchplay_handicap slått av ga rå 12/5 i stedet for differensial 7/0. Slagspill upåvirket (regresjon bekreftet).
Steg 2 (kode-fiks, lag-gruppering): ScorecardGrid (live-scoring) og MatchScorecardGrid (post-runde/fremtidig sammenslått side) sorterer nå stabilt på round_side_id før rendering. Bygget en fersk scratch-fourball-runde med akkurat ditt bug-scenario (A→Rødt, B→Blått, C→Rødt, D→Blått, lagt til i den rekkefølgen) og bekreftet visuelt i nettleseren i begge visningene: Rødt lag vises samlet, deretter Blått — ikke lenger interleaved.
Typesjekk + full produksjonsbuild kompilerte rent gjennom hele runden.
Gjenstår før jeg går videre til V0-promptene: migrasjon 051 er kun kjørt i scratch, ikke mot ekte teecup_db. Planen er:
docker exec -i teeoff_db psql -U teeoff_admin -d teecup_db -v ON_ERROR_STOP=1 < 051_round_allowance_override.sql
— ett rent additivt ALTER TABLE round ADD COLUMN allowance_override jsonb, ingen eksisterende rader berøres. Deretter docker compose up -d --build teecup_api teecup_frontend (backend for allowance-koblingen, frontend for gruppering-fiksen).
Bekreft så kjører jeg dette, og går videre til V0-prompt 1 (rundeoppsett-veiviseren).
2026-08-02 08:24:01 +02:00
|
|
|
|
|
2026-08-08 12:34:03 +02:00
|
|
|
|
<BottomNav />
|
2026-07-18 06:46:09 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Ja, jeg forsto rekkefølgen — steg 1 og 2 er nå ferdig og scratch-verifisert:
Steg 1 (backend, HCP-prosent + Match-HCP): ny migrasjon 051_round_allowance_override.sql (round.allowance_override jsonb), koblet inn i _recompute_side_handicaps/_relative_strokes_for_round + POST/PATCH /rounds. Verifisert presist i scratch: 100%→50%-prosent ga playing_handicap 24/10 → 12/5 nøyaktig som beregnet for hånd, og use_matchplay_handicap slått av ga rå 12/5 i stedet for differensial 7/0. Slagspill upåvirket (regresjon bekreftet).
Steg 2 (kode-fiks, lag-gruppering): ScorecardGrid (live-scoring) og MatchScorecardGrid (post-runde/fremtidig sammenslått side) sorterer nå stabilt på round_side_id før rendering. Bygget en fersk scratch-fourball-runde med akkurat ditt bug-scenario (A→Rødt, B→Blått, C→Rødt, D→Blått, lagt til i den rekkefølgen) og bekreftet visuelt i nettleseren i begge visningene: Rødt lag vises samlet, deretter Blått — ikke lenger interleaved.
Typesjekk + full produksjonsbuild kompilerte rent gjennom hele runden.
Gjenstår før jeg går videre til V0-promptene: migrasjon 051 er kun kjørt i scratch, ikke mot ekte teecup_db. Planen er:
docker exec -i teeoff_db psql -U teeoff_admin -d teecup_db -v ON_ERROR_STOP=1 < 051_round_allowance_override.sql
— ett rent additivt ALTER TABLE round ADD COLUMN allowance_override jsonb, ingen eksisterende rader berøres. Deretter docker compose up -d --build teecup_api teecup_frontend (backend for allowance-koblingen, frontend for gruppering-fiksen).
Bekreft så kjører jeg dette, og går videre til V0-prompt 1 (rundeoppsett-veiviseren).
2026-08-02 08:24:01 +02:00
|
|
|
|
|
Varsler (fra zip 20): in-app varslingssenter er live — bjelle med uleste-tall i dashbord-headeren, /my-notifications-side. Trigges i dag ved venneforespørsel sendt/akseptert; flere hendelser kan kobles på senere.
Rundeleaderboard: ny GET /rounds/{id}/leaderboard-backend er live (rangering, thru-tall, brutto+netto til par, håndterer 1 til 15+ deltakere). V0-prompten for selve visningen ligger i FEATURE_BACKLOG.md, klar til å limes inn i v0.app — send meg zip-en når du har den, så kobler jeg den på (foreslått rute /my-rounds/[id]/leaderboard, lenket fra rundesiden).
Begge deler scratch-verifisert (39/39 sjekker, inkl. en uavhengig kryssjekk av netto-beregningen mot handicap_engine direkte), rullet ut mot ekte teecup_db/containere, teeoff.no upåvirket.
2026-07-26 06:53:58 +02:00
|
|
|
|
// V0-designet (varsler-runden, 2026-07-25) -- lenker til /my-notifications,
|
|
|
|
|
|
// IKKE /notifications (API-prefiks, samme kollisjonsklasse som
|
|
|
|
|
|
// /rounds->/my-rounds og /friends->/my-friends).
|
|
|
|
|
|
function NotificationBell({ unreadCount }: { unreadCount: number }) {
|
|
|
|
|
|
const hasUnread = unreadCount > 0
|
|
|
|
|
|
const label = hasUnread ? `Varsler, ${unreadCount} uleste` : "Varsler, ingen uleste"
|
|
|
|
|
|
const badgeText = unreadCount > 9 ? "9+" : String(unreadCount)
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Link
|
|
|
|
|
|
href="/my-notifications"
|
|
|
|
|
|
aria-label={label}
|
2026-08-07 22:02:56 +02:00
|
|
|
|
className="relative inline-flex size-11 items-center justify-center rounded-lg text-clubhouse-muted transition-all duration-200 ease-in-out hover:bg-clubhouse-field hover:text-clubhouse-ink active:opacity-70 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tee-strong"
|
Varsler (fra zip 20): in-app varslingssenter er live — bjelle med uleste-tall i dashbord-headeren, /my-notifications-side. Trigges i dag ved venneforespørsel sendt/akseptert; flere hendelser kan kobles på senere.
Rundeleaderboard: ny GET /rounds/{id}/leaderboard-backend er live (rangering, thru-tall, brutto+netto til par, håndterer 1 til 15+ deltakere). V0-prompten for selve visningen ligger i FEATURE_BACKLOG.md, klar til å limes inn i v0.app — send meg zip-en når du har den, så kobler jeg den på (foreslått rute /my-rounds/[id]/leaderboard, lenket fra rundesiden).
Begge deler scratch-verifisert (39/39 sjekker, inkl. en uavhengig kryssjekk av netto-beregningen mot handicap_engine direkte), rullet ut mot ekte teecup_db/containere, teeoff.no upåvirket.
2026-07-26 06:53:58 +02:00
|
|
|
|
>
|
|
|
|
|
|
<Bell aria-hidden="true" className="size-5" />
|
|
|
|
|
|
{hasUnread && (
|
|
|
|
|
|
<span
|
|
|
|
|
|
aria-hidden="true"
|
2026-08-07 22:02:56 +02:00
|
|
|
|
className="absolute right-1 top-1 flex h-[18px] min-w-[18px] items-center justify-center rounded-full bg-cup px-1 text-[11px] font-bold leading-none text-white ring-2 ring-clubhouse-card"
|
Varsler (fra zip 20): in-app varslingssenter er live — bjelle med uleste-tall i dashbord-headeren, /my-notifications-side. Trigges i dag ved venneforespørsel sendt/akseptert; flere hendelser kan kobles på senere.
Rundeleaderboard: ny GET /rounds/{id}/leaderboard-backend er live (rangering, thru-tall, brutto+netto til par, håndterer 1 til 15+ deltakere). V0-prompten for selve visningen ligger i FEATURE_BACKLOG.md, klar til å limes inn i v0.app — send meg zip-en når du har den, så kobler jeg den på (foreslått rute /my-rounds/[id]/leaderboard, lenket fra rundesiden).
Begge deler scratch-verifisert (39/39 sjekker, inkl. en uavhengig kryssjekk av netto-beregningen mot handicap_engine direkte), rullet ut mot ekte teecup_db/containere, teeoff.no upåvirket.
2026-07-26 06:53:58 +02:00
|
|
|
|
>
|
|
|
|
|
|
{badgeText}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-01 21:15:07 +02:00
|
|
|
|
// --- 4. Hurtighandlinger -----------------------------------------------------
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
|
2026-07-25 07:51:32 +02:00
|
|
|
|
function QuickActions({
|
|
|
|
|
|
organizations,
|
|
|
|
|
|
onCreateTournament,
|
2026-07-29 12:44:51 +02:00
|
|
|
|
open,
|
|
|
|
|
|
onOpenChange,
|
2026-07-25 07:51:32 +02:00
|
|
|
|
}: {
|
|
|
|
|
|
organizations: MyOrg[]
|
2026-07-30 07:55:35 +02:00
|
|
|
|
onCreateTournament: (name: string, orgId: string | null, format: "team" | "individual") => Promise<void>
|
2026-07-29 12:44:51 +02:00
|
|
|
|
open: "tournament" | "code" | null
|
|
|
|
|
|
onOpenChange: (value: "tournament" | "code" | null) => void
|
2026-07-25 07:51:32 +02:00
|
|
|
|
}) {
|
Update Todos
Migrasjon 015: app_user-profilfelt + player_organizations_for_user()-bro
Backend: utvid Me + PATCH /auth/profile + avatar-opplasting/sletting
Backend: 'mine runder'-data + check_visibility-utvidelse for deltakere
Frontend: profil-seksjon i /account
Frontend: 'Mine runder'-seksjon + betinget tom-tilstand i dashboard.tsx
Scratch-verifisere alt (15 sjekker bestått)
Typesjekket frontend-build
ADR-031 + .md-oppdatering
Bygget og scratch-verifisert (15 automatiserte sjekker). Oppsummering:
Personlig profil — nye felt på selve kontoen (ikke på org-ens spillerdata, det er bevisst holdt atskilt siden en person kan ha ulike spiller-rader i ulike klubber): profilbilde, fornavn, etternavn, fødselsdato, kjønn, HCP, hjemmeklubb. Redigeres i en ny seksjon på /account. Sletting av enkeltfelt fungerer (send tomt/null), profilbilde kan lastes opp og fjernes.
"Mine runder" — ny seksjon øverst på dashbordet, viser turneringer du er rostret i på tvers av alle organisasjoner, uavhengig av om du er medlem noe sted.
Et reelt sikkerhetshull jeg fant underveis, ikke antatt på forhånd: da jeg testet "Mine runder" mot en faktisk ren spiller (ingen organisasjonsmedlemskap), oppdaget jeg at synlighetsregelen kun ga deltakere tilgang for det strengeste synlighetsnivået — ikke for standard-nivået («org»), som er det ALLE nye turneringer får automatisk. En ren spiller ville altså vært låst ute av sin egen, helt normale turnering. Fikset og verifisert grundig at det er en ren utvidelse: en fremmed innlogget bruker og en anonym leser blir fortsatt korrekt avvist som før.
Bevisst utenfor omfang, tydelig flagget: "Mine runder" lenker til den offentlige turnering-siden, ikke til lagets private chat eller scorekortet ennå — de krever fortsatt ekte organisasjonsmedlemskap, en strengere sperre brukt bredt i hele appen som jeg ikke ville endre uten en egen, forsiktig runde. Notert som naturlig neste steg.
Ingen kode for punkt 2 (midlertidige spillere) i denne runden, som avtalt.
2026-07-20 10:41:30 +02:00
|
|
|
|
return (
|
2026-07-29 12:44:51 +02:00
|
|
|
|
<section id="hurtighandlinger" aria-label="Hurtighandlinger" className="flex flex-col gap-3">
|
|
|
|
|
|
<div className="grid grid-cols-3 gap-2 sm:gap-3">
|
2026-08-07 22:02:56 +02:00
|
|
|
|
<QuickAction icon={Flag} label="Ny runde" href="/my-rounds/new" primary />
|
|
|
|
|
|
<QuickAction
|
|
|
|
|
|
icon={Trophy}
|
|
|
|
|
|
label="Ny turnering"
|
|
|
|
|
|
onClick={() => onOpenChange(open === "tournament" ? null : "tournament")}
|
|
|
|
|
|
expanded={open === "tournament"}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<QuickAction
|
|
|
|
|
|
icon={KeyRound}
|
|
|
|
|
|
label="Bli med med kode"
|
|
|
|
|
|
onClick={() => onOpenChange(open === "code" ? null : "code")}
|
|
|
|
|
|
expanded={open === "code"}
|
|
|
|
|
|
/>
|
Update Todos
Migrasjon 015: app_user-profilfelt + player_organizations_for_user()-bro
Backend: utvid Me + PATCH /auth/profile + avatar-opplasting/sletting
Backend: 'mine runder'-data + check_visibility-utvidelse for deltakere
Frontend: profil-seksjon i /account
Frontend: 'Mine runder'-seksjon + betinget tom-tilstand i dashboard.tsx
Scratch-verifisere alt (15 sjekker bestått)
Typesjekket frontend-build
ADR-031 + .md-oppdatering
Bygget og scratch-verifisert (15 automatiserte sjekker). Oppsummering:
Personlig profil — nye felt på selve kontoen (ikke på org-ens spillerdata, det er bevisst holdt atskilt siden en person kan ha ulike spiller-rader i ulike klubber): profilbilde, fornavn, etternavn, fødselsdato, kjønn, HCP, hjemmeklubb. Redigeres i en ny seksjon på /account. Sletting av enkeltfelt fungerer (send tomt/null), profilbilde kan lastes opp og fjernes.
"Mine runder" — ny seksjon øverst på dashbordet, viser turneringer du er rostret i på tvers av alle organisasjoner, uavhengig av om du er medlem noe sted.
Et reelt sikkerhetshull jeg fant underveis, ikke antatt på forhånd: da jeg testet "Mine runder" mot en faktisk ren spiller (ingen organisasjonsmedlemskap), oppdaget jeg at synlighetsregelen kun ga deltakere tilgang for det strengeste synlighetsnivået — ikke for standard-nivået («org»), som er det ALLE nye turneringer får automatisk. En ren spiller ville altså vært låst ute av sin egen, helt normale turnering. Fikset og verifisert grundig at det er en ren utvidelse: en fremmed innlogget bruker og en anonym leser blir fortsatt korrekt avvist som før.
Bevisst utenfor omfang, tydelig flagget: "Mine runder" lenker til den offentlige turnering-siden, ikke til lagets private chat eller scorekortet ennå — de krever fortsatt ekte organisasjonsmedlemskap, en strengere sperre brukt bredt i hele appen som jeg ikke ville endre uten en egen, forsiktig runde. Notert som naturlig neste steg.
Ingen kode for punkt 2 (midlertidige spillere) i denne runden, som avtalt.
2026-07-20 10:41:30 +02:00
|
|
|
|
</div>
|
2026-07-25 07:51:32 +02:00
|
|
|
|
{open === "tournament" && (
|
2026-07-29 12:44:51 +02:00
|
|
|
|
<NewTournamentInline organizations={organizations} onCreate={onCreateTournament} onClose={() => onOpenChange(null)} />
|
2026-07-25 07:51:32 +02:00
|
|
|
|
)}
|
2026-07-29 12:44:51 +02:00
|
|
|
|
{open === "code" && <JoinByCodeInline onClose={() => onOpenChange(null)} />}
|
Update Todos
Migrasjon 015: app_user-profilfelt + player_organizations_for_user()-bro
Backend: utvid Me + PATCH /auth/profile + avatar-opplasting/sletting
Backend: 'mine runder'-data + check_visibility-utvidelse for deltakere
Frontend: profil-seksjon i /account
Frontend: 'Mine runder'-seksjon + betinget tom-tilstand i dashboard.tsx
Scratch-verifisere alt (15 sjekker bestått)
Typesjekket frontend-build
ADR-031 + .md-oppdatering
Bygget og scratch-verifisert (15 automatiserte sjekker). Oppsummering:
Personlig profil — nye felt på selve kontoen (ikke på org-ens spillerdata, det er bevisst holdt atskilt siden en person kan ha ulike spiller-rader i ulike klubber): profilbilde, fornavn, etternavn, fødselsdato, kjønn, HCP, hjemmeklubb. Redigeres i en ny seksjon på /account. Sletting av enkeltfelt fungerer (send tomt/null), profilbilde kan lastes opp og fjernes.
"Mine runder" — ny seksjon øverst på dashbordet, viser turneringer du er rostret i på tvers av alle organisasjoner, uavhengig av om du er medlem noe sted.
Et reelt sikkerhetshull jeg fant underveis, ikke antatt på forhånd: da jeg testet "Mine runder" mot en faktisk ren spiller (ingen organisasjonsmedlemskap), oppdaget jeg at synlighetsregelen kun ga deltakere tilgang for det strengeste synlighetsnivået — ikke for standard-nivået («org»), som er det ALLE nye turneringer får automatisk. En ren spiller ville altså vært låst ute av sin egen, helt normale turnering. Fikset og verifisert grundig at det er en ren utvidelse: en fremmed innlogget bruker og en anonym leser blir fortsatt korrekt avvist som før.
Bevisst utenfor omfang, tydelig flagget: "Mine runder" lenker til den offentlige turnering-siden, ikke til lagets private chat eller scorekortet ennå — de krever fortsatt ekte organisasjonsmedlemskap, en strengere sperre brukt bredt i hele appen som jeg ikke ville endre uten en egen, forsiktig runde. Notert som naturlig neste steg.
Ingen kode for punkt 2 (midlertidige spillere) i denne runden, som avtalt.
2026-07-20 10:41:30 +02:00
|
|
|
|
</section>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-25 07:51:32 +02:00
|
|
|
|
function QuickAction({
|
|
|
|
|
|
icon: Icon,
|
|
|
|
|
|
label,
|
|
|
|
|
|
href,
|
|
|
|
|
|
onClick,
|
2026-08-07 22:02:56 +02:00
|
|
|
|
primary = false,
|
|
|
|
|
|
expanded = false,
|
2026-07-25 07:51:32 +02:00
|
|
|
|
}: {
|
|
|
|
|
|
icon: typeof Flag
|
|
|
|
|
|
label: string
|
|
|
|
|
|
href?: string
|
|
|
|
|
|
onClick?: () => void
|
2026-08-07 22:02:56 +02:00
|
|
|
|
primary?: boolean
|
|
|
|
|
|
expanded?: boolean
|
2026-07-25 07:51:32 +02:00
|
|
|
|
}) {
|
2026-08-07 22:02:56 +02:00
|
|
|
|
const className = cn(
|
|
|
|
|
|
"flex min-h-[84px] flex-1 flex-col items-center justify-center gap-1.5 rounded-2xl border px-2 py-3 text-center font-semibold transition-all duration-200 ease-in-out active:scale-[0.98] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tee-strong",
|
|
|
|
|
|
primary
|
|
|
|
|
|
? "border-transparent bg-tee-strong text-white shadow-sm hover:brightness-110"
|
|
|
|
|
|
: expanded
|
|
|
|
|
|
? "border-tee-strong bg-tee/10 text-clubhouse-ink"
|
|
|
|
|
|
: "border-clubhouse-border bg-clubhouse-card text-clubhouse-ink shadow-sm hover:border-tee-strong/50 hover:bg-tee/5",
|
|
|
|
|
|
)
|
2026-07-25 07:51:32 +02:00
|
|
|
|
const inner = (
|
|
|
|
|
|
<>
|
2026-08-07 22:02:56 +02:00
|
|
|
|
<Icon aria-hidden="true" className={cn("size-5 sm:size-6", primary ? "text-white" : "text-tee-strong")} />
|
2026-07-29 12:44:51 +02:00
|
|
|
|
<span className="text-xs font-bold leading-tight text-balance sm:text-sm">{label}</span>
|
2026-07-25 07:51:32 +02:00
|
|
|
|
</>
|
|
|
|
|
|
)
|
|
|
|
|
|
if (href) {
|
|
|
|
|
|
return (
|
2026-08-07 22:02:56 +02:00
|
|
|
|
<Link href={href} className={className}>
|
2026-07-25 07:51:32 +02:00
|
|
|
|
{inner}
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
return (
|
2026-08-07 22:02:56 +02:00
|
|
|
|
<button type="button" onClick={onClick} aria-expanded={primary ? undefined : expanded} className={className}>
|
2026-07-25 07:51:32 +02:00
|
|
|
|
{inner}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)
|
Update Todos
Migrasjon 015: app_user-profilfelt + player_organizations_for_user()-bro
Backend: utvid Me + PATCH /auth/profile + avatar-opplasting/sletting
Backend: 'mine runder'-data + check_visibility-utvidelse for deltakere
Frontend: profil-seksjon i /account
Frontend: 'Mine runder'-seksjon + betinget tom-tilstand i dashboard.tsx
Scratch-verifisere alt (15 sjekker bestått)
Typesjekket frontend-build
ADR-031 + .md-oppdatering
Bygget og scratch-verifisert (15 automatiserte sjekker). Oppsummering:
Personlig profil — nye felt på selve kontoen (ikke på org-ens spillerdata, det er bevisst holdt atskilt siden en person kan ha ulike spiller-rader i ulike klubber): profilbilde, fornavn, etternavn, fødselsdato, kjønn, HCP, hjemmeklubb. Redigeres i en ny seksjon på /account. Sletting av enkeltfelt fungerer (send tomt/null), profilbilde kan lastes opp og fjernes.
"Mine runder" — ny seksjon øverst på dashbordet, viser turneringer du er rostret i på tvers av alle organisasjoner, uavhengig av om du er medlem noe sted.
Et reelt sikkerhetshull jeg fant underveis, ikke antatt på forhånd: da jeg testet "Mine runder" mot en faktisk ren spiller (ingen organisasjonsmedlemskap), oppdaget jeg at synlighetsregelen kun ga deltakere tilgang for det strengeste synlighetsnivået — ikke for standard-nivået («org»), som er det ALLE nye turneringer får automatisk. En ren spiller ville altså vært låst ute av sin egen, helt normale turnering. Fikset og verifisert grundig at det er en ren utvidelse: en fremmed innlogget bruker og en anonym leser blir fortsatt korrekt avvist som før.
Bevisst utenfor omfang, tydelig flagget: "Mine runder" lenker til den offentlige turnering-siden, ikke til lagets private chat eller scorekortet ennå — de krever fortsatt ekte organisasjonsmedlemskap, en strengere sperre brukt bredt i hele appen som jeg ikke ville endre uten en egen, forsiktig runde. Notert som naturlig neste steg.
Ingen kode for punkt 2 (midlertidige spillere) i denne runden, som avtalt.
2026-07-20 10:41:30 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-01 21:15:07 +02:00
|
|
|
|
// Skjemaene under (opprett turnering / bli med med kode) beholdt UENDRET --
|
|
|
|
|
|
// egne, allerede etablerte shadcn-baserte skjemaer, ikke del av selve
|
|
|
|
|
|
// Forest Green-visuell-redesignet (Stitch/V0 designet aldri disse to).
|
|
|
|
|
|
|
2026-07-25 07:51:32 +02:00
|
|
|
|
function NewTournamentInline({
|
|
|
|
|
|
organizations,
|
Update Todos
Migrasjon 015: app_user-profilfelt + player_organizations_for_user()-bro
Backend: utvid Me + PATCH /auth/profile + avatar-opplasting/sletting
Backend: 'mine runder'-data + check_visibility-utvidelse for deltakere
Frontend: profil-seksjon i /account
Frontend: 'Mine runder'-seksjon + betinget tom-tilstand i dashboard.tsx
Scratch-verifisere alt (15 sjekker bestått)
Typesjekket frontend-build
ADR-031 + .md-oppdatering
Bygget og scratch-verifisert (15 automatiserte sjekker). Oppsummering:
Personlig profil — nye felt på selve kontoen (ikke på org-ens spillerdata, det er bevisst holdt atskilt siden en person kan ha ulike spiller-rader i ulike klubber): profilbilde, fornavn, etternavn, fødselsdato, kjønn, HCP, hjemmeklubb. Redigeres i en ny seksjon på /account. Sletting av enkeltfelt fungerer (send tomt/null), profilbilde kan lastes opp og fjernes.
"Mine runder" — ny seksjon øverst på dashbordet, viser turneringer du er rostret i på tvers av alle organisasjoner, uavhengig av om du er medlem noe sted.
Et reelt sikkerhetshull jeg fant underveis, ikke antatt på forhånd: da jeg testet "Mine runder" mot en faktisk ren spiller (ingen organisasjonsmedlemskap), oppdaget jeg at synlighetsregelen kun ga deltakere tilgang for det strengeste synlighetsnivået — ikke for standard-nivået («org»), som er det ALLE nye turneringer får automatisk. En ren spiller ville altså vært låst ute av sin egen, helt normale turnering. Fikset og verifisert grundig at det er en ren utvidelse: en fremmed innlogget bruker og en anonym leser blir fortsatt korrekt avvist som før.
Bevisst utenfor omfang, tydelig flagget: "Mine runder" lenker til den offentlige turnering-siden, ikke til lagets private chat eller scorekortet ennå — de krever fortsatt ekte organisasjonsmedlemskap, en strengere sperre brukt bredt i hele appen som jeg ikke ville endre uten en egen, forsiktig runde. Notert som naturlig neste steg.
Ingen kode for punkt 2 (midlertidige spillere) i denne runden, som avtalt.
2026-07-20 10:41:30 +02:00
|
|
|
|
onCreate,
|
2026-07-25 07:51:32 +02:00
|
|
|
|
onClose,
|
Update Todos
Migrasjon 015: app_user-profilfelt + player_organizations_for_user()-bro
Backend: utvid Me + PATCH /auth/profile + avatar-opplasting/sletting
Backend: 'mine runder'-data + check_visibility-utvidelse for deltakere
Frontend: profil-seksjon i /account
Frontend: 'Mine runder'-seksjon + betinget tom-tilstand i dashboard.tsx
Scratch-verifisere alt (15 sjekker bestått)
Typesjekket frontend-build
ADR-031 + .md-oppdatering
Bygget og scratch-verifisert (15 automatiserte sjekker). Oppsummering:
Personlig profil — nye felt på selve kontoen (ikke på org-ens spillerdata, det er bevisst holdt atskilt siden en person kan ha ulike spiller-rader i ulike klubber): profilbilde, fornavn, etternavn, fødselsdato, kjønn, HCP, hjemmeklubb. Redigeres i en ny seksjon på /account. Sletting av enkeltfelt fungerer (send tomt/null), profilbilde kan lastes opp og fjernes.
"Mine runder" — ny seksjon øverst på dashbordet, viser turneringer du er rostret i på tvers av alle organisasjoner, uavhengig av om du er medlem noe sted.
Et reelt sikkerhetshull jeg fant underveis, ikke antatt på forhånd: da jeg testet "Mine runder" mot en faktisk ren spiller (ingen organisasjonsmedlemskap), oppdaget jeg at synlighetsregelen kun ga deltakere tilgang for det strengeste synlighetsnivået — ikke for standard-nivået («org»), som er det ALLE nye turneringer får automatisk. En ren spiller ville altså vært låst ute av sin egen, helt normale turnering. Fikset og verifisert grundig at det er en ren utvidelse: en fremmed innlogget bruker og en anonym leser blir fortsatt korrekt avvist som før.
Bevisst utenfor omfang, tydelig flagget: "Mine runder" lenker til den offentlige turnering-siden, ikke til lagets private chat eller scorekortet ennå — de krever fortsatt ekte organisasjonsmedlemskap, en strengere sperre brukt bredt i hele appen som jeg ikke ville endre uten en egen, forsiktig runde. Notert som naturlig neste steg.
Ingen kode for punkt 2 (midlertidige spillere) i denne runden, som avtalt.
2026-07-20 10:41:30 +02:00
|
|
|
|
}: {
|
2026-07-25 07:51:32 +02:00
|
|
|
|
organizations: MyOrg[]
|
2026-07-30 07:55:35 +02:00
|
|
|
|
onCreate: (name: string, orgId: string | null, format: "team" | "individual") => Promise<void>
|
2026-07-25 07:51:32 +02:00
|
|
|
|
onClose: () => void
|
Update Todos
Migrasjon 015: app_user-profilfelt + player_organizations_for_user()-bro
Backend: utvid Me + PATCH /auth/profile + avatar-opplasting/sletting
Backend: 'mine runder'-data + check_visibility-utvidelse for deltakere
Frontend: profil-seksjon i /account
Frontend: 'Mine runder'-seksjon + betinget tom-tilstand i dashboard.tsx
Scratch-verifisere alt (15 sjekker bestått)
Typesjekket frontend-build
ADR-031 + .md-oppdatering
Bygget og scratch-verifisert (15 automatiserte sjekker). Oppsummering:
Personlig profil — nye felt på selve kontoen (ikke på org-ens spillerdata, det er bevisst holdt atskilt siden en person kan ha ulike spiller-rader i ulike klubber): profilbilde, fornavn, etternavn, fødselsdato, kjønn, HCP, hjemmeklubb. Redigeres i en ny seksjon på /account. Sletting av enkeltfelt fungerer (send tomt/null), profilbilde kan lastes opp og fjernes.
"Mine runder" — ny seksjon øverst på dashbordet, viser turneringer du er rostret i på tvers av alle organisasjoner, uavhengig av om du er medlem noe sted.
Et reelt sikkerhetshull jeg fant underveis, ikke antatt på forhånd: da jeg testet "Mine runder" mot en faktisk ren spiller (ingen organisasjonsmedlemskap), oppdaget jeg at synlighetsregelen kun ga deltakere tilgang for det strengeste synlighetsnivået — ikke for standard-nivået («org»), som er det ALLE nye turneringer får automatisk. En ren spiller ville altså vært låst ute av sin egen, helt normale turnering. Fikset og verifisert grundig at det er en ren utvidelse: en fremmed innlogget bruker og en anonym leser blir fortsatt korrekt avvist som før.
Bevisst utenfor omfang, tydelig flagget: "Mine runder" lenker til den offentlige turnering-siden, ikke til lagets private chat eller scorekortet ennå — de krever fortsatt ekte organisasjonsmedlemskap, en strengere sperre brukt bredt i hele appen som jeg ikke ville endre uten en egen, forsiktig runde. Notert som naturlig neste steg.
Ingen kode for punkt 2 (midlertidige spillere) i denne runden, som avtalt.
2026-07-20 10:41:30 +02:00
|
|
|
|
}) {
|
2026-07-18 06:46:09 +02:00
|
|
|
|
const [name, setName] = useState("")
|
2026-07-25 07:51:32 +02:00
|
|
|
|
const [orgId, setOrgId] = useState(organizations[0]?.organization_id ?? "")
|
2026-07-30 07:55:35 +02:00
|
|
|
|
const [format, setFormat] = useState<"team" | "individual">("team")
|
2026-07-25 07:51:32 +02:00
|
|
|
|
const [submitting, setSubmitting] = useState(false)
|
2026-07-18 06:46:09 +02:00
|
|
|
|
const valid = name.trim().length >= 2
|
|
|
|
|
|
|
2026-07-25 07:51:32 +02:00
|
|
|
|
async function handleSubmit(e: React.FormEvent) {
|
2026-07-18 06:46:09 +02:00
|
|
|
|
e.preventDefault()
|
2026-07-25 07:51:32 +02:00
|
|
|
|
if (!valid || submitting) return
|
|
|
|
|
|
setSubmitting(true)
|
2026-07-30 07:55:35 +02:00
|
|
|
|
await onCreate(name.trim(), organizations.length > 1 ? orgId : null, format)
|
2026-07-25 07:51:32 +02:00
|
|
|
|
setSubmitting(false)
|
2026-07-18 06:46:09 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2026-07-30 07:55:35 +02:00
|
|
|
|
<form onSubmit={handleSubmit} className="flex flex-col gap-3 rounded-2xl border border-border bg-card p-4 shadow-md shadow-black/8">
|
|
|
|
|
|
<div className="flex flex-col gap-1.5">
|
|
|
|
|
|
<span className="text-sm font-semibold text-foreground">Type turnering</span>
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-2">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setFormat("team")}
|
|
|
|
|
|
aria-pressed={format === "team"}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"flex min-h-11 flex-col items-start gap-0.5 rounded-xl border px-3 py-2 text-left transition-colors",
|
|
|
|
|
|
format === "team"
|
|
|
|
|
|
? "border-primary bg-primary/10"
|
|
|
|
|
|
: "border-border bg-background hover:bg-accent/50",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span className="text-sm font-bold text-foreground">Lag</span>
|
|
|
|
|
|
<span className="text-xs font-medium text-muted-foreground">To lag mot hverandre (Ryder Cup)</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setFormat("individual")}
|
|
|
|
|
|
aria-pressed={format === "individual"}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"flex min-h-11 flex-col items-start gap-0.5 rounded-xl border px-3 py-2 text-left transition-colors",
|
|
|
|
|
|
format === "individual"
|
|
|
|
|
|
? "border-info bg-info/10"
|
|
|
|
|
|
: "border-border bg-background hover:bg-accent/50",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span className="text-sm font-bold text-foreground">Individuell</span>
|
|
|
|
|
|
<span className="text-xs font-medium text-muted-foreground">Flatt felt, én eller flere runder</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2026-07-25 07:51:32 +02:00
|
|
|
|
</div>
|
2026-07-30 07:55:35 +02:00
|
|
|
|
|
|
|
|
|
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-end">
|
|
|
|
|
|
<div className="flex flex-1 flex-col gap-1.5">
|
|
|
|
|
|
<Label htmlFor="new-tournament-name" className="text-sm font-semibold">
|
|
|
|
|
|
Navn på turnering
|
2026-07-25 07:51:32 +02:00
|
|
|
|
</Label>
|
2026-07-30 07:55:35 +02:00
|
|
|
|
<Input
|
|
|
|
|
|
id="new-tournament-name"
|
|
|
|
|
|
autoFocus
|
|
|
|
|
|
placeholder="F.eks. Sommercup 2026"
|
|
|
|
|
|
value={name}
|
|
|
|
|
|
onChange={(e) => setName(e.target.value)}
|
|
|
|
|
|
className="h-11 rounded-xl"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{organizations.length > 1 && (
|
|
|
|
|
|
<div className="flex flex-col gap-1.5">
|
|
|
|
|
|
<Label htmlFor="new-tournament-org" className="text-sm font-semibold">
|
|
|
|
|
|
Organisasjon
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<select
|
|
|
|
|
|
id="new-tournament-org"
|
|
|
|
|
|
value={orgId}
|
|
|
|
|
|
onChange={(e) => setOrgId(e.target.value)}
|
|
|
|
|
|
className="h-11 rounded-xl border border-border bg-card px-3 text-sm font-medium text-foreground outline-none sm:w-48"
|
|
|
|
|
|
>
|
|
|
|
|
|
{organizations.map((o) => (
|
|
|
|
|
|
<option key={o.organization_id} value={o.organization_id}>
|
|
|
|
|
|
{o.name}
|
|
|
|
|
|
</option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
|
<Button type="submit" disabled={!valid || submitting} className="h-11 rounded-xl font-bold">
|
|
|
|
|
|
{submitting ? "Oppretter…" : "Opprett"}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button type="button" variant="ghost" size="icon" onClick={onClose} className="size-11 rounded-xl text-muted-foreground" aria-label="Avbryt">
|
|
|
|
|
|
<X aria-hidden="true" className="size-5" />
|
|
|
|
|
|
</Button>
|
Update Todos
Migrasjon 015: app_user-profilfelt + player_organizations_for_user()-bro
Backend: utvid Me + PATCH /auth/profile + avatar-opplasting/sletting
Backend: 'mine runder'-data + check_visibility-utvidelse for deltakere
Frontend: profil-seksjon i /account
Frontend: 'Mine runder'-seksjon + betinget tom-tilstand i dashboard.tsx
Scratch-verifisere alt (15 sjekker bestått)
Typesjekket frontend-build
ADR-031 + .md-oppdatering
Bygget og scratch-verifisert (15 automatiserte sjekker). Oppsummering:
Personlig profil — nye felt på selve kontoen (ikke på org-ens spillerdata, det er bevisst holdt atskilt siden en person kan ha ulike spiller-rader i ulike klubber): profilbilde, fornavn, etternavn, fødselsdato, kjønn, HCP, hjemmeklubb. Redigeres i en ny seksjon på /account. Sletting av enkeltfelt fungerer (send tomt/null), profilbilde kan lastes opp og fjernes.
"Mine runder" — ny seksjon øverst på dashbordet, viser turneringer du er rostret i på tvers av alle organisasjoner, uavhengig av om du er medlem noe sted.
Et reelt sikkerhetshull jeg fant underveis, ikke antatt på forhånd: da jeg testet "Mine runder" mot en faktisk ren spiller (ingen organisasjonsmedlemskap), oppdaget jeg at synlighetsregelen kun ga deltakere tilgang for det strengeste synlighetsnivået — ikke for standard-nivået («org»), som er det ALLE nye turneringer får automatisk. En ren spiller ville altså vært låst ute av sin egen, helt normale turnering. Fikset og verifisert grundig at det er en ren utvidelse: en fremmed innlogget bruker og en anonym leser blir fortsatt korrekt avvist som før.
Bevisst utenfor omfang, tydelig flagget: "Mine runder" lenker til den offentlige turnering-siden, ikke til lagets private chat eller scorekortet ennå — de krever fortsatt ekte organisasjonsmedlemskap, en strengere sperre brukt bredt i hele appen som jeg ikke ville endre uten en egen, forsiktig runde. Notert som naturlig neste steg.
Ingen kode for punkt 2 (midlertidige spillere) i denne runden, som avtalt.
2026-07-20 10:41:30 +02:00
|
|
|
|
</div>
|
2026-07-18 06:46:09 +02:00
|
|
|
|
</div>
|
2026-07-25 07:51:32 +02:00
|
|
|
|
</form>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
2026-07-18 06:46:09 +02:00
|
|
|
|
|
2026-07-25 07:51:32 +02:00
|
|
|
|
function JoinByCodeInline({ onClose }: { onClose: () => void }) {
|
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
|
const [code, setCode] = useState("")
|
|
|
|
|
|
const [loading, setLoading] = useState(false)
|
|
|
|
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
|
|
|
|
|
|
|
|
async function handleSubmit(e: React.FormEvent) {
|
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
|
const trimmed = code.trim()
|
|
|
|
|
|
if (!trimmed || loading) return
|
|
|
|
|
|
setLoading(true)
|
|
|
|
|
|
setError(null)
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await fetch(`/public/tournaments/by-code/${encodeURIComponent(trimmed)}`)
|
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
|
const body = await res.json().catch(() => null)
|
|
|
|
|
|
setError(body?.detail?.message ?? "Fant ingen turnering med denne koden.")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const data: { tournament_id: string } = await res.json()
|
|
|
|
|
|
router.push(`/t/${data.tournament_id}?code=${encodeURIComponent(trimmed)}`)
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
setError("Klarte ikke å slå opp koden. Prøv igjen.")
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2026-07-29 12:44:51 +02:00
|
|
|
|
<form onSubmit={handleSubmit} className="flex flex-col gap-3 rounded-2xl border border-border bg-card p-4 shadow-md shadow-black/8 sm:flex-row sm:items-end">
|
2026-07-25 07:51:32 +02:00
|
|
|
|
<div className="flex flex-1 flex-col gap-1.5">
|
|
|
|
|
|
<Label htmlFor="join-code-inline" className="text-sm font-semibold">
|
|
|
|
|
|
Invitasjonskode
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="join-code-inline"
|
|
|
|
|
|
autoFocus
|
|
|
|
|
|
placeholder="F.eks. 7K2M9P"
|
|
|
|
|
|
value={code}
|
|
|
|
|
|
onChange={(e) => setCode(e.target.value.toUpperCase())}
|
|
|
|
|
|
className="h-11 rounded-xl uppercase tracking-widest"
|
|
|
|
|
|
/>
|
|
|
|
|
|
{error && <p className="text-sm font-medium text-destructive">{error}</p>}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
|
<Button type="submit" disabled={!code.trim() || loading} className="h-11 rounded-xl font-bold">
|
|
|
|
|
|
{loading ? "Søker…" : "Bli med"}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button type="button" variant="ghost" size="icon" onClick={onClose} className="size-11 rounded-xl text-muted-foreground" aria-label="Avbryt">
|
|
|
|
|
|
<X aria-hidden="true" className="size-5" />
|
2026-07-18 06:46:09 +02:00
|
|
|
|
</Button>
|
2026-07-25 07:51:32 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Delt seksjon-"chrome" ----------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
function SectionHeader({ title, action }: { title: string; action?: React.ReactNode }) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="mb-3 flex items-center justify-between gap-3">
|
2026-08-07 22:02:56 +02:00
|
|
|
|
<h2 className="text-lg font-extrabold tracking-tight text-clubhouse-ink">{title}</h2>
|
2026-07-25 07:51:32 +02:00
|
|
|
|
{action}
|
2026-07-18 06:46:09 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-25 07:51:32 +02:00
|
|
|
|
function SeeAllLink({ href, label }: { href: string; label: string }) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Link
|
|
|
|
|
|
href={href}
|
2026-08-07 22:02:56 +02:00
|
|
|
|
className="inline-flex min-h-[44px] items-center gap-1 rounded-lg px-2 text-sm font-bold text-tee-strong transition-all duration-200 ease-in-out focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tee-strong"
|
2026-07-25 07:51:32 +02:00
|
|
|
|
>
|
|
|
|
|
|
{label}
|
|
|
|
|
|
<ChevronRight aria-hidden="true" className="size-4" />
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
2026-07-18 06:46:09 +02:00
|
|
|
|
|
2026-07-25 07:51:32 +02:00
|
|
|
|
function EmptyState({
|
|
|
|
|
|
icon: Icon,
|
|
|
|
|
|
title,
|
|
|
|
|
|
description,
|
|
|
|
|
|
children,
|
2026-07-18 06:46:09 +02:00
|
|
|
|
}: {
|
2026-07-25 07:51:32 +02:00
|
|
|
|
icon: typeof Flag
|
|
|
|
|
|
title: string
|
|
|
|
|
|
description: string
|
|
|
|
|
|
children?: React.ReactNode
|
2026-07-18 06:46:09 +02:00
|
|
|
|
}) {
|
|
|
|
|
|
return (
|
2026-08-07 22:02:56 +02:00
|
|
|
|
<div className="flex flex-col items-center gap-3 rounded-2xl border border-dashed border-clubhouse-border bg-clubhouse-card/60 px-6 py-8 text-center">
|
|
|
|
|
|
<div className="flex size-12 items-center justify-center rounded-xl bg-clubhouse-field">
|
|
|
|
|
|
<Icon aria-hidden="true" className="size-6 text-clubhouse-muted" />
|
2026-07-18 06:46:09 +02:00
|
|
|
|
</div>
|
2026-07-25 07:51:32 +02:00
|
|
|
|
<div className="flex flex-col gap-1">
|
2026-08-07 22:02:56 +02:00
|
|
|
|
<h3 className="text-base font-bold text-balance text-clubhouse-ink">{title}</h3>
|
|
|
|
|
|
<p className="text-sm leading-relaxed text-pretty text-clubhouse-muted">{description}</p>
|
2026-07-25 07:51:32 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
{children ? <div className="mt-1 flex flex-wrap justify-center gap-2">{children}</div> : null}
|
2026-07-18 06:46:09 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-25 07:51:32 +02:00
|
|
|
|
function ShortcutButton({
|
|
|
|
|
|
icon: Icon,
|
|
|
|
|
|
label,
|
|
|
|
|
|
href,
|
2026-07-29 12:44:51 +02:00
|
|
|
|
onClick,
|
2026-07-25 07:51:32 +02:00
|
|
|
|
variant = "primary",
|
2026-07-18 06:46:09 +02:00
|
|
|
|
}: {
|
2026-07-25 07:51:32 +02:00
|
|
|
|
icon: typeof Flag
|
|
|
|
|
|
label: string
|
|
|
|
|
|
href?: string
|
2026-07-29 12:44:51 +02:00
|
|
|
|
onClick?: () => void
|
2026-07-25 07:51:32 +02:00
|
|
|
|
variant?: "primary" | "outline"
|
2026-07-18 06:46:09 +02:00
|
|
|
|
}) {
|
2026-08-07 22:02:56 +02:00
|
|
|
|
const className = cn(
|
|
|
|
|
|
"inline-flex min-h-[44px] items-center gap-2 rounded-xl px-4 text-sm font-bold transition-all duration-200 ease-in-out active:scale-[0.98] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tee-strong",
|
2026-08-01 21:15:07 +02:00
|
|
|
|
variant === "primary"
|
2026-08-07 22:02:56 +02:00
|
|
|
|
? "bg-tee-strong text-white hover:brightness-110"
|
|
|
|
|
|
: "border border-clubhouse-border bg-clubhouse-card text-clubhouse-ink hover:bg-clubhouse-field",
|
|
|
|
|
|
)
|
2026-07-25 07:51:32 +02:00
|
|
|
|
const inner = (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Icon aria-hidden="true" className="size-4" />
|
|
|
|
|
|
{label}
|
|
|
|
|
|
</>
|
|
|
|
|
|
)
|
|
|
|
|
|
if (href) {
|
|
|
|
|
|
return (
|
2026-08-07 22:02:56 +02:00
|
|
|
|
<Link href={href} className={className}>
|
2026-07-25 07:51:32 +02:00
|
|
|
|
{inner}
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
return (
|
2026-08-07 22:02:56 +02:00
|
|
|
|
<button type="button" onClick={onClick} className={className}>
|
2026-07-25 07:51:32 +02:00
|
|
|
|
{inner}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-01 21:15:07 +02:00
|
|
|
|
// --- 5. Venner på banen -------------------------------------------------------
|
|
|
|
|
|
// Ny seksjon (2026-08-01), plassert rett under hurtighandlingene per
|
|
|
|
|
|
// brukerens eksplisitte ønske. Ekte data fra GET /friends/on-course --
|
|
|
|
|
|
// venner med en AKTIV runde nå, eller fullført innen 24 timer. Bevisst
|
|
|
|
|
|
// UTEN boxed empty-state (kun én rolig linje) -- skal ikke prompte en
|
|
|
|
|
|
// handling når den er tom, ulikt de andre seksjonene.
|
|
|
|
|
|
|
|
|
|
|
|
function friendOnCourseInitials(name: string): string {
|
|
|
|
|
|
const parts = name.trim().split(/\s+/)
|
|
|
|
|
|
const first = parts[0]?.[0] ?? ""
|
|
|
|
|
|
const last = parts.length > 1 ? parts[parts.length - 1][0] ?? "" : ""
|
|
|
|
|
|
return (first + last).toUpperCase() || "?"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function LiveFriends({ friends }: { friends: ApiFriendOnCourse[] }) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<section aria-label="Venner på banen">
|
2026-08-11 08:01:42 +02:00
|
|
|
|
{/* "Se feed" var for anonymt/uspesifikt til å si hva som faktisk
|
|
|
|
|
|
venter bak lenken (2026-08-11, brukertilbakemelding) -- knyttet
|
|
|
|
|
|
teksten til seksjonens egen kontekst i stedet. */}
|
|
|
|
|
|
<SectionHeader title="Venner på banen" action={<SeeAllLink href="/my-feed" label="Se venneaktivitet" />} />
|
2026-08-01 21:15:07 +02:00
|
|
|
|
{friends.length > 0 ? (
|
|
|
|
|
|
<div className="-mx-4 flex snap-x gap-3 overflow-x-auto px-4 pb-1 sm:mx-0 sm:px-0">
|
Ja, jeg forsto rekkefølgen — steg 1 og 2 er nå ferdig og scratch-verifisert:
Steg 1 (backend, HCP-prosent + Match-HCP): ny migrasjon 051_round_allowance_override.sql (round.allowance_override jsonb), koblet inn i _recompute_side_handicaps/_relative_strokes_for_round + POST/PATCH /rounds. Verifisert presist i scratch: 100%→50%-prosent ga playing_handicap 24/10 → 12/5 nøyaktig som beregnet for hånd, og use_matchplay_handicap slått av ga rå 12/5 i stedet for differensial 7/0. Slagspill upåvirket (regresjon bekreftet).
Steg 2 (kode-fiks, lag-gruppering): ScorecardGrid (live-scoring) og MatchScorecardGrid (post-runde/fremtidig sammenslått side) sorterer nå stabilt på round_side_id før rendering. Bygget en fersk scratch-fourball-runde med akkurat ditt bug-scenario (A→Rødt, B→Blått, C→Rødt, D→Blått, lagt til i den rekkefølgen) og bekreftet visuelt i nettleseren i begge visningene: Rødt lag vises samlet, deretter Blått — ikke lenger interleaved.
Typesjekk + full produksjonsbuild kompilerte rent gjennom hele runden.
Gjenstår før jeg går videre til V0-promptene: migrasjon 051 er kun kjørt i scratch, ikke mot ekte teecup_db. Planen er:
docker exec -i teeoff_db psql -U teeoff_admin -d teecup_db -v ON_ERROR_STOP=1 < 051_round_allowance_override.sql
— ett rent additivt ALTER TABLE round ADD COLUMN allowance_override jsonb, ingen eksisterende rader berøres. Deretter docker compose up -d --build teecup_api teecup_frontend (backend for allowance-koblingen, frontend for gruppering-fiksen).
Bekreft så kjører jeg dette, og går videre til V0-prompt 1 (rundeoppsett-veiviseren).
2026-08-02 08:24:01 +02:00
|
|
|
|
{friends.map((friend, i) => {
|
2026-08-01 21:15:07 +02:00
|
|
|
|
const playing = friend.status === "playing"
|
Ja, jeg forsto rekkefølgen — steg 1 og 2 er nå ferdig og scratch-verifisert:
Steg 1 (backend, HCP-prosent + Match-HCP): ny migrasjon 051_round_allowance_override.sql (round.allowance_override jsonb), koblet inn i _recompute_side_handicaps/_relative_strokes_for_round + POST/PATCH /rounds. Verifisert presist i scratch: 100%→50%-prosent ga playing_handicap 24/10 → 12/5 nøyaktig som beregnet for hånd, og use_matchplay_handicap slått av ga rå 12/5 i stedet for differensial 7/0. Slagspill upåvirket (regresjon bekreftet).
Steg 2 (kode-fiks, lag-gruppering): ScorecardGrid (live-scoring) og MatchScorecardGrid (post-runde/fremtidig sammenslått side) sorterer nå stabilt på round_side_id før rendering. Bygget en fersk scratch-fourball-runde med akkurat ditt bug-scenario (A→Rødt, B→Blått, C→Rødt, D→Blått, lagt til i den rekkefølgen) og bekreftet visuelt i nettleseren i begge visningene: Rødt lag vises samlet, deretter Blått — ikke lenger interleaved.
Typesjekk + full produksjonsbuild kompilerte rent gjennom hele runden.
Gjenstår før jeg går videre til V0-promptene: migrasjon 051 er kun kjørt i scratch, ikke mot ekte teecup_db. Planen er:
docker exec -i teeoff_db psql -U teeoff_admin -d teecup_db -v ON_ERROR_STOP=1 < 051_round_allowance_override.sql
— ett rent additivt ALTER TABLE round ADD COLUMN allowance_override jsonb, ingen eksisterende rader berøres. Deretter docker compose up -d --build teecup_api teecup_frontend (backend for allowance-koblingen, frontend for gruppering-fiksen).
Bekreft så kjører jeg dette, og går videre til V0-prompt 1 (rundeoppsett-veiviseren).
2026-08-02 08:24:01 +02:00
|
|
|
|
const hue = AVATAR_HUES[i % AVATAR_HUES.length]
|
2026-08-01 21:15:07 +02:00
|
|
|
|
return (
|
|
|
|
|
|
<Link
|
|
|
|
|
|
key={friend.round_id}
|
2026-08-07 22:02:56 +02:00
|
|
|
|
href={`/watch/${friend.round_id}`}
|
|
|
|
|
|
className="flex min-w-[15rem] max-w-[16rem] shrink-0 snap-start flex-col gap-3 rounded-2xl border border-clubhouse-border bg-clubhouse-card p-4 shadow-sm transition hover:border-tee-strong/50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tee-strong"
|
2026-08-01 21:15:07 +02:00
|
|
|
|
>
|
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
|
<div className="relative shrink-0">
|
|
|
|
|
|
<span
|
|
|
|
|
|
className="flex size-11 items-center justify-center rounded-full text-sm font-bold tabular-nums"
|
Ja, jeg forsto rekkefølgen — steg 1 og 2 er nå ferdig og scratch-verifisert:
Steg 1 (backend, HCP-prosent + Match-HCP): ny migrasjon 051_round_allowance_override.sql (round.allowance_override jsonb), koblet inn i _recompute_side_handicaps/_relative_strokes_for_round + POST/PATCH /rounds. Verifisert presist i scratch: 100%→50%-prosent ga playing_handicap 24/10 → 12/5 nøyaktig som beregnet for hånd, og use_matchplay_handicap slått av ga rå 12/5 i stedet for differensial 7/0. Slagspill upåvirket (regresjon bekreftet).
Steg 2 (kode-fiks, lag-gruppering): ScorecardGrid (live-scoring) og MatchScorecardGrid (post-runde/fremtidig sammenslått side) sorterer nå stabilt på round_side_id før rendering. Bygget en fersk scratch-fourball-runde med akkurat ditt bug-scenario (A→Rødt, B→Blått, C→Rødt, D→Blått, lagt til i den rekkefølgen) og bekreftet visuelt i nettleseren i begge visningene: Rødt lag vises samlet, deretter Blått — ikke lenger interleaved.
Typesjekk + full produksjonsbuild kompilerte rent gjennom hele runden.
Gjenstår før jeg går videre til V0-promptene: migrasjon 051 er kun kjørt i scratch, ikke mot ekte teecup_db. Planen er:
docker exec -i teeoff_db psql -U teeoff_admin -d teecup_db -v ON_ERROR_STOP=1 < 051_round_allowance_override.sql
— ett rent additivt ALTER TABLE round ADD COLUMN allowance_override jsonb, ingen eksisterende rader berøres. Deretter docker compose up -d --build teecup_api teecup_frontend (backend for allowance-koblingen, frontend for gruppering-fiksen).
Bekreft så kjører jeg dette, og går videre til V0-prompt 1 (rundeoppsett-veiviseren).
2026-08-02 08:24:01 +02:00
|
|
|
|
style={{ backgroundColor: hue.bg, color: hue.ink }}
|
2026-08-01 21:15:07 +02:00
|
|
|
|
>
|
|
|
|
|
|
{friendOnCourseInitials(friend.friend_name)}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
{playing && (
|
2026-08-07 22:02:56 +02:00
|
|
|
|
<span aria-hidden="true" className="absolute -right-0.5 -top-0.5 flex size-3.5 items-center justify-center rounded-full bg-clubhouse-card">
|
|
|
|
|
|
<span className="absolute inline-flex size-2.5 animate-ping rounded-full bg-cup opacity-75" />
|
|
|
|
|
|
<span className="relative inline-flex size-2.5 rounded-full bg-cup" />
|
2026-08-01 21:15:07 +02:00
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex min-w-0 flex-col">
|
2026-08-07 22:02:56 +02:00
|
|
|
|
<span className="truncate text-base font-bold text-clubhouse-ink">{friend.friend_name}</span>
|
|
|
|
|
|
<span
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"inline-flex items-center gap-1 text-xs font-bold uppercase tracking-wide",
|
|
|
|
|
|
playing ? "text-cup-strong" : "text-clubhouse-muted",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
2026-08-01 21:15:07 +02:00
|
|
|
|
{playing ? "Spiller nå" : friend.completed_at ? `Fullførte ${timeAgoLabel(friend.completed_at)}` : "Fullførte nylig"}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex items-center justify-between gap-2">
|
2026-08-07 22:02:56 +02:00
|
|
|
|
<span className="truncate text-sm text-clubhouse-muted">{friend.course_name}</span>
|
|
|
|
|
|
<span
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"shrink-0 rounded-full px-2.5 py-1 text-xs font-bold tabular-nums",
|
|
|
|
|
|
playing ? "bg-tee/15 text-tee-strong" : "bg-clubhouse-field text-clubhouse-muted",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
2026-08-01 21:15:07 +02:00
|
|
|
|
{playing
|
|
|
|
|
|
? `Hull ${friend.holes_played} av ${friend.holes_planned}`
|
|
|
|
|
|
: friend.score_to_par !== null
|
|
|
|
|
|
? formatSigned(friend.score_to_par, 0)
|
|
|
|
|
|
: "Fullført"}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
2026-08-07 22:02:56 +02:00
|
|
|
|
<p className="text-sm leading-relaxed text-clubhouse-muted">Ingen venner er på banen akkurat nå.</p>
|
2026-08-01 21:15:07 +02:00
|
|
|
|
)}
|
|
|
|
|
|
</section>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- 6. Runder --------------------------------------------------------
|
2026-07-25 07:51:32 +02:00
|
|
|
|
|
|
|
|
|
|
function UpcomingRounds({ rounds, loading }: { rounds: Round[]; loading: boolean }) {
|
|
|
|
|
|
const visible = rounds.slice(0, 3)
|
2026-07-18 06:46:09 +02:00
|
|
|
|
return (
|
All done and verified live in the browser. Summary of this round:
"Kommende runder" → "Runder" on the dashboard.
Tjøme course-name duplicate fixed in the real database (one round's stale course_name_snapshot corrected) — all three of your rounds now group under one "Tjøme Golfklubb" entry.
Two new colors, per your request for two: lifted the already-validated --chart-3 (blue → --info) and --chart-4 (gold → --gold) into core design tokens. First real uses: a blue "Hcp spilt til X" badge on round cards, and a gold "Personlig rekord" badge on your best completed round.
Clicking a course under "Spilte baner" now opens a new page listing every round played there (/my-rounds/course/[name]) — caught and fixed a real double-encoding bug here via an actual browser screenshot before shipping.
Aggregated statistics, clickable from the dashboard's "Statistikk" section (/my-rounds/stats): rounds completed, avg to-par, putts/18 holes (implementing your padding rule exactly — unplayed holes count as 2 putts, only for rounds where putt-tracking was on), fairway%, GIR%, one-putt%, scrambling%, sand save%, and chip/bunker/penalty/anywayslag averages per round.
Everything is typechecked, the putts-padding logic is verified against a hand-computed synthetic dataset, and all new screens were checked live in the browser with no console errors.
2026-07-28 07:04:56 +02:00
|
|
|
|
<section aria-label="Runder">
|
|
|
|
|
|
<SectionHeader title="Runder" action={rounds.length > 0 ? <SeeAllLink href="/my-rounds" label="Se alle" /> : null} />
|
2026-07-25 07:51:32 +02:00
|
|
|
|
{loading ? (
|
|
|
|
|
|
<div className="flex justify-center py-8">
|
2026-08-07 22:02:56 +02:00
|
|
|
|
<div aria-hidden="true" className="size-8 animate-spin rounded-full border-4 border-tee-strong/20 border-t-tee-strong" />
|
2026-07-18 06:46:09 +02:00
|
|
|
|
</div>
|
2026-07-25 07:51:32 +02:00
|
|
|
|
) : visible.length > 0 ? (
|
|
|
|
|
|
<div className="flex flex-col gap-3">
|
|
|
|
|
|
{visible.map((round) => (
|
|
|
|
|
|
<RoundCard key={round.id} round={round} />
|
|
|
|
|
|
))}
|
2026-07-18 06:46:09 +02:00
|
|
|
|
</div>
|
2026-07-25 07:51:32 +02:00
|
|
|
|
) : (
|
|
|
|
|
|
<EmptyState icon={Flag} title="Ingen runder på gang" description="Start en runde når du er på banen, så dukker den opp her mens du spiller.">
|
2026-08-01 21:15:07 +02:00
|
|
|
|
<ShortcutButton icon={Flag} label="Ny runde" href="/my-rounds/new" />
|
2026-07-25 07:51:32 +02:00
|
|
|
|
</EmptyState>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</section>
|
2026-07-18 06:46:09 +02:00
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-01 21:15:07 +02:00
|
|
|
|
// --- 7. Kommende turneringer -----------------------------------------------
|
2026-07-21 09:18:07 +02:00
|
|
|
|
|
2026-07-29 12:44:51 +02:00
|
|
|
|
function UpcomingTournaments({
|
|
|
|
|
|
entries,
|
|
|
|
|
|
onNewTournament,
|
|
|
|
|
|
onJoinWithCode,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
entries: CombinedTournament[]
|
|
|
|
|
|
onNewTournament: () => void
|
|
|
|
|
|
onJoinWithCode: () => void
|
|
|
|
|
|
}) {
|
2026-07-25 07:51:32 +02:00
|
|
|
|
return (
|
Ja, jeg forsto rekkefølgen — steg 1 og 2 er nå ferdig og scratch-verifisert:
Steg 1 (backend, HCP-prosent + Match-HCP): ny migrasjon 051_round_allowance_override.sql (round.allowance_override jsonb), koblet inn i _recompute_side_handicaps/_relative_strokes_for_round + POST/PATCH /rounds. Verifisert presist i scratch: 100%→50%-prosent ga playing_handicap 24/10 → 12/5 nøyaktig som beregnet for hånd, og use_matchplay_handicap slått av ga rå 12/5 i stedet for differensial 7/0. Slagspill upåvirket (regresjon bekreftet).
Steg 2 (kode-fiks, lag-gruppering): ScorecardGrid (live-scoring) og MatchScorecardGrid (post-runde/fremtidig sammenslått side) sorterer nå stabilt på round_side_id før rendering. Bygget en fersk scratch-fourball-runde med akkurat ditt bug-scenario (A→Rødt, B→Blått, C→Rødt, D→Blått, lagt til i den rekkefølgen) og bekreftet visuelt i nettleseren i begge visningene: Rødt lag vises samlet, deretter Blått — ikke lenger interleaved.
Typesjekk + full produksjonsbuild kompilerte rent gjennom hele runden.
Gjenstår før jeg går videre til V0-promptene: migrasjon 051 er kun kjørt i scratch, ikke mot ekte teecup_db. Planen er:
docker exec -i teeoff_db psql -U teeoff_admin -d teecup_db -v ON_ERROR_STOP=1 < 051_round_allowance_override.sql
— ett rent additivt ALTER TABLE round ADD COLUMN allowance_override jsonb, ingen eksisterende rader berøres. Deretter docker compose up -d --build teecup_api teecup_frontend (backend for allowance-koblingen, frontend for gruppering-fiksen).
Bekreft så kjører jeg dette, og går videre til V0-prompt 1 (rundeoppsett-veiviseren).
2026-08-02 08:24:01 +02:00
|
|
|
|
<section id="kommende-turneringer" aria-label="Kommende turneringer" className="scroll-mt-20">
|
2026-07-25 07:51:32 +02:00
|
|
|
|
<SectionHeader title="Kommende turneringer" />
|
|
|
|
|
|
{entries.length > 0 ? (
|
|
|
|
|
|
<div className="flex flex-col gap-3">
|
|
|
|
|
|
{entries.map((e) => (
|
|
|
|
|
|
<TournamentCard key={e.tournament.id} tournament={e.tournament} orgId={e.orgId} organizer={e.organizer} />
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<EmptyState icon={Trophy} title="Ingen turneringer ennå" description="Bli med i en turnering med en kode fra arrangøren, eller opprett din egen.">
|
2026-07-29 12:44:51 +02:00
|
|
|
|
<ShortcutButton icon={Trophy} label="Ny turnering" onClick={onNewTournament} />
|
|
|
|
|
|
<ShortcutButton icon={KeyRound} label="Bli med med kode" variant="outline" onClick={onJoinWithCode} />
|
2026-07-25 07:51:32 +02:00
|
|
|
|
</EmptyState>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</section>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
2026-07-21 09:18:07 +02:00
|
|
|
|
|
2026-08-01 21:15:07 +02:00
|
|
|
|
// --- 8. Statistikk ---------------------------------------------------------
|
2026-07-21 09:18:07 +02:00
|
|
|
|
|
2026-07-25 07:51:32 +02:00
|
|
|
|
function Sparkline({ values }: { values: number[] }) {
|
|
|
|
|
|
if (values.length < 2) return null
|
|
|
|
|
|
const max = Math.max(...values)
|
|
|
|
|
|
const min = Math.min(...values)
|
|
|
|
|
|
const range = max - min || 1
|
2026-07-21 09:18:07 +02:00
|
|
|
|
return (
|
2026-07-25 07:51:32 +02:00
|
|
|
|
<div aria-hidden="true" className="flex h-5 items-end gap-0.5">
|
|
|
|
|
|
{values.map((v, i) => {
|
|
|
|
|
|
const height = 30 + ((v - min) / range) * 70
|
2026-08-01 21:15:07 +02:00
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={i}
|
2026-08-07 22:02:56 +02:00
|
|
|
|
className={cn("w-1 rounded-full", i === values.length - 1 ? "bg-tee-strong" : "bg-clubhouse-border")}
|
|
|
|
|
|
style={{ height: `${height}%` }}
|
2026-08-01 21:15:07 +02:00
|
|
|
|
/>
|
|
|
|
|
|
)
|
2026-07-25 07:51:32 +02:00
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
2026-07-21 09:18:07 +02:00
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-25 07:51:32 +02:00
|
|
|
|
function StatTile({ label, value, children }: { label: string; value: string; children?: React.ReactNode }) {
|
|
|
|
|
|
return (
|
2026-08-07 22:02:56 +02:00
|
|
|
|
<div className="flex flex-col gap-1.5 rounded-2xl border border-clubhouse-border bg-clubhouse-card p-4 shadow-sm">
|
|
|
|
|
|
<span className="text-xs font-medium uppercase tracking-wide text-clubhouse-muted">{label}</span>
|
|
|
|
|
|
<span className="text-2xl font-extrabold tabular-nums text-clubhouse-ink sm:text-3xl">{value}</span>
|
2026-07-25 07:51:32 +02:00
|
|
|
|
{children}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
2026-07-18 06:46:09 +02:00
|
|
|
|
|
2026-07-25 07:51:32 +02:00
|
|
|
|
function StatsSection({
|
|
|
|
|
|
roundsCount,
|
|
|
|
|
|
hcpNow,
|
|
|
|
|
|
hcpTrend,
|
|
|
|
|
|
hcpHistory,
|
|
|
|
|
|
avgToPar,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
roundsCount: number
|
|
|
|
|
|
hcpNow: string
|
|
|
|
|
|
hcpTrend: number | null
|
|
|
|
|
|
hcpHistory: number[]
|
|
|
|
|
|
avgToPar: string
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const TrendIcon = hcpTrend !== null && hcpTrend < 0 ? TrendingDown : TrendingUp
|
|
|
|
|
|
return (
|
|
|
|
|
|
<section aria-label="Statistikk">
|
All done and verified live in the browser. Summary of this round:
"Kommende runder" → "Runder" on the dashboard.
Tjøme course-name duplicate fixed in the real database (one round's stale course_name_snapshot corrected) — all three of your rounds now group under one "Tjøme Golfklubb" entry.
Two new colors, per your request for two: lifted the already-validated --chart-3 (blue → --info) and --chart-4 (gold → --gold) into core design tokens. First real uses: a blue "Hcp spilt til X" badge on round cards, and a gold "Personlig rekord" badge on your best completed round.
Clicking a course under "Spilte baner" now opens a new page listing every round played there (/my-rounds/course/[name]) — caught and fixed a real double-encoding bug here via an actual browser screenshot before shipping.
Aggregated statistics, clickable from the dashboard's "Statistikk" section (/my-rounds/stats): rounds completed, avg to-par, putts/18 holes (implementing your padding rule exactly — unplayed holes count as 2 putts, only for rounds where putt-tracking was on), fairway%, GIR%, one-putt%, scrambling%, sand save%, and chip/bunker/penalty/anywayslag averages per round.
Everything is typechecked, the putts-padding logic is verified against a hand-computed synthetic dataset, and all new screens were checked live in the browser with no console errors.
2026-07-28 07:04:56 +02:00
|
|
|
|
<SectionHeader
|
|
|
|
|
|
title="Statistikk"
|
|
|
|
|
|
action={roundsCount > 0 ? <SeeAllLink href="/my-rounds/stats" label="Se full statistikk" /> : null}
|
|
|
|
|
|
/>
|
2026-07-25 07:51:32 +02:00
|
|
|
|
<div className="grid grid-cols-3 gap-3">
|
|
|
|
|
|
<StatTile label="Runder" value={String(roundsCount)} />
|
|
|
|
|
|
<StatTile label="HCP nå" value={hcpNow}>
|
|
|
|
|
|
{hcpTrend !== null ? (
|
|
|
|
|
|
<div className="flex items-center justify-between gap-1">
|
2026-08-07 22:02:56 +02:00
|
|
|
|
<span className="inline-flex items-center gap-1 text-xs font-bold text-tee-strong">
|
|
|
|
|
|
<TrendIcon aria-hidden="true" className="size-4" />
|
2026-07-25 07:51:32 +02:00
|
|
|
|
<span className="tabular-nums">{formatSigned(hcpTrend, 1)}</span>
|
|
|
|
|
|
<span className="sr-only">{hcpTrend < 0 ? "handicap gått ned" : "handicap gått opp"}</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<Sparkline values={hcpHistory} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
2026-08-07 22:02:56 +02:00
|
|
|
|
<span className="text-xs font-medium text-clubhouse-muted">Ingen data</span>
|
2026-07-25 07:51:32 +02:00
|
|
|
|
)}
|
|
|
|
|
|
</StatTile>
|
|
|
|
|
|
<StatTile label="Snitt til par" value={avgToPar} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
2026-07-18 06:46:09 +02:00
|
|
|
|
|
2026-08-01 21:15:07 +02:00
|
|
|
|
// --- 9. Spilte baner ---------------------------------------------------------
|
2026-07-18 06:46:09 +02:00
|
|
|
|
|
2026-07-25 07:51:32 +02:00
|
|
|
|
function PlayedCourses({ courses }: { courses: { name: string; visits: number; lastPlayed: string }[] }) {
|
2026-07-18 06:46:09 +02:00
|
|
|
|
return (
|
2026-07-25 07:51:32 +02:00
|
|
|
|
<section aria-label="Spilte baner">
|
|
|
|
|
|
<SectionHeader title="Spilte baner" />
|
|
|
|
|
|
{courses.length > 0 ? (
|
2026-08-07 22:02:56 +02:00
|
|
|
|
<ul className="flex flex-col overflow-hidden rounded-2xl border border-clubhouse-border bg-clubhouse-card shadow-sm">
|
2026-08-01 21:15:07 +02:00
|
|
|
|
{courses.map((course, i) => {
|
2026-07-25 07:51:32 +02:00
|
|
|
|
const roundLabel = course.visits === 1 ? "runde" : "runder"
|
|
|
|
|
|
return (
|
2026-08-07 22:02:56 +02:00
|
|
|
|
<li key={course.name} className={cn(i > 0 && "border-t border-clubhouse-border")}>
|
All done and verified live in the browser. Summary of this round:
"Kommende runder" → "Runder" on the dashboard.
Tjøme course-name duplicate fixed in the real database (one round's stale course_name_snapshot corrected) — all three of your rounds now group under one "Tjøme Golfklubb" entry.
Two new colors, per your request for two: lifted the already-validated --chart-3 (blue → --info) and --chart-4 (gold → --gold) into core design tokens. First real uses: a blue "Hcp spilt til X" badge on round cards, and a gold "Personlig rekord" badge on your best completed round.
Clicking a course under "Spilte baner" now opens a new page listing every round played there (/my-rounds/course/[name]) — caught and fixed a real double-encoding bug here via an actual browser screenshot before shipping.
Aggregated statistics, clickable from the dashboard's "Statistikk" section (/my-rounds/stats): rounds completed, avg to-par, putts/18 holes (implementing your padding rule exactly — unplayed holes count as 2 putts, only for rounds where putt-tracking was on), fairway%, GIR%, one-putt%, scrambling%, sand save%, and chip/bunker/penalty/anywayslag averages per round.
Everything is typechecked, the putts-padding logic is verified against a hand-computed synthetic dataset, and all new screens were checked live in the browser with no console errors.
2026-07-28 07:04:56 +02:00
|
|
|
|
<Link
|
|
|
|
|
|
href={`/my-rounds/course/${encodeURIComponent(course.name)}`}
|
2026-08-07 22:02:56 +02:00
|
|
|
|
className="flex min-h-[60px] items-center gap-3 px-4 py-3 transition-all duration-200 ease-in-out hover:bg-clubhouse-field focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-tee-strong"
|
All done and verified live in the browser. Summary of this round:
"Kommende runder" → "Runder" on the dashboard.
Tjøme course-name duplicate fixed in the real database (one round's stale course_name_snapshot corrected) — all three of your rounds now group under one "Tjøme Golfklubb" entry.
Two new colors, per your request for two: lifted the already-validated --chart-3 (blue → --info) and --chart-4 (gold → --gold) into core design tokens. First real uses: a blue "Hcp spilt til X" badge on round cards, and a gold "Personlig rekord" badge on your best completed round.
Clicking a course under "Spilte baner" now opens a new page listing every round played there (/my-rounds/course/[name]) — caught and fixed a real double-encoding bug here via an actual browser screenshot before shipping.
Aggregated statistics, clickable from the dashboard's "Statistikk" section (/my-rounds/stats): rounds completed, avg to-par, putts/18 holes (implementing your padding rule exactly — unplayed holes count as 2 putts, only for rounds where putt-tracking was on), fairway%, GIR%, one-putt%, scrambling%, sand save%, and chip/bunker/penalty/anywayslag averages per round.
Everything is typechecked, the putts-padding logic is verified against a hand-computed synthetic dataset, and all new screens were checked live in the browser with no console errors.
2026-07-28 07:04:56 +02:00
|
|
|
|
>
|
2026-08-07 22:02:56 +02:00
|
|
|
|
<span className="flex size-10 shrink-0 items-center justify-center rounded-xl bg-tee/12 text-tee-strong">
|
|
|
|
|
|
<MapPin aria-hidden="true" className="size-5" />
|
All done and verified live in the browser. Summary of this round:
"Kommende runder" → "Runder" on the dashboard.
Tjøme course-name duplicate fixed in the real database (one round's stale course_name_snapshot corrected) — all three of your rounds now group under one "Tjøme Golfklubb" entry.
Two new colors, per your request for two: lifted the already-validated --chart-3 (blue → --info) and --chart-4 (gold → --gold) into core design tokens. First real uses: a blue "Hcp spilt til X" badge on round cards, and a gold "Personlig rekord" badge on your best completed round.
Clicking a course under "Spilte baner" now opens a new page listing every round played there (/my-rounds/course/[name]) — caught and fixed a real double-encoding bug here via an actual browser screenshot before shipping.
Aggregated statistics, clickable from the dashboard's "Statistikk" section (/my-rounds/stats): rounds completed, avg to-par, putts/18 holes (implementing your padding rule exactly — unplayed holes count as 2 putts, only for rounds where putt-tracking was on), fairway%, GIR%, one-putt%, scrambling%, sand save%, and chip/bunker/penalty/anywayslag averages per round.
Everything is typechecked, the putts-padding logic is verified against a hand-computed synthetic dataset, and all new screens were checked live in the browser with no console errors.
2026-07-28 07:04:56 +02:00
|
|
|
|
</span>
|
|
|
|
|
|
<span className="flex min-w-0 flex-1 flex-col">
|
2026-08-07 22:02:56 +02:00
|
|
|
|
<span className="truncate text-base font-bold text-clubhouse-ink">{course.name}</span>
|
|
|
|
|
|
<span className="truncate text-sm font-medium text-clubhouse-muted">
|
All done and verified live in the browser. Summary of this round:
"Kommende runder" → "Runder" on the dashboard.
Tjøme course-name duplicate fixed in the real database (one round's stale course_name_snapshot corrected) — all three of your rounds now group under one "Tjøme Golfklubb" entry.
Two new colors, per your request for two: lifted the already-validated --chart-3 (blue → --info) and --chart-4 (gold → --gold) into core design tokens. First real uses: a blue "Hcp spilt til X" badge on round cards, and a gold "Personlig rekord" badge on your best completed round.
Clicking a course under "Spilte baner" now opens a new page listing every round played there (/my-rounds/course/[name]) — caught and fixed a real double-encoding bug here via an actual browser screenshot before shipping.
Aggregated statistics, clickable from the dashboard's "Statistikk" section (/my-rounds/stats): rounds completed, avg to-par, putts/18 holes (implementing your padding rule exactly — unplayed holes count as 2 putts, only for rounds where putt-tracking was on), fairway%, GIR%, one-putt%, scrambling%, sand save%, and chip/bunker/penalty/anywayslag averages per round.
Everything is typechecked, the putts-padding logic is verified against a hand-computed synthetic dataset, and all new screens were checked live in the browser with no console errors.
2026-07-28 07:04:56 +02:00
|
|
|
|
{course.visits} {roundLabel} · Sist {formatDate(course.lastPlayed)}
|
|
|
|
|
|
</span>
|
2026-07-25 07:51:32 +02:00
|
|
|
|
</span>
|
2026-08-07 22:02:56 +02:00
|
|
|
|
<ChevronRight aria-hidden="true" className="size-5 shrink-0 text-clubhouse-muted" />
|
All done and verified live in the browser. Summary of this round:
"Kommende runder" → "Runder" on the dashboard.
Tjøme course-name duplicate fixed in the real database (one round's stale course_name_snapshot corrected) — all three of your rounds now group under one "Tjøme Golfklubb" entry.
Two new colors, per your request for two: lifted the already-validated --chart-3 (blue → --info) and --chart-4 (gold → --gold) into core design tokens. First real uses: a blue "Hcp spilt til X" badge on round cards, and a gold "Personlig rekord" badge on your best completed round.
Clicking a course under "Spilte baner" now opens a new page listing every round played there (/my-rounds/course/[name]) — caught and fixed a real double-encoding bug here via an actual browser screenshot before shipping.
Aggregated statistics, clickable from the dashboard's "Statistikk" section (/my-rounds/stats): rounds completed, avg to-par, putts/18 holes (implementing your padding rule exactly — unplayed holes count as 2 putts, only for rounds where putt-tracking was on), fairway%, GIR%, one-putt%, scrambling%, sand save%, and chip/bunker/penalty/anywayslag averages per round.
Everything is typechecked, the putts-padding logic is verified against a hand-computed synthetic dataset, and all new screens were checked live in the browser with no console errors.
2026-07-28 07:04:56 +02:00
|
|
|
|
</Link>
|
2026-07-25 07:51:32 +02:00
|
|
|
|
</li>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<EmptyState icon={MapPin} title="Ingen baner registrert" description="Banene du spiller dukker opp her, med antall besøk og når du sist spilte." />
|
|
|
|
|
|
)}
|
|
|
|
|
|
</section>
|
2026-07-18 06:46:09 +02:00
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-01 21:15:07 +02:00
|
|
|
|
// --- 10. Venner ---------------------------------------------------------------
|
|
|
|
|
|
// ADR-036 fase 1 -- ekte data fra GET /friends. Distinkt fra "Venner på
|
|
|
|
|
|
// banen" (seksjon 5) -- dette er den generelle vennlisten, ikke aktivitet.
|
2026-07-25 08:37:07 +02:00
|
|
|
|
|
|
|
|
|
|
function friendInitials(first: string | null, last: string | null): string {
|
|
|
|
|
|
const f = (first ?? "").trim()[0] ?? ""
|
|
|
|
|
|
const l = (last ?? "").trim()[0] ?? ""
|
|
|
|
|
|
return (f + l).toUpperCase() || "?"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function FriendsSection({ summary }: { summary: ApiFriendsSummary | null }) {
|
|
|
|
|
|
const friends = summary?.friends ?? []
|
|
|
|
|
|
const pending = summary?.incoming_requests.length ?? 0
|
2026-08-01 21:15:07 +02:00
|
|
|
|
const visible = friends.slice(0, 4)
|
|
|
|
|
|
const overflow = friends.length - visible.length
|
2026-07-25 07:51:32 +02:00
|
|
|
|
|
2026-07-18 06:46:09 +02:00
|
|
|
|
return (
|
2026-07-25 07:51:32 +02:00
|
|
|
|
<section aria-label="Venner">
|
|
|
|
|
|
<SectionHeader title="Venner" />
|
2026-07-25 08:37:07 +02:00
|
|
|
|
{friends.length > 0 ? (
|
2026-08-07 22:02:56 +02:00
|
|
|
|
<div className="flex flex-col gap-4 rounded-2xl border border-clubhouse-border bg-clubhouse-card p-4 shadow-sm sm:flex-row sm:items-center sm:justify-between">
|
2026-07-25 08:37:07 +02:00
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
|
<div className="flex items-center">
|
2026-08-01 21:15:07 +02:00
|
|
|
|
{visible.map((friend, i) => (
|
2026-07-25 08:37:07 +02:00
|
|
|
|
<span
|
|
|
|
|
|
key={i}
|
2026-08-07 22:02:56 +02:00
|
|
|
|
className="flex size-11 items-center justify-center rounded-full bg-clubhouse-field text-sm font-bold text-clubhouse-ink ring-2 ring-clubhouse-card"
|
|
|
|
|
|
style={{ marginLeft: i > 0 ? "-0.75rem" : undefined }}
|
2026-07-25 08:37:07 +02:00
|
|
|
|
>
|
|
|
|
|
|
{friendInitials(friend.first_name, friend.last_name)}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
))}
|
2026-08-01 21:15:07 +02:00
|
|
|
|
{overflow > 0 && (
|
|
|
|
|
|
<span
|
2026-08-07 22:02:56 +02:00
|
|
|
|
className="flex size-11 items-center justify-center rounded-full bg-clubhouse-ink text-xs font-bold tabular-nums text-white ring-2 ring-clubhouse-card"
|
|
|
|
|
|
style={{ marginLeft: "-0.75rem" }}
|
2026-08-01 21:15:07 +02:00
|
|
|
|
>
|
|
|
|
|
|
+{overflow}
|
2026-07-29 12:44:51 +02:00
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
2026-07-25 08:37:07 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex flex-col">
|
2026-08-07 22:02:56 +02:00
|
|
|
|
<span className="text-base font-bold text-clubhouse-ink">
|
2026-07-25 08:37:07 +02:00
|
|
|
|
{friends.length} {friends.length === 1 ? "venn" : "venner"}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
{pending > 0 ? (
|
2026-08-07 22:02:56 +02:00
|
|
|
|
<span className="inline-flex w-fit items-center gap-1.5 rounded-full bg-cup/15 px-2.5 py-0.5 text-xs font-bold text-cup-strong">
|
|
|
|
|
|
<span aria-hidden="true" className="size-1.5 rounded-full bg-cup-strong" />
|
2026-07-25 08:37:07 +02:00
|
|
|
|
{pending} ventende {pending === 1 ? "forespørsel" : "forespørsler"}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
) : (
|
2026-08-07 22:02:56 +02:00
|
|
|
|
<span className="text-sm font-medium text-clubhouse-muted">Ingen nye forespørsler</span>
|
2026-07-25 08:37:07 +02:00
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<ShortcutButton icon={ArrowRight} label="Se venner" href="/my-friends" variant="outline" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<EmptyState icon={UserPlus} title="Finn vennene dine" description="Søk opp folk du spiller med, så kan dere følge hverandres runder og resultater.">
|
|
|
|
|
|
<ShortcutButton icon={UserPlus} label="Søk etter venner" href="/my-friends" />
|
|
|
|
|
|
</EmptyState>
|
|
|
|
|
|
)}
|
2026-07-25 07:51:32 +02:00
|
|
|
|
</section>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-01 21:15:07 +02:00
|
|
|
|
// --- 11. Organisasjoner (bevisst nedtonet) -----------------------------------
|
2026-07-25 07:51:32 +02:00
|
|
|
|
|
|
|
|
|
|
function OrganizationsFooter({ organizations }: { organizations: MyOrg[] }) {
|
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
|
if (organizations.length === 0) return null
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="flex justify-center pt-2">
|
|
|
|
|
|
<DropdownMenu>
|
2026-08-07 22:02:56 +02:00
|
|
|
|
<DropdownMenuTrigger className="inline-flex min-h-[44px] items-center gap-2 rounded-lg px-3 text-sm font-medium text-clubhouse-muted transition-all duration-200 ease-in-out hover:text-clubhouse-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tee-strong">
|
2026-07-25 07:51:32 +02:00
|
|
|
|
<Building2 aria-hidden="true" className="size-4" />
|
|
|
|
|
|
Dine organisasjoner ({organizations.length})
|
|
|
|
|
|
</DropdownMenuTrigger>
|
|
|
|
|
|
<DropdownMenuContent align="center" className="w-64 rounded-2xl p-1.5">
|
|
|
|
|
|
{organizations.map((o) => (
|
|
|
|
|
|
<DropdownMenuItem
|
|
|
|
|
|
key={o.organization_id}
|
|
|
|
|
|
onClick={() => router.push(`/organizations/${o.organization_id}/members?name=${encodeURIComponent(o.name)}`)}
|
|
|
|
|
|
className="cursor-pointer rounded-xl px-3 py-2.5 text-sm font-semibold"
|
|
|
|
|
|
>
|
|
|
|
|
|
{o.name}
|
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</DropdownMenuContent>
|
|
|
|
|
|
</DropdownMenu>
|
2026-07-18 06:46:09 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|