wip event list

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-02-15 21:29:45 +01:00
parent 1328825e7c
commit a74acda707
29 changed files with 995 additions and 883 deletions

View File

@@ -0,0 +1,34 @@
'use client';
import { Button } from '@/components/ui/button';
import {
useEventFilters,
useEventQueryFilters,
} from '@/hooks/useEventQueryFilters';
import { cn } from '@/utils/cn';
import { X } from 'lucide-react';
export function OverviewFiltersButtons() {
const eventQueryFilters = useEventQueryFilters();
const filters = Object.entries(eventQueryFilters).filter(
([, filter]) => filter.get !== null
);
return (
<div
className={cn('flex flex-wrap gap-2', filters.length > 0 && 'px-4 pb-4')}
>
{filters.map(([key, filter]) => (
<Button
key={key}
size="sm"
variant="outline"
icon={X}
onClick={() => filter.set(null)}
>
<span className="mr-1">{key} is</span>
<strong>{filter.get}</strong>
</Button>
))}
</div>
);
}

View File

@@ -0,0 +1,92 @@
'use client';
import { api } from '@/app/_trpc/client';
import { Button } from '@/components/ui/button';
import { Combobox } from '@/components/ui/combobox';
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
import { XIcon } from 'lucide-react';
interface OverviewFiltersProps {
projectId: string;
}
export function OverviewFiltersDrawerContent({
projectId,
}: OverviewFiltersProps) {
const eventQueryFilters = useEventQueryFilters();
return (
<div>
<h2 className="text-xl font-medium mb-8">Overview filters</h2>
<Combobox
className="w-full"
onChange={(value) => {
// @ts-expect-error
eventQueryFilters[value].set('');
}}
value=""
placeholder="Filter by..."
label="What do you want to filter by?"
items={Object.entries(eventQueryFilters)
.filter(([, filter]) => filter.get === null)
.map(([name]) => ({
label: name,
value: name,
}))}
searchable
/>
<div className="flex flex-col gap-4 mt-8">
{Object.entries(eventQueryFilters)
.filter(([, filter]) => filter.get !== null)
.map(([name, filter]) => (
<FilterOption
key={name}
projectId={projectId}
name={name}
{...filter}
/>
))}
</div>
</div>
);
}
export function FilterOption({
name,
get,
set,
projectId,
}: {
name: string;
get: string | null;
set: (value: string | null) => void;
projectId: string;
}) {
const { data } = api.chart.values.useQuery({
projectId,
event: name === 'path' ? 'screen_view' : 'session_start',
property: name,
});
return (
<div className="flex gap-2 items-center">
<div>{name}</div>
<Combobox
className="flex-1"
onChange={(value) => set(value)}
placeholder={'Select a value'}
items={
data?.values.filter(Boolean).map((value) => ({
value,
label: value,
})) ?? []
}
value={get}
/>
<Button size="icon" variant="ghost" onClick={() => set(null)}>
<XIcon />
</Button>
</div>
);
}

View File

@@ -0,0 +1,28 @@
'use client';
import { Button } from '@/components/ui/button';
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
import { FilterIcon } from 'lucide-react';
import { OverviewFiltersDrawerContent } from './overview-filters-drawer-content';
interface OverviewFiltersDrawerProps {
projectId: string;
}
export function OverviewFiltersDrawer({
projectId,
}: OverviewFiltersDrawerProps) {
return (
<Sheet>
<SheetTrigger asChild>
<Button variant="outline" responsive icon={FilterIcon}>
Filters
</Button>
</SheetTrigger>
<SheetContent className="!max-w-lg w-full" side="right">
<OverviewFiltersDrawerContent projectId={projectId} />
</SheetContent>
</Sheet>
);
}

View File

