diff --git a/frontend/app/globals.css b/frontend/app/globals.css index 8ac73cf..7c89034 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -237,3 +237,34 @@ --color-clubhouse-border: var(--clubhouse-border); --color-clubhouse-field: var(--clubhouse-field); } + +/* ------------------------------------------------------------------ */ +/* /my-rounds/new -- egen V0-utforskning (2026-08-08), sendt som en */ +/* rendyrket TEKNISK spesifikasjon uten noen visuell føring -- V0 fikk */ +/* fritt spillerom og valgte en tredje, egen palett (kald blå/grå), */ +/* uavhengig av både "Forest Green" og "clubhouse". Skopet KUN til */ +/* components/ny-runde/* via arbitrary-value var()-referanser */ +/* (bg-[var(--nr-x)]), ikke via @theme inline -- rører derfor ingen */ +/* andre tokens/sider. Eksporten refererte `font-display`/Bricolage -- */ +/* bevisst IKKE tatt med her (samme utelatelse som clubhouse-runden), */ +/* for å unngå en tredje font-avhengighet for én skjerm; ingen av */ +/* filene under bruker faktisk `font-display` etter integrering. */ +/* ------------------------------------------------------------------ */ +:root { + --nr-bg: #e9edf2; + --nr-surface: #ffffff; + --nr-surface-2: #f4f6f9; + --nr-ink: #171b21; + --nr-muted: #59616d; + --nr-faint: #8b94a1; + --nr-border: #d5dae1; + --nr-border-strong: #b9c1cc; + --nr-accent: #1f5fd6; + --nr-accent-ink: #ffffff; + --nr-accent-soft: #e6eefc; + --nr-accent-ring: #9fbdf3; + --nr-danger: #b42318; + --nr-danger-soft: #fdeceb; + --nr-ok: #1f7a44; + --nr-ok-soft: #e6f4ec; +} diff --git a/frontend/components/account-settings.tsx b/frontend/components/account-settings.tsx index c3db3ab..b651d40 100644 --- a/frontend/components/account-settings.tsx +++ b/frontend/components/account-settings.tsx @@ -35,7 +35,7 @@ import { Label } from "@/components/ui/label" import { Wordmark } from "@/components/wordmark" import { TwoFactorSetupForm } from "@/components/two-factor-flow" import { CourseTemplateEditor, type CourseTemplateValues } from "@/components/course-template-editor" -import { BottomTabBar } from "@/components/dashboard" +import { BottomNav } from "@/components/teecup/bottom-nav" // ADR-031: personlig profil, ETT sett per konto (app_user), atskilt fra de // org-scopede `player`-radene organisatorer administrerer. @@ -339,7 +339,7 @@ export function AccountSettings() { )} - + ) } @@ -416,7 +416,7 @@ function ProfileOnboarding({ me, onComplete }: { me: Me; onComplete: () => void try { await fetch("/auth/logout", { method: "POST", credentials: "include" }) } finally { - router.replace("/") + router.replace("/logg-inn") } } diff --git a/frontend/components/course-rounds.tsx b/frontend/components/course-rounds.tsx index c5f02f8..69aa5e1 100644 --- a/frontend/components/course-rounds.tsx +++ b/frontend/components/course-rounds.tsx @@ -101,7 +101,7 @@ export function CourseRounds({ courseName }: { courseName: string }) { try { const res = await fetch("/rounds", { credentials: "include" }) if (res.status === 401) { - router.replace("/") + router.replace("/logg-inn") return } if (!res.ok) throw new Error(`rounds: ${res.status}`) diff --git a/frontend/components/dashboard.tsx b/frontend/components/dashboard.tsx index 671f550..28b5aeb 100644 --- a/frontend/components/dashboard.tsx +++ b/frontend/components/dashboard.tsx @@ -36,11 +36,18 @@ // visningsfonten (samme utelatelse som `/logg-inn` selv gjorde) -- Nunito // beholdt for å unngå en ekstra font-avhengighet for kun to skjermer. // -// Omfangsgrenser (delte komponenter IKKE re-stylet her, se CHANGELOG.md): -// `RoundCard`/`TournamentCard` (også brukt av /my-rounds) og `BottomTabBar` -// (delt med de fem sidene som nettopp fikk den, venter på egen V0-prompt) -// beholder sin eksisterende styling inntil videre -- kun selve -// dashbord-skjermens EGNE, ikke-delte JSX er re-stylet i denne runden. +// 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). import type React from "react" import { useCallback, useEffect, useState } from "react" @@ -50,18 +57,14 @@ import { ArrowRight, Bell, Building2, - CalendarDays, ChevronRight, Flag, - Home, KeyRound, LogOut, MapPin, - MoreHorizontal, Trophy, TrendingDown, TrendingUp, - User, UserCircle, UserPlus, X, @@ -77,36 +80,12 @@ import { } from "@/components/ui/dropdown-menu" import { InstallPrompt } from "@/components/install-prompt" import { TeeCupWordmark } from "@/components/teecup/wordmark" +import { BottomNav } from "@/components/teecup/bottom-nav" import { RoundCard, type Round } from "@/components/round-card" import { TournamentCard, type Tournament } from "@/components/tournament-card" import { type TournamentStatus } from "@/components/tournament-status-badge" import { cn } from "@/lib/utils" -// Låst "Forest Green"-palett -- v2 (2026-08-01, dashbord-fargekorrigering -// mot Stitch sitt faktiske skjermbilde). ALDRI #84b089/#a4a5a5 som tekst på -// mørkegrønn (#1a4325) -- målt ~4.5:1, under vårt AAA-mål. `primary` er -// bevisst IKKE Stitch sin rå pikselverdi (#2d950c, som målte kun 3,88:1 med -// hvit tekst -- feiler AA for vanlig tekst) -- egen kontrastkorrigert -// variant (#1f6b08, ~6,6:1) i samme fargefamilie. -const C = { - card: "#ffffff", - glass: "rgba(255, 255, 255, 0.78)", - ink: "#012c11", // primærtekst (nesten-svart grønn), IKKE lenger knappe-bakgrunn - inkOn: "#ffffff", - primary: "#1f6b08", // lys, innbydende CTA-grønn -- hvit tekst/ikon (~6,6:1) - primaryInk: "#ffffff", - accent: "#1a4325", // mørkegrønn aksent (installer-banner-gradient) - accentMuted: "#bfeec4", - successBg: "#91f78e", - successInk: "#00731e", - muted: "#424941", - border: "#c1c9bf", - borderSoft: "#e4ebe3", // lysere strek for flatere/lysere overflater (Venner på banen) - tint: "#edf3ef", - warmBg: "#ffedd5", - warmInk: "#9a3412", -} as const - // 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. @@ -328,7 +307,7 @@ export function Dashboard() { try { const res = await fetch("/auth/me", { credentials: "include" }) if (!res.ok) { - router.replace("/") + router.replace("/logg-inn") return } const data: Me = await res.json() @@ -338,7 +317,7 @@ export function Dashboard() { } setMe(data) } catch { - router.replace("/") + router.replace("/logg-inn") } finally { setLoadingMe(false) } @@ -446,7 +425,7 @@ export function Dashboard() { try { await fetch("/auth/logout", { method: "POST", credentials: "include" }) } finally { - router.replace("/") + router.replace("/logg-inn") } } @@ -458,7 +437,7 @@ export function Dashboard() { ) } - if (!me) return null // router.replace("/") er allerede utløst + if (!me) return null // router.replace("/logg-inn") er allerede utløst const rows = rounds ?? [] const activeRounds = rows.filter((r) => !r.completed_at) @@ -565,57 +544,11 @@ export function Dashboard() { - + ) } -// Fast bunn-fanerad (2026-08-01, brukerens eget ønske etter å ha sett -// Stitch sin versjon) -- app-bred navigasjon, vist på ALLE skjermer som -// importerer denne. hrefs er bevisst RETTET fra V0 sitt forslag (/rounds, -// /tournaments) til de faktiske frontend-rutene -- /rounds er reservert -// API-prefiks (samme kollisjonsklasse som /rounds->/my-rounds tidligere i -// prosjektet), og det finnes ingen egen "/tournaments"-liste-side, kun -// /tournaments/[id]. "Turneringer" scroller derfor til dashbordets egen -// "Kommende turneringer"-seksjon i stedet for å lenke til en side som ikke -// finnes. "Mer" peker til en ny, minimal samleside (se app/more/page.tsx). -type TabKey = "home" | "rounds" | "tournaments" | "profile" | "more" - -export function BottomTabBar({ active }: { active: TabKey }) { - const tabs: { key: TabKey; icon: typeof Flag; label: string; href: string }[] = [ - { key: "home", icon: Home, label: "Hjem", href: "/dashboard" }, - { key: "rounds", icon: CalendarDays, label: "Runder", href: "/my-rounds" }, - { key: "tournaments", icon: Trophy, label: "Turneringer", href: "/dashboard#kommende-turneringer" }, - { key: "profile", icon: User, label: "Profil", href: "/account" }, - { key: "more", icon: MoreHorizontal, label: "Mer", href: "/more" }, - ] - return ( - - ) -} // V0-designet (varsler-runden, 2026-07-25) -- lenker til /my-notifications, // IKKE /notifications (API-prefiks, samme kollisjonsklasse som diff --git a/frontend/components/feed.tsx b/frontend/components/feed.tsx index e1d9986..14a46c6 100644 --- a/frontend/components/feed.tsx +++ b/frontend/components/feed.tsx @@ -5,7 +5,7 @@ import Link from "next/link" import { cn } from "@/lib/utils" import { PostEngagement, type ReactionSummary } from "./post-engagement" -import { BottomTabBar } from "@/components/dashboard" +import { BottomNav } from "@/components/teecup/bottom-nav" /** Rå feed-oppføring fra backend. */ export type ApiFeedEntry = { @@ -185,7 +185,7 @@ export function Feed() { )} - + ) } diff --git a/frontend/components/friends.tsx b/frontend/components/friends.tsx index efc46f1..467c813 100644 --- a/frontend/components/friends.tsx +++ b/frontend/components/friends.tsx @@ -23,7 +23,7 @@ import { import { Input } from "@/components/ui/input" import { Wordmark } from "@/components/wordmark" import { cn } from "@/lib/utils" -import { BottomTabBar } from "@/components/dashboard" +import { BottomNav } from "@/components/teecup/bottom-nav" // --- Kategorier -------------------------------------------------------------- // Rekkefølge og koder MÅ matche `Category` i app/routers/friends.py nøyaktig. @@ -130,7 +130,7 @@ export function Friends() { try { const res = await fetch("/friends", { credentials: "include" }) if (res.status === 401) { - router.replace("/") + router.replace("/logg-inn") return } if (!res.ok) throw new Error(`friends: ${res.status}`) @@ -186,7 +186,7 @@ export function Friends() { - + ) } diff --git a/frontend/components/more-menu.tsx b/frontend/components/more-menu.tsx index 7faecdf..4ffdad1 100644 --- a/frontend/components/more-menu.tsx +++ b/frontend/components/more-menu.tsx @@ -1,16 +1,17 @@ "use client" -// "Mer"-siden (2026-08-01) -- den femte fanen i den nye bunn-navigasjonen -// (dashboard.tsx sin BottomTabBar) trengte et reelt mål, ikke en lenke til -// en side som ikke fantes. Samler destinasjoner som allerede finnes andre -// steder i appen (header, dashbordets organisasjon-fot) på ett sted, i -// stedet for å bygge noe nytt -- en ren navigasjons-samleside. +// "Mer"-siden (2026-08-01) -- den femte fanen i den faste bunn- +// navigasjonen (`components/teecup/bottom-nav.tsx`, ADR/V0 2026-08-08) +// trengte et reelt mål, ikke en lenke til en side som ikke fantes. +// Samler destinasjoner som allerede finnes andre steder i appen (header, +// dashbordets organisasjon-fot) på ett sted, i stedet for å bygge noe +// nytt -- en ren navigasjons-samleside. import { useEffect, useState } from "react" import { useRouter } from "next/navigation" import Link from "next/link" import { Bell, Building2, ChevronRight, LogOut, MessageSquare, UserCircle, UserPlus } from "lucide-react" -import { BottomTabBar } from "@/components/dashboard" +import { BottomNav } from "@/components/teecup/bottom-nav" const C = { card: "#ffffff", @@ -38,7 +39,7 @@ export function MoreMenu() { try { await fetch("/auth/logout", { method: "POST", credentials: "include" }) } finally { - router.replace("/") + router.replace("/logg-inn") } } @@ -88,7 +89,7 @@ export function MoreMenu() { - + ) } diff --git a/frontend/components/notifications.tsx b/frontend/components/notifications.tsx index 34cd0c9..ed9b21d 100644 --- a/frontend/components/notifications.tsx +++ b/frontend/components/notifications.tsx @@ -16,7 +16,7 @@ import { } from "lucide-react" import { Wordmark } from "@/components/wordmark" import { cn } from "@/lib/utils" -import { BottomTabBar } from "@/components/dashboard" +import { BottomNav } from "@/components/teecup/bottom-nav" // V0-designet (zip 20, varsler-runden 2026-07-25) -- datalag skrevet om fra // mock til ekte fetch mot GET /notifications + POST .../read + .../read-all @@ -74,7 +74,7 @@ export function Notifications() { fetch("/notifications", { credentials: "include" }) .then((res) => { if (res.status === 401) { - router.replace("/") + router.replace("/logg-inn") return null } return res.ok ? res.json() : [] @@ -159,7 +159,7 @@ export function Notifications() { )} - + ) } diff --git a/frontend/components/own-rounds.tsx b/frontend/components/own-rounds.tsx index e691cd3..0cd0d67 100644 --- a/frontend/components/own-rounds.tsx +++ b/frontend/components/own-rounds.tsx @@ -12,7 +12,7 @@ import { Button } from "@/components/ui/button" import { Wordmark } from "@/components/wordmark" import { RoundCard, type Round } from "@/components/round-card" import { ArrowLeft } from "lucide-react" -import { BottomTabBar } from "@/components/dashboard" +import { BottomNav } from "@/components/teecup/bottom-nav" type ApiRoundParticipant = { id: string @@ -105,7 +105,7 @@ export function OwnRounds() { try { const res = await fetch("/rounds", { credentials: "include" }) if (res.status === 401) { - router.replace("/") + router.replace("/logg-inn") return } if (!res.ok) throw new Error(`rounds: ${res.status}`) @@ -181,7 +181,7 @@ export function OwnRounds() { - + ) } diff --git a/frontend/components/rounds-stats-summary.tsx b/frontend/components/rounds-stats-summary.tsx index 55708f3..fcfd7ac 100644 --- a/frontend/components/rounds-stats-summary.tsx +++ b/frontend/components/rounds-stats-summary.tsx @@ -411,7 +411,7 @@ export function RoundsStatsSummary() { try { const res = await fetch(`/rounds/stats/summary?window=${window}`, { credentials: "include" }) if (res.status === 401) { - router.replace("/") + router.replace("/logg-inn") return } if (!res.ok) throw new Error(`stats: ${res.status}`) diff --git a/frontend/components/teecup/bottom-nav.tsx b/frontend/components/teecup/bottom-nav.tsx new file mode 100644 index 0000000..5c71dc1 --- /dev/null +++ b/frontend/components/teecup/bottom-nav.tsx @@ -0,0 +1,119 @@ +"use client" + +import Link from "next/link" +import { usePathname } from "next/navigation" +import { Flag, Home, MoreHorizontal, Trophy, UserRound } from "lucide-react" +import type { LucideIcon } from "lucide-react" +import { cn } from "@/lib/utils" + +type Tab = { + label: string + icon: LucideIcon + href: string + /** Optional count shown as a small badge (e.g. unread items). */ + badge?: number + /** Ekstra ruter som også skal telle som "denne fanen er aktiv" -- + * for sider som ikke selv er et direkte fanemål, men som appens egen + * IA (se more-menu.tsx) allerede grupperer under denne fanen. */ + matchPaths?: string[] +} + +// hrefs rettet til de faktiske frontend-rutene (V0 kjente ikke disse) -- +// /rounds er reservert API-prefiks (samme kollisjonsklasse som +// /rounds->/my-rounds tidligere i prosjektet), og det finnes ingen egen +// "/tournaments"-liste-side, kun /tournaments/[id]. "Turneringer" peker +// derfor til dashbordets egen "Kommende turneringer"-seksjon. +const TABS: Tab[] = [ + { label: "Hjem", icon: Home, href: "/dashboard" }, + { label: "Runder", icon: Flag, href: "/my-rounds" }, + { label: "Turneringer", icon: Trophy, href: "/dashboard#kommende-turneringer" }, + { label: "Profil", icon: UserRound, href: "/account" }, + // /my-friends, /my-feed og /my-notifications er ikke egne fanemål, men er + // allerede listet som "Mer"-sidens egne undersider (more-menu.tsx) -- uten + // matchPaths viste ingen fane som aktiv i det hele tatt på disse tre + // sidene (reelt funnet under scratch-verifisering, se CHANGELOG.md). + { label: "Mer", icon: MoreHorizontal, href: "/more", matchPaths: ["/my-friends", "/my-feed", "/my-notifications"] }, +] + +function isActive(pathname: string, href: string) { + // IKKE strippe et evt. #hash før sammenligning (fristende, men galt -- + // forsøkt og reelt browser-verifisert som en regresjon, se CHANGELOG.md): + // "Turneringer" sin href er et anker PÅ dashbordet (/dashboard#kommende- + // turneringer), og usePathname() inneholder ALDRI hash. Strippes hashen + // før sammenligning, matcher BÅDE "Hjem" og "Turneringer" samtidig på + // /dashboard -- to faner fremhevet på én gang. Rå strengsammenligning + // (V0s opprinnelige, ikke rørt her) unngår dette: "Turneringer" matcher + // aldri via ren pathname, nøyaktig som dagens BottomTabBar-erstatning + // uansett aldri fremhevet den (dashbordet satte alltid active="home"). + if (href === "/") return pathname === "/" + return pathname === href || pathname.startsWith(`${href}/`) +} + +export function BottomNav({ tabs = TABS }: { tabs?: Tab[] }) { + const pathname = usePathname() ?? "" + + return ( + // The bar is deliberately self-contained: it renders on six screens that + // belong to two different visual languages (the new "clubhouse" look and + // the older one). Rather than inherit any page palette, it reads as its + // own floating white surface with a hairline + soft lift, so it looks + // intentional on any background. The only brand token it leans on is the + // green accent, which is the one constant shared across both palettes. + + ) +}