|
1 | 1 | 'use client'; |
2 | 2 |
|
3 | | -import { type DynamicQueryFilter, filterOptions } from '@databuddy/shared'; |
4 | | -import { ArrowClockwiseIcon, XIcon } from '@phosphor-icons/react'; |
| 3 | +import { ArrowClockwiseIcon } from '@phosphor-icons/react'; |
5 | 4 | import dayjs from 'dayjs'; |
6 | 5 | import { useCallback, useMemo } from 'react'; |
7 | 6 | import type { DateRange as DayPickerRange } from 'react-day-picker'; |
8 | 7 | import { DateRangePicker } from '@/components/date-range-picker'; |
9 | 8 | import { Button } from '@/components/ui/button'; |
10 | 9 | import { useDateFilters } from '@/hooks/use-date-filters'; |
11 | | -import { operatorOptions, useFilters } from '@/hooks/use-filters'; |
12 | | -import { AddFilterForm, getOperatorShorthand } from './utils/add-filters'; |
13 | 10 |
|
14 | 11 | interface AnalyticsToolbarProps { |
15 | 12 | isRefreshing: boolean; |
16 | 13 | onRefresh: () => void; |
17 | | - selectedFilters: DynamicQueryFilter[]; |
18 | | - onFiltersChange: (filters: DynamicQueryFilter[]) => void; |
19 | 14 | } |
20 | 15 |
|
21 | 16 | export function AnalyticsToolbar({ |
22 | 17 | isRefreshing, |
23 | 18 | onRefresh, |
24 | | - selectedFilters, |
25 | | - onFiltersChange, |
26 | 19 | }: AnalyticsToolbarProps) { |
27 | | - const { addFilter, removeFilter } = useFilters({ |
28 | | - filters: selectedFilters, |
29 | | - onFiltersChange, |
30 | | - }); |
31 | | - |
32 | 20 | const { |
33 | 21 | currentDateRange, |
34 | 22 | currentGranularity, |
@@ -70,153 +58,93 @@ export function AnalyticsToolbar({ |
70 | 58 | ); |
71 | 59 |
|
72 | 60 | return ( |
73 | | - <> |
74 | | - <div className="mt-3 flex flex-col gap-3 rounded-lg border bg-muted/30 p-2.5"> |
75 | | - <div className="flex items-center justify-between gap-3"> |
76 | | - <div className="flex h-8 overflow-hidden rounded-md border bg-background shadow-sm"> |
77 | | - <Button |
78 | | - className={`h-8 cursor-pointer touch-manipulation rounded-none px-2 text-xs sm:px-3 ${currentGranularity === 'daily' ? 'bg-primary/10 font-medium text-primary' : 'text-muted-foreground'}`} |
79 | | - onClick={() => setCurrentGranularityAtomState('daily')} |
80 | | - size="sm" |
81 | | - title="View daily aggregated data" |
82 | | - variant="ghost" |
83 | | - > |
84 | | - Daily |
85 | | - </Button> |
86 | | - <Button |
87 | | - className={`h-8 cursor-pointer touch-manipulation rounded-none px-2 text-xs sm:px-3 ${currentGranularity === 'hourly' ? 'bg-primary/10 font-medium text-primary' : 'text-muted-foreground'}`} |
88 | | - onClick={() => setCurrentGranularityAtomState('hourly')} |
89 | | - size="sm" |
90 | | - title="View hourly data (best for 24h periods)" |
91 | | - variant="ghost" |
92 | | - > |
93 | | - Hourly |
94 | | - </Button> |
95 | | - </div> |
96 | | - |
| 61 | + <div className="mt-3 flex flex-col gap-3 rounded border bg-card p-4 shadow-sm"> |
| 62 | + <div className="flex items-center justify-between gap-3"> |
| 63 | + <div className="flex h-8 overflow-hidden rounded-md border bg-background shadow-sm"> |
97 | 64 | <Button |
98 | | - aria-label="Refresh data" |
99 | | - className="h-8 w-8" |
100 | | - disabled={isRefreshing} |
101 | | - onClick={onRefresh} |
102 | | - size="icon" |
103 | | - variant="outline" |
| 65 | + className={`h-8 cursor-pointer touch-manipulation rounded-none px-2 text-xs sm:px-3 ${currentGranularity === 'daily' ? 'bg-primary/10 font-medium text-primary' : 'text-muted-foreground'}`} |
| 66 | + onClick={() => setCurrentGranularityAtomState('daily')} |
| 67 | + size="sm" |
| 68 | + title="View daily aggregated data" |
| 69 | + variant="ghost" |
104 | 70 | > |
105 | | - <ArrowClockwiseIcon |
106 | | - aria-hidden="true" |
107 | | - className={`h-4 w-4 ${isRefreshing ? 'animate-spin' : ''}`} |
108 | | - /> |
| 71 | + Daily |
| 72 | + </Button> |
| 73 | + <Button |
| 74 | + className={`h-8 cursor-pointer touch-manipulation rounded-none px-2 text-xs sm:px-3 ${currentGranularity === 'hourly' ? 'bg-primary/10 font-medium text-primary' : 'text-muted-foreground'}`} |
| 75 | + onClick={() => setCurrentGranularityAtomState('hourly')} |
| 76 | + size="sm" |
| 77 | + title="View hourly data (best for 24h periods)" |
| 78 | + variant="ghost" |
| 79 | + > |
| 80 | + Hourly |
109 | 81 | </Button> |
110 | 82 | </div> |
111 | 83 |
|
112 | | - <div className="flex items-center gap-2 overflow-x-auto rounded-md border bg-background p-1 shadow-sm"> |
113 | | - {quickRanges.map((range) => { |
114 | | - const now = new Date(); |
115 | | - const start = range.hours |
116 | | - ? dayjs(now).subtract(range.hours, 'hour').toDate() |
117 | | - : dayjs(now) |
118 | | - .subtract(range.days || 7, 'day') |
119 | | - .toDate(); |
120 | | - const dayPickerCurrentRange = dayPickerSelectedRange; |
121 | | - const isActive = |
122 | | - dayPickerCurrentRange?.from && |
123 | | - dayPickerCurrentRange?.to && |
124 | | - dayjs(dayPickerCurrentRange.from).format('YYYY-MM-DD') === |
125 | | - dayjs(start).format('YYYY-MM-DD') && |
126 | | - dayjs(dayPickerCurrentRange.to).format('YYYY-MM-DD') === |
127 | | - dayjs(now).format('YYYY-MM-DD'); |
128 | | - |
129 | | - return ( |
130 | | - <Button |
131 | | - 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'}`} |
132 | | - key={range.label} |
133 | | - onClick={() => handleQuickRangeSelect(range)} |
134 | | - size="sm" |
135 | | - title={range.fullLabel} |
136 | | - variant={isActive ? 'default' : 'ghost'} |
137 | | - > |
138 | | - <span className="sm:hidden">{range.label}</span> |
139 | | - <span className="hidden sm:inline">{range.fullLabel}</span> |
140 | | - </Button> |
141 | | - ); |
142 | | - })} |
143 | | - |
144 | | - <div className="ml-1 border-border/50 border-l pl-2 sm:pl-3"> |
145 | | - <DateRangePicker |
146 | | - className="w-auto" |
147 | | - maxDate={new Date()} |
148 | | - minDate={new Date(2020, 0, 1)} |
149 | | - onChange={(range) => { |
150 | | - if (range?.from && range?.to) { |
151 | | - setDateRangeAction({ |
152 | | - startDate: range.from, |
153 | | - endDate: range.to, |
154 | | - }); |
155 | | - } |
156 | | - }} |
157 | | - value={dayPickerSelectedRange} |
158 | | - /> |
159 | | - </div> |
160 | | - |
161 | | - <div className="ml-2 flex items-center"> |
162 | | - <AddFilterForm addFilter={addFilter} /> |
163 | | - </div> |
164 | | - </div> |
| 84 | + <Button |
| 85 | + aria-label="Refresh data" |
| 86 | + className="h-8 w-8" |
| 87 | + disabled={isRefreshing} |
| 88 | + onClick={onRefresh} |
| 89 | + size="icon" |
| 90 | + variant="outline" |
| 91 | + > |
| 92 | + <ArrowClockwiseIcon |
| 93 | + aria-hidden="true" |
| 94 | + className={`h-4 w-4 ${isRefreshing ? 'animate-spin' : ''}`} |
| 95 | + /> |
| 96 | + </Button> |
165 | 97 | </div> |
166 | 98 |
|
167 | | - {selectedFilters.length > 0 && ( |
168 | | - <div className="mt-3 rounded-lg border bg-muted/30 p-2.5"> |
169 | | - <div className="flex items-center justify-between gap-3"> |
170 | | - <div className="flex items-center gap-2 overflow-x-auto"> |
171 | | - <div className="font-semibold text-sm">Filters</div> |
172 | | - <div className="flex flex-wrap items-center gap-2"> |
173 | | - {selectedFilters.map((filter, index) => { |
174 | | - const fieldLabel = filterOptions.find( |
175 | | - (o) => o.value === filter.field |
176 | | - )?.label; |
177 | | - const operatorLabel = operatorOptions.find( |
178 | | - (o) => getOperatorShorthand(o.value) === filter.operator |
179 | | - )?.label; |
180 | | - const valueLabel = Array.isArray(filter.value) |
181 | | - ? filter.value.join(', ') |
182 | | - : filter.value; |
183 | | - |
184 | | - return ( |
185 | | - <div |
186 | | - className="flex items-center gap-0 rounded border bg-background py-1 pr-2 pl-3 shadow-sm" |
187 | | - key={`filter-${index}-${filter.field}-${filter.operator}`} |
188 | | - > |
189 | | - <div className="flex items-center gap-1"> |
190 | | - <span className="font-medium text-foreground text-sm"> |
191 | | - {fieldLabel} |
192 | | - </span> |
193 | | - <span className="text-muted-foreground/70 text-sm"> |
194 | | - {operatorLabel} |
195 | | - </span> |
196 | | - <span className="font-medium text-foreground text-sm"> |
197 | | - {valueLabel} |
198 | | - </span> |
199 | | - </div> |
200 | | - <button |
201 | | - aria-label={`Remove filter ${fieldLabel} ${operatorLabel} ${valueLabel}`} |
202 | | - className="flex h-6 w-6 items-center justify-center rounded hover:bg-muted/50" |
203 | | - onClick={() => removeFilter(index)} |
204 | | - type="button" |
205 | | - > |
206 | | - <XIcon aria-hidden="true" className="h-3 w-3" /> |
207 | | - </button> |
208 | | - </div> |
209 | | - ); |
210 | | - })} |
211 | | - </div> |
212 | | - </div> |
213 | | - |
214 | | - <Button onClick={() => onFiltersChange([])} variant="outline"> |
215 | | - Clear all filters |
| 99 | + <div className="flex items-center gap-2 overflow-x-auto rounded-md border bg-background p-1 shadow-sm"> |
| 100 | + {quickRanges.map((range) => { |
| 101 | + const now = new Date(); |
| 102 | + const start = range.hours |
| 103 | + ? dayjs(now).subtract(range.hours, 'hour').toDate() |
| 104 | + : dayjs(now) |
| 105 | + .subtract(range.days || 7, 'day') |
| 106 | + .toDate(); |
| 107 | + const dayPickerCurrentRange = dayPickerSelectedRange; |
| 108 | + const isActive = |
| 109 | + dayPickerCurrentRange?.from && |
| 110 | + dayPickerCurrentRange?.to && |
| 111 | + dayjs(dayPickerCurrentRange.from).format('YYYY-MM-DD') === |
| 112 | + dayjs(start).format('YYYY-MM-DD') && |
| 113 | + dayjs(dayPickerCurrentRange.to).format('YYYY-MM-DD') === |
| 114 | + dayjs(now).format('YYYY-MM-DD'); |
| 115 | + |
| 116 | + return ( |
| 117 | + <Button |
| 118 | + 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'}`} |
| 119 | + key={range.label} |
| 120 | + onClick={() => handleQuickRangeSelect(range)} |
| 121 | + size="sm" |
| 122 | + title={range.fullLabel} |
| 123 | + variant={isActive ? 'default' : 'ghost'} |
| 124 | + > |
| 125 | + <span className="sm:hidden">{range.label}</span> |
| 126 | + <span className="hidden sm:inline">{range.fullLabel}</span> |
216 | 127 | </Button> |
217 | | - </div> |
| 128 | + ); |
| 129 | + })} |
| 130 | + |
| 131 | + <div className="ml-1 border-border/50 border-l pl-2 sm:pl-3"> |
| 132 | + <DateRangePicker |
| 133 | + className="w-auto" |
| 134 | + maxDate={new Date()} |
| 135 | + minDate={new Date(2020, 0, 1)} |
| 136 | + onChange={(range) => { |
| 137 | + if (range?.from && range?.to) { |
| 138 | + setDateRangeAction({ |
| 139 | + startDate: range.from, |
| 140 | + endDate: range.to, |
| 141 | + }); |
| 142 | + } |
| 143 | + }} |
| 144 | + value={dayPickerSelectedRange} |
| 145 | + /> |
218 | 146 | </div> |
219 | | - )} |
220 | | - </> |
| 147 | + </div> |
| 148 | + </div> |
221 | 149 | ); |
222 | 150 | } |
0 commit comments