Reskinn dashbord + erstatt bunn-navigasjon med clubhouse-palett
Dashbordets egne JSX re-stylet til --clubhouse-*/--tee*/--cup*-tokens (V0, egen parallell utforskning ved siden av Forest Green) -- delte komponenter som RoundCard/TournamentCard urørt. Den gamle, lokale BottomTabBar erstattet med components/teecup/bottom-nav.tsx (egen V0-prompt, usePathname()-drevet aktiv-fane), koblet inn på alle syv sider som viser den. To reelle bugs funnet og rettet under scratch-verifisering: aktiv-fane-sammenligning som feilaktig strippet #hash (ga to samtidig aktive faner), og manglende fane-gruppering for my-friends/my-feed/my-notifications (løst med en matchPaths-mekanisme). Se CHANGELOG.md punkt 39-40 for full verifiseringsdetalj.
This commit is contained in:
parent
12f10d92a3
commit
8ef2f61f4d
11 changed files with 193 additions and 109 deletions
|
|
@ -237,3 +237,34 @@
|
||||||
--color-clubhouse-border: var(--clubhouse-border);
|
--color-clubhouse-border: var(--clubhouse-border);
|
||||||
--color-clubhouse-field: var(--clubhouse-field);
|
--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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ import { Label } from "@/components/ui/label"
|
||||||
import { Wordmark } from "@/components/wordmark"
|
import { Wordmark } from "@/components/wordmark"
|
||||||
import { TwoFactorSetupForm } from "@/components/two-factor-flow"
|
import { TwoFactorSetupForm } from "@/components/two-factor-flow"
|
||||||
import { CourseTemplateEditor, type CourseTemplateValues } from "@/components/course-template-editor"
|
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
|
// ADR-031: personlig profil, ETT sett per konto (app_user), atskilt fra de
|
||||||
// org-scopede `player`-radene organisatorer administrerer.
|
// org-scopede `player`-radene organisatorer administrerer.
|
||||||
|
|
@ -339,7 +339,7 @@ export function AccountSettings() {
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<BottomTabBar active="profile" />
|
<BottomNav />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -416,7 +416,7 @@ function ProfileOnboarding({ me, onComplete }: { me: Me; onComplete: () => void
|
||||||
try {
|
try {
|
||||||
await fetch("/auth/logout", { method: "POST", credentials: "include" })
|
await fetch("/auth/logout", { method: "POST", credentials: "include" })
|
||||||
} finally {
|
} finally {
|
||||||
router.replace("/")
|
router.replace("/logg-inn")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@ export function CourseRounds({ courseName }: { courseName: string }) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/rounds", { credentials: "include" })
|
const res = await fetch("/rounds", { credentials: "include" })
|
||||||
if (res.status === 401) {
|
if (res.status === 401) {
|
||||||
router.replace("/")
|
router.replace("/logg-inn")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!res.ok) throw new Error(`rounds: ${res.status}`)
|
if (!res.ok) throw new Error(`rounds: ${res.status}`)
|
||||||
|
|
|
||||||
|
|
@ -36,11 +36,18 @@
|
||||||
// visningsfonten (samme utelatelse som `/logg-inn` selv gjorde) -- Nunito
|
// visningsfonten (samme utelatelse som `/logg-inn` selv gjorde) -- Nunito
|
||||||
// beholdt for å unngå en ekstra font-avhengighet for kun to skjermer.
|
// beholdt for å unngå en ekstra font-avhengighet for kun to skjermer.
|
||||||
//
|
//
|
||||||
// Omfangsgrenser (delte komponenter IKKE re-stylet her, se CHANGELOG.md):
|
// Omfangsgrense (delt komponent IKKE re-stylet her, se CHANGELOG.md):
|
||||||
// `RoundCard`/`TournamentCard` (også brukt av /my-rounds) og `BottomTabBar`
|
// `RoundCard`/`TournamentCard` (også brukt av /my-rounds) beholder sin
|
||||||
// (delt med de fem sidene som nettopp fikk den, venter på egen V0-prompt)
|
// eksisterende styling inntil videre -- kun selve dashbord-skjermens
|
||||||
// beholder sin eksisterende styling inntil videre -- kun selve
|
// EGNE, ikke-delte JSX er re-stylet i denne runden.
|
||||||
// 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 type React from "react"
|
||||||
import { useCallback, useEffect, useState } from "react"
|
import { useCallback, useEffect, useState } from "react"
|
||||||
|
|
@ -50,18 +57,14 @@ import {
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
Bell,
|
Bell,
|
||||||
Building2,
|
Building2,
|
||||||
CalendarDays,
|
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Flag,
|
Flag,
|
||||||
Home,
|
|
||||||
KeyRound,
|
KeyRound,
|
||||||
LogOut,
|
LogOut,
|
||||||
MapPin,
|
MapPin,
|
||||||
MoreHorizontal,
|
|
||||||
Trophy,
|
Trophy,
|
||||||
TrendingDown,
|
TrendingDown,
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
User,
|
|
||||||
UserCircle,
|
UserCircle,
|
||||||
UserPlus,
|
UserPlus,
|
||||||
X,
|
X,
|
||||||
|
|
@ -77,36 +80,12 @@ import {
|
||||||
} from "@/components/ui/dropdown-menu"
|
} from "@/components/ui/dropdown-menu"
|
||||||
import { InstallPrompt } from "@/components/install-prompt"
|
import { InstallPrompt } from "@/components/install-prompt"
|
||||||
import { TeeCupWordmark } from "@/components/teecup/wordmark"
|
import { TeeCupWordmark } from "@/components/teecup/wordmark"
|
||||||
|
import { BottomNav } from "@/components/teecup/bottom-nav"
|
||||||
import { RoundCard, type Round } from "@/components/round-card"
|
import { RoundCard, type Round } from "@/components/round-card"
|
||||||
import { TournamentCard, type Tournament } from "@/components/tournament-card"
|
import { TournamentCard, type Tournament } from "@/components/tournament-card"
|
||||||
import { type TournamentStatus } from "@/components/tournament-status-badge"
|
import { type TournamentStatus } from "@/components/tournament-status-badge"
|
||||||
import { cn } from "@/lib/utils"
|
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
|
// 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,
|
// stedet for én ensfarget tone. Hver par en lys bakgrunn med en mørk,
|
||||||
// kontrastrik tekst-tone for initialene.
|
// kontrastrik tekst-tone for initialene.
|
||||||
|
|
@ -328,7 +307,7 @@ export function Dashboard() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/auth/me", { credentials: "include" })
|
const res = await fetch("/auth/me", { credentials: "include" })
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
router.replace("/")
|
router.replace("/logg-inn")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const data: Me = await res.json()
|
const data: Me = await res.json()
|
||||||
|
|
@ -338,7 +317,7 @@ export function Dashboard() {
|
||||||
}
|
}
|
||||||
setMe(data)
|
setMe(data)
|
||||||
} catch {
|
} catch {
|
||||||
router.replace("/")
|
router.replace("/logg-inn")
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingMe(false)
|
setLoadingMe(false)
|
||||||
}
|
}
|
||||||
|
|
@ -446,7 +425,7 @@ export function Dashboard() {
|
||||||
try {
|
try {
|
||||||
await fetch("/auth/logout", { method: "POST", credentials: "include" })
|
await fetch("/auth/logout", { method: "POST", credentials: "include" })
|
||||||
} finally {
|
} 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 rows = rounds ?? []
|
||||||
const activeRounds = rows.filter((r) => !r.completed_at)
|
const activeRounds = rows.filter((r) => !r.completed_at)
|
||||||
|
|
@ -565,57 +544,11 @@ export function Dashboard() {
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<BottomTabBar active="home" />
|
<BottomNav />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 (
|
|
||||||
<nav
|
|
||||||
aria-label="Hovednavigasjon"
|
|
||||||
className="fixed inset-x-0 bottom-0 z-20 border-t"
|
|
||||||
style={{ backgroundColor: C.card, borderColor: C.border, paddingBottom: "env(safe-area-inset-bottom)" }}
|
|
||||||
>
|
|
||||||
<ul className="mx-auto flex w-full max-w-2xl items-stretch justify-around px-1">
|
|
||||||
{tabs.map(({ key, icon: Icon, label, href }) => {
|
|
||||||
const isActive = key === active
|
|
||||||
return (
|
|
||||||
<li key={key} className="flex-1">
|
|
||||||
<Link
|
|
||||||
href={href}
|
|
||||||
aria-current={isActive ? "page" : undefined}
|
|
||||||
className="flex min-h-[56px] flex-col items-center justify-center gap-1 rounded-lg px-1 py-1.5 text-center transition-colors focus-visible:outline-none focus-visible:ring-2"
|
|
||||||
style={{ color: isActive ? C.primary : C.muted, outlineColor: C.accent }}
|
|
||||||
>
|
|
||||||
<Icon aria-hidden="true" className="size-6" strokeWidth={isActive ? 2.4 : 2} />
|
|
||||||
<span className="text-[11px] font-bold leading-none">{label}</span>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// V0-designet (varsler-runden, 2026-07-25) -- lenker til /my-notifications,
|
// V0-designet (varsler-runden, 2026-07-25) -- lenker til /my-notifications,
|
||||||
// IKKE /notifications (API-prefiks, samme kollisjonsklasse som
|
// IKKE /notifications (API-prefiks, samme kollisjonsklasse som
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import Link from "next/link"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { PostEngagement, type ReactionSummary } from "./post-engagement"
|
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. */
|
/** Rå feed-oppføring fra backend. */
|
||||||
export type ApiFeedEntry = {
|
export type ApiFeedEntry = {
|
||||||
|
|
@ -185,7 +185,7 @@ export function Feed() {
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<BottomTabBar active="more" />
|
<BottomNav />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import {
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Wordmark } from "@/components/wordmark"
|
import { Wordmark } from "@/components/wordmark"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { BottomTabBar } from "@/components/dashboard"
|
import { BottomNav } from "@/components/teecup/bottom-nav"
|
||||||
|
|
||||||
// --- Kategorier --------------------------------------------------------------
|
// --- Kategorier --------------------------------------------------------------
|
||||||
// Rekkefølge og koder MÅ matche `Category` i app/routers/friends.py nøyaktig.
|
// Rekkefølge og koder MÅ matche `Category` i app/routers/friends.py nøyaktig.
|
||||||
|
|
@ -130,7 +130,7 @@ export function Friends() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/friends", { credentials: "include" })
|
const res = await fetch("/friends", { credentials: "include" })
|
||||||
if (res.status === 401) {
|
if (res.status === 401) {
|
||||||
router.replace("/")
|
router.replace("/logg-inn")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!res.ok) throw new Error(`friends: ${res.status}`)
|
if (!res.ok) throw new Error(`friends: ${res.status}`)
|
||||||
|
|
@ -186,7 +186,7 @@ export function Friends() {
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<BottomTabBar active="more" />
|
<BottomNav />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,17 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
// "Mer"-siden (2026-08-01) -- den femte fanen i den nye bunn-navigasjonen
|
// "Mer"-siden (2026-08-01) -- den femte fanen i den faste bunn-
|
||||||
// (dashboard.tsx sin BottomTabBar) trengte et reelt mål, ikke en lenke til
|
// navigasjonen (`components/teecup/bottom-nav.tsx`, ADR/V0 2026-08-08)
|
||||||
// en side som ikke fantes. Samler destinasjoner som allerede finnes andre
|
// trengte et reelt mål, ikke en lenke til en side som ikke fantes.
|
||||||
// steder i appen (header, dashbordets organisasjon-fot) på ett sted, i
|
// Samler destinasjoner som allerede finnes andre steder i appen (header,
|
||||||
// stedet for å bygge noe nytt -- en ren navigasjons-samleside.
|
// dashbordets organisasjon-fot) på ett sted, i stedet for å bygge noe
|
||||||
|
// nytt -- en ren navigasjons-samleside.
|
||||||
|
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { Bell, Building2, ChevronRight, LogOut, MessageSquare, UserCircle, UserPlus } from "lucide-react"
|
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 = {
|
const C = {
|
||||||
card: "#ffffff",
|
card: "#ffffff",
|
||||||
|
|
@ -38,7 +39,7 @@ export function MoreMenu() {
|
||||||
try {
|
try {
|
||||||
await fetch("/auth/logout", { method: "POST", credentials: "include" })
|
await fetch("/auth/logout", { method: "POST", credentials: "include" })
|
||||||
} finally {
|
} finally {
|
||||||
router.replace("/")
|
router.replace("/logg-inn")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -88,7 +89,7 @@ export function MoreMenu() {
|
||||||
</button>
|
</button>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<BottomTabBar active="more" />
|
<BottomNav />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import {
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { Wordmark } from "@/components/wordmark"
|
import { Wordmark } from "@/components/wordmark"
|
||||||
import { cn } from "@/lib/utils"
|
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
|
// V0-designet (zip 20, varsler-runden 2026-07-25) -- datalag skrevet om fra
|
||||||
// mock til ekte fetch mot GET /notifications + POST .../read + .../read-all
|
// mock til ekte fetch mot GET /notifications + POST .../read + .../read-all
|
||||||
|
|
@ -74,7 +74,7 @@ export function Notifications() {
|
||||||
fetch("/notifications", { credentials: "include" })
|
fetch("/notifications", { credentials: "include" })
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.status === 401) {
|
if (res.status === 401) {
|
||||||
router.replace("/")
|
router.replace("/logg-inn")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return res.ok ? res.json() : []
|
return res.ok ? res.json() : []
|
||||||
|
|
@ -159,7 +159,7 @@ export function Notifications() {
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<BottomTabBar active="more" />
|
<BottomNav />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import { Button } from "@/components/ui/button"
|
||||||
import { Wordmark } from "@/components/wordmark"
|
import { Wordmark } from "@/components/wordmark"
|
||||||
import { RoundCard, type Round } from "@/components/round-card"
|
import { RoundCard, type Round } from "@/components/round-card"
|
||||||
import { ArrowLeft } from "lucide-react"
|
import { ArrowLeft } from "lucide-react"
|
||||||
import { BottomTabBar } from "@/components/dashboard"
|
import { BottomNav } from "@/components/teecup/bottom-nav"
|
||||||
|
|
||||||
type ApiRoundParticipant = {
|
type ApiRoundParticipant = {
|
||||||
id: string
|
id: string
|
||||||
|
|
@ -105,7 +105,7 @@ export function OwnRounds() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/rounds", { credentials: "include" })
|
const res = await fetch("/rounds", { credentials: "include" })
|
||||||
if (res.status === 401) {
|
if (res.status === 401) {
|
||||||
router.replace("/")
|
router.replace("/logg-inn")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!res.ok) throw new Error(`rounds: ${res.status}`)
|
if (!res.ok) throw new Error(`rounds: ${res.status}`)
|
||||||
|
|
@ -181,7 +181,7 @@ export function OwnRounds() {
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<BottomTabBar active="rounds" />
|
<BottomNav />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -411,7 +411,7 @@ export function RoundsStatsSummary() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/rounds/stats/summary?window=${window}`, { credentials: "include" })
|
const res = await fetch(`/rounds/stats/summary?window=${window}`, { credentials: "include" })
|
||||||
if (res.status === 401) {
|
if (res.status === 401) {
|
||||||
router.replace("/")
|
router.replace("/logg-inn")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!res.ok) throw new Error(`stats: ${res.status}`)
|
if (!res.ok) throw new Error(`stats: ${res.status}`)
|
||||||
|
|
|
||||||
119
frontend/components/teecup/bottom-nav.tsx
Normal file
119
frontend/components/teecup/bottom-nav.tsx
Normal file
|
|
@ -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.
|
||||||
|
<nav
|
||||||
|
aria-label="Hovednavigasjon"
|
||||||
|
className="fixed inset-x-0 bottom-0 z-40 border-t border-black/10 bg-white/90 shadow-[0_-1px_12px_rgba(23,32,15,0.06)] backdrop-blur-md supports-[backdrop-filter]:bg-white/80"
|
||||||
|
>
|
||||||
|
<ul className="mx-auto flex max-w-xl items-stretch justify-around gap-1 px-2 pb-[env(safe-area-inset-bottom)]">
|
||||||
|
{tabs.map((tab) => {
|
||||||
|
const Icon = tab.icon
|
||||||
|
const active =
|
||||||
|
isActive(pathname, tab.href) || (tab.matchPaths?.some((p) => isActive(pathname, p)) ?? false)
|
||||||
|
return (
|
||||||
|
<li key={tab.href} className="flex-1">
|
||||||
|
<Link
|
||||||
|
href={tab.href}
|
||||||
|
aria-current={active ? "page" : undefined}
|
||||||
|
className={cn(
|
||||||
|
"group relative flex min-h-[56px] flex-col items-center justify-center gap-1 rounded-xl px-1 py-2 text-[11px] font-semibold leading-none transition-colors",
|
||||||
|
"focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-tee-strong",
|
||||||
|
active
|
||||||
|
? "text-tee-strong"
|
||||||
|
: "text-neutral-500 hover:text-neutral-900",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"relative flex h-8 w-12 items-center justify-center rounded-full transition-colors",
|
||||||
|
active
|
||||||
|
? "bg-tee/20"
|
||||||
|
: "bg-transparent group-hover:bg-black/[0.04]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
className="h-[22px] w-[22px]"
|
||||||
|
strokeWidth={active ? 2.6 : 2}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
{typeof tab.badge === "number" && tab.badge > 0 && (
|
||||||
|
<span
|
||||||
|
className="absolute -right-0.5 -top-0.5 flex h-[18px] min-w-[18px] items-center justify-center rounded-full bg-cup px-1 text-[10px] font-bold text-white ring-2 ring-white"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{tab.badge > 9 ? "9+" : tab.badge}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{tab.label}
|
||||||
|
{typeof tab.badge === "number" && tab.badge > 0 && (
|
||||||
|
<span className="sr-only">{`, ${tab.badge} nye`}</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue