refactor packages
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import {
|
||||
useEventQueryFilters,
|
||||
useEventQueryNamesFilter,
|
||||
} from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { X } from 'lucide-react';
|
||||
import { Options as NuqsOptions } from 'nuqs';
|
||||
import type { Options as NuqsOptions } from 'nuqs';
|
||||
|
||||
interface OverviewFiltersButtonsProps {
|
||||
className?: string;
|
||||
@@ -15,25 +18,40 @@ export function OverviewFiltersButtons({
|
||||
className,
|
||||
nuqsOptions,
|
||||
}: OverviewFiltersButtonsProps) {
|
||||
const eventQueryFilters = useEventQueryFilters(nuqsOptions);
|
||||
const filters = Object.entries(eventQueryFilters).filter(
|
||||
([, filter]) => filter.get !== null
|
||||
);
|
||||
if (filters.length === 0) return null;
|
||||
const [events, setEvents] = useEventQueryNamesFilter(nuqsOptions);
|
||||
const [filters, setFilter] = useEventQueryFilters(nuqsOptions);
|
||||
if (filters.length === 0 && events.length === 0) return null;
|
||||
return (
|
||||
<div className={cn('flex flex-wrap gap-2 px-4 pb-4', className)}>
|
||||
{filters.map(([key, filter]) => (
|
||||
{events.map((event) => (
|
||||
<Button
|
||||
key={key}
|
||||
key={event}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
icon={X}
|
||||
onClick={() => filter.set(null)}
|
||||
onClick={() => setEvents((p) => p.filter((e) => e !== event))}
|
||||
>
|
||||
<span className="mr-1">{key} is</span>
|
||||
<strong>{filter.get}</strong>
|
||||
<strong>{event}</strong>
|
||||
</Button>
|
||||
))}
|
||||
{filters.map((filter) => {
|
||||
if (!filter.value[0]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={filter.name}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
icon={X}
|
||||
onClick={() => setFilter(filter.name, filter.value[0], 'is')}
|
||||
>
|
||||
<span className="mr-1">{filter.name} is</span>
|
||||
<strong>{filter.value[0]}</strong>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,93 +1,131 @@
|
||||
'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 { ComboboxAdvanced } from '@/components/ui/combobox-advanced';
|
||||
import { SheetHeader, SheetTitle } from '@/components/ui/sheet';
|
||||
import { useEventNames } from '@/hooks/useEventNames';
|
||||
import { useEventProperties } from '@/hooks/useEventProperties';
|
||||
import {
|
||||
useEventQueryFilters,
|
||||
useEventQueryNamesFilter,
|
||||
} from '@/hooks/useEventQueryFilters';
|
||||
import { useEventValues } from '@/hooks/useEventValues';
|
||||
import { XIcon } from 'lucide-react';
|
||||
import { Options as NuqsOptions } from 'nuqs';
|
||||
import type { Options as NuqsOptions } from 'nuqs';
|
||||
|
||||
import type {
|
||||
IChartEventFilter,
|
||||
IChartEventFilterOperator,
|
||||
IChartEventFilterValue,
|
||||
} from '@mixan/validation';
|
||||
|
||||
interface OverviewFiltersProps {
|
||||
projectId: string;
|
||||
nuqsOptions?: NuqsOptions;
|
||||
enableEventsFilter?: boolean;
|
||||
}
|
||||
|
||||
export function OverviewFiltersDrawerContent({
|
||||
projectId,
|
||||
nuqsOptions,
|
||||
enableEventsFilter,
|
||||
}: OverviewFiltersProps) {
|
||||
const eventQueryFilters = useEventQueryFilters(nuqsOptions);
|
||||
const [filters, setFilter] = useEventQueryFilters(nuqsOptions);
|
||||
const [event, setEvent] = useEventQueryNamesFilter(nuqsOptions);
|
||||
const eventNames = useEventNames(projectId);
|
||||
const eventProperties = useEventProperties(projectId);
|
||||
|
||||
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,
|
||||
<SheetHeader className="mb-8">
|
||||
<SheetTitle>Overview filters</SheetTitle>
|
||||
</SheetHeader>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
{enableEventsFilter && (
|
||||
<ComboboxAdvanced
|
||||
className="w-full"
|
||||
value={event}
|
||||
onChange={setEvent}
|
||||
// First items is * which is only used for report editing
|
||||
items={eventNames.slice(1).map((item) => ({
|
||||
label: item.name,
|
||||
value: item.name,
|
||||
}))}
|
||||
placeholder="Select event"
|
||||
/>
|
||||
)}
|
||||
<Combobox
|
||||
className="w-full"
|
||||
onChange={(value) => {
|
||||
setFilter(value, '');
|
||||
}}
|
||||
value=""
|
||||
placeholder="Filter by property"
|
||||
label="What do you want to filter by?"
|
||||
items={eventProperties.map((item) => ({
|
||||
label: item,
|
||||
value: item,
|
||||
}))}
|
||||
searchable
|
||||
/>
|
||||
searchable
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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}
|
||||
/>
|
||||
))}
|
||||
{filters
|
||||
.filter((filter) => filter.value[0] !== null)
|
||||
.map((filter) => {
|
||||
return (
|
||||
<FilterOption
|
||||
key={filter.name}
|
||||
projectId={projectId}
|
||||
setFilter={setFilter}
|
||||
{...filter}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function FilterOption({
|
||||
name,
|
||||
get,
|
||||
set,
|
||||
setFilter,
|
||||
projectId,
|
||||
}: {
|
||||
name: string;
|
||||
get: string | null;
|
||||
set: (value: string | null) => void;
|
||||
...filter
|
||||
}: IChartEventFilter & {
|
||||
projectId: string;
|
||||
setFilter: (
|
||||
name: string,
|
||||
value: IChartEventFilterValue,
|
||||
operator: IChartEventFilterOperator
|
||||
) => void;
|
||||
}) {
|
||||
const { data } = api.chart.values.useQuery({
|
||||
const values = useEventValues(
|
||||
projectId,
|
||||
event: name === 'path' ? 'screen_view' : 'session_start',
|
||||
property: name,
|
||||
});
|
||||
filter.name === 'path' ? 'screen_view' : 'session_start',
|
||||
filter.name
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex gap-2 items-center">
|
||||
<div>{name}</div>
|
||||
<div>{filter.name}</div>
|
||||
<Combobox
|
||||
className="flex-1"
|
||||
onChange={(value) => set(value)}
|
||||
onChange={(value) => setFilter(filter.name, value, filter.operator)}
|
||||
placeholder={'Select a value'}
|
||||
items={
|
||||
data?.values.filter(Boolean).map((value) => ({
|
||||
value,
|
||||
label: value,
|
||||
})) ?? []
|
||||
}
|
||||
value={get}
|
||||
items={values.map((value) => ({
|
||||
value,
|
||||
label: value,
|
||||
}))}
|
||||
value={String(filter.value[0] ?? '')}
|
||||
/>
|
||||
<Button size="icon" variant="ghost" onClick={() => set(null)}>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
setFilter(filter.name, filter.value[0] ?? '', filter.operator)
|
||||
}
|
||||
>
|
||||
<XIcon />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -3,18 +3,20 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
|
||||
import { FilterIcon } from 'lucide-react';
|
||||
import { Options as NuqsOptions } from 'nuqs';
|
||||
import type { Options as NuqsOptions } from 'nuqs';
|
||||
|
||||
import { OverviewFiltersDrawerContent } from './overview-filters-drawer-content';
|
||||
|
||||
interface OverviewFiltersDrawerProps {
|
||||
projectId: string;
|
||||
nuqsOptions?: NuqsOptions;
|
||||
enableEventsFilter?: boolean;
|
||||
}
|
||||
|
||||
export function OverviewFiltersDrawer({
|
||||
projectId,
|
||||
nuqsOptions,
|
||||
enableEventsFilter,
|
||||
}: OverviewFiltersDrawerProps) {
|
||||
return (
|
||||
<Sheet>
|
||||
@@ -27,6 +29,7 @@ export function OverviewFiltersDrawer({
|
||||
<OverviewFiltersDrawerContent
|
||||
projectId={projectId}
|
||||
nuqsOptions={nuqsOptions}
|
||||
enableEventsFilter={enableEventsFilter}
|
||||
/>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import type { IChartInput } from '@/types';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { ChevronsUpDownIcon } from 'lucide-react';
|
||||
import AnimateHeight from 'react-animate-height';
|
||||
|
||||
import type { IChartInput } from '@mixan/validation';
|
||||
|
||||
import { Chart } from '../report/chart';
|
||||
import { Widget, WidgetBody, WidgetHead } from '../Widget';
|
||||
import { useOverviewOptions } from './useOverviewOptions';
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Chart } from '@/components/report/chart';
|
||||
import {
|
||||
useEventFilters,
|
||||
useEventQueryFilters,
|
||||
} from '@/hooks/useEventQueryFilters';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { Widget, WidgetBody } from '../Widget';
|
||||
@@ -19,9 +16,7 @@ export default function OverviewTopDevices({
|
||||
projectId,
|
||||
}: OverviewTopDevicesProps) {
|
||||
const { interval, range, previous } = useOverviewOptions();
|
||||
const filters = useEventFilters();
|
||||
const { device, browser, browserVersion, os, osVersion } =
|
||||
useEventQueryFilters();
|
||||
const [filters, setFilter] = useEventQueryFilters();
|
||||
const [widget, setWidget, widgets] = useOverviewWidget('tech', {
|
||||
devices: {
|
||||
title: 'Top devices',
|
||||
@@ -190,21 +185,21 @@ export default function OverviewTopDevices({
|
||||
onClick={(item) => {
|
||||
switch (widget.key) {
|
||||
case 'devices':
|
||||
device.set(item.name);
|
||||
setFilter('device', item.name);
|
||||
break;
|
||||
case 'browser':
|
||||
setWidget('browser_version');
|
||||
browser.set(item.name);
|
||||
setFilter('browser', item.name);
|
||||
break;
|
||||
case 'browser_version':
|
||||
browserVersion.set(item.name);
|
||||
setFilter('browser_version', item.name);
|
||||
break;
|
||||
case 'os':
|
||||
setWidget('os_version');
|
||||
os.set(item.name);
|
||||
setFilter('os', item.name);
|
||||
break;
|
||||
case 'os_version':
|
||||
osVersion.set(item.name);
|
||||
setFilter('os_version', item.name);
|
||||
break;
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Chart } from '@/components/report/chart';
|
||||
import { useEventFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { Widget, WidgetBody } from '../Widget';
|
||||
@@ -16,7 +16,7 @@ export default function OverviewTopEvents({
|
||||
projectId,
|
||||
}: OverviewTopEventsProps) {
|
||||
const { interval, range, previous } = useOverviewOptions();
|
||||
const filters = useEventFilters();
|
||||
const [filters] = useEventQueryFilters();
|
||||
const [widget, setWidget, widgets] = useOverviewWidget('ev', {
|
||||
all: {
|
||||
title: 'Top events',
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Chart } from '@/components/report/chart';
|
||||
import {
|
||||
useEventFilters,
|
||||
useEventQueryFilters,
|
||||
} from '@/hooks/useEventQueryFilters';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { Widget, WidgetBody } from '../Widget';
|
||||
@@ -17,8 +14,7 @@ interface OverviewTopGeoProps {
|
||||
}
|
||||
export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
||||
const { interval, range, previous } = useOverviewOptions();
|
||||
const filters = useEventFilters();
|
||||
const { region, country, city } = useEventQueryFilters();
|
||||
const [filters, setFilter] = useEventQueryFilters();
|
||||
const [widget, setWidget, widgets] = useOverviewWidget('geo', {
|
||||
map: {
|
||||
title: 'Map',
|
||||
@@ -160,14 +156,14 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
||||
switch (widget.key) {
|
||||
case 'countries':
|
||||
setWidget('regions');
|
||||
country.set(item.name);
|
||||
setFilter('country', item.name);
|
||||
break;
|
||||
case 'regions':
|
||||
setWidget('cities');
|
||||
region.set(item.name);
|
||||
setFilter('region', item.name);
|
||||
break;
|
||||
case 'cities':
|
||||
city.set(item.name);
|
||||
setFilter('city', item.name);
|
||||
break;
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Chart } from '@/components/report/chart';
|
||||
import {
|
||||
useEventFilters,
|
||||
useEventQueryFilters,
|
||||
} from '@/hooks/useEventQueryFilters';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { Widget, WidgetBody } from '../Widget';
|
||||
@@ -17,8 +14,7 @@ interface OverviewTopPagesProps {
|
||||
}
|
||||
export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
||||
const { interval, range, previous } = useOverviewOptions();
|
||||
const filters = useEventFilters();
|
||||
const { path } = useEventQueryFilters();
|
||||
const [filters, setFilter] = useEventQueryFilters();
|
||||
const [widget, setWidget, widgets] = useOverviewWidget('pages', {
|
||||
top: {
|
||||
title: 'Top pages',
|
||||
@@ -129,7 +125,7 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
||||
{...widget.chart}
|
||||
previous={false}
|
||||
onClick={(item) => {
|
||||
path.set(item.name);
|
||||
setFilter('path', item.name);
|
||||
}}
|
||||
/>
|
||||
</WidgetBody>
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Chart } from '@/components/report/chart';
|
||||
import {
|
||||
useEventFilters,
|
||||
useEventQueryFilters,
|
||||
} from '@/hooks/useEventQueryFilters';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { Widget, WidgetBody } from '../Widget';
|
||||
@@ -19,17 +16,7 @@ export default function OverviewTopSources({
|
||||
projectId,
|
||||
}: OverviewTopSourcesProps) {
|
||||
const { interval, range, previous } = useOverviewOptions();
|
||||
const {
|
||||
referrer,
|
||||
referrerName,
|
||||
referrerType,
|
||||
utmCampaign,
|
||||
utmContent,
|
||||
utmMedium,
|
||||
utmSource,
|
||||
utmTerm,
|
||||
} = useEventQueryFilters();
|
||||
const filters = useEventFilters();
|
||||
const [filters, setFilter] = useEventQueryFilters();
|
||||
const [widget, setWidget, widgets] = useOverviewWidget('sources', {
|
||||
all: {
|
||||
title: 'Top sources',
|
||||
@@ -282,30 +269,30 @@ export default function OverviewTopSources({
|
||||
onClick={(item) => {
|
||||
switch (widget.key) {
|
||||
case 'all':
|
||||
referrerName.set(item.name);
|
||||
setFilter('referrer_name', item.name);
|
||||
setWidget('domain');
|
||||
break;
|
||||
case 'domain':
|
||||
referrer.set(item.name);
|
||||
setFilter('referrer', item.name);
|
||||
break;
|
||||
case 'type':
|
||||
referrerType.set(item.name);
|
||||
setFilter('referrer_type', item.name);
|
||||
setWidget('domain');
|
||||
break;
|
||||
case 'utm_source':
|
||||
utmSource.set(item.name);
|
||||
setFilter('utm_source', item.name);
|
||||
break;
|
||||
case 'utm_medium':
|
||||
utmMedium.set(item.name);
|
||||
setFilter('utm_medium', item.name);
|
||||
break;
|
||||
case 'utm_campaign':
|
||||
utmCampaign.set(item.name);
|
||||
setFilter('utm_campaign', item.name);
|
||||
break;
|
||||
case 'utm_term':
|
||||
utmTerm.set(item.name);
|
||||
setFilter('utm_term', item.name);
|
||||
break;
|
||||
case 'utm_content':
|
||||
utmContent.set(item.name);
|
||||
setFilter('utm_content', item.name);
|
||||
break;
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { getDefaultIntervalByRange, timeRanges } from '@/utils/constants';
|
||||
import { mapKeys } from '@/utils/validation';
|
||||
import {
|
||||
parseAsBoolean,
|
||||
parseAsInteger,
|
||||
@@ -8,6 +5,9 @@ import {
|
||||
useQueryState,
|
||||
} from 'nuqs';
|
||||
|
||||
import { getDefaultIntervalByRange, timeRanges } from '@mixan/constants';
|
||||
import { mapKeys } from '@mixan/validation';
|
||||
|
||||
const nuqsOptions = { history: 'push' } as const;
|
||||
|
||||
export function useOverviewOptions() {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { IChartInput } from '@/types';
|
||||
import { mapKeys } from '@/utils/validation';
|
||||
import { parseAsStringEnum, useQueryState } from 'nuqs';
|
||||
|
||||
import { mapKeys } from '@mixan/validation';
|
||||
import type { IChartInput } from '@mixan/validation';
|
||||
|
||||
export function useOverviewWidget<T extends string>(
|
||||
key: string,
|
||||
widgets: Record<T, { title: string; btn: string; chart: IChartInput }>
|
||||
@@ -15,7 +16,7 @@ export function useOverviewWidget<T extends string>(
|
||||
);
|
||||
return [
|
||||
{
|
||||
...widgets[widget]!,
|
||||
...widgets[widget],
|
||||
key: widget,
|
||||
},
|
||||
setWidget,
|
||||
|
||||
Reference in New Issue
Block a user