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.
+
+ )
+}