diff --git a/app/sites/[id]/funnels/[funnelId]/page.tsx b/app/sites/[id]/funnels/[funnelId]/page.tsx index 5e688d9..7050a1d 100644 --- a/app/sites/[id]/funnels/[funnelId]/page.tsx +++ b/app/sites/[id]/funnels/[funnelId]/page.tsx @@ -3,7 +3,7 @@ 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 { getFunnel, getFunnelStats, getFunnelTrends, deleteFunnel, type Funnel, type FunnelStats, type FunnelTrends } from '@/lib/api/funnels' import FilterBar from '@/components/dashboard/FilterBar' import AddFilterDropdown from '@/components/dashboard/AddFilterDropdown' import { type DimensionFilter, serializeFilters } from '@/lib/filters' @@ -13,6 +13,7 @@ import { FunnelDetailSkeleton, useMinimumLoading, useSkeletonFade } from '@/comp import Link from 'next/link' import { FunnelChart } from '@/components/ui/funnel-chart' import { getDateRange } from '@ciphera-net/ui' +import { ResponsiveContainer, LineChart, Line, XAxis, YAxis, Tooltip, CartesianGrid } from 'recharts' export default function FunnelReportPage() { const params = useParams() @@ -29,17 +30,22 @@ export default function FunnelReportPage() { const [loadError, setLoadError] = useState<'not_found' | 'forbidden' | 'error' | null>(null) const [filters, setFilters] = useState([]) const [expandedExitStep, setExpandedExitStep] = useState(null) + const [trends, setTrends] = useState(null) + const [visibleSteps, setVisibleSteps] = useState>(new Set()) const loadData = useCallback(async () => { setLoadError(null) try { setLoading(true) - const [funnelData, statsData] = await Promise.all([ + const filterStr = serializeFilters(filters) || undefined + const [funnelData, statsData, trendsData] = await Promise.all([ getFunnel(siteId, funnelId), - getFunnelStats(siteId, funnelId, dateRange.start, dateRange.end, serializeFilters(filters) || undefined) + getFunnelStats(siteId, funnelId, dateRange.start, dateRange.end, filterStr), + getFunnelTrends(siteId, funnelId, dateRange.start, dateRange.end, 'day', filterStr) ]) setFunnel(funnelData) setStats(statsData) + setTrends(trendsData) } catch (error) { const status = error instanceof ApiError ? error.status : 0 if (status === 404) setLoadError('not_found') @@ -119,6 +125,21 @@ export default function FunnelReportPage() { value: s.visitors, })) + const STEP_COLORS = ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899', '#06B6D4', '#84CC16'] + + const trendsChartData = trends ? trends.dates.map((date, idx) => { + const point: Record = { + date: new Date(date).toLocaleDateString('en-GB', { day: 'numeric', month: 'short' }), + overall: Math.round(trends.overall[idx] * 10) / 10, + } + for (const [stepKey, values] of Object.entries(trends.steps)) { + if (visibleSteps.has(stepKey)) { + point[`step_${stepKey}`] = Math.round(values[idx] * 10) / 10 + } + } + return point + }) : [] + return (
@@ -206,6 +227,90 @@ export default function FunnelReportPage() { />
+ {/* Conversion Trends */} + {trends && trends.dates.length > 1 && ( +
+
+

+ Conversion Trends +

+
+ {stats?.steps.map((s, i) => ( + + ))} +
+
+ +
+ + + + + `${v}%`} + tick={{ fontSize: 12 }} + className="text-neutral-500" + /> + [`${value}%`]} + contentStyle={{ + backgroundColor: 'var(--color-neutral-900, #171717)', + border: '1px solid var(--color-neutral-700, #404040)', + borderRadius: '8px', + color: '#fff', + fontSize: '12px', + }} + /> + + {Array.from(visibleSteps).map((stepKey) => ( + + ))} + + +
+
+ )} + {/* Detailed Stats Table */}