@@ -12,6 +12,31 @@ import { useDateFilters } from '@/hooks/use-date-filters';
1212import { addDynamicFilterAtom } from '@/stores/jotai/filterAtoms' ;
1313import { AddFilterForm } from './utils/add-filters' ;
1414
15+ const MAX_HOURLY_DAYS = 7 ;
16+
17+ type QuickRange = {
18+ label : string ;
19+ fullLabel : string ;
20+ hours ?: number ;
21+ days ?: number ;
22+ } ;
23+
24+ const QUICK_RANGES : QuickRange [ ] = [
25+ { label : '24h' , fullLabel : 'Last 24 hours' , hours : 24 } ,
26+ { label : '7d' , fullLabel : 'Last 7 days' , days : 7 } ,
27+ { label : '30d' , fullLabel : 'Last 30 days' , days : 30 } ,
28+ { label : '90d' , fullLabel : 'Last 90 days' , days : 90 } ,
29+ { label : '180d' , fullLabel : 'Last 180 days' , days : 180 } ,
30+ { label : '365d' , fullLabel : 'Last 365 days' , days : 365 } ,
31+ ] ;
32+
33+ const getStartDateForRange = ( range : QuickRange ) => {
34+ const now = new Date ( ) ;
35+ return range . hours
36+ ? dayjs ( now ) . subtract ( range . hours , 'hour' ) . toDate ( )
37+ : dayjs ( now ) . subtract ( range . days ?? 7 , 'day' ) . toDate ( ) ;
38+ } ;
39+
1540interface AnalyticsToolbarProps {
1641 isRefreshing : boolean ;
1742 onRefresh : ( ) => void ;
@@ -32,45 +57,68 @@ export function AnalyticsToolbar({
3257
3358 const [ , addFilter ] = useAtom ( addDynamicFilterAtom ) ;
3459
35- const dayPickerSelectedRange : DayPickerRange | undefined = useMemo (
60+ const dateRangeDays = useMemo (
61+ ( ) =>
62+ dayjs ( currentDateRange . endDate ) . diff (
63+ currentDateRange . startDate ,
64+ 'day'
65+ ) ,
66+ [ currentDateRange ]
67+ ) ;
68+
69+ const isHourlyDisabled = dateRangeDays > MAX_HOURLY_DAYS ;
70+
71+ const selectedRange : DayPickerRange | undefined = useMemo (
3672 ( ) => ( {
3773 from : currentDateRange . startDate ,
3874 to : currentDateRange . endDate ,
3975 } ) ,
4076 [ currentDateRange ]
4177 ) ;
4278
43- const quickRanges = useMemo (
44- ( ) => [
45- { label : '24h' , fullLabel : 'Last 24 hours' , hours : 24 } ,
46- { label : '7d' , fullLabel : 'Last 7 days' , days : 7 } ,
47- { label : '30d' , fullLabel : 'Last 30 days' , days : 30 } ,
48- { label : '90d' , fullLabel : 'Last 90 days' , days : 90 } ,
49- { label : '180d' , fullLabel : 'Last 180 days' , days : 180 } ,
50- { label : '365d' , fullLabel : 'Last 365 days' , days : 365 } ,
51- ] ,
52- [ ]
79+ const handleQuickRangeSelect = useCallback (
80+ ( range : QuickRange ) => {
81+ const start = getStartDateForRange ( range ) ;
82+ setDateRangeAction ( { startDate : start , endDate : new Date ( ) } ) ;
83+ } ,
84+ [ setDateRangeAction ]
5385 ) ;
5486
55- const handleQuickRangeSelect = useCallback (
56- ( range : ( typeof quickRanges ) [ 0 ] ) => {
87+ const getGranularityButtonClass = ( type : 'daily' | 'hourly' ) => {
88+ const isActive = currentGranularity === type ;
89+ const baseClass =
90+ 'h-8 cursor-pointer touch-manipulation rounded-none px-3 text-sm' ;
91+ const activeClass = isActive
92+ ? 'bg-primary/10 font-medium text-primary'
93+ : 'text-muted-foreground' ;
94+ const disabledClass =
95+ type === 'hourly' && isHourlyDisabled
96+ ? 'cursor-not-allowed opacity-50'
97+ : '' ;
98+ return `${ baseClass } ${ activeClass } ${ disabledClass } ` . trim ( ) ;
99+ } ;
100+
101+ const isQuickRangeActive = useCallback (
102+ ( range : QuickRange ) => {
103+ if ( ! selectedRange ?. from || ! selectedRange ?. to ) return false ;
104+
57105 const now = new Date ( ) ;
58- const start = range . hours
59- ? dayjs ( now ) . subtract ( range . hours , 'hour' ) . toDate ( )
60- : dayjs ( now )
61- . subtract ( range . days || 7 , 'day' )
62- . toDate ( ) ;
63- setDateRangeAction ( { startDate : start , endDate : now } ) ;
106+ const start = getStartDateForRange ( range ) ;
107+
108+ return (
109+ dayjs ( selectedRange . from ) . isSame ( start , 'day' ) &&
110+ dayjs ( selectedRange . to ) . isSame ( now , 'day' )
111+ ) ;
64112 } ,
65- [ setDateRangeAction ]
113+ [ selectedRange ]
66114 ) ;
67115
68116 return (
69117 < div className = "mt-3 flex flex-col gap-2 rounded border bg-card p-3 shadow-sm" >
70118 < div className = "flex items-center justify-between gap-3" >
71119 < div className = "flex h-8 overflow-hidden rounded border bg-background shadow-sm" >
72120 < Button
73- className = { `h-8 cursor-pointer touch-manipulation rounded-none px-3 text-sm ${ currentGranularity === 'daily' ? 'bg-primary/10 font-medium text-primary' : 'text-muted-foreground' } ` }
121+ className = { getGranularityButtonClass ( 'daily' ) }
74122 onClick = { ( ) => setCurrentGranularityAtomState ( 'daily' ) }
75123 size = "sm"
76124 title = "View daily aggregated data"
@@ -79,10 +127,15 @@ export function AnalyticsToolbar({
79127 Daily
80128 </ Button >
81129 < Button
82- className = { `h-8 cursor-pointer touch-manipulation rounded-none px-3 text-sm ${ currentGranularity === 'hourly' ? 'bg-primary/10 font-medium text-primary' : 'text-muted-foreground' } ` }
130+ className = { getGranularityButtonClass ( 'hourly' ) }
131+ disabled = { isHourlyDisabled }
83132 onClick = { ( ) => setCurrentGranularityAtomState ( 'hourly' ) }
84133 size = "sm"
85- title = "View hourly data (best for 24h periods)"
134+ title = {
135+ isHourlyDisabled
136+ ? `Hourly view is only available for ${ MAX_HOURLY_DAYS } days or less`
137+ : `View hourly data (up to ${ MAX_HOURLY_DAYS } days)`
138+ }
86139 variant = "ghost"
87140 >
88141 Hourly
@@ -109,22 +162,8 @@ export function AnalyticsToolbar({
109162 </ div >
110163
111164 < div className = "flex items-center gap-1 overflow-x-auto rounded border bg-background p-1 shadow-sm" >
112- { quickRanges . map ( ( range ) => {
113- const now = new Date ( ) ;
114- const start = range . hours
115- ? dayjs ( now ) . subtract ( range . hours , 'hour' ) . toDate ( )
116- : dayjs ( now )
117- . subtract ( range . days || 7 , 'day' )
118- . toDate ( ) ;
119- const dayPickerCurrentRange = dayPickerSelectedRange ;
120- const isActive =
121- dayPickerCurrentRange ?. from &&
122- dayPickerCurrentRange ?. to &&
123- dayjs ( dayPickerCurrentRange . from ) . format ( 'YYYY-MM-DD' ) ===
124- dayjs ( start ) . format ( 'YYYY-MM-DD' ) &&
125- dayjs ( dayPickerCurrentRange . to ) . format ( 'YYYY-MM-DD' ) ===
126- dayjs ( now ) . format ( 'YYYY-MM-DD' ) ;
127-
165+ { QUICK_RANGES . map ( ( range ) => {
166+ const isActive = isQuickRangeActive ( range ) ;
128167 return (
129168 < Button
130169 className = { `h-8 cursor-pointer touch-manipulation whitespace-nowrap px-2 font-medium text-xs ${ isActive ? 'bg-primary/10 text-primary shadow-sm' : 'text-muted-foreground hover:text-foreground' } ` }
@@ -152,7 +191,7 @@ export function AnalyticsToolbar({
152191 } ) ;
153192 }
154193 } }
155- value = { dayPickerSelectedRange }
194+ value = { selectedRange }
156195 />
157196 </ div >
158197 </ div >
0 commit comments