[PULSE-36] Funnels UI - builder and report #8

Merged
uz1mani merged 12 commits from staging into main 2026-02-04 23:16:04 +00:00
7 changed files with 746 additions and 5 deletions
Showing only changes of commit 40dffcdb1e - Show all commits

View File

@@ -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
greptile-apps[bot] commented 2026-02-04 23:08:05 +00:00 (Migrated from github.com)
Review

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
This is a comment left during a code review.
Path: app/sites/[id]/funnels/[funnelId]/page.tsx
Line: 91:96

Comment:
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).

How can I resolve this? If you propose a fix, please make it concise.
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). <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: app/sites/[id]/funnels/[funnelId]/page.tsx Line: 91:96 Comment: 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). How can I resolve this? If you propose a fix, please make it concise. ````` </details>
uz1mani commented 2026-02-04 23:09:39 +00:00 (Migrated from github.com)
Review

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”.

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”.
}))
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>

View File

@@ -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 && (