feat: instant tab navigation by moving SiteNav to shared layout

SiteNav now lives in the [id] layout instead of each page, so it stays
mounted during route transitions. Switching between Dashboard, Uptime,
Funnels, and Settings no longer flashes a full-page skeleton.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Usman Baig
2026-03-09 23:35:06 +01:00
parent 92fae83772
commit 330cc134aa
11 changed files with 46 additions and 32 deletions

View File

@@ -0,0 +1,20 @@
'use client'
import SiteNav from '@/components/dashboard/SiteNav'
export default function SiteLayoutShell({
siteId,
children,
}: {
siteId: string
children: React.ReactNode
}) {
return (
<>
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 pt-8">
<SiteNav siteId={siteId} />
</div>
{children}
</>
)
}

View File

@@ -69,7 +69,7 @@ export default function FunnelReportPage() {
if (loadError === 'not_found' || (!funnel && !stats && !loadError)) {
return (
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 py-8">
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 pb-8">
<p className="text-neutral-600 dark:text-neutral-400">Funnel not found</p>
</div>
)
@@ -77,7 +77,7 @@ export default function FunnelReportPage() {
if (loadError === 'forbidden') {
return (
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 py-8">
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 pb-8">
<p className="text-neutral-600 dark:text-neutral-400">Access denied</p>
<Link href={`/sites/${siteId}/funnels`}>
<Button variant="primary" className="mt-4">
@@ -90,7 +90,7 @@ export default function FunnelReportPage() {
if (loadError === 'error') {
return (
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 py-8">
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 pb-8">
<p className="text-neutral-600 dark:text-neutral-400 mb-4">Unable to load funnel</p>
<Button type="button" onClick={() => loadData()} variant="primary">
Try again
@@ -101,7 +101,7 @@ export default function FunnelReportPage() {
if (!funnel || !stats) {
return (
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 py-8">
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 pb-8">
<p className="text-neutral-600 dark:text-neutral-400">Funnel not found</p>
</div>
)
@@ -113,7 +113,7 @@ export default function FunnelReportPage() {
}))
return (
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 py-8">
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 pb-8">
<div className="mb-8">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-4">

View File

@@ -91,7 +91,7 @@ export default function CreateFunnelPage() {
}
return (
<div className="w-full max-w-3xl mx-auto px-4 sm:px-6 py-8">
<div className="w-full max-w-3xl mx-auto px-4 sm:px-6 pb-8">
<div className="mb-8">
<Link
href={`/sites/${siteId}/funnels`}

View File

@@ -6,7 +6,6 @@ import { listFunnels, deleteFunnel, type Funnel } from '@/lib/api/funnels'
import { toast, PlusIcon, ArrowRightIcon, ChevronLeftIcon, TrashIcon, Button } from '@ciphera-net/ui'
import { FunnelsListSkeleton, useMinimumLoading } from '@/components/skeletons'
import Link from 'next/link'
import SiteNav from '@/components/dashboard/SiteNav'
export default function FunnelsPage() {
const params = useParams()
@@ -52,9 +51,7 @@ export default function FunnelsPage() {
}
return (
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 py-8">
<SiteNav siteId={siteId} />
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 pb-8">
<div className="mb-8">
<div className="flex items-center justify-between mb-6">
<div>

View File

@@ -1,4 +1,5 @@
import type { Metadata } from 'next'
import SiteLayoutShell from './SiteLayoutShell'
export const metadata: Metadata = {
title: 'Dashboard | Pulse',
@@ -6,10 +7,13 @@ export const metadata: Metadata = {
robots: { index: false, follow: false },
}
export default function SiteLayout({
export default async function SiteLayout({
children,
params,
}: {
children: React.ReactNode
params: Promise<{ id: string }>
}) {
return children
const { id } = await params
return <SiteLayoutShell siteId={id}>{children}</SiteLayoutShell>
}

View File

@@ -34,7 +34,6 @@ import PerformanceStats from '@/components/dashboard/PerformanceStats'
import GoalStats from '@/components/dashboard/GoalStats'
import ScrollDepth from '@/components/dashboard/ScrollDepth'
import Campaigns from '@/components/dashboard/Campaigns'
import SiteNav from '@/components/dashboard/SiteNav'
import FilterBar from '@/components/dashboard/FilterBar'
import AddFilterDropdown, { type FilterSuggestion, type FilterSuggestions } from '@/components/dashboard/AddFilterDropdown'
import EventProperties from '@/components/dashboard/EventProperties'
@@ -426,7 +425,7 @@ export default function SiteDashboardPage() {
if (!site) {
return (
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 py-8">
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 pb-8">
<p className="text-neutral-600 dark:text-neutral-400">Site not found</p>
</div>
)
@@ -437,10 +436,8 @@ export default function SiteDashboardPage() {
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2 }}
className="w-full max-w-6xl mx-auto px-4 sm:px-6 py-8"
className="w-full max-w-6xl mx-auto px-4 sm:px-6 pb-8"
>
<SiteNav siteId={siteId} />
<div className="mb-8">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-4">

View File

@@ -101,7 +101,7 @@ export default function RealtimePage() {
if (!site) return <div className="p-8">Site not found</div>
return (
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 py-8 h-[calc(100vh-64px)] flex flex-col">
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 pb-8 h-[calc(100vh-64px)] flex flex-col">
<div className="mb-6 flex items-center justify-between">
<div>
<div className="flex items-center gap-2 mb-1">

View File

@@ -15,7 +15,6 @@ import { APP_URL } from '@/lib/api/client'
import { generatePrivacySnippet } from '@/lib/utils/privacySnippet'
import { useUnsavedChanges } from '@/lib/hooks/useUnsavedChanges'
import { getSubscription, type SubscriptionDetails } from '@/lib/api/billing'
import SiteNav from '@/components/dashboard/SiteNav'
import { getRetentionOptionsForPlan, formatRetentionMonths } from '@/lib/plans'
import { motion, AnimatePresence } from 'framer-motion'
import { useAuth } from '@/lib/auth/context'
@@ -382,7 +381,7 @@ export default function SiteSettingsPage() {
if (showSkeleton) {
return (
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 py-8">
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 pb-8">
<div className="space-y-8">
<div>
<div className="h-8 w-40 animate-pulse rounded bg-neutral-100 dark:bg-neutral-800 mb-2" />
@@ -405,15 +404,14 @@ export default function SiteSettingsPage() {
if (!site) {
return (
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 py-8">
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 pb-8">
<p className="text-neutral-600 dark:text-neutral-400">Site not found</p>
</div>
)
}
return (
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 py-8">
<SiteNav siteId={siteId} />
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 pb-8">
<div className="space-y-8">
<div>

View File

@@ -21,7 +21,6 @@ import { toast } from '@ciphera-net/ui'
import { useTheme } from '@ciphera-net/ui'
import { getAuthErrorMessage } from '@ciphera-net/ui'
import { Button, Modal } from '@ciphera-net/ui'
import SiteNav from '@/components/dashboard/SiteNav'
import { UptimeSkeleton, ChecksSkeleton, useMinimumLoading } from '@/components/skeletons'
import {
AreaChart,
@@ -693,10 +692,8 @@ export default function UptimePage() {
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2 }}
className="w-full max-w-6xl mx-auto px-4 sm:px-6 py-8"
className="w-full max-w-6xl mx-auto px-4 sm:px-6 pb-8"
>
<SiteNav siteId={siteId} />
{/* Header */}
<div className="mb-8 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>