[PULSE-36] Funnels UI - builder and report #8
@@ -4,8 +4,9 @@ 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, Select, DatePicker, ChevronLeftIcon, ArrowRightIcon, TrashIcon } from '@ciphera-net/ui'
|
||||
import { toast, LoadingOverlay, Select, DatePicker, ChevronLeftIcon, ArrowRightIcon, TrashIcon, useTheme } from '@ciphera-net/ui'
|
||||
import Link from 'next/link'
|
||||
import { useMemo } from 'react'
|
||||
import {
|
||||
BarChart,
|
||||
Bar,
|
||||
@@ -18,6 +19,22 @@ import {
|
||||
} from 'recharts'
|
||||
import { getDateRange } from '@/lib/utils/format'
|
||||
|
||||
const CHART_COLORS_LIGHT = {
|
||||
border: '#E5E5E5',
|
||||
axis: '#A3A3A3',
|
||||
tooltipBg: '#ffffff',
|
||||
tooltipBorder: '#E5E5E5',
|
||||
}
|
||||
|
||||
const CHART_COLORS_DARK = {
|
||||
border: '#404040',
|
||||
axis: '#737373',
|
||||
tooltipBg: '#262626',
|
||||
tooltipBorder: '#404040',
|
||||
}
|
||||
|
||||
const BRAND_ORANGE = '#FD5E0F'
|
||||
|
||||
export default function FunnelReportPage() {
|
||||
const params = useParams()
|
||||
const router = useRouter()
|
||||
@@ -81,6 +98,12 @@ export default function FunnelReportPage() {
|
||||
conversion: s.conversion
|
||||
|
|
||||
}))
|
||||
|
||||
const { resolvedTheme } = useTheme()
|
||||
const chartColors = useMemo(
|
||||
() => (resolvedTheme === 'dark' ? CHART_COLORS_DARK : CHART_COLORS_LIGHT),
|
||||
[resolvedTheme]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 py-8">
|
||||
<div className="mb-8">
|
||||
@@ -136,23 +159,23 @@ export default function FunnelReportPage() {
|
||||
</div>
|
||||
|
||||
{/* Chart */}
|
||||
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl p-6 mb-8">
|
||||
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl overflow-hidden shadow-sm p-6 mb-8">
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white mb-6">
|
||||
Funnel Visualization
|
||||
</h3>
|
||||
<div className="h-[400px] w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={chartData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#E5E5E5" />
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke={chartColors.border} />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
stroke="#A3A3A3"
|
||||
stroke={chartColors.axis}
|
||||
fontSize={12}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
<YAxis
|
||||
stroke="#A3A3A3"
|
||||
stroke={chartColors.axis}
|
||||
fontSize={12}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
@@ -163,7 +186,13 @@ export default function FunnelReportPage() {
|
||||
if (active && payload && payload.length) {
|
||||
const data = payload[0].payload;
|
||||
return (
|
||||
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 p-3 rounded-xl shadow-lg">
|
||||
<div
|
||||
className="p-3 rounded-xl shadow-lg border"
|
||||
style={{
|
||||
backgroundColor: chartColors.tooltipBg,
|
||||
borderColor: chartColors.tooltipBorder,
|
||||
}}
|
||||
>
|
||||
<p className="font-medium text-neutral-900 dark:text-white mb-1">{label}</p>
|
||||
<p className="text-brand-orange font-bold text-lg">
|
||||
{data.visitors.toLocaleString()} visitors
|
||||
@@ -186,7 +215,7 @@ export default function FunnelReportPage() {
|
||||
/>
|
||||
<Bar dataKey="visitors" radius={[4, 4, 0, 0]} barSize={60}>
|
||||
{chartData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill="#FD5E0F" fillOpacity={1 - (index * 0.15)} />
|
||||
<Cell key={`cell-${index}`} fill={BRAND_ORANGE} fillOpacity={1 - (index * 0.15)} />
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
|
||||
@@ -80,8 +80,8 @@ export default function FunnelsPage() {
|
||||
|
||||
{funnels.length === 0 ? (
|
||||
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl p-12 text-center">
|
||||
<div className="w-16 h-16 mx-auto mb-4 bg-neutral-100 dark:bg-neutral-800 rounded-full flex items-center justify-center">
|
||||
<ArrowRightIcon className="w-8 h-8 text-neutral-400" />
|
||||
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4 mx-auto mb-4 w-fit">
|
||||
<ArrowRightIcon className="w-8 h-8 text-neutral-500 dark:text-neutral-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white mb-2">
|
||||
No funnels yet
|
||||
@@ -119,7 +119,7 @@ export default function FunnelsPage() {
|
||||
<div className="flex items-center gap-2 mt-4">
|
||||
{funnel.steps.map((step, i) => (
|
||||
<div key={i} className="flex items-center text-sm text-neutral-500">
|
||||
<span className="px-2 py-1 bg-neutral-100 dark:bg-neutral-800 rounded text-neutral-700 dark:text-neutral-300">
|
||||
<span className="px-2 py-1 bg-neutral-100 dark:bg-neutral-800 rounded-lg text-neutral-700 dark:text-neutral-300">
|
||||
{step.name}
|
||||
</span>
|
||||
{i < funnel.steps.length - 1 && (
|
||||
|
||||
Reference in New Issue
Block a user
Shows "Funnel not found" for all errors (404, 403, 500, network failures). For transient errors, this suggests the funnel doesn't exist when it might just be a temporary issue. Consider tracking error type separately to show appropriate messages (e.g., "Unable to load funnel" for 500/network, "Access denied" for 403).
Prompt To Fix With AI
Change:
loadError is now 'not_found' | 'forbidden' | 'error' | null.
In loadData catch: status === 404 → setLoadError('not_found'); status === 403 → setLoadError('forbidden'); otherwise → setLoadError('error'). Toast only for non-404, non-403.
Render:
loadError === 'not_found' (or fallback) → "Funnel not found" (unchanged).
loadError === 'forbidden' → "Access denied" and a "Back to Funnels" link.
loadError === 'error' → "Unable to load funnel" and "Try again" button that calls loadData().
Why: 404 stays “Funnel not found”, 403 shows “Access denied” with a way back, and 500/network show “Unable to load funnel” with retry so transient errors are no longer treated as “not found”.