11'use client' ;
22
33import { FunnelIcon , TrendDownIcon } from '@phosphor-icons/react' ;
4+ import { format , subDays , subHours } from 'date-fns' ;
45import { useAtom } from 'jotai' ;
56import { useParams } from 'next/navigation' ;
67import { lazy , Suspense , useCallback , useMemo , useRef , useState } from 'react' ;
8+ import type { DateRange as DayPickerRange } from 'react-day-picker' ;
9+ import { DateRangePicker } from '@/components/date-range-picker' ;
10+ import { Button } from '@/components/ui/button' ;
711import { Card , CardContent } from '@/components/ui/card' ;
812import {
913 type CreateFunnelData ,
@@ -13,8 +17,11 @@ import {
1317 useFunnelAnalyticsByReferrer ,
1418 useFunnels ,
1519} from '@/hooks/use-funnels' ;
20+ import { trpc } from '@/lib/trpc' ;
1621import {
22+ dateRangeAtom ,
1723 formattedDateRangeAtom ,
24+ setDateRangeAndAdjustGranularityAtom ,
1825 timeGranularityAtom ,
1926} from '@/stores/jotai/filterAtoms' ;
2027import { WebsitePageHeader } from '../_components/website-page-header' ;
@@ -93,9 +100,58 @@ export default function FunnelsPage() {
93100 // Intersection observer for lazy loading
94101 const pageRef = useRef < HTMLDivElement > ( null ) ;
95102
103+ // Date range state
104+ const [ currentDateRange ] = useAtom ( dateRangeAtom ) ;
96105 const [ currentGranularity ] = useAtom ( timeGranularityAtom ) ;
106+ const [ , setDateRangeAction ] = useAtom ( setDateRangeAndAdjustGranularityAtom ) ;
97107 const [ formattedDateRangeState ] = useAtom ( formattedDateRangeAtom ) ;
98108
109+ // Check tracking setup
110+ const { data : trackingSetupData , isLoading : isTrackingSetupLoading } =
111+ trpc . websites . isTrackingSetup . useQuery (
112+ { websiteId } ,
113+ { enabled : ! ! websiteId }
114+ ) ;
115+
116+ const isTrackingSetup = useMemo ( ( ) => {
117+ if ( isTrackingSetupLoading ) {
118+ return null ;
119+ }
120+ return trackingSetupData ?. tracking_setup ?? false ;
121+ } , [ isTrackingSetupLoading , trackingSetupData ?. tracking_setup ] ) ;
122+
123+ // Date picker helpers
124+ const dayPickerSelectedRange : DayPickerRange | undefined = useMemo (
125+ ( ) => ( {
126+ from : currentDateRange . startDate ,
127+ to : currentDateRange . endDate ,
128+ } ) ,
129+ [ currentDateRange ]
130+ ) ;
131+
132+ const quickRanges = useMemo (
133+ ( ) => [
134+ { label : '24h' , fullLabel : 'Last 24 hours' , hours : 24 } ,
135+ { label : '7d' , fullLabel : 'Last 7 days' , days : 7 } ,
136+ { label : '30d' , fullLabel : 'Last 30 days' , days : 30 } ,
137+ { label : '90d' , fullLabel : 'Last 90 days' , days : 90 } ,
138+ { label : '180d' , fullLabel : 'Last 180 days' , days : 180 } ,
139+ { label : '365d' , fullLabel : 'Last 365 days' , days : 365 } ,
140+ ] ,
141+ [ ]
142+ ) ;
143+
144+ const handleQuickRangeSelect = useCallback (
145+ ( range : ( typeof quickRanges ) [ 0 ] ) => {
146+ const now = new Date ( ) ;
147+ const start = range . hours
148+ ? subHours ( now , range . hours )
149+ : subDays ( now , range . days || 7 ) ;
150+ setDateRangeAction ( { startDate : start , endDate : now } ) ;
151+ } ,
152+ [ setDateRangeAction ]
153+ ) ;
154+
99155 const memoizedDateRangeForTabs = useMemo (
100156 ( ) => ( {
101157 start_date : formattedDateRangeState . startDate ,
@@ -277,6 +333,59 @@ export default function FunnelsPage() {
277333 websiteId = { websiteId }
278334 />
279335
336+ { /* Date Range Controls - Only show if tracking is set up */ }
337+ { isTrackingSetup && (
338+ < div className = "mt-3 flex flex-col gap-3 rounded-lg border bg-muted/30 p-2.5" >
339+ < div className = "flex items-center gap-2 overflow-x-auto rounded-md border bg-background p-1 shadow-sm" >
340+ { quickRanges . map ( ( range ) => {
341+ const now = new Date ( ) ;
342+ const start = range . hours
343+ ? subHours ( now , range . hours )
344+ : subDays ( now , range . days || 7 ) ;
345+ const dayPickerCurrentRange = dayPickerSelectedRange ;
346+ const isActive =
347+ dayPickerCurrentRange ?. from &&
348+ dayPickerCurrentRange ?. to &&
349+ format ( dayPickerCurrentRange . from , 'yyyy-MM-dd' ) ===
350+ format ( start , 'yyyy-MM-dd' ) &&
351+ format ( dayPickerCurrentRange . to , 'yyyy-MM-dd' ) ===
352+ format ( now , 'yyyy-MM-dd' ) ;
353+
354+ return (
355+ < Button
356+ className = { `h-6 cursor-pointer touch-manipulation whitespace-nowrap px-2 text-xs sm:px-2.5 ${ isActive ? 'shadow-sm' : 'text-muted-foreground hover:text-foreground' } ` }
357+ key = { range . label }
358+ onClick = { ( ) => handleQuickRangeSelect ( range ) }
359+ size = "sm"
360+ title = { range . fullLabel }
361+ variant = { isActive ? 'default' : 'ghost' }
362+ >
363+ < span className = "sm:hidden" > { range . label } </ span >
364+ < span className = "hidden sm:inline" > { range . fullLabel } </ span >
365+ </ Button >
366+ ) ;
367+ } ) }
368+
369+ < div className = "ml-1 border-border/50 border-l pl-2 sm:pl-3" >
370+ < DateRangePicker
371+ className = "w-auto"
372+ maxDate = { new Date ( ) }
373+ minDate = { new Date ( 2020 , 0 , 1 ) }
374+ onChange = { ( range ) => {
375+ if ( range ?. from && range ?. to ) {
376+ setDateRangeAction ( {
377+ startDate : range . from ,
378+ endDate : range . to ,
379+ } ) ;
380+ }
381+ } }
382+ value = { dayPickerSelectedRange }
383+ />
384+ </ div >
385+ </ div >
386+ </ div >
387+ ) }
388+
280389 < Suspense fallback = { < FunnelsListSkeleton /> } >
281390 < FunnelsList
282391 expandedFunnelId = { expandedFunnelId }
0 commit comments