Consistency fixes: - Extract getThisWeekRange/getThisMonthRange to shared lib/utils/dateRanges.ts (removed 4 identical copy-pasted definitions) - Add error boundaries for behavior, cdn, search, pagespeed pages (4 new error.tsx files — previously fell through to generic parent error) - Add "View setup guide" CTA to empty states on journeys and behavior pages (previously showed text with no actionable button) - Fix non-lazy useState initializer in funnel detail page - Fix Bot & Spam settings header from text-xl to text-2xl (matches all other sections) - Add useMinimumLoading to PageSpeed skeleton (consistent with all other pages) Cleanup: - Remove 438 redundant dark: class prefixes (app is dark-mode only) text-neutral-500 dark:text-neutral-400 → text-neutral-400 (206 occurrences) text-neutral-900 dark:text-white → text-white (232 occurrences) - Remove dead @stripe/react-stripe-js and @stripe/stripe-js packages (billing migrated to Polar, no code imports Stripe) - Remove duplicate motion package (framer-motion is the one actually used)
133 lines
5.3 KiB
TypeScript
133 lines
5.3 KiB
TypeScript
'use client'
|
|
|
|
import { useParams, useRouter } from 'next/navigation'
|
|
import { deleteFunnel, type Funnel } from '@/lib/api/funnels'
|
|
import { useFunnels } from '@/lib/swr/dashboard'
|
|
import { toast, PlusIcon, ArrowRightIcon, ChevronLeftIcon, TrashIcon, Button } from '@ciphera-net/ui'
|
|
import { FunnelsListSkeleton, useMinimumLoading, useSkeletonFade } from '@/components/skeletons'
|
|
import Link from 'next/link'
|
|
import Image from 'next/image'
|
|
|
|
export default function FunnelsPage() {
|
|
const params = useParams()
|
|
const router = useRouter()
|
|
const siteId = params.id as string
|
|
|
|
const { data: funnels = [], isLoading, mutate } = useFunnels(siteId)
|
|
|
|
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')
|
|
mutate()
|
|
} catch (error) {
|
|
toast.error('Failed to delete funnel')
|
|
}
|
|
}
|
|
|
|
const showSkeleton = useMinimumLoading(isLoading && !funnels.length)
|
|
const fadeClass = useSkeletonFade(showSkeleton)
|
|
|
|
if (showSkeleton) {
|
|
return <FunnelsListSkeleton />
|
|
}
|
|
|
|
return (
|
|
<div className={`w-full max-w-6xl mx-auto px-4 sm:px-6 pb-8 ${fadeClass}`}>
|
|
<div className="mb-8">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-white">
|
|
Funnels
|
|
</h1>
|
|
<p className="text-neutral-600 dark:text-neutral-400">
|
|
Track user journeys and identify drop-off points
|
|
</p>
|
|
</div>
|
|
<Link href={`/sites/${siteId}/funnels/new`}>
|
|
<Button variant="primary" className="inline-flex items-center gap-2">
|
|
<PlusIcon className="w-4 h-4" />
|
|
<span>Create Funnel</span>
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
|
|
{funnels.length === 0 ? (
|
|
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-12 text-center flex flex-col items-center">
|
|
<Image
|
|
src="/illustrations/data-trends.svg"
|
|
alt="Create your first funnel"
|
|
width={260}
|
|
height={195}
|
|
className="mb-6"
|
|
unoptimized
|
|
/>
|
|
<h3 className="text-lg font-semibold text-white mb-2">
|
|
No funnels yet
|
|
</h3>
|
|
<p className="text-neutral-600 dark:text-neutral-400 mb-6 max-w-md mx-auto">
|
|
Create a funnel to track how users move through your site and where they drop off.
|
|
</p>
|
|
<Link href={`/sites/${siteId}/funnels/new`}>
|
|
<Button variant="primary" className="inline-flex items-center gap-2">
|
|
<PlusIcon className="w-4 h-4" />
|
|
<span>Create Funnel</span>
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
) : (
|
|
<div className="grid gap-4">
|
|
{funnels.map((funnel) => (
|
|
<Link
|
|
key={funnel.id}
|
|
href={`/sites/${siteId}/funnels/${funnel.id}`}
|
|
className="block group"
|
|
>
|
|
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 hover:border-brand-orange/50 transition-colors">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h3 className="text-xl font-bold text-white group-hover:text-brand-orange transition-colors">
|
|
{funnel.name}
|
|
</h3>
|
|
{funnel.description && (
|
|
<p className="text-sm text-neutral-600 dark:text-neutral-400 mt-1">
|
|
{funnel.description}
|
|
</p>
|
|
)}
|
|
<div className="flex items-center gap-2 mt-4">
|
|
{funnel.steps.map((step, i) => (
|
|
<div key={step.name} className="flex items-center text-sm text-neutral-500">
|
|
<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 && (
|
|
<ArrowRightIcon className="w-4 h-4 mx-2 text-neutral-300" />
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-4">
|
|
<button
|
|
onClick={(e) => handleDelete(e, funnel.id)}
|
|
className="p-2 text-neutral-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-xl transition-colors"
|
|
aria-label="Delete funnel"
|
|
>
|
|
<TrashIcon className="w-5 h-5" />
|
|
</button>
|
|
<ChevronLeftIcon className="w-5 h-5 text-neutral-300 rotate-180" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|