PWA-installasjonstekst — skiller nå Safari/Chrome på iOS, ikke lenger feilaktig Safari-only. Beskrivelsestekst — "golfklubber" fjernet, nytt punkt om at turneringsadmin/-presentasjon fungerer godt på PC. Dette krever både teecup_api og teecup_frontend (ingen migrasjon).
144 lines
6.3 KiB
TypeScript
144 lines
6.3 KiB
TypeScript
"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,
|
|
disabledHrefs,
|
|
onDisabledClick,
|
|
}: {
|
|
tabs?: Tab[]
|
|
/**
|
|
* Fanenes hrefs som skal vises låst (ADR-049, /velkommen — innlogget-
|
|
* men-ikke-fullført-profil skal se HELE den vanlige navigasjonen, ikke
|
|
* en avkortet versjon, men kun kunne gå til profilen før resten låses
|
|
* opp). Rendres som en `<button>` i stedet for `<Link>`, aldri
|
|
* `aria-current`, og navigerer aldri.
|
|
*/
|
|
disabledHrefs?: string[]
|
|
onDisabledClick?: (tab: Tab) => void
|
|
}) {
|
|
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 disabled = disabledHrefs?.includes(tab.href) ?? false
|
|
const active =
|
|
!disabled &&
|
|
(isActive(pathname, tab.href) || (tab.matchPaths?.some((p) => isActive(pathname, p)) ?? false))
|
|
const itemClassName = 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",
|
|
disabled
|
|
? "text-neutral-400"
|
|
: active
|
|
? "text-tee-strong"
|
|
: "text-neutral-500 hover:text-neutral-900",
|
|
)
|
|
const iconWrapClassName = 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]",
|
|
)
|
|
const inner = (
|
|
<>
|
|
<span className={iconWrapClassName}>
|
|
<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>
|
|
</>
|
|
)
|
|
return (
|
|
<li key={tab.href} className="flex-1">
|
|
{disabled ? (
|
|
<button
|
|
type="button"
|
|
aria-disabled="true"
|
|
onClick={() => onDisabledClick?.(tab)}
|
|
className={itemClassName}
|
|
>
|
|
{inner}
|
|
</button>
|
|
) : (
|
|
<Link href={tab.href} aria-current={active ? "page" : undefined} className={itemClassName}>
|
|
{inner}
|
|
</Link>
|
|
)}
|
|
</li>
|
|
)
|
|
})}
|
|
</ul>
|
|
</nav>
|
|
)
|
|
}
|