Files
textbee/web/components/shared/route-tabs.tsx
isra el 18e7d1b3d8 perf(web): make dashboard navigation fast and cut session fetches
- prefetch route-tab links so tab clicks reuse a cached payload instead
  of paying an uncached server round trip each time
- add loading.tsx boundaries (dashboard root and each section) so clicks
  paint within a frame; section-level files keep the header and tabs
  mounted while only the content area swaps
- link the sidebar Account item and the checkout page straight to
  /dashboard/account/billing. The /dashboard/account redirect stub stays
  for old links, but no internal link pays the extra hop anymore. The
  stub also dropped query params, which silently ate the plan-change
  success toast.
- set QueryClient defaults (staleTime 60s, no focus refetch, retry 1);
  mutations already invalidate their keys, so the user's own changes
  stay instant. Device messages and webhook deliveries get a 15s
  staleTime since they change from outside the tab.
- replace the axios getCachedSession TTL cache with a token seeded from
  the server session in Providers and kept in sync by a session bridge.
  Requests attach the token synchronously; /api/auth/session is only a
  deduped fallback, instead of a refetch every 2 minutes with a
  thundering herd on expiry.
- swap the billing card's 16px loading spinner for a card-shaped
  skeleton so the tab no longer looks blank while loading

Tests: interceptor seeding/dedupe/signed-out behavior, provider
defaults and token seeding, nav active-state matching, tab prefetch,
billing loading state, plus e2e coverage for direct-to-billing
navigation and an at-most-one-session-call budget guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 13:49:14 +03:00

76 lines
2.1 KiB
TypeScript

'use client'
import { useEffect, useRef } from 'react'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { cn } from '@/lib/utils'
export type RouteTab = {
href: string
label: string
// Exact-match only (used for index routes like /dashboard/messaging so they
// don't stay active on sibling subroutes).
exact?: boolean
}
function isTabActive(tab: RouteTab, pathname: string): boolean {
if (tab.exact) return pathname === tab.href
return pathname === tab.href || pathname.startsWith(`${tab.href}/`)
}
// Link-based segmented control: tabs are real routes, so the active tab
// survives refresh and deep links are shareable. Mobile: horizontally
// scrollable pills; the active pill scrolls into view on load. Links opt into
// full prefetch because these routes are dynamic (session cookie in the app
// layout), which the router's default prefetch skips.
export default function RouteTabs({
tabs,
className,
}: {
tabs: RouteTab[]
className?: string
}) {
const pathname = usePathname()
const activeRef = useRef<HTMLAnchorElement | null>(null)
useEffect(() => {
// Deep links must land with the selected pill visible on small screens.
activeRef.current?.scrollIntoView({
block: 'nearest',
inline: 'center',
})
}, [pathname])
return (
<nav
className={cn(
'flex gap-1 overflow-x-auto rounded-lg bg-muted p-1',
'scrollbar-none w-full sm:w-fit',
className
)}
aria-label='Section navigation'
>
{tabs.map((tab) => {
const active = isTabActive(tab, pathname)
return (
<Link
key={tab.href}
href={tab.href}
prefetch
ref={active ? activeRef : undefined}
aria-current={active ? 'page' : undefined}
className={cn(
'shrink-0 rounded-md px-3 py-1.5 text-sm font-medium transition-colors',
active
? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'
)}
>
{tab.label}
</Link>
)
})}
</nav>
)
}