refactor packages

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-02-19 10:55:15 +01:00
parent ae8482c1e3
commit 2f3c5ddf76
142 changed files with 2234 additions and 5507 deletions

View File

@@ -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>
);
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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';

View File

@@ -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;
}
}}

View File

@@ -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',

View File

@@ -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;
}
}}

View File

@@ -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>

View File

@@ -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;
}
}}

View File

@@ -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() {

View File

@@ -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,