From 2811945d3e0fc996321ec42d8a6e86c0a2297b66 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Wed, 18 Mar 2026 14:26:26 +0100 Subject: [PATCH] feat(funnels): add filter bar and exit path display to funnel detail --- app/sites/[id]/funnels/[funnelId]/page.tsx | 121 ++++++++++++++------- 1 file changed, 84 insertions(+), 37 deletions(-) diff --git a/app/sites/[id]/funnels/[funnelId]/page.tsx b/app/sites/[id]/funnels/[funnelId]/page.tsx index 655abe3..4936335 100644 --- a/app/sites/[id]/funnels/[funnelId]/page.tsx +++ b/app/sites/[id]/funnels/[funnelId]/page.tsx @@ -1,9 +1,12 @@ 'use client' -import { useCallback, useEffect, useState } from 'react' +import React, { useCallback, useEffect, useState } from 'react' import { useParams, useRouter } from 'next/navigation' import { ApiError } from '@/lib/api/client' import { getFunnel, getFunnelStats, deleteFunnel, type Funnel, type FunnelStats } from '@/lib/api/funnels' +import FilterBar from '@/components/dashboard/FilterBar' +import AddFilterDropdown from '@/components/dashboard/AddFilterDropdown' +import { type DimensionFilter, serializeFilters } from '@/lib/filters' import { toast, Select, DatePicker, ChevronLeftIcon, ArrowRightIcon, TrashIcon, Button } from '@ciphera-net/ui' import { FunnelDetailSkeleton, useMinimumLoading, useSkeletonFade } from '@/components/skeletons' import Link from 'next/link' @@ -23,6 +26,8 @@ export default function FunnelReportPage() { const [datePreset, setDatePreset] = useState<'7' | '30' | 'custom'>('30') const [isDatePickerOpen, setIsDatePickerOpen] = useState(false) const [loadError, setLoadError] = useState<'not_found' | 'forbidden' | 'error' | null>(null) + const [filters, setFilters] = useState([]) + const [expandedExitStep, setExpandedExitStep] = useState(null) const loadData = useCallback(async () => { setLoadError(null) @@ -30,7 +35,7 @@ export default function FunnelReportPage() { setLoading(true) const [funnelData, statsData] = await Promise.all([ getFunnel(siteId, funnelId), - getFunnelStats(siteId, funnelId, dateRange.start, dateRange.end) + getFunnelStats(siteId, funnelId, dateRange.start, dateRange.end, serializeFilters(filters) || undefined) ]) setFunnel(funnelData) setStats(statsData) @@ -43,7 +48,7 @@ export default function FunnelReportPage() { } finally { setLoading(false) } - }, [siteId, funnelId, dateRange]) + }, [siteId, funnelId, dateRange, filters]) useEffect(() => { loadData() @@ -167,6 +172,18 @@ export default function FunnelReportPage() { + {/* Filters */} +
+ setFilters(prev => [...prev, f])} + /> + setFilters(prev => prev.filter((_, idx) => idx !== i))} + onClear={() => setFilters([])} + /> +
+ {/* Chart */}

@@ -195,42 +212,72 @@ export default function FunnelReportPage() { {stats.steps.map((step, i) => ( - - -
- - {i + 1} - -
-

{step.step.name}

-

{step.step.value}

+ + + +
+ + {i + 1} + +
+

{step.step.name}

+

{step.step.value}

+
-
- - - - {step.visitors.toLocaleString()} - - - - {i > 0 ? ( - 50 - ? 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400' - : 'bg-neutral-100 text-neutral-800 dark:bg-neutral-800 dark:text-neutral-300' - }`}> - {Math.round(step.dropoff)}% + + + + {step.visitors.toLocaleString()} - ) : ( - - - )} - - - - {Math.round(step.conversion)}% - - - + + + {i > 0 ? ( + 50 + ? 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400' + : 'bg-neutral-100 text-neutral-800 dark:bg-neutral-800 dark:text-neutral-300' + }`}> + {Math.round(step.dropoff)}% + + ) : ( + - + )} + + + + {Math.round(step.conversion)}% + + + + {step.exit_pages && step.exit_pages.length > 0 && ( + + +
+

+ Where visitors went after dropping off: +

+
+ {(expandedExitStep === i ? step.exit_pages : step.exit_pages.slice(0, 3)).map(ep => ( + + {ep.path} + {ep.visitors} + + ))} +
+ {step.exit_pages.length > 3 && ( + + )} +
+ + + )} + ))}