feat: replace ghost buttons with underline tab bar for site navigation
Dashboard, Uptime, Funnels, and Settings now use a consistent underline tab bar with orange active indicator, matching the existing panel tab design language.
This commit is contained in:
@@ -6,6 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Improved
|
||||
|
||||
- **Cleaner site navigation.** Dashboard, Uptime, Funnels, and Settings now use an underline tab bar instead of floating buttons. The active section is highlighted with an orange underline, making it easy to see where you are and switch between views.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **City and region data is now accurate.** Location data was incorrectly showing the CDN server's location (e.g. Paris, Villeurbanne) instead of the visitor's actual city. Fixed by reading the correct visitor IP header from Bunny CDN.
|
||||
- **"Reset Data" now clears everything.** Previously, resetting a site's data in Settings only removed pageviews and daily stats. Uptime check history, uptime daily stats, and cached dashboard data were left behind. All collected data is now properly cleared when you reset, while your site configuration, goals, funnels, and uptime monitors are kept.
|
||||
|
||||
## [0.13.0-alpha] - 2026-03-07
|
||||
|
||||
### Added
|
||||
|
||||
@@ -6,6 +6,7 @@ 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,14 +53,10 @@ 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="mb-8">
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<Link
|
||||
href={`/sites/${siteId}`}
|
||||
className="p-2 -ml-2 text-neutral-500 hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-white rounded-xl hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-colors"
|
||||
>
|
||||
<ChevronLeftIcon className="w-5 h-5" />
|
||||
</Link>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white">
|
||||
Funnels
|
||||
@@ -68,14 +65,12 @@ export default function FunnelsPage() {
|
||||
Track user journeys and identify drop-off points
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-auto">
|
||||
<Link href={`/sites/${siteId}/funnels/new`}>
|
||||
<Button variant="primary" className="inline-flex items-center gap-2">
|
||||
<PlusIcon className="w-4 h-4" />
|
||||
<span>Create Funnel</span>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<Link href={`/sites/${siteId}/funnels/new`}>
|
||||
<Button variant="primary" className="inline-flex items-center gap-2">
|
||||
<PlusIcon className="w-4 h-4" />
|
||||
<span>Create Funnel</span>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{funnels.length === 0 ? (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useAuth } from '@/lib/auth/context'
|
||||
|
||||
import { logger } from '@/lib/utils/logger'
|
||||
import { useCallback, useEffect, useState, useMemo } from 'react'
|
||||
import { useParams, useRouter, useSearchParams } from 'next/navigation'
|
||||
@@ -34,6 +34,7 @@ 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'
|
||||
@@ -79,8 +80,8 @@ function getInitialDateRange(): { start: string; end: string } {
|
||||
}
|
||||
|
||||
export default function SiteDashboardPage() {
|
||||
const { user } = useAuth()
|
||||
const canEdit = user?.role === 'owner' || user?.role === 'admin'
|
||||
|
||||
|
||||
|
||||
const params = useParams()
|
||||
const router = useRouter()
|
||||
@@ -494,39 +495,12 @@ export default function SiteDashboardPage() {
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="h-6 w-px bg-neutral-200 dark:bg-neutral-700 flex-shrink-0"
|
||||
aria-hidden
|
||||
/>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
onClick={() => router.push(`/sites/${siteId}/uptime`)}
|
||||
variant="ghost"
|
||||
className="text-sm"
|
||||
>
|
||||
Uptime
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => router.push(`/sites/${siteId}/funnels`)}
|
||||
variant="ghost"
|
||||
className="text-sm"
|
||||
>
|
||||
Funnels
|
||||
</Button>
|
||||
{canEdit && (
|
||||
<Button
|
||||
onClick={() => router.push(`/sites/${siteId}/settings`)}
|
||||
variant="ghost"
|
||||
className="text-sm"
|
||||
>
|
||||
Settings
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SiteNav siteId={siteId} />
|
||||
|
||||
{/* Dimension Filters */}
|
||||
<div className="flex items-center gap-2 flex-wrap mb-2">
|
||||
<AddFilterDropdown onAdd={handleAddFilter} suggestions={filterSuggestions} onFetchSuggestions={handleFetchSuggestions} />
|
||||
|
||||
@@ -15,6 +15,7 @@ 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'
|
||||
@@ -403,6 +404,8 @@ export default function SiteSettingsPage() {
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 py-8">
|
||||
<SiteNav siteId={siteId} />
|
||||
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white">Site Settings</h1>
|
||||
|
||||
@@ -21,6 +21,7 @@ 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,
|
||||
@@ -723,21 +724,14 @@ export default function UptimePage() {
|
||||
transition={{ duration: 0.2 }}
|
||||
className="w-full max-w-6xl mx-auto px-4 sm:px-6 py-8"
|
||||
>
|
||||
<SiteNav siteId={siteId} />
|
||||
|
||||
{/* Header */}
|
||||
<div className="mb-8 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<button
|
||||
onClick={() => router.push(`/sites/${siteId}`)}
|
||||
className="text-sm text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200 transition-colors"
|
||||
>
|
||||
{site.name}
|
||||
</button>
|
||||
<span className="text-neutral-300 dark:text-neutral-600">/</span>
|
||||
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white">
|
||||
Uptime
|
||||
</h1>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white mb-1">
|
||||
Uptime
|
||||
</h1>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">
|
||||
Monitor your endpoints and track availability over time
|
||||
</p>
|
||||
|
||||
54
components/dashboard/SiteNav.tsx
Normal file
54
components/dashboard/SiteNav.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
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: 'Uptime', href: `/sites/${siteId}/uptime` },
|
||||
{ label: 'Funnels', href: `/sites/${siteId}/funnels` },
|
||||
...(canEdit ? [{ label: 'Settings', href: `/sites/${siteId}/settings` }] : []),
|
||||
]
|
||||
|
||||
const isActive = (href: string) => {
|
||||
if (href === `/sites/${siteId}`) {
|
||||
return pathname === href || pathname === `${href}/realtime`
|
||||
}
|
||||
return pathname.startsWith(href)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border-b border-neutral-200 dark:border-neutral-800 mb-6">
|
||||
<nav className="flex gap-1" 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={`px-3 py-2 text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-brand-orange rounded-t cursor-pointer border-b-2 -mb-px ${
|
||||
isActive(tab.href)
|
||||
? 'border-brand-orange text-neutral-900 dark:text-white'
|
||||
: 'border-transparent text-neutral-400 dark:text-neutral-500 hover:text-neutral-700 dark:hover:text-neutral-300'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user