[PULSE-36] Funnels UI - builder and report #8
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useCallback, useEffect, useMemo, 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 { toast, LoadingOverlay, Select, DatePicker, ChevronLeftIcon, ArrowRightIcon, TrashIcon, useTheme } from '@ciphera-net/ui'
|
||||
import Link from 'next/link'
|
||||
@@ -45,8 +46,10 @@ export default function FunnelReportPage() {
|
||||
const [dateRange, setDateRange] = useState(getDateRange(30))
|
||||
const [datePreset, setDatePreset] = useState<'7' | '30' | 'custom'>('30')
|
||||
const [isDatePickerOpen, setIsDatePickerOpen] = useState(false)
|
||||
const [loadError, setLoadError] = useState<'not_found' | 'error' | null>(null)
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
setLoadError(null)
|
||||
try {
|
||||
setLoading(true)
|
||||
const [funnelData, statsData] = await Promise.all([
|
||||
@@ -56,7 +59,9 @@ export default function FunnelReportPage() {
|
||||
setFunnel(funnelData)
|
||||
setStats(statsData)
|
||||
} catch (error) {
|
||||
toast.error('Failed to load funnel data')
|
||||
const is404 = error instanceof ApiError && error.status === 404
|
||||
setLoadError(is404 ? 'not_found' : 'error')
|
||||
if (!is404) toast.error('Failed to load funnel data')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -88,6 +93,25 @@ export default function FunnelReportPage() {
|
||||
return <LoadingOverlay logoSrc="/pulse_icon_no_margins.png" title="Pulse" />
|
||||
}
|
||||
|
||||
if (loadError === 'not_found' || (!funnel && !stats && !loadError)) {
|
||||
return (
|
||||
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 py-8">
|
||||
|
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 AIShows "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>
Change: 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”.
|
||||
<p className="text-neutral-600 dark:text-neutral-400">Funnel not found</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (loadError === 'error') {
|
||||
return (
|
||||
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 py-8">
|
||||
<p className="text-neutral-600 dark:text-neutral-400 mb-4">Failed to load funnel data</p>
|
||||
<button type="button" onClick={() => loadData()} className="btn-primary">
|
||||
Try again
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!funnel || !stats) {
|
||||
return (
|
||||
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 py-8">
|
||||
|
||||
@@ -53,6 +53,11 @@ export default function CreateFunnelPage() {
|
||||
return
|
||||
}
|
||||
|
||||
if (steps.some(s => !s.name.trim())) {
|
||||
toast.error('Please enter a name for all steps')
|
||||
return
|
||||
}
|
||||
|
Validates Prompt To Fix With AIValidates `step.value` but not `step.name`. Empty/whitespace step names will render as blank labels throughout the UI (list page line 121, report page line 246).
```suggestion
if (steps.some(s => !s.value.trim())) {
toast.error('Please enter a path for all steps')
return
}
if (steps.some(s => !s.name.trim())) {
toast.error('Please enter a name for all steps')
return
}
```
<details><summary>Prompt To Fix With AI</summary>
`````markdown
This is a comment left during a code review.
Path: app/sites/[id]/funnels/new/page.tsx
Line: 56:59
Comment:
Validates `step.value` but not `step.name`. Empty/whitespace step names will render as blank labels throughout the UI (list page line 121, report page line 246).
```suggestion
if (steps.some(s => !s.value.trim())) {
toast.error('Please enter a path for all steps')
return
}
if (steps.some(s => !s.name.trim())) {
toast.error('Please enter a name for all steps')
return
}
```
How can I resolve this? If you propose a fix, please make it concise.
`````
</details>
Status: Already implemented. Status: Already implemented.
handleSubmit already has if (steps.some(s => !s.name.trim())) { toast.error('Please enter a name for all steps'); return } (lines 56–59), before the path check.
|
||||
|
||||
if (steps.some(s => !s.value.trim())) {
|
||||
|
Step validation only checks if Add a helper: Prompt To Fix With AIStep validation only checks if `value` is empty but doesn't validate regex syntax when `type === 'regex'`. Invalid regex will cause backend errors or runtime issues when matching. Consider adding client-side regex validation:
```suggestion
if (steps.some(s => !s.value.trim() || (s.type === 'regex' && !isValidRegex(s.value)))) {
```
Add a helper:
```typescript
function isValidRegex(pattern: string): boolean {
try { new RegExp(pattern); return true } catch { return false }
}
```
<details><summary>Prompt To Fix With AI</summary>
`````markdown
This is a comment left during a code review.
Path: app/sites/[id]/funnels/new/page.tsx
Line: 46:46
Comment:
Step validation only checks if `value` is empty but doesn't validate regex syntax when `type === 'regex'`. Invalid regex will cause backend errors or runtime issues when matching. Consider adding client-side regex validation:
```suggestion
if (steps.some(s => !s.value.trim() || (s.type === 'regex' && !isValidRegex(s.value)))) {
```
Add a helper:
```typescript
function isValidRegex(pattern: string): boolean {
try { new RegExp(pattern); return true } catch { return false }
}
```
How can I resolve this? If you propose a fix, please make it concise.
`````
</details>
Change: Change:
Added isValidRegex(pattern: string): boolean that does try { new RegExp(pattern); return true } catch { return false }.
After the “path for all steps” check, added a check: steps.some(s => s.type === 'regex' && !isValidRegex(s.value)). If true, show toast.error('Invalid regex in one or more steps. Check the pattern for steps with type "regex".') and return without submitting.
Why: Invalid regex for type === 'regex' is caught on the client so the user gets a clear error instead of a backend/runtime failure.
|
||||
toast.error('Please enter a path for all steps')
|
||||
return
|
||||
|
||||
Unused auth import
useAuthis imported here but never referenced, which will fail lint/typecheck in projects enforcingno-unused-vars. Remove the import to avoid CI failures.Prompt To Fix With AI
Already fixed.