Skip to content

Commit 864cc39

Browse files
committed
feat: time range in funnels
1 parent ff0b216 commit 864cc39

File tree

5 files changed

+121
-291
lines changed

5 files changed

+121
-291
lines changed

apps/dashboard/app/(main)/websites/[id]/_components/website-page-header.tsx

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,36 +12,28 @@ import { Card, CardContent } from '@/components/ui/card';
1212
import { Skeleton } from '@/components/ui/skeleton';
1313

1414
interface WebsitePageHeaderProps {
15-
// Basic page info
1615
title: string;
1716
description?: string;
1817
icon: ReactNode;
1918

20-
// Website context
2119
websiteId: string;
2220
websiteName?: string;
2321

24-
// Loading states
2522
isLoading?: boolean;
2623
isRefreshing?: boolean;
2724

28-
// Error handling
2925
hasError?: boolean;
3026
errorMessage?: string;
3127

32-
// Actions
3328
onRefresh?: () => void;
3429
onCreateAction?: () => void;
3530
createActionLabel?: string;
3631

37-
// Additional info
3832
subtitle?: string | ReactNode;
3933

40-
// Layout options
4134
showBackButton?: boolean;
4235
variant?: 'default' | 'minimal';
4336

44-
// Custom actions
4537
additionalActions?: ReactNode;
4638
}
4739

@@ -127,7 +119,6 @@ export function WebsitePageHeader({
127119

128120
return (
129121
<div className="space-y-6">
130-
{/* Main Header */}
131122
<div className="border-b pb-6">
132123
<div className="flex flex-col justify-between gap-4 sm:flex-row sm:items-center">
133124
<div className="space-y-2">
@@ -180,7 +171,6 @@ export function WebsitePageHeader({
180171
</div>
181172
</div>
182173

183-
{/* Error State */}
184174
{hasError && (
185175
<Card className="rounded-xl border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-950">
186176
<CardContent className="pt-6">
@@ -216,7 +206,6 @@ export function WebsitePageHeader({
216206
);
217207
}
218208

219-
// Skeleton component for loading states
220209
export function WebsitePageHeaderSkeleton() {
221210
return (
222211
<div className="space-y-6">

apps/dashboard/app/(main)/websites/[id]/funnels/_components/page-header.tsx

Lines changed: 0 additions & 136 deletions
This file was deleted.

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

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
'use client';
22

33
import { FunnelIcon, TrendDownIcon } 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 { 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';
711
import { Card, CardContent } from '@/components/ui/card';
812
import {
913
type CreateFunnelData,
@@ -13,8 +17,11 @@ import {
1317
useFunnelAnalyticsByReferrer,
1418
useFunnels,
1519
} from '@/hooks/use-funnels';
20+
import { trpc } from '@/lib/trpc';
1621
import {
22+
dateRangeAtom,
1723
formattedDateRangeAtom,
24+
setDateRangeAndAdjustGranularityAtom,
1825
timeGranularityAtom,
1926
} from '@/stores/jotai/filterAtoms';
2027
import { 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

Comments
 (0)