Skip to content

Commit 95cf20a

Browse files
committed
feat: time range in goals
1 parent 864cc39 commit 95cf20a

File tree

1 file changed

+104
-7
lines changed
  • apps/dashboard/app/(main)/websites/[id]/goals

1 file changed

+104
-7
lines changed

apps/dashboard/app/(main)/websites/[id]/goals/page.tsx

Lines changed: 104 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use client';
22

33
import { TargetIcon } from '@phosphor-icons/react';
4+
import { format, subDays, subHours } from 'date-fns';
45
import { useAtom } from 'jotai';
56
import { useParams } from 'next/navigation';
67
import {
@@ -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';
1418
import { Card, CardContent } from '@/components/ui/card';
1519
import { useAutocompleteData } from '@/hooks/use-funnels';
1620
import {
@@ -20,9 +24,11 @@ import {
2024
useGoals,
2125
} from '@/hooks/use-goals';
2226
import { useWebsite } from '@/hooks/use-websites';
27+
import { trpc } from '@/lib/trpc';
2328
import {
2429
dateRangeAtom,
2530
formattedDateRangeAtom,
31+
setDateRangeAndAdjustGranularityAtom,
2632
timeGranularityAtom,
2733
} from '@/stores/jotai/filterAtoms';
2834
import { 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

Comments
 (0)