feat: redesign top paths as breadcrumb cards with icons

This commit is contained in:
Usman Baig
2026-03-15 11:47:52 +01:00
parent 47d884e47b
commit 7e30d04df3
2 changed files with 61 additions and 35 deletions

View File

@@ -5,10 +5,12 @@ import { useParams } from 'next/navigation'
import { getDateRange, formatDate } from '@ciphera-net/ui'
import { Select, DatePicker } from '@ciphera-net/ui'
import SankeyDiagram from '@/components/journeys/SankeyDiagram'
import TopPathsTable from '@/components/journeys/TopPathsTable'
import { JourneysSkeleton, useMinimumLoading, useSkeletonFade } from '@/components/skeletons'
import {
useDashboard,
useJourneyTransitions,
useJourneyTopPaths,
useJourneyEntryPoints,
} from '@/lib/swr/dashboard'
@@ -44,6 +46,9 @@ export default function JourneysPage() {
const { data: transitionsData, isLoading: transitionsLoading } = useJourneyTransitions(
siteId, dateRange.start, dateRange.end, depth, 1, entryPath || undefined
)
const { data: topPaths, isLoading: topPathsLoading } = useJourneyTopPaths(
siteId, dateRange.start, dateRange.end, 20, 1, entryPath || undefined
)
const { data: entryPoints } = useJourneyEntryPoints(siteId, dateRange.start, dateRange.end)
const { data: dashboard } = useDashboard(siteId, dateRange.start, dateRange.end)
@@ -185,6 +190,11 @@ export default function JourneysPage() {
)}
</div>
{/* Top Paths */}
<div className="mt-6">
<TopPathsTable paths={topPaths ?? []} loading={topPathsLoading} />
</div>
{/* Date Picker Modal */}
<DatePicker
isOpen={isDatePickerOpen}

View File

@@ -2,7 +2,7 @@
import type { TopPath } from '@/lib/api/journeys'
import { TableSkeleton } from '@/components/skeletons'
import { Path } from '@phosphor-icons/react'
import { Path, ArrowRight, Clock, Users } from '@phosphor-icons/react'
interface TopPathsTableProps {
paths: TopPath[]
@@ -17,57 +17,73 @@ function formatDuration(seconds: number): string {
return `${m}m ${s}s`
}
function smartLabel(path: string): string {
if (path === '/') return '/'
const segments = path.replace(/\/$/, '').split('/')
if (segments.length <= 2) return path
return `…/${segments[segments.length - 1]}`
}
export default function TopPathsTable({ paths, loading }: TopPathsTableProps) {
const hasData = paths.length > 0
return (
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6">
<div className="flex items-center justify-between mb-1">
<div className="mb-1">
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
Top Paths
</h3>
</div>
<p className="text-sm text-neutral-500 dark:text-neutral-400 mb-4">
<p className="text-sm text-neutral-500 dark:text-neutral-400 mb-5">
Most common navigation paths across sessions
</p>
{loading ? (
<TableSkeleton rows={7} cols={4} />
) : hasData ? (
<div>
{/* Header */}
<div className="flex items-center px-2 -mx-2 mb-2 text-xs font-medium text-neutral-400 dark:text-neutral-500 uppercase tracking-wider">
<span className="w-8 text-right shrink-0">#</span>
<span className="flex-1 ml-3">Path</span>
<span className="w-20 text-right shrink-0">Sessions</span>
<span className="w-16 text-right shrink-0">Dur.</span>
</div>
{/* Rows */}
<div className="space-y-0.5">
{paths.map((path, i) => (
<div
key={i}
className="flex items-center h-9 group hover:bg-neutral-50 dark:hover:bg-neutral-800/50 rounded-lg px-2 -mx-2 transition-colors"
>
<span className="w-8 text-right shrink-0 text-sm tabular-nums text-neutral-400">
{i + 1}
</span>
<span
className="flex-1 ml-3 text-sm text-neutral-900 dark:text-white truncate"
title={path.page_sequence.join(' → ')}
>
{path.page_sequence.join(' → ')}
</span>
<span className="w-20 text-right shrink-0 text-sm tabular-nums text-neutral-600 dark:text-neutral-400">
{path.session_count.toLocaleString()}
</span>
<span className="w-16 text-right shrink-0 text-sm tabular-nums text-neutral-600 dark:text-neutral-400">
{formatDuration(path.avg_duration)}
<div className="space-y-2">
{paths.map((path, i) => (
<div
key={i}
className="group rounded-xl border border-neutral-100 dark:border-neutral-800 hover:border-neutral-200 dark:hover:border-neutral-700 p-4 transition-colors"
>
<div className="flex items-center justify-between mb-3">
<span className="text-xs font-medium text-neutral-400 dark:text-neutral-500">
#{i + 1}
</span>
<div className="flex items-center gap-4 text-xs text-neutral-500 dark:text-neutral-400">
<span className="flex items-center gap-1.5">
<Users weight="bold" className="w-3.5 h-3.5" />
{path.session_count.toLocaleString()}
</span>
{path.avg_duration > 0 && (
<span className="flex items-center gap-1.5">
<Clock weight="bold" className="w-3.5 h-3.5" />
{formatDuration(path.avg_duration)}
</span>
)}
</div>
</div>
))}
</div>
<div className="flex items-center flex-wrap gap-1.5">
{path.page_sequence.map((page, j) => (
<div key={j} className="flex items-center gap-1.5">
{j > 0 && (
<ArrowRight
weight="bold"
className="w-3 h-3 text-neutral-300 dark:text-neutral-600 shrink-0"
/>
)}
<span
className="inline-flex px-2.5 py-1 rounded-md text-xs font-medium bg-neutral-100 dark:bg-neutral-800 text-neutral-700 dark:text-neutral-300"
title={page}
>
{smartLabel(page)}
</span>
</div>
))}
</div>
</div>
))}
</div>
) : (
<div className="flex flex-col items-center justify-center text-center px-6 py-8 gap-3">