mirror of
https://github.com/vernu/textbee.git
synced 2026-08-01 09:59:02 -04:00
- 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>
76 lines
2.1 KiB
TypeScript
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>
|
|
)
|
|
}
|