diff --git a/app/sites/[id]/funnels/[funnelId]/page.tsx b/app/sites/[id]/funnels/[funnelId]/page.tsx index 1026cec..43ebae6 100644 --- a/app/sites/[id]/funnels/[funnelId]/page.tsx +++ b/app/sites/[id]/funnels/[funnelId]/page.tsx @@ -7,24 +7,9 @@ import { getFunnel, getFunnelStats, deleteFunnel, type Funnel, type FunnelStats import { toast, Select, DatePicker, ChevronLeftIcon, ArrowRightIcon, TrashIcon, Button } from '@ciphera-net/ui' import { FunnelDetailSkeleton, useMinimumLoading } from '@/components/skeletons' import Link from 'next/link' -import { - BarChart, - Bar, - XAxis, - YAxis, - CartesianGrid, - Cell -} from 'recharts' -import { ChartContainer, ChartTooltip, type ChartConfig } from '@/components/charts' +import FunnelChart from '@/components/dashboard/FunnelChart' import { getDateRange } from '@ciphera-net/ui' -const chartConfig = { - visitors: { - label: 'Visitors', - color: 'var(--chart-1)', - }, -} satisfies ChartConfig - export default function FunnelReportPage() { const params = useParams() const router = useRouter() @@ -188,56 +173,7 @@ export default function FunnelReportPage() {

Funnel Visualization

- - - - - - { - if (active && payload && payload.length) { - const data = payload[0].payload; - return ( -
-

{label}

-

- {data.visitors.toLocaleString()} visitors -

- {data.dropoff > 0 && ( -

- {Math.round(data.dropoff)}% drop-off -

- )} - {data.conversion > 0 && ( -

- {Math.round(data.conversion)}% conversion (overall) -

- )} -
- ); - } - return null; - }} - /> - - {chartData.map((entry, index) => ( - - ))} - -
-
+ {/* Detailed Stats Table */} diff --git a/components/dashboard/FunnelChart.tsx b/components/dashboard/FunnelChart.tsx new file mode 100644 index 0000000..5408c5e --- /dev/null +++ b/components/dashboard/FunnelChart.tsx @@ -0,0 +1,121 @@ +'use client' + +import { useState } from 'react' +import { motion } from 'framer-motion' +import { cn, formatNumber } from '@ciphera-net/ui' + +interface FunnelChartProps { + steps: Array<{ + name: string + visitors: number + dropoff: number + conversion: number + }> + className?: string +} + +export default function FunnelChart({ steps, className }: FunnelChartProps) { + const [hoveredIndex, setHoveredIndex] = useState(null) + + if (!steps.length) return null + + const maxVisitors = steps[0].visitors + const n = steps.length + + return ( +
+ {/* Left labels */} +
+ {steps.map((step, i) => ( +
+
+ + Step {i + 1} + +

+ {step.name} +

+
+
+ ))} +
+ + {/* Funnel segments */} +
+ {steps.map((step, i) => { + const topPct = maxVisitors > 0 + ? Math.max(30, (step.visitors / maxVisitors) * 100) + : 100 + const bottomPct = steps[i + 1] + ? Math.max(30, (steps[i + 1].visitors / maxVisitors) * 100) + : topPct * 0.7 + const topInset = (100 - topPct) / 2 + const bottomInset = (100 - bottomPct) / 2 + const opacity = Math.max(0.3, 1 - i * (0.55 / n)) + const isHovered = hoveredIndex === i + + return ( + setHoveredIndex(i)} + onMouseLeave={() => setHoveredIndex(null)} + /> + ) + })} +
+ + {/* Right stats */} +
+ {steps.map((step, i) => ( +
+
+

+ {formatNumber(step.visitors)} +

+ {i > 0 ? ( +
+ + ↓{Math.round(step.dropoff)}% + + + {Math.round(step.conversion)}% + +
+ ) : ( + + baseline + + )} +
+
+ ))} +
+
+ ) +}