diff --git a/app/sites/[id]/funnels/[funnelId]/page.tsx b/app/sites/[id]/funnels/[funnelId]/page.tsx new file mode 100644 index 0000000..ba8da73 --- /dev/null +++ b/app/sites/[id]/funnels/[funnelId]/page.tsx @@ -0,0 +1,265 @@ +'use client' + +import { useAuth } from '@/lib/auth/context' +import { useEffect, useState } from 'react' +import { useParams, useRouter } from 'next/navigation' +import { getFunnel, getFunnelStats, deleteFunnel, type Funnel, type FunnelStats } from '@/lib/api/funnels' +import { toast, LoadingOverlay, Card, Select, DatePicker } from '@ciphera-net/ui' +import Link from 'next/link' +import { LuChevronLeft as ChevronLeftIcon, LuTrash as TrashIcon, LuEdit as EditIcon, LuArrowRight as ArrowRightIcon } from 'react-icons/lu' +import { + BarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + Cell +} from 'recharts' +import { getDateRange } from '@/lib/utils/format' + +export default function FunnelReportPage() { + const params = useParams() + const router = useRouter() + const siteId = params.id as string + const funnelId = params.funnelId as string + + const [funnel, setFunnel] = useState(null) + const [stats, setStats] = useState(null) + const [loading, setLoading] = useState(true) + const [dateRange, setDateRange] = useState(getDateRange(30)) + const [isDatePickerOpen, setIsDatePickerOpen] = useState(false) + + useEffect(() => { + loadData() + }, [siteId, funnelId, dateRange]) + + const loadData = async () => { + try { + setLoading(true) + const [funnelData, statsData] = await Promise.all([ + getFunnel(siteId, funnelId), + getFunnelStats(siteId, funnelId, dateRange.start, dateRange.end) + ]) + setFunnel(funnelData) + setStats(statsData) + } catch (error) { + toast.error('Failed to load funnel data') + } finally { + setLoading(false) + } + } + + const handleDelete = async () => { + if (!confirm('Are you sure you want to delete this funnel?')) return + + try { + await deleteFunnel(siteId, funnelId) + toast.success('Funnel deleted') + router.push(`/sites/${siteId}/funnels`) + } catch (error) { + toast.error('Failed to delete funnel') + } + } + + if (loading && !funnel) { + return + } + + if (!funnel || !stats) { + return ( +
+

Funnel not found

+
+ ) + } + + const chartData = stats.steps.map(s => ({ + name: s.step.name, + visitors: s.visitors, + dropoff: s.dropoff, + conversion: s.conversion + })) + + return ( +
+
+
+
+ + + +
+

+ {funnel.name} +

+ {funnel.description && ( +

+ {funnel.description} +

+ )} +
+
+ +
+ setName(e.target.value)} + placeholder="e.g. Signup Flow" + required + /> +
+
+ + setDescription(e.target.value)} + placeholder="Tracks users from landing page to signup" + /> +
+
+ + +
+
+

+ Funnel Steps +

+
+ + {steps.map((step, index) => ( + +
+
+
+ {index + 1} +
+
+ +
+
+ + handleUpdateStep(index, 'name', e.target.value)} + placeholder="e.g. Landing Page" + /> +
+
+ +
+ + handleUpdateStep(index, 'value', e.target.value)} + placeholder={step.type === 'exact' ? '/pricing' : 'pricing'} + className="flex-1" + /> +
+
+
+ + +
+
+ ))} + + +
+ +
+ + Cancel + + +
+ +
+ ) +} diff --git a/app/sites/[id]/funnels/page.tsx b/app/sites/[id]/funnels/page.tsx new file mode 100644 index 0000000..d47dd50 --- /dev/null +++ b/app/sites/[id]/funnels/page.tsx @@ -0,0 +1,151 @@ +'use client' + +import { useAuth } from '@/lib/auth/context' +import { useEffect, useState } from 'react' +import { useParams, useRouter } from 'next/navigation' +import { listFunnels, deleteFunnel, type Funnel } from '@/lib/api/funnels' +import { toast, LoadingOverlay, Card } from '@ciphera-net/ui' +import Link from 'next/link' +import { LuPlus as PlusIcon, LuTrash as TrashIcon, LuArrowRight as ArrowRightIcon, LuChevronLeft as ChevronLeftIcon } from 'react-icons/lu' + +export default function FunnelsPage() { + const { user } = useAuth() + const params = useParams() + const router = useRouter() + const siteId = params.id as string + + const [funnels, setFunnels] = useState([]) + const [loading, setLoading] = useState(true) + + useEffect(() => { + loadFunnels() + }, [siteId]) + + const loadFunnels = async () => { + try { + setLoading(true) + const data = await listFunnels(siteId) + setFunnels(data) + } catch (error) { + toast.error('Failed to load funnels') + } finally { + setLoading(false) + } + } + + const handleDelete = async (e: React.MouseEvent, funnelId: string) => { + e.preventDefault() // Prevent navigation + if (!confirm('Are you sure you want to delete this funnel?')) return + + try { + await deleteFunnel(siteId, funnelId) + toast.success('Funnel deleted') + loadFunnels() + } catch (error) { + toast.error('Failed to delete funnel') + } + } + + if (loading) { + return + } + + return ( +
+
+
+ + + +
+

+ Funnels +

+

+ Track user journeys and identify drop-off points +

+
+
+ + + Create Funnel + +
+
+ + {funnels.length === 0 ? ( + +
+ +
+

+ No funnels yet +

+

+ Create a funnel to track how users move through your site and where they drop off. +

+ + + Create Funnel + +
+ ) : ( +
+ {funnels.map((funnel) => ( + + +
+
+

+ {funnel.name} +

+ {funnel.description && ( +

+ {funnel.description} +

+ )} +
+ {funnel.steps.map((step, i) => ( +
+ + {step.name} + + {i < funnel.steps.length - 1 && ( + + )} +
+ ))} +
+
+
+ + +
+
+
+ + ))} +
+ )} +
+
+ ) +} diff --git a/app/sites/[id]/page.tsx b/app/sites/[id]/page.tsx index d3ffc9e..154a206 100644 --- a/app/sites/[id]/page.tsx +++ b/app/sites/[id]/page.tsx @@ -297,6 +297,12 @@ export default function SiteDashboardPage() { { value: 'custom', label: 'Custom' }, ]} /> + {canEdit && (