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
+
+ )}
+
+
+ ))}
+
+
+ )
+}