@@ -1,204 +0,0 @@
'use client';
import { cn } from '@/utils/cn';
import { X } from 'lucide-react';
import { Button } from '../ui/button';
import { useOverviewOptions } from './useOverviewOptions';
export function OverviewFiltersButtons() {
const options = useOverviewOptions();
const activeFilter = options.filters.length > 0;
return (
<div className={cn('flex flex-wrap gap-2', activeFilter && 'px-4 pb-4')}>
{options.referrer && (
<Button
size="sm"
variant="outline"
icon={X}
onClick={() => options.setReferrer(null)}
>
<span className="mr-1">Referrer is</span>
<strong>{options.referrer}</strong>
</Button>
)}
{options.referrerName && (
<Button
size="sm"
variant="outline"
icon={X}
onClick={() => options.setReferrerName(null)}
>
<span className="mr-1">Referrer name is</span>
<strong>{options.referrerName}</strong>
</Button>
)}
{options.referrerType && (
<Button
size="sm"
variant="outline"
icon={X}
onClick={() => options.setReferrerType(null)}
>
<span className="mr-1">Referrer type is</span>
<strong>{options.referrerType}</strong>
</Button>
)}
{options.device && (
<Button
size="sm"
variant="outline"
icon={X}
onClick={() => options.setDevice(null)}
>
<span className="mr-1">Device is</span>
<strong>{options.device}</strong>
</Button>
)}
{options.page && (
<Button
size="sm"
variant="outline"
icon={X}
onClick={() => options.setPage(null)}
>
<span className="mr-1">Page is</span>
<strong>{options.page}</strong>
</Button>
)}
{options.utmSource && (
<Button
size="sm"
variant="outline"
icon={X}
onClick={() => options.setUtmSource(null)}
>
<span className="mr-1">Utm Source is</span>
<strong>{options.utmSource}</strong>
</Button>
)}
{options.utmMedium && (
<Button
size="sm"
variant="outline"
icon={X}
onClick={() => options.setUtmMedium(null)}
>
<span className="mr-1">Utm Medium is</span>
<strong>{options.utmMedium}</strong>
</Button>
)}
{options.utmCampaign && (
<Button
size="sm"
variant="outline"
icon={X}
onClick={() => options.setUtmCampaign(null)}
>
<span className="mr-1">Utm Campaign is</span>
<strong>{options.utmCampaign}</strong>
</Button>
)}
{options.utmTerm && (
<Button
size="sm"
variant="outline"
icon={X}
onClick={() => options.setUtmTerm(null)}
>
<span className="mr-1">Utm Term is</span>
<strong>{options.utmTerm}</strong>
</Button>
)}
{options.utmContent && (
<Button
size="sm"
variant="outline"
icon={X}
onClick={() => options.setUtmContent(null)}
>
<span className="mr-1">Utm Content is</span>
<strong>{options.utmContent}</strong>
</Button>
)}
{options.country && (
<Button
size="sm"
variant="outline"
icon={X}
onClick={() => options.setCountry(null)}
>
<span className="mr-1">Country is</span>
<strong>{options.country}</strong>
</Button>
)}
{options.region && (
<Button
size="sm"
variant="outline"
icon={X}
onClick={() => options.setRegion(null)}
>
<span className="mr-1">Region is</span>
<strong>{options.region}</strong>
</Button>
)}
{options.city && (
<Button
size="sm"
variant="outline"
icon={X}
onClick={() => options.setCity(null)}
>
<span className="mr-1">City is</span>
<strong>{options.city}</strong>
</Button>
)}
{options.browser && (
<Button
size="sm"
variant="outline"
icon={X}
onClick={() => options.setBrowser(null)}
>
<span className="mr-1">Browser is</span>
<strong>{options.browser}</strong>
</Button>
)}
{options.browserVersion && (
<Button
size="sm"
variant="outline"
icon={X}
onClick={() => options.setBrowserVersion(null)}
>
<span className="mr-1">Browser Version is</span>
<strong>{options.browserVersion}</strong>
</Button>
)}
{options.os && (
<Button
size="sm"
variant="outline"
icon={X}
onClick={() => options.setOS(null)}
>
<span className="mr-1">OS is</span>
<strong>{options.os}</strong>
</Button>
)}
{options.osVersion && (
<Button
size="sm"
variant="outline"
icon={X}
onClick={() => options.setOSVersion(null)}
>
<span className="mr-1">OS Version is</span>
<strong>{options.osVersion}</strong>
</Button>
)}
</div>
);
}

