BunnyCDN, Search tab, journeys redesign, and dashboard polish #52

Merged
uz1mani merged 86 commits from staging into main 2026-03-17 10:08:26 +00:00
32 changed files with 3101 additions and 1128 deletions
Showing only changes of commit 722b5de88d - Show all commits

View File

@@ -1,6 +1,6 @@
'use client' 'use client'
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import { Fragment, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
import { TreeStructure } from '@phosphor-icons/react' import { TreeStructure } from '@phosphor-icons/react'
import type { PathTransition } from '@/lib/api/journeys' import type { PathTransition } from '@/lib/api/journeys'
@@ -121,20 +121,24 @@ function ColumnHeader({
color: string color: string
}) { }) {
return ( return (
<div className="flex flex-col items-center gap-2 mb-4"> <div className="flex flex-col items-center gap-1.5 mb-3">
<span <span
className="flex items-center justify-center w-9 h-9 rounded-full text-sm font-bold text-white" className="flex items-center justify-center w-9 h-9 rounded-full text-sm font-bold text-white"
style={{ backgroundColor: color }} style={{ backgroundColor: color }}
> >
{column.index + 1} {column.index + 1}
</span> </span>
<div className="flex items-baseline gap-2"> <div className="flex flex-col items-center">
<span className="text-sm font-semibold text-neutral-900 dark:text-white"> <span className="text-lg font-bold text-neutral-900 dark:text-white tabular-nums">
{column.totalSessions.toLocaleString()} visitors {column.totalSessions.toLocaleString()}
</span>
<div className="flex items-center gap-1.5">
<span className="text-xs text-neutral-500 dark:text-neutral-400">
visitors
</span> </span>
{column.dropOffPercent !== 0 && ( {column.dropOffPercent !== 0 && (
<span <span
className={`text-sm font-semibold ${ className={`text-xs font-semibold ${
column.dropOffPercent < 0 ? 'text-red-500' : 'text-emerald-500' column.dropOffPercent < 0 ? 'text-red-500' : 'text-emerald-500'
}`} }`}
> >
@@ -144,25 +148,32 @@ function ColumnHeader({
)} )}
</div> </div>
</div> </div>
{/* Colored connector line from header to cards */}
<div className="w-px h-3" style={{ backgroundColor: color, opacity: 0.3 }} />
</div>
) )
} }
function PageRow({ function PageRow({
page, page,
colIndex, colIndex,
colColor,
columnTotal, columnTotal,
maxCount,
isSelected, isSelected,
isOther, isOther,
onClick, onClick,
}: { }: {
page: ColumnPage page: ColumnPage
colIndex: number colIndex: number
colColor: string
columnTotal: number columnTotal: number
maxCount: number
isSelected: boolean isSelected: boolean
isOther: boolean isOther: boolean
onClick: () => void onClick: () => void
}) { }) {
const pct = columnTotal > 0 ? Math.round((page.sessionCount / columnTotal) * 100) : 0 const barWidth = maxCount > 0 ? (page.sessionCount / maxCount) * 100 : 0
return ( return (
<button <button
@@ -173,22 +184,36 @@ function PageRow({
data-col={colIndex} data-col={colIndex}
data-path={page.path} data-path={page.path}
className={` className={`
group flex items-center justify-between w-full group flex items-center justify-between w-full relative overflow-hidden
px-3 py-2.5 rounded-lg text-left transition-all px-3 py-2.5 rounded-lg text-left transition-all
${isOther ? 'cursor-default' : 'cursor-pointer'} ${isOther ? 'cursor-default' : 'cursor-pointer'}
${ ${
isSelected isSelected
? 'bg-neutral-900 dark:bg-white border border-neutral-900 dark:border-white' ? 'border-l-[3px] border border-neutral-200 dark:border-neutral-700'
: isOther : isOther
? 'border border-neutral-100 dark:border-neutral-800' ? 'border border-neutral-100 dark:border-neutral-800'
: 'border border-neutral-200 dark:border-neutral-700 hover:border-neutral-300 dark:hover:border-neutral-600 hover:shadow-sm' : 'border border-neutral-200 dark:border-neutral-700 hover:border-neutral-300 dark:hover:border-neutral-600 hover:shadow-sm'
} }
`} `}
style={isSelected ? { borderLeftColor: colColor, backgroundColor: `${colColor}08` } : undefined}
> >
{/* Background bar */}
{!isOther && !isSelected && (
<div
className="absolute inset-y-0 left-0 bg-neutral-100 dark:bg-neutral-800/50 transition-all"
style={{ width: `${barWidth}%` }}
/>
)}
{isSelected && (
<div
className="absolute inset-y-0 left-0 transition-all"
style={{ width: `${barWidth}%`, backgroundColor: `${colColor}15` }}
/>
)}
<span <span
className={`flex-1 truncate text-sm ${ className={`relative flex-1 truncate text-sm ${
isSelected isSelected
? 'text-white dark:text-neutral-900 font-medium' ? 'text-neutral-900 dark:text-white font-medium'
: isOther : isOther
? 'italic text-neutral-400 dark:text-neutral-500' ? 'italic text-neutral-400 dark:text-neutral-500'
: 'text-neutral-700 dark:text-neutral-200' : 'text-neutral-700 dark:text-neutral-200'
@@ -197,12 +222,12 @@ function PageRow({
{isOther ? page.path : smartLabel(page.path)} {isOther ? page.path : smartLabel(page.path)}
</span> </span>
<span <span
className={`ml-3 shrink-0 text-sm tabular-nums font-semibold ${ className={`relative ml-3 shrink-0 text-xs tabular-nums font-semibold px-1.5 py-0.5 rounded ${
isSelected isSelected
? 'text-white dark:text-neutral-900' ? 'text-neutral-900 dark:text-white bg-white/60 dark:bg-neutral-800/60'
: isOther : isOther
? 'text-neutral-400 dark:text-neutral-500' ? 'text-neutral-400 dark:text-neutral-500'
: 'text-neutral-900 dark:text-white' : 'text-neutral-600 dark:text-neutral-300 bg-neutral-100 dark:bg-neutral-800 group-hover:bg-neutral-200 dark:group-hover:bg-neutral-700'
}`} }`}
> >
{page.sessionCount.toLocaleString()} {page.sessionCount.toLocaleString()}
@@ -237,6 +262,8 @@ function JourneyColumn({
) )
} }
const maxCount = Math.max(...column.pages.map((p) => p.sessionCount), 0)
return ( return (
<div className="w-60 shrink-0"> <div className="w-60 shrink-0">
<ColumnHeader column={column} color={color} /> <ColumnHeader column={column} color={color} />
@@ -248,7 +275,9 @@ function JourneyColumn({
key={page.path} key={page.path}
page={page} page={page}
colIndex={column.index} colIndex={column.index}
colColor={color}
columnTotal={column.totalSessions} columnTotal={column.totalSessions}
maxCount={maxCount}
isSelected={selectedPath === page.path} isSelected={selectedPath === page.path}
isOther={isOther} isOther={isOther}
onClick={() => { onClick={() => {
@@ -499,23 +528,28 @@ export default function ColumnJourney({
ref={containerRef} ref={containerRef}
className="overflow-x-auto -mx-6 px-6 pb-2 relative" className="overflow-x-auto -mx-6 px-6 pb-2 relative"
> >
<div className="flex gap-8 min-w-fit py-2"> <div className="flex min-w-fit py-2">
{columns.map((col) => { {columns.map((col, i) => {
// Show exit card in this column if the previous column has a selection
const prevSelection = selections.get(col.index - 1) const prevSelection = selections.get(col.index - 1)
const exitCount = prevSelection const exitCount = prevSelection
? getExitCount(col.index - 1, prevSelection, columns, transitions) ? getExitCount(col.index - 1, prevSelection, columns, transitions)
: 0 : 0
return ( return (
<Fragment key={col.index}>
{i > 0 && (
<div className="flex items-center justify-center w-8 shrink-0 pt-16">
<div className="w-full border-t border-dashed border-neutral-200 dark:border-neutral-700" />
</div>
)}
<JourneyColumn <JourneyColumn
key={col.index}
column={col} column={col}
color={colorForColumn(col.index)} color={colorForColumn(col.index)}
selectedPath={selections.get(col.index)} selectedPath={selections.get(col.index)}
exitCount={exitCount} exitCount={exitCount}
onSelect={(path) => handleSelect(col.index, path)} onSelect={(path) => handleSelect(col.index, path)}
/> />
</Fragment>
) )
})} })}
</div> </div>