Consistency fixes: - Extract getThisWeekRange/getThisMonthRange to shared lib/utils/dateRanges.ts (removed 4 identical copy-pasted definitions) - Add error boundaries for behavior, cdn, search, pagespeed pages (4 new error.tsx files — previously fell through to generic parent error) - Add "View setup guide" CTA to empty states on journeys and behavior pages (previously showed text with no actionable button) - Fix non-lazy useState initializer in funnel detail page - Fix Bot & Spam settings header from text-xl to text-2xl (matches all other sections) - Add useMinimumLoading to PageSpeed skeleton (consistent with all other pages) Cleanup: - Remove 438 redundant dark: class prefixes (app is dark-mode only) text-neutral-500 dark:text-neutral-400 → text-neutral-400 (206 occurrences) text-neutral-900 dark:text-white → text-white (232 occurrences) - Remove dead @stripe/react-stripe-js and @stripe/stripe-js packages (billing migrated to Polar, no code imports Stripe) - Remove duplicate motion package (framer-motion is the one actually used)
67 lines
2.4 KiB
TypeScript
67 lines
2.4 KiB
TypeScript
'use client'
|
|
|
|
import Link from 'next/link'
|
|
import { usePathname } from 'next/navigation'
|
|
import { motion } from 'framer-motion'
|
|
import { useTabListKeyboard } from '@/lib/hooks/useTabListKeyboard'
|
|
import { useAuth } from '@/lib/auth/context'
|
|
|
|
interface SiteNavProps {
|
|
siteId: string
|
|
}
|
|
|
|
export default function SiteNav({ siteId }: SiteNavProps) {
|
|
const pathname = usePathname()
|
|
const handleTabKeyDown = useTabListKeyboard()
|
|
const { user } = useAuth()
|
|
const canEdit = user?.role === 'owner' || user?.role === 'admin'
|
|
|
|
const tabs = [
|
|
{ label: 'Dashboard', href: `/sites/${siteId}` },
|
|
{ label: 'Journeys', href: `/sites/${siteId}/journeys` },
|
|
{ label: 'Funnels', href: `/sites/${siteId}/funnels` },
|
|
{ label: 'Behavior', href: `/sites/${siteId}/behavior` },
|
|
{ label: 'Search', href: `/sites/${siteId}/search` },
|
|
{ label: 'CDN', href: `/sites/${siteId}/cdn` },
|
|
{ label: 'Uptime', href: `/sites/${siteId}/uptime` },
|
|
...(canEdit ? [{ label: 'Settings', href: `/sites/${siteId}/settings` }] : []),
|
|
]
|
|
|
|
const isActive = (href: string) => {
|
|
if (href === `/sites/${siteId}`) {
|
|
return pathname === href
|
|
}
|
|
return pathname.startsWith(href)
|
|
}
|
|
|
|
return (
|
|
<div className="mb-6 overflow-x-auto scrollbar-hide">
|
|
<nav className="flex gap-1 min-w-max border-b border-neutral-200 dark:border-neutral-800" role="tablist" aria-label="Site navigation" onKeyDown={handleTabKeyDown}>
|
|
{tabs.map((tab) => (
|
|
<Link
|
|
key={tab.href}
|
|
href={tab.href}
|
|
role="tab"
|
|
aria-selected={isActive(tab.href)}
|
|
tabIndex={isActive(tab.href) ? 0 : -1}
|
|
className={`relative shrink-0 whitespace-nowrap px-3 py-2 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange rounded-t cursor-pointer -mb-px ${
|
|
isActive(tab.href)
|
|
? 'text-white'
|
|
: 'text-neutral-400 dark:text-neutral-500 hover:text-neutral-700 dark:hover:text-neutral-300'
|
|
}`}
|
|
>
|
|
{tab.label}
|
|
{isActive(tab.href) && (
|
|
<motion.div
|
|
layoutId="activeTab"
|
|
className="absolute inset-x-0 -bottom-px h-0.5 bg-brand-orange"
|
|
transition={{ type: 'spring', stiffness: 500, damping: 35 }}
|
|
/>
|
|
)}
|
|
</Link>
|
|
))}
|
|
</nav>
|
|
</div>
|
|
)
|
|
}
|