View File

@@ -1,123 +0,0 @@
'use client';
import { api } from '@/app/_trpc/client';
import { useAppParams } from '@/hooks/useAppParams';
import { cn } from '@/utils/cn';
import { Combobox } from '../ui/combobox';
import { Label } from '../ui/label';
import { useOverviewOptions } from './useOverviewOptions';
interface OverviewFiltersProps {
projectId: string;
}
export function OverviewFilters({ projectId }: OverviewFiltersProps) {
const options = useOverviewOptions();
const { data: referrers } = api.chart.values.useQuery({
projectId,
property: 'referrer',
event: 'session_start',
});
const { data: devices } = api.chart.values.useQuery({
projectId,
property: 'device',
event: 'session_start',
});
const { data: pages } = api.chart.values.useQuery({
projectId,
property: 'path',
event: 'screen_view',
});
return (
<div>
<h2 className="text-xl font-medium mb-8">Overview filters</h2>
<div className="flex flex-col gap-4">
<div>
<Label className="flex justify-between">
Referrer
<button
className={cn(
'text-slate-500 transition-opacity opacity-100',
options.referrer === null && 'opacity-0'
)}
onClick={() => options.setReferrer(null)}
>
Reset
</button>
</Label>
<Combobox
className="w-full"
onChange={(value) => options.setReferrer(value)}
label="Referrer"
placeholder="Referrer"
items={
referrers?.values?.filter(Boolean)?.map((value) => ({
value,
label: value,
})) ?? []
}
value={options.referrer}
/>
</div>
<div>
<Label className="flex justify-between">
Device
<button
className={cn(
'opacity-100 text-slate-500 transition-opacity',
options.device === null && 'opacity-0'
)}
onClick={() => options.setDevice(null)}
>
Reset
</button>
</Label>
<Combobox
className="w-full"
onChange={(value) => options.setDevice(value)}
label="Device"
placeholder="Device"
items={
devices?.values?.filter(Boolean)?.map((value) => ({
value,
label: value,
})) ?? []
}
value={options.device}
/>
</div>
<div>
<Label className="flex justify-between">
Page
<button
className={cn(
'opacity-100 text-slate-500 transition-opacity',
options.page === null && 'opacity-0'
)}
onClick={() => options.setPage(null)}
>
Reset
</button>
</Label>
<Combobox
className="w-full"
onChange={(value) => options.setPage(value)}
label="Page"
placeholder="Page"
items={
pages?.values?.filter(Boolean)?.map((value) => ({
value,
label: value,
})) ?? []
}
value={options.page}
/>
</div>
</div>
</div>
);
}

View File

@@ -1,6 +1,10 @@
'use client';
import { Chart } from '@/components/report/chart';
import {
useEventFilters,
useEventQueryFilters,
} from '@/hooks/useEventQueryFilters';
import { cn } from '@/utils/cn';
import { Widget, WidgetBody } from '../Widget';
@@ -14,17 +18,10 @@ interface OverviewTopDevicesProps {
export default function OverviewTopDevices({
projectId,
}: OverviewTopDevicesProps) {
const {
filters,
interval,
range,
previous,
setBrowser,
setBrowserVersion,
setOS,
setOSVersion,
setDevice,
} = useOverviewOptions();
const { interval, range, previous } = useOverviewOptions();
const filters = useEventFilters();
const { device, browser, browserVersion, os, osVersion } =
useEventQueryFilters();
const [widget, setWidget, widgets] = useOverviewWidget('tech', {
devices: {
title: 'Top devices',
@@ -193,21 +190,21 @@ export default function OverviewTopDevices({
onClick={(item) => {
switch (widget.key) {
case 'devices':
setDevice(item.name);
device.set(item.name);
break;
case 'browser':
setWidget('browser_version');
setBrowser(item.name);
browser.set(item.name);
break;
case 'browser_version':
setBrowserVersion(item.name);
browserVersion.set(item.name);
break;
case 'os':
setWidget('os_version');
setOS(item.name);
os.set(item.name);
break;
case 'os_version':
setOSVersion(item.name);
osVersion.set(item.name);
break;
}
}}

View File

@@ -1,8 +1,7 @@
'use client';
import { Suspense } from 'react';
import { Chart } from '@/components/report/chart';
import { ChartLoading } from '@/components/report/chart/ChartLoading';
import { useEventFilters } from '@/hooks/useEventQueryFilters';
import { cn } from '@/utils/cn';
import { Widget, WidgetBody } from '../Widget';
@@ -16,7 +15,8 @@ interface OverviewTopEventsProps {
export default function OverviewTopEvents({
projectId,
}: OverviewTopEventsProps) {
const { filters, interval, range, previous } = useOverviewOptions();
const { interval, range, previous } = useOverviewOptions();
const filters = useEventFilters();
const [widget, setWidget, widgets] = useOverviewWidget('ev', {
all: {
title: 'Top events',

View File

@@ -1,8 +1,10 @@
'use client';
import { Suspense } from 'react';
import { Chart } from '@/components/report/chart';
import { ChartLoading } from '@/components/report/chart/ChartLoading';
import {
useEventFilters,
useEventQueryFilters,
} from '@/hooks/useEventQueryFilters';
import { cn } from '@/utils/cn';
import { Widget, WidgetBody } from '../Widget';
@@ -14,8 +16,9 @@ interface OverviewTopGeoProps {
projectId: string;
}
export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
const { filters, interval, range, previous, setCountry, setRegion, setCity } =
useOverviewOptions();
const { interval, range, previous } = useOverviewOptions();
const filters = useEventFilters();
const { region, country, city } = useEventQueryFilters();
const [widget, setWidget, widgets] = useOverviewWidget('geo', {
map: {
title: 'Map',
@@ -157,14 +160,14 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
switch (widget.key) {
case 'countries':
setWidget('regions');
setCountry(item.name);
country.set(item.name);
break;
case 'regions':
setWidget('cities');
setRegion(item.name);
region.set(item.name);
break;
case 'cities':
setCity(item.name);
city.set(item.name);
break;
}
}}

View File

@@ -1,8 +1,10 @@
'use client';
import { Suspense } from 'react';
import { Chart } from '@/components/report/chart';
import { ChartLoading } from '@/components/report/chart/ChartLoading';
import {
useEventFilters,
useEventQueryFilters,
} from '@/hooks/useEventQueryFilters';
import { cn } from '@/utils/cn';
import { Widget, WidgetBody } from '../Widget';
@@ -14,7 +16,9 @@ interface OverviewTopPagesProps {
projectId: string;
}
export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
const { filters, interval, range, previous, setPage } = useOverviewOptions();
const { interval, range, previous } = useOverviewOptions();
const filters = useEventFilters();
const { path } = useEventQueryFilters();
const [widget, setWidget, widgets] = useOverviewWidget('pages', {
top: {
title: 'Top pages',
@@ -125,7 +129,7 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
{...widget.chart}
previous={false}
onClick={(item) => {
setPage(item.name);
path.set(item.name);
}}
/>
</WidgetBody>

View File

@@ -1,8 +1,10 @@
'use client';
import { Suspense } from 'react';
import { Chart } from '@/components/report/chart';
import { ChartLoading } from '@/components/report/chart/ChartLoading';
import {
useEventFilters,
useEventQueryFilters,
} from '@/hooks/useEventQueryFilters';
import { cn } from '@/utils/cn';
import { Widget, WidgetBody } from '../Widget';
@@ -16,20 +18,18 @@ interface OverviewTopSourcesProps {
export default function OverviewTopSources({
projectId,
}: OverviewTopSourcesProps) {
const { interval, range, previous } = useOverviewOptions();
const {
filters,
interval,
range,
previous,
setReferrer,
setUtmSource,
setUtmMedium,
setUtmCampaign,
setUtmTerm,
setUtmContent,
setReferrerName,
setReferrerType,
} = useOverviewOptions();
referrer,
referrerName,
referrerType,
utmCampaign,
utmContent,
utmMedium,
utmSource,
utmTerm,
} = useEventQueryFilters();
const filters = useEventFilters();
const [widget, setWidget, widgets] = useOverviewWidget('sources', {
all: {
title: 'Top sources',
@@ -282,30 +282,30 @@ export default function OverviewTopSources({
onClick={(item) => {
switch (widget.key) {
case 'all':
setReferrerName(item.name);
referrerName.set(item.name);
setWidget('domain');
break;
case 'domain':
setReferrer(item.name);
referrer.set(item.name);
break;
case 'type':
setReferrerType(item.name);
referrerType.set(item.name);
setWidget('domain');
break;
case 'utm_source':
setUtmSource(item.name);
utmSource.set(item.name);
break;
case 'utm_medium':
setUtmMedium(item.name);
utmMedium.set(item.name);
break;
case 'utm_campaign':
setUtmCampaign(item.name);
utmCampaign.set(item.name);
break;
case 'utm_term':
setUtmTerm(item.name);
utmTerm.set(item.name);
break;
case 'utm_content':
setUtmContent(item.name);
utmContent.set(item.name);
break;
}
}}

View File

@@ -1,11 +1,9 @@
import { useMemo } from 'react';
import type { IChartInput } from '@/types';
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
import { getDefaultIntervalByRange, timeRanges } from '@/utils/constants';
import { mapKeys } from '@/utils/validation';
import {
parseAsBoolean,
parseAsInteger,
parseAsString,
parseAsStringEnum,
useQueryState,
} from 'nuqs';
@@ -29,267 +27,12 @@ export function useOverviewOptions() {
parseAsInteger.withDefault(0).withOptions(nuqsOptions)
);
// Filters
const [page, setPage] = useQueryState(
'page',
parseAsString.withOptions(nuqsOptions)
);
// Referrer
const [referrer, setReferrer] = useQueryState(
'referrer',
parseAsString.withOptions(nuqsOptions)
);
const [referrerName, setReferrerName] = useQueryState(
'referrer_name',
parseAsString.withOptions(nuqsOptions)
);
const [referrerType, setReferrerType] = useQueryState(
'referrer_type',
parseAsString.withOptions(nuqsOptions)
);
// Sources
const [utmSource, setUtmSource] = useQueryState(
'utm_source',
parseAsString.withOptions(nuqsOptions)
);
const [utmMedium, setUtmMedium] = useQueryState(
'utm_medium',
parseAsString.withOptions(nuqsOptions)
);
const [utmCampaign, setUtmCampaign] = useQueryState(
'utm_campaign',
parseAsString.withOptions(nuqsOptions)
);
const [utmContent, setUtmContent] = useQueryState(
'utm_content',
parseAsString.withOptions(nuqsOptions)
);
const [utmTerm, setUtmTerm] = useQueryState(
'utm_term',
parseAsString.withOptions(nuqsOptions)
);
// Geo
const [country, setCountry] = useQueryState(
'country',
parseAsString.withOptions(nuqsOptions)
);
const [region, setRegion] = useQueryState(
'region',
parseAsString.withOptions(nuqsOptions)
);
const [city, setCity] = useQueryState(
'city',
parseAsString.withOptions(nuqsOptions)
);
//
const [device, setDevice] = useQueryState(
'device',
parseAsString.withOptions(nuqsOptions)
);
const [browser, setBrowser] = useQueryState(
'browser',
parseAsString.withOptions(nuqsOptions)
);
const [browserVersion, setBrowserVersion] = useQueryState(
'browser_version',
parseAsString.withOptions(nuqsOptions)
);
const [os, setOS] = useQueryState(
'os',
parseAsString.withOptions(nuqsOptions)
);
const [osVersion, setOSVersion] = useQueryState(
'os_version',
parseAsString.withOptions(nuqsOptions)
);
// Toggles
const [liveHistogram, setLiveHistogram] = useQueryState(
'live',
parseAsBoolean.withDefault(false).withOptions(nuqsOptions)
);
const filters = useMemo(() => {
const filters: IChartInput['events'][number]['filters'] = [];
if (page) {
filters.push({
id: 'path',
operator: 'is',
name: 'path',
value: [page],
});
}
if (device) {
filters.push({
id: 'device',
operator: 'is',
name: 'device',
value: [device],
});
}
if (referrer) {
filters.push({
id: 'referrer',
operator: 'is',
name: 'referrer',
value: [referrer],
});
}
if (referrerName) {
filters.push({
id: 'referrer_name',
operator: 'is',
name: 'referrer_name',
value: [referrerName],
});
}
if (referrerType) {
filters.push({
id: 'referrer_type',
operator: 'is',
name: 'referrer_type',
value: [referrerType],
});
}
if (utmSource) {
filters.push({
id: 'utm_source',
operator: 'is',
name: 'properties.query.utm_source',
value: [utmSource],
});
}
if (utmMedium) {
filters.push({
id: 'utm_medium',
operator: 'is',
name: 'properties.query.utm_medium',
value: [utmMedium],
});
}
if (utmCampaign) {
filters.push({
id: 'utm_campaign',
operator: 'is',
name: 'properties.query.utm_campaign',
value: [utmCampaign],
});
}
if (utmContent) {
filters.push({
id: 'utm_content',
operator: 'is',
name: 'properties.query.utm_content',
value: [utmContent],
});
}
if (utmTerm) {
filters.push({
id: 'utm_term',
operator: 'is',
name: 'properties.query.utm_term',
value: [utmTerm],
});
}
if (country) {
filters.push({
id: 'country',
operator: 'is',
name: 'country',
value: [country],
});
}
if (region) {
filters.push({
id: 'region',
operator: 'is',
name: 'region',
value: [region],
});
}
if (city) {
filters.push({
id: 'city',
operator: 'is',
name: 'city',
value: [city],
});
}
if (browser) {
filters.push({
id: 'browser',
operator: 'is',
name: 'browser',
value: [browser],
});
}
if (browserVersion) {
filters.push({
id: 'browser_version',
operator: 'is',
name: 'browser_version',
value: [browserVersion],
});
}
if (os) {
filters.push({
id: 'os',
operator: 'is',
name: 'os',
value: [os],
});
}
if (osVersion) {
filters.push({
id: 'os_version',
operator: 'is',
name: 'os_version',
value: [osVersion],
});
}
return filters;
}, [
page,
device,
referrer,
referrerName,
referrerType,
utmSource,
utmMedium,
utmCampaign,
utmContent,
utmTerm,
country,
region,
city,
browser,
browserVersion,
os,
osVersion,
]);
return {
previous,
setPrevious,
@@ -297,52 +40,9 @@ export function useOverviewOptions() {
setRange,
metric,
setMetric,
page,
setPage,
// Computed
interval,
filters,
// Refs
referrer,
setReferrer,
referrerName,
setReferrerName,
referrerType,
setReferrerType,
// UTM
utmSource,
setUtmSource,
utmMedium,
setUtmMedium,
utmCampaign,
setUtmCampaign,
utmContent,
setUtmContent,
utmTerm,
setUtmTerm,
// GEO
country,
setCountry,
region,
setRegion,
city,
setCity,
// Tech
device,
setDevice,
browser,
setBrowser,
browserVersion,
setBrowserVersion,
os,
setOS,
osVersion,
setOSVersion,
// Toggles
liveHistogram,