11'use client' ;
22
33import { TargetIcon } from '@phosphor-icons/react' ;
4+ import { format , subDays , subHours } from 'date-fns' ;
45import { useAtom } from 'jotai' ;
56import { useParams } from 'next/navigation' ;
67import {
@@ -11,6 +12,9 @@ import {
1112 useRef ,
1213 useState ,
1314} from 'react' ;
15+ import type { DateRange as DayPickerRange } from 'react-day-picker' ;
16+ import { DateRangePicker } from '@/components/date-range-picker' ;
17+ import { Button } from '@/components/ui/button' ;
1418import { Card , CardContent } from '@/components/ui/card' ;
1519import { useAutocompleteData } from '@/hooks/use-funnels' ;
1620import {
@@ -20,9 +24,11 @@ import {
2024 useGoals ,
2125} from '@/hooks/use-goals' ;
2226import { useWebsite } from '@/hooks/use-websites' ;
27+ import { trpc } from '@/lib/trpc' ;
2328import {
2429 dateRangeAtom ,
2530 formattedDateRangeAtom ,
31+ setDateRangeAndAdjustGranularityAtom ,
2632 timeGranularityAtom ,
2733} from '@/stores/jotai/filterAtoms' ;
2834import { WebsitePageHeader } from '../_components/website-page-header' ;
@@ -73,7 +79,6 @@ export default function GoalsPage() {
7379 const [ editingGoal , setEditingGoal ] = useState < Goal | null > ( null ) ;
7480 const [ deletingGoalId , setDeletingGoalId ] = useState < string | null > ( null ) ;
7581
76- // Intersection observer for lazy loading
7782 const [ isVisible , setIsVisible ] = useState ( false ) ;
7883 const pageRef = useRef < HTMLDivElement > ( null ) ;
7984
@@ -95,10 +100,55 @@ export default function GoalsPage() {
95100 return ( ) => observer . disconnect ( ) ;
96101 } , [ ] ) ;
97102
98- const [ , ] = useAtom ( dateRangeAtom ) ;
103+ const [ currentDateRange ] = useAtom ( dateRangeAtom ) ;
99104 const [ currentGranularity ] = useAtom ( timeGranularityAtom ) ;
105+ const [ , setDateRangeAction ] = useAtom ( setDateRangeAndAdjustGranularityAtom ) ;
100106 const [ formattedDateRangeState ] = useAtom ( formattedDateRangeAtom ) ;
101107
108+ const { data : trackingSetupData , isLoading : isTrackingSetupLoading } =
109+ trpc . websites . isTrackingSetup . useQuery (
110+ { websiteId } ,
111+ { enabled : ! ! websiteId }
112+ ) ;
113+
114+ const isTrackingSetup = useMemo ( ( ) => {
115+ if ( isTrackingSetupLoading ) {
116+ return null ;
117+ }
118+ return trackingSetupData ?. tracking_setup ?? false ;
119+ } , [ isTrackingSetupLoading , trackingSetupData ?. tracking_setup ] ) ;
120+
121+ const dayPickerSelectedRange : DayPickerRange | undefined = useMemo (
122+ ( ) => ( {
123+ from : currentDateRange . startDate ,
124+ to : currentDateRange . endDate ,
125+ } ) ,
126+ [ currentDateRange ]
127+ ) ;
128+
129+ const quickRanges = useMemo (
130+ ( ) => [
131+ { label : '24h' , fullLabel : 'Last 24 hours' , hours : 24 } ,
132+ { label : '7d' , fullLabel : 'Last 7 days' , days : 7 } ,
133+ { label : '30d' , fullLabel : 'Last 30 days' , days : 30 } ,
134+ { label : '90d' , fullLabel : 'Last 90 days' , days : 90 } ,
135+ { label : '180d' , fullLabel : 'Last 180 days' , days : 180 } ,
136+ { label : '365d' , fullLabel : 'Last 365 days' , days : 365 } ,
137+ ] ,
138+ [ ]
139+ ) ;
140+
141+ const handleQuickRangeSelect = useCallback (
142+ ( range : ( typeof quickRanges ) [ 0 ] ) => {
143+ const now = new Date ( ) ;
144+ const start = range . hours
145+ ? subHours ( now , range . hours )
146+ : subDays ( now , range . days || 7 ) ;
147+ setDateRangeAction ( { startDate : start , endDate : now } ) ;
148+ } ,
149+ [ setDateRangeAction ]
150+ ) ;
151+
102152 const memoizedDateRangeForTabs = useMemo (
103153 ( ) => ( {
104154 start_date : formattedDateRangeState . startDate ,
@@ -122,18 +172,14 @@ export default function GoalsPage() {
122172 isUpdating,
123173 } = useGoals ( websiteId ) ;
124174
125- // Get goal IDs for bulk analytics
126175 const goalIds = useMemo ( ( ) => goals . map ( ( goal ) => goal . id ) , [ goals ] ) ;
127176
128- // Fetch analytics for all goals
129177 const {
130178 data : goalAnalytics ,
131179 isLoading : analyticsLoading ,
132- error : analyticsError ,
133180 refetch : refetchAnalytics ,
134181 } = useBulkGoalAnalytics ( websiteId , goalIds , memoizedDateRangeForTabs ) ;
135182
136- // Preload autocomplete data for instant suggestions in dialogs
137183 const autocompleteQuery = useAutocompleteData ( websiteId ) ;
138184
139185 const handleRefresh = useCallback ( async ( ) => {
@@ -160,7 +206,6 @@ export default function GoalsPage() {
160206 ) => {
161207 try {
162208 if ( 'id' in data ) {
163- // Updating existing goal
164209 await updateGoal ( {
165210 goalId : data . id ,
166211 updates : {
@@ -248,6 +293,58 @@ export default function GoalsPage() {
248293 websiteName = { websiteData ?. name || undefined }
249294 />
250295
296+ { isTrackingSetup && (
297+ < div className = "mt-3 flex flex-col gap-3 rounded-lg border bg-muted/30 p-2.5" >
298+ < div className = "flex items-center gap-2 overflow-x-auto rounded-md border bg-background p-1 shadow-sm" >
299+ { quickRanges . map ( ( range ) => {
300+ const now = new Date ( ) ;
301+ const start = range . hours
302+ ? subHours ( now , range . hours )
303+ : subDays ( now , range . days || 7 ) ;
304+ const dayPickerCurrentRange = dayPickerSelectedRange ;
305+ const isActive =
306+ dayPickerCurrentRange ?. from &&
307+ dayPickerCurrentRange ?. to &&
308+ format ( dayPickerCurrentRange . from , 'yyyy-MM-dd' ) ===
309+ format ( start , 'yyyy-MM-dd' ) &&
310+ format ( dayPickerCurrentRange . to , 'yyyy-MM-dd' ) ===
311+ format ( now , 'yyyy-MM-dd' ) ;
312+
313+ return (
314+ < Button
315+ 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' } ` }
316+ key = { range . label }
317+ onClick = { ( ) => handleQuickRangeSelect ( range ) }
318+ size = "sm"
319+ title = { range . fullLabel }
320+ variant = { isActive ? 'default' : 'ghost' }
321+ >
322+ < span className = "sm:hidden" > { range . label } </ span >
323+ < span className = "hidden sm:inline" > { range . fullLabel } </ span >
324+ </ Button >
325+ ) ;
326+ } ) }
327+
328+ < div className = "ml-1 border-border/50 border-l pl-2 sm:pl-3" >
329+ < DateRangePicker
330+ className = "w-auto"
331+ maxDate = { new Date ( ) }
332+ minDate = { new Date ( 2020 , 0 , 1 ) }
333+ onChange = { ( range ) => {
334+ if ( range ?. from && range ?. to ) {
335+ setDateRangeAction ( {
336+ startDate : range . from ,
337+ endDate : range . to ,
338+ } ) ;
339+ }
340+ } }
341+ value = { dayPickerSelectedRange }
342+ />
343+ </ div >
344+ </ div >
345+ </ div >
346+ ) }
347+
251348 { isVisible && (
252349 < Suspense fallback = { < GoalsListSkeleton /> } >
253350 < GoalsList
0 commit comments