wip event list
This commit is contained in:
@@ -1,7 +1,28 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { api } from '@/app/_trpc/client';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
Sheet,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from '@/components/ui/sheet';
|
||||
import { cn } from '@/utils/cn';
|
||||
import type { VariantProps } from 'class-variance-authority';
|
||||
import { cva } from 'class-variance-authority';
|
||||
import { ActivityIcon, BotIcon, MonitorPlayIcon } from 'lucide-react';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import { ActivityIcon, BotIcon, DotIcon, MonitorPlayIcon } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import type { EventMeta } from '@mixan/db';
|
||||
|
||||
const variants = cva('flex items-center justify-center shrink-0', {
|
||||
variants: {
|
||||
@@ -17,30 +38,208 @@ const variants = cva('flex items-center justify-center shrink-0', {
|
||||
|
||||
type EventIconProps = VariantProps<typeof variants> & {
|
||||
name: string;
|
||||
meta?: EventMeta;
|
||||
projectId: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const records = {
|
||||
default: { Icon: BotIcon, text: 'text-chart-0', bg: 'bg-chart-0/10' },
|
||||
const records: Record<
|
||||
string,
|
||||
{
|
||||
icon: string;
|
||||
color: string;
|
||||
}
|
||||
> = {
|
||||
default: {
|
||||
icon: 'BotIcon',
|
||||
color: 'slate',
|
||||
},
|
||||
screen_view: {
|
||||
Icon: MonitorPlayIcon,
|
||||
text: 'text-chart-3',
|
||||
bg: 'bg-chart-3/10',
|
||||
icon: 'MonitorPlayIcon',
|
||||
color: 'blue',
|
||||
},
|
||||
session_start: {
|
||||
Icon: ActivityIcon,
|
||||
text: 'text-chart-2',
|
||||
bg: 'bg-chart-2/10',
|
||||
icon: 'ActivityIcon',
|
||||
color: 'teal',
|
||||
},
|
||||
};
|
||||
|
||||
export function EventIcon({ className, name, size }: EventIconProps) {
|
||||
const { Icon, text, bg } =
|
||||
name in records ? records[name as keyof typeof records] : records.default;
|
||||
const icons: Record<string, LucideIcon> = {
|
||||
BotIcon,
|
||||
MonitorPlayIcon,
|
||||
ActivityIcon,
|
||||
};
|
||||
|
||||
const colors = [
|
||||
'rose',
|
||||
'pink',
|
||||
'fuchsia',
|
||||
'purple',
|
||||
'violet',
|
||||
'indigo',
|
||||
'blue',
|
||||
'sky',
|
||||
'cyan',
|
||||
'teal',
|
||||
'emerald',
|
||||
'green',
|
||||
'lime',
|
||||
'yellow',
|
||||
'amber',
|
||||
'orange',
|
||||
'red',
|
||||
'stone',
|
||||
'neutral',
|
||||
'zinc',
|
||||
'grey',
|
||||
'slate',
|
||||
];
|
||||
|
||||
export function EventIcon({
|
||||
className,
|
||||
name,
|
||||
size,
|
||||
meta,
|
||||
projectId,
|
||||
}: EventIconProps) {
|
||||
const router = useRouter();
|
||||
const [selectedIcon, setIcon] = useState(
|
||||
meta?.icon ?? records[name]?.icon ?? records.default?.icon ?? ''
|
||||
);
|
||||
const [selectedColor, setColor] = useState(
|
||||
meta?.color ?? records[name]?.color ?? records.default?.color ?? ''
|
||||
);
|
||||
const [conversion, setConversion] = useState(!!meta?.conversion);
|
||||
|
||||
useEffect(() => {
|
||||
if (meta?.icon) {
|
||||
setIcon(meta.icon);
|
||||
}
|
||||
}, [meta?.icon]);
|
||||
useEffect(() => {
|
||||
if (meta?.color) {
|
||||
setColor(meta.color);
|
||||
}
|
||||
}, [meta?.color]);
|
||||
useEffect(() => {
|
||||
setConversion(meta?.conversion ?? false);
|
||||
}, [meta?.conversion]);
|
||||
|
||||
const SelectedIcon = icons[selectedIcon]!;
|
||||
const Icon =
|
||||
icons[meta?.icon ?? records[name]?.icon ?? records.default?.icon ?? '']!;
|
||||
const color =
|
||||
meta?.color ?? records[name]?.color ?? records.default?.color ?? '';
|
||||
|
||||
const mutation = api.event.updateEventMeta.useMutation({
|
||||
onSuccess() {
|
||||
document.querySelector('#close-sheet')?.click();
|
||||
toast('Event updated');
|
||||
router.refresh();
|
||||
},
|
||||
});
|
||||
const getBg = (color: string) => `bg-${color}-200`;
|
||||
const getText = (color: string) => `text-${color}-700`;
|
||||
|
||||
return (
|
||||
<div className={cn(variants({ size }), bg, className)}>
|
||||
<Icon size={20} className={text} />
|
||||
</div>
|
||||
<Sheet>
|
||||
<SheetTrigger className={cn(getBg(color), variants({ size }), className)}>
|
||||
<Icon size={20} className={getText(color)} />
|
||||
</SheetTrigger>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Edit "{name}"</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div className="flex flex-col gap-8 my-8">
|
||||
<div>
|
||||
<Label className="mb-4 block">Conversion</Label>
|
||||
<label className="cursor-pointer flex items-center select-none border border-border rounded-md p-4 gap-4">
|
||||
<Checkbox
|
||||
checked={conversion}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked === 'indeterminate') return;
|
||||
setConversion(checked);
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<span>Yes, this event is important!</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="mb-4 block">Pick a icon</Label>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{Object.entries(icons).map(([name, Icon]) => (
|
||||
<button
|
||||
key={name}
|
||||
onClick={() => {
|
||||
setIcon(name);
|
||||
}}
|
||||
className={cn(
|
||||
'flex-shrink-0 rounded-md w-8 h-8 cursor-pointer inline-flex transition-all bg-slate-100 flex items-center justify-center',
|
||||
name === selectedIcon
|
||||
? 'scale-110 ring-1 ring-black'
|
||||
: '[&_svg]:opacity-50'
|
||||
)}
|
||||
>
|
||||
<Icon size={16} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="mb-4 block">Pick a color</Label>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{colors.map((color) => (
|
||||
<button
|
||||
key={color}
|
||||
onClick={() => {
|
||||
setColor(color);
|
||||
}}
|
||||
className={cn(
|
||||
'flex-shrink-0 rounded-md w-8 h-8 cursor-pointer transition-all flex justify-center items-center',
|
||||
color === selectedColor ? 'ring-1 ring-black' : '',
|
||||
getBg(color)
|
||||
)}
|
||||
>
|
||||
{SelectedIcon ? (
|
||||
<SelectedIcon size={16} />
|
||||
) : (
|
||||
<svg
|
||||
className={`${getText(color)} opacity-70`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle cx="12.1" cy="12.1" r="4" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SheetFooter>
|
||||
<Button
|
||||
onClick={() =>
|
||||
mutation.mutate({
|
||||
projectId,
|
||||
name,
|
||||
icon: selectedIcon,
|
||||
color: selectedColor,
|
||||
conversion,
|
||||
})
|
||||
}
|
||||
>
|
||||
Update event
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,13 +4,16 @@ import type { RouterOutputs } from '@/app/_trpc/client';
|
||||
import { ExpandableListItem } from '@/components/general/ExpandableListItem';
|
||||
import { KeyValue, KeyValueSubtle } from '@/components/ui/key-value';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { getProfileName } from '@/utils/getters';
|
||||
import { round } from '@/utils/math';
|
||||
import { useQueryState } from 'nuqs';
|
||||
|
||||
import type { IServiceCreateEventPayload } from '@mixan/db';
|
||||
|
||||
import { EventIcon } from './event-icon';
|
||||
|
||||
type EventListItemProps = RouterOutputs['event']['list'][number];
|
||||
type EventListItemProps = IServiceCreateEventPayload;
|
||||
|
||||
export function EventListItem({
|
||||
profile,
|
||||
@@ -33,25 +36,11 @@ export function EventListItem({
|
||||
country,
|
||||
continent,
|
||||
device,
|
||||
projectId,
|
||||
meta,
|
||||
}: EventListItemProps) {
|
||||
const params = useAppParams();
|
||||
|
||||
const [, setPath] = useQueryState('path');
|
||||
const [, setReferrer] = useQueryState('referrer');
|
||||
const [, setReferrerName] = useQueryState('referrerName');
|
||||
const [, setReferrerType] = useQueryState('referrerType');
|
||||
const [, setBrand] = useQueryState('brand');
|
||||
const [, setModel] = useQueryState('model');
|
||||
const [, setBrowser] = useQueryState('browser');
|
||||
const [, setBrowserVersion] = useQueryState('browserVersion');
|
||||
const [, setOs] = useQueryState('os');
|
||||
const [, setOsVersion] = useQueryState('osVersion');
|
||||
const [, setCity] = useQueryState('city');
|
||||
const [, setRegion] = useQueryState('region');
|
||||
const [, setCountry] = useQueryState('country');
|
||||
const [, setContinent] = useQueryState('continent');
|
||||
const [, setDevice] = useQueryState('device');
|
||||
|
||||
const eventQueryFilters = useEventQueryFilters({ shallow: false });
|
||||
const keyValueList = [
|
||||
{
|
||||
name: 'Duration',
|
||||
@@ -61,98 +50,98 @@ export function EventListItem({
|
||||
name: 'Referrer',
|
||||
value: referrer,
|
||||
onClick() {
|
||||
setReferrer(referrer ?? null);
|
||||
eventQueryFilters.referrer.set(referrer ?? null);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Referrer name',
|
||||
value: referrerName,
|
||||
onClick() {
|
||||
setReferrerName(referrerName ?? null);
|
||||
eventQueryFilters.referrerName.set(referrerName ?? null);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Referrer type',
|
||||
value: referrerType,
|
||||
onClick() {
|
||||
setReferrerType(referrerType ?? null);
|
||||
eventQueryFilters.referrerType.set(referrerType ?? null);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Brand',
|
||||
value: brand,
|
||||
onClick() {
|
||||
setBrand(brand ?? null);
|
||||
eventQueryFilters.brand.set(brand ?? null);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Model',
|
||||
value: model,
|
||||
onClick() {
|
||||
setModel(model ?? null);
|
||||
eventQueryFilters.model.set(model ?? null);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Browser',
|
||||
value: browser,
|
||||
onClick() {
|
||||
setBrowser(browser ?? null);
|
||||
eventQueryFilters.browser.set(browser ?? null);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Browser version',
|
||||
value: browserVersion,
|
||||
onClick() {
|
||||
setBrowserVersion(browserVersion ?? null);
|
||||
eventQueryFilters.browserVersion.set(browserVersion ?? null);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'OS',
|
||||
value: os,
|
||||
onClick() {
|
||||
setOs(os ?? null);
|
||||
eventQueryFilters.os.set(os ?? null);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'OS cersion',
|
||||
value: osVersion,
|
||||
onClick() {
|
||||
setOsVersion(osVersion ?? null);
|
||||
eventQueryFilters.osVersion.set(osVersion ?? null);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'City',
|
||||
value: city,
|
||||
onClick() {
|
||||
setCity(city ?? null);
|
||||
eventQueryFilters.city.set(city ?? null);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Region',
|
||||
value: region,
|
||||
onClick() {
|
||||
setRegion(region ?? null);
|
||||
eventQueryFilters.region.set(region ?? null);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Country',
|
||||
value: country,
|
||||
onClick() {
|
||||
setCountry(country ?? null);
|
||||
eventQueryFilters.country.set(country ?? null);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Continent',
|
||||
value: continent,
|
||||
onClick() {
|
||||
setContinent(continent ?? null);
|
||||
eventQueryFilters.continent.set(continent ?? null);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Device',
|
||||
value: device,
|
||||
onClick() {
|
||||
setDevice(device ?? null);
|
||||
eventQueryFilters.device.set(device ?? null);
|
||||
},
|
||||
},
|
||||
].filter((item) => typeof item.value === 'string' && item.value);
|
||||
@@ -166,6 +155,7 @@ export function EventListItem({
|
||||
|
||||
return (
|
||||
<ExpandableListItem
|
||||
className={cn(meta?.conversion && 'ring-2 ring-primary-500')}
|
||||
title={name.split('_').join(' ')}
|
||||
content={
|
||||
<>
|
||||
@@ -182,13 +172,13 @@ export function EventListItem({
|
||||
name="Path"
|
||||
value={path}
|
||||
onClick={() => {
|
||||
setPath(path);
|
||||
eventQueryFilters.path.set(path);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
image={<EventIcon name={name} />}
|
||||
image={<EventIcon name={name} meta={meta} projectId={projectId} />}
|
||||
>
|
||||
<div className="p-2">
|
||||
<div className="bg-gradient-to-tr from-slate-100 to-white rounded-md">
|
||||
|
||||
@@ -1,53 +1,69 @@
|
||||
'use client';
|
||||
|
||||
import { Suspense } from 'react';
|
||||
import { FullPageEmptyState } from '@/components/FullPageEmptyState';
|
||||
import { Pagination } from '@/components/Pagination';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useCursor } from '@/hooks/useCursor';
|
||||
import { useEventFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { GanttChartIcon } from 'lucide-react';
|
||||
import { last } from 'ramda';
|
||||
|
||||
import { IServiceCreateEventPayload } from '@mixan/db';
|
||||
import type { IServiceCreateEventPayload } from '@mixan/db';
|
||||
|
||||
import { EventListItem } from './event-list-item';
|
||||
|
||||
interface EventListProps {
|
||||
data: IServiceCreateEventPayload[];
|
||||
count: number;
|
||||
}
|
||||
export function EventList({ data }: EventListProps) {
|
||||
export function EventList({ data, count }: EventListProps) {
|
||||
const { cursor, setCursor } = useCursor();
|
||||
const filters = useEventFilters();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Suspense>
|
||||
<div className="p-4">
|
||||
{data.length === 0 ? (
|
||||
<FullPageEmptyState title="No events here" icon={GanttChartIcon}>
|
||||
{/* {filterEvents.length ? (
|
||||
<p>Could not find any events with your filter</p>
|
||||
{cursor !== 0 ? (
|
||||
<>
|
||||
<p>Looks like you have reached the end of the list</p>
|
||||
<Button
|
||||
className="mt-4"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCursor((p) => Math.max(0, p - 1))}
|
||||
>
|
||||
Go back
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<p>We have not recieved any events yet</p>
|
||||
)} */}
|
||||
<p>We have not recieved any events yet</p>
|
||||
<>
|
||||
{filters.length ? (
|
||||
<p>Could not find any events with your filter</p>
|
||||
) : (
|
||||
<p>We have not recieved any events yet</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</FullPageEmptyState>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Pagination
|
||||
cursor={cursor}
|
||||
setCursor={setCursor}
|
||||
count={count}
|
||||
take={50}
|
||||
/>
|
||||
<div className="flex flex-col gap-4 my-4">
|
||||
{data.map((item) => (
|
||||
<EventListItem
|
||||
key={item.createdAt.toString() + item.name + item.profileId}
|
||||
{...item}
|
||||
/>
|
||||
<EventListItem key={item.id} {...item} />
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCursor(last(data)?.createdAt ?? null)}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
<Pagination cursor={cursor} setCursor={setCursor} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Suspense } from 'react';
|
||||
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
||||
import { OverviewFiltersButtons } from '@/components/overview/filters/overview-filters-buttons';
|
||||
import { OverviewFiltersDrawer } from '@/components/overview/filters/overview-filters-drawer';
|
||||
import { getEventFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { getExists } from '@/server/pageExists';
|
||||
|
||||
import { getEventList, getEvents } from '@mixan/db';
|
||||
import { getEventList, getEventsCount } from '@mixan/db';
|
||||
|
||||
import { StickyBelowHeader } from '../layout-sticky-below-header';
|
||||
import { EventList } from './event-list';
|
||||
@@ -15,25 +16,114 @@ interface PageProps {
|
||||
};
|
||||
searchParams: {
|
||||
cursor?: string;
|
||||
path?: string;
|
||||
device?: string;
|
||||
referrer?: string;
|
||||
referrerName?: string;
|
||||
referrerType?: string;
|
||||
utmSource?: string;
|
||||
utmMedium?: string;
|
||||
utmCampaign?: string;
|
||||
utmContent?: string;
|
||||
utmTerm?: string;
|
||||
continent?: string;
|
||||
country?: string;
|
||||
region?: string;
|
||||
city?: string;
|
||||
browser?: string;
|
||||
browserVersion?: string;
|
||||
os?: string;
|
||||
osVersion?: string;
|
||||
brand?: string;
|
||||
model?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const nuqsOptions = {
|
||||
shallow: false,
|
||||
};
|
||||
|
||||
function parseQueryAsNumber(value: string | undefined) {
|
||||
if (typeof value === 'string') {
|
||||
return parseInt(value, 10);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export default async function Page({
|
||||
params: { projectId, organizationId },
|
||||
searchParams: { cursor },
|
||||
searchParams,
|
||||
}: PageProps) {
|
||||
await getExists(organizationId, projectId);
|
||||
const events = await getEventList({
|
||||
cursor,
|
||||
projectId,
|
||||
take: 50,
|
||||
});
|
||||
const [events, count] = await Promise.all([
|
||||
getEventList({
|
||||
cursor: parseQueryAsNumber(searchParams.cursor),
|
||||
projectId,
|
||||
take: 50,
|
||||
filters: getEventFilters({
|
||||
path: searchParams.path ?? null,
|
||||
device: searchParams.device ?? null,
|
||||
referrer: searchParams.referrer ?? null,
|
||||
referrerName: searchParams.referrerName ?? null,
|
||||
referrerType: searchParams.referrerType ?? null,
|
||||
utmSource: searchParams.utmSource ?? null,
|
||||
utmMedium: searchParams.utmMedium ?? null,
|
||||
utmCampaign: searchParams.utmCampaign ?? null,
|
||||
utmContent: searchParams.utmContent ?? null,
|
||||
utmTerm: searchParams.utmTerm ?? null,
|
||||
continent: searchParams.continent ?? null,
|
||||
country: searchParams.country ?? null,
|
||||
region: searchParams.region ?? null,
|
||||
city: searchParams.city ?? null,
|
||||
browser: searchParams.browser ?? null,
|
||||
browserVersion: searchParams.browserVersion ?? null,
|
||||
os: searchParams.os ?? null,
|
||||
osVersion: searchParams.osVersion ?? null,
|
||||
brand: searchParams.brand ?? null,
|
||||
model: searchParams.model ?? null,
|
||||
}),
|
||||
}),
|
||||
getEventsCount({
|
||||
projectId,
|
||||
filters: getEventFilters({
|
||||
path: searchParams.path ?? null,
|
||||
device: searchParams.device ?? null,
|
||||
referrer: searchParams.referrer ?? null,
|
||||
referrerName: searchParams.referrerName ?? null,
|
||||
referrerType: searchParams.referrerType ?? null,
|
||||
utmSource: searchParams.utmSource ?? null,
|
||||
utmMedium: searchParams.utmMedium ?? null,
|
||||
utmCampaign: searchParams.utmCampaign ?? null,
|
||||
utmContent: searchParams.utmContent ?? null,
|
||||
utmTerm: searchParams.utmTerm ?? null,
|
||||
continent: searchParams.continent ?? null,
|
||||
country: searchParams.country ?? null,
|
||||
region: searchParams.region ?? null,
|
||||
city: searchParams.city ?? null,
|
||||
browser: searchParams.browser ?? null,
|
||||
browserVersion: searchParams.browserVersion ?? null,
|
||||
os: searchParams.os ?? null,
|
||||
osVersion: searchParams.osVersion ?? null,
|
||||
brand: searchParams.brand ?? null,
|
||||
model: searchParams.model ?? null,
|
||||
}),
|
||||
}),
|
||||
getExists(organizationId, projectId),
|
||||
]);
|
||||
console.log(events[0]);
|
||||
|
||||
return (
|
||||
<PageLayout title="Events" organizationSlug={organizationId}>
|
||||
<StickyBelowHeader className="p-4 flex justify-between">
|
||||
<OverviewFiltersDrawer projectId={projectId} />
|
||||
<OverviewFiltersDrawer
|
||||
projectId={projectId}
|
||||
nuqsOptions={nuqsOptions}
|
||||
/>
|
||||
<OverviewFiltersButtons
|
||||
className="p-0 justify-end"
|
||||
nuqsOptions={nuqsOptions}
|
||||
/>
|
||||
</StickyBelowHeader>
|
||||
<EventList data={events} />
|
||||
<EventList data={events} count={count} />
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,42 +1,54 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Button } from './ui/button';
|
||||
|
||||
export function usePagination(take = 100) {
|
||||
const [skip, setSkip] = useState(0);
|
||||
return useMemo(
|
||||
() => ({
|
||||
skip,
|
||||
next: () => setSkip((p) => p + take),
|
||||
prev: () => setSkip((p) => Math.max(p - take)),
|
||||
take,
|
||||
canPrev: skip > 0,
|
||||
canNext: true,
|
||||
page: skip / take + 1,
|
||||
}),
|
||||
[skip, setSkip, take]
|
||||
);
|
||||
export function usePagination(take: number) {
|
||||
const [page, setPage] = useState(0);
|
||||
return {
|
||||
take,
|
||||
skip: page * take,
|
||||
setPage,
|
||||
page,
|
||||
paginate: <T,>(data: T[]): T[] =>
|
||||
data.slice(page * take, (page + 1) * take),
|
||||
};
|
||||
}
|
||||
|
||||
export type PaginationProps = ReturnType<typeof usePagination>;
|
||||
export function Pagination({
|
||||
take,
|
||||
count,
|
||||
cursor,
|
||||
setCursor,
|
||||
}: {
|
||||
take?: number;
|
||||
count?: number;
|
||||
cursor: number;
|
||||
setCursor: Dispatch<SetStateAction<number>>;
|
||||
}) {
|
||||
const isNextDisabled =
|
||||
count !== undefined && take !== undefined && cursor * take + take >= count;
|
||||
|
||||
export function Pagination(props: PaginationProps) {
|
||||
return (
|
||||
<div className="flex select-none items-center justify-end gap-2">
|
||||
<div className="font-medium text-xs">Page: {props.page}</div>
|
||||
<div className="font-medium text-xs">Page: {cursor + 1}</div>
|
||||
{typeof count === 'number' && (
|
||||
<div className="font-medium text-xs">Total rows: {count}</div>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => props.prev()}
|
||||
disabled={!props.canPrev}
|
||||
onClick={() => setCursor((p) => Math.max(0, p - 1))}
|
||||
disabled={cursor === 0}
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => props.next()}
|
||||
disabled={!props.canNext}
|
||||
onClick={() => setCursor((p) => p + 1)}
|
||||
disabled={isNextDisabled}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
|
||||
@@ -11,6 +11,7 @@ interface ExpandableListItemProps {
|
||||
title: string;
|
||||
image?: React.ReactNode;
|
||||
initialOpen?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
export function ExpandableListItem({
|
||||
title,
|
||||
@@ -18,14 +19,17 @@ export function ExpandableListItem({
|
||||
image,
|
||||
initialOpen = false,
|
||||
children,
|
||||
className,
|
||||
}: ExpandableListItemProps) {
|
||||
const [open, setOpen] = useState(initialOpen ?? false);
|
||||
return (
|
||||
<div className="bg-white shadow rounded-xl overflow-hidden">
|
||||
<div className="p-3 sm:p-6 flex gap-4 items-center">
|
||||
<div
|
||||
className={cn('bg-white shadow rounded-xl overflow-hidden', className)}
|
||||
>
|
||||
<div className="p-2 sm:p-4 flex gap-4">
|
||||
<div className="flex gap-1">{image}</div>
|
||||
<div className="flex flex-col flex-1 gap-1 min-w-0">
|
||||
<span className="text-lg font-medium leading-none mb-1">{title}</span>
|
||||
<span className="text-md font-medium leading-none mb-1">{title}</span>
|
||||
{!!content && (
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-4">
|
||||
{content}
|
||||
|
||||
@@ -1,22 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
useEventFilters,
|
||||
useEventQueryFilters,
|
||||
} from '@/hooks/useEventQueryFilters';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { X } from 'lucide-react';
|
||||
import { Options as NuqsOptions } from 'nuqs';
|
||||
|
||||
export function OverviewFiltersButtons() {
|
||||
const eventQueryFilters = useEventQueryFilters();
|
||||
interface OverviewFiltersButtonsProps {
|
||||
className?: string;
|
||||
nuqsOptions?: NuqsOptions;
|
||||
}
|
||||
|
||||
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;
|
||||
return (
|
||||
<div
|
||||
className={cn('flex flex-wrap gap-2', filters.length > 0 && 'px-4 pb-4')}
|
||||
>
|
||||
<div className={cn('flex flex-wrap gap-2 px-4 pb-4', className)}>
|
||||
{filters.map(([key, filter]) => (
|
||||
<Button
|
||||
key={key}
|
||||
|
||||
@@ -5,15 +5,18 @@ import { Button } from '@/components/ui/button';
|
||||
import { Combobox } from '@/components/ui/combobox';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { XIcon } from 'lucide-react';
|
||||
import { Options as NuqsOptions } from 'nuqs';
|
||||
|
||||
interface OverviewFiltersProps {
|
||||
projectId: string;
|
||||
nuqsOptions?: NuqsOptions;
|
||||
}
|
||||
|
||||
export function OverviewFiltersDrawerContent({
|
||||
projectId,
|
||||
nuqsOptions,
|
||||
}: OverviewFiltersProps) {
|
||||
const eventQueryFilters = useEventQueryFilters();
|
||||
const eventQueryFilters = useEventQueryFilters(nuqsOptions);
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -3,15 +3,18 @@
|
||||
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 { OverviewFiltersDrawerContent } from './overview-filters-drawer-content';
|
||||
|
||||
interface OverviewFiltersDrawerProps {
|
||||
projectId: string;
|
||||
nuqsOptions?: NuqsOptions;
|
||||
}
|
||||
|
||||
export function OverviewFiltersDrawer({
|
||||
projectId,
|
||||
nuqsOptions,
|
||||
}: OverviewFiltersDrawerProps) {
|
||||
return (
|
||||
<Sheet>
|
||||
@@ -21,7 +24,10 @@ export function OverviewFiltersDrawer({
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className="!max-w-lg w-full" side="right">
|
||||
<OverviewFiltersDrawerContent projectId={projectId} />
|
||||
<OverviewFiltersDrawerContent
|
||||
projectId={projectId}
|
||||
nuqsOptions={nuqsOptions}
|
||||
/>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import type { IChartData } from '@/app/_trpc/client';
|
||||
import { Pagination, usePagination } from '@/components/Pagination';
|
||||
@@ -37,7 +35,7 @@ export function ReportTable({
|
||||
visibleSeries,
|
||||
setVisibleSeries,
|
||||
}: ReportTableProps) {
|
||||
const pagination = usePagination(50);
|
||||
const { setPage, paginate, page } = usePagination(50);
|
||||
const number = useNumber();
|
||||
const interval = useSelector((state) => state.report.interval);
|
||||
const formatDate = useFormatDateInterval(interval);
|
||||
@@ -63,46 +61,44 @@ export function ReportTable({
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.series
|
||||
.slice(pagination.skip, pagination.skip + pagination.take)
|
||||
.map((serie, index) => {
|
||||
const checked = !!visibleSeries.find(
|
||||
(item) => item.name === serie.name
|
||||
);
|
||||
{paginate(data.series).map((serie, index) => {
|
||||
const checked = !!visibleSeries.find(
|
||||
(item) => item.name === serie.name
|
||||
);
|
||||
|
||||
return (
|
||||
<TableRow key={serie.name}>
|
||||
<TableCell className="h-10">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
onCheckedChange={(checked) =>
|
||||
handleChange(serie.name, !!checked)
|
||||
}
|
||||
style={
|
||||
checked
|
||||
? {
|
||||
background: getChartColor(index),
|
||||
borderColor: getChartColor(index),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
checked={checked}
|
||||
/>
|
||||
<Tooltip delayDuration={200}>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="min-w-0 overflow-hidden whitespace-nowrap text-ellipsis">
|
||||
{getLabel(serie.name)}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{getLabel(serie.name)}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
return (
|
||||
<TableRow key={serie.name}>
|
||||
<TableCell className="h-10">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
onCheckedChange={(checked) =>
|
||||
handleChange(serie.name, !!checked)
|
||||
}
|
||||
style={
|
||||
checked
|
||||
? {
|
||||
background: getChartColor(index),
|
||||
borderColor: getChartColor(index),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
checked={checked}
|
||||
/>
|
||||
<Tooltip delayDuration={200}>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="min-w-0 overflow-hidden whitespace-nowrap text-ellipsis">
|
||||
{getLabel(serie.name)}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{getLabel(serie.name)}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="overflow-auto">
|
||||
@@ -122,44 +118,39 @@ export function ReportTable({
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.series
|
||||
.slice(pagination.skip, pagination.skip + pagination.take)
|
||||
.map((serie) => {
|
||||
return (
|
||||
<TableRow key={serie.name}>
|
||||
<TableCell className="h-10">
|
||||
<div className="flex items-center gap-2 font-medium">
|
||||
{number.format(serie.metrics.sum)}
|
||||
<PreviousDiffIndicator
|
||||
{...serie.metrics.previous.sum}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="h-10">
|
||||
<div className="flex items-center gap-2 font-medium">
|
||||
{number.format(serie.metrics.average)}
|
||||
<PreviousDiffIndicator
|
||||
{...serie.metrics.previous.average}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
{paginate(data.series).map((serie) => {
|
||||
return (
|
||||
<TableRow key={serie.name}>
|
||||
<TableCell className="h-10">
|
||||
<div className="flex items-center gap-2 font-medium">
|
||||
{number.format(serie.metrics.sum)}
|
||||
<PreviousDiffIndicator
|
||||
{...serie.metrics.previous.sum}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="h-10">
|
||||
<div className="flex items-center gap-2 font-medium">
|
||||
{number.format(serie.metrics.average)}
|
||||
<PreviousDiffIndicator
|
||||
{...serie.metrics.previous.average}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
{serie.data.map((item) => {
|
||||
return (
|
||||
<TableCell
|
||||
className="h-10"
|
||||
key={item.date.toString()}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{number.format(item.count)}
|
||||
<PreviousDiffIndicator {...item.previous} />
|
||||
</div>
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
{serie.data.map((item) => {
|
||||
return (
|
||||
<TableCell className="h-10" key={item.date.toString()}>
|
||||
<div className="flex items-center gap-2">
|
||||
{number.format(item.count)}
|
||||
<PreviousDiffIndicator {...item.previous} />
|
||||
</div>
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
@@ -171,7 +162,7 @@ export function ReportTable({
|
||||
<Badge>Min: {number.format(data.metrics.min)}</Badge>
|
||||
<Badge>Max: {number.format(data.metrics.max)}</Badge>
|
||||
</div>
|
||||
<Pagination {...pagination} />
|
||||
<Pagination cursor={page} setCursor={setPage} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -37,13 +37,13 @@ export function KeyValueSubtle({ href, onClick, name, value }: KeyValueProps) {
|
||||
const Component = href ? (Link as any) : onClick ? 'button' : 'div';
|
||||
return (
|
||||
<Component
|
||||
className="group flex text-xs gap-2 font-medium self-start min-w-0 max-w-full items-center"
|
||||
className="group flex text-[10px] sm:text-xs gap-2 font-medium self-start min-w-0 max-w-full items-center"
|
||||
{...{ href, onClick }}
|
||||
>
|
||||
<div className="text-gray-400">{name}</div>
|
||||
<div
|
||||
className={cn(
|
||||
'bg-slate-100 rounded p-1 px-2 text-gray-600 whitespace-nowrap overflow-hidden text-ellipsis',
|
||||
'bg-slate-100 rounded p-0.5 px-1 sm:p-1 sm:px-2 text-gray-600 whitespace-nowrap overflow-hidden text-ellipsis',
|
||||
clickable && 'group-hover:underline'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -3,10 +3,9 @@
|
||||
import * as React from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import * as SheetPrimitive from '@radix-ui/react-dialog';
|
||||
import { ScrollArea } from '@radix-ui/react-scroll-area';
|
||||
import { cva } from 'class-variance-authority';
|
||||
import type { VariantProps } from 'class-variance-authority';
|
||||
import { X } from 'lucide-react';
|
||||
import { XIcon } from 'lucide-react';
|
||||
|
||||
const Sheet = SheetPrimitive.Root;
|
||||
|
||||
@@ -22,7 +21,7 @@ const SheetOverlay = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Overlay
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||
'fixed inset-0 z-50 bg-black/20 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -32,7 +31,7 @@ const SheetOverlay = React.forwardRef<
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
||||
|
||||
const sheetVariants = cva(
|
||||
'fixed z-50 gap-4 bg-background shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-150 data-[state=open]:duration-150',
|
||||
'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
|
||||
{
|
||||
variants: {
|
||||
side: {
|
||||
@@ -41,7 +40,7 @@ const sheetVariants = cva(
|
||||
'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
|
||||
left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
|
||||
right:
|
||||
'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
|
||||
'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
@@ -59,17 +58,16 @@ const SheetContent = React.forwardRef<
|
||||
SheetContentProps
|
||||
>(({ side = 'right', className, children, ...props }, ref) => (
|
||||
<SheetPortal>
|
||||
<SheetOverlay className="backdrop-blur-none bg-transparent" />
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(sheetVariants({ side }), className)}
|
||||
{...props}
|
||||
>
|
||||
<div className="h-screen p-6 overflow-y-auto overflow-x-hidden">
|
||||
{children}
|
||||
</div>
|
||||
{children}
|
||||
<SheetPrimitive.Close id="close-sheet" className="hidden" />
|
||||
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
||||
<X className="h-4 w-4" />
|
||||
<XIcon className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { parseAsIsoDateTime, useQueryState } from 'nuqs';
|
||||
import { parseAsInteger, useQueryState } from 'nuqs';
|
||||
|
||||
export function useCursor() {
|
||||
const [cursor, setCursor] = useQueryState(
|
||||
'cursor',
|
||||
parseAsIsoDateTime.withOptions({ shallow: false })
|
||||
parseAsInteger
|
||||
.withOptions({ shallow: false, history: 'push' })
|
||||
.withDefault(0)
|
||||
);
|
||||
return {
|
||||
cursor,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useMemo } from 'react';
|
||||
import type { IChartInput } from '@/types';
|
||||
|
||||
// prettier-ignore
|
||||
import type { UseQueryStateReturn } from 'nuqs';
|
||||
import type { Options as NuqsOptions, UseQueryStateReturn } from 'nuqs';
|
||||
|
||||
import { parseAsString, useQueryState } from 'nuqs';
|
||||
|
||||
@@ -18,210 +18,309 @@ function useFix<T>(hook: UseQueryStateReturn<T, undefined>) {
|
||||
);
|
||||
}
|
||||
|
||||
export function useEventQueryFilters() {
|
||||
export function useEventQueryFilters(options: NuqsOptions = {}) {
|
||||
// Ignore prettier so that we have all one same line
|
||||
// prettier-ignore
|
||||
return {
|
||||
path: useFix(useQueryState('path', parseAsString.withOptions(nuqsOptions))),
|
||||
referrer: useFix(useQueryState('referrer', parseAsString.withOptions(nuqsOptions))),
|
||||
referrerName: useFix(useQueryState('referrerName',parseAsString.withOptions(nuqsOptions))),
|
||||
referrerType: useFix(useQueryState('referrerType',parseAsString.withOptions(nuqsOptions))),
|
||||
utmSource: useFix(useQueryState('utmSource',parseAsString.withOptions(nuqsOptions))),
|
||||
utmMedium: useFix(useQueryState('utmMedium',parseAsString.withOptions(nuqsOptions))),
|
||||
utmCampaign: useFix(useQueryState('utmCampaign',parseAsString.withOptions(nuqsOptions))),
|
||||
utmContent: useFix(useQueryState('utmContent',parseAsString.withOptions(nuqsOptions))),
|
||||
utmTerm: useFix(useQueryState('utmTerm', parseAsString.withOptions(nuqsOptions))),
|
||||
country: useFix(useQueryState('country', parseAsString.withOptions(nuqsOptions))),
|
||||
region: useFix(useQueryState('region', parseAsString.withOptions(nuqsOptions))),
|
||||
city: useFix(useQueryState('city', parseAsString.withOptions(nuqsOptions))),
|
||||
device: useFix(useQueryState('device', parseAsString.withOptions(nuqsOptions))),
|
||||
browser: useFix(useQueryState('browser', parseAsString.withOptions(nuqsOptions))),
|
||||
browserVersion: useFix(useQueryState('browserVersion',parseAsString.withOptions(nuqsOptions))),
|
||||
os: useFix(useQueryState('os', parseAsString.withOptions(nuqsOptions))),
|
||||
osVersion: useFix(useQueryState('osVersion',parseAsString.withOptions(nuqsOptions))),
|
||||
path: useFix(useQueryState('path', parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
referrer: useFix(useQueryState('referrer', parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
referrerName: useFix(useQueryState('referrerName',parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
referrerType: useFix(useQueryState('referrerType',parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
utmSource: useFix(useQueryState('utmSource',parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
utmMedium: useFix(useQueryState('utmMedium',parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
utmCampaign: useFix(useQueryState('utmCampaign',parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
utmContent: useFix(useQueryState('utmContent',parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
utmTerm: useFix(useQueryState('utmTerm', parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
continent: useFix(useQueryState('continent', parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
country: useFix(useQueryState('country', parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
region: useFix(useQueryState('region', parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
city: useFix(useQueryState('city', parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
device: useFix(useQueryState('device', parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
browser: useFix(useQueryState('browser', parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
browserVersion: useFix(useQueryState('browserVersion',parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
os: useFix(useQueryState('os', parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
osVersion: useFix(useQueryState('osVersion',parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
brand: useFix(useQueryState('brand',parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
model: useFix(useQueryState('model',parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
} as const;
|
||||
}
|
||||
|
||||
export function useEventFilters() {
|
||||
const hej = useEventQueryFilters();
|
||||
const eventQueryFilters = useEventQueryFilters();
|
||||
|
||||
const filters = useMemo(() => {
|
||||
const filters: IChartInput['events'][number]['filters'] = [];
|
||||
|
||||
if (hej.path.get) {
|
||||
filters.push({
|
||||
id: 'path',
|
||||
operator: 'is',
|
||||
name: 'path' as const,
|
||||
value: [hej.path.get],
|
||||
});
|
||||
}
|
||||
|
||||
if (hej.device.get) {
|
||||
filters.push({
|
||||
id: 'device',
|
||||
operator: 'is',
|
||||
name: 'device' as const,
|
||||
value: [hej.device.get],
|
||||
});
|
||||
}
|
||||
|
||||
if (hej.referrer.get) {
|
||||
filters.push({
|
||||
id: 'referrer',
|
||||
operator: 'is',
|
||||
name: 'referrer' as const,
|
||||
value: [hej.referrer.get],
|
||||
});
|
||||
}
|
||||
console.log('hej.referrerName.get', hej.referrerName.get);
|
||||
|
||||
if (hej.referrerName.get) {
|
||||
filters.push({
|
||||
id: 'referrerName',
|
||||
operator: 'is',
|
||||
name: 'referrer_name' as const,
|
||||
value: [hej.referrerName.get],
|
||||
});
|
||||
}
|
||||
|
||||
if (hej.referrerType.get) {
|
||||
filters.push({
|
||||
id: 'referrerType',
|
||||
operator: 'is',
|
||||
name: 'referrer_type' as const,
|
||||
value: [hej.referrerType.get],
|
||||
});
|
||||
}
|
||||
|
||||
if (hej.utmSource.get) {
|
||||
filters.push({
|
||||
id: 'utmSource',
|
||||
operator: 'is',
|
||||
name: 'properties.query.utm_source' as const,
|
||||
value: [hej.utmSource.get],
|
||||
});
|
||||
}
|
||||
|
||||
if (hej.utmMedium.get) {
|
||||
filters.push({
|
||||
id: 'utmMedium',
|
||||
operator: 'is',
|
||||
name: 'properties.query.utm_medium' as const,
|
||||
value: [hej.utmMedium.get],
|
||||
});
|
||||
}
|
||||
|
||||
if (hej.utmCampaign.get) {
|
||||
filters.push({
|
||||
id: 'utmCampaign',
|
||||
operator: 'is',
|
||||
name: 'properties.query.utm_campaign' as const,
|
||||
value: [hej.utmCampaign.get],
|
||||
});
|
||||
}
|
||||
|
||||
if (hej.utmContent.get) {
|
||||
filters.push({
|
||||
id: 'utmContent',
|
||||
operator: 'is',
|
||||
name: 'properties.query.utm_content' as const,
|
||||
value: [hej.utmContent.get],
|
||||
});
|
||||
}
|
||||
|
||||
if (hej.utmTerm.get) {
|
||||
filters.push({
|
||||
id: 'utmTerm',
|
||||
operator: 'is',
|
||||
name: 'properties.query.utm_term' as const,
|
||||
value: [hej.utmTerm.get],
|
||||
});
|
||||
}
|
||||
|
||||
if (hej.country.get) {
|
||||
filters.push({
|
||||
id: 'country',
|
||||
operator: 'is',
|
||||
name: 'country' as const,
|
||||
value: [hej.country.get],
|
||||
});
|
||||
}
|
||||
|
||||
if (hej.region.get) {
|
||||
filters.push({
|
||||
id: 'region',
|
||||
operator: 'is',
|
||||
name: 'region' as const,
|
||||
value: [hej.region.get],
|
||||
});
|
||||
}
|
||||
|
||||
if (hej.city.get) {
|
||||
filters.push({
|
||||
id: 'city',
|
||||
operator: 'is',
|
||||
name: 'city' as const,
|
||||
value: [hej.city.get],
|
||||
});
|
||||
}
|
||||
|
||||
if (hej.browser.get) {
|
||||
filters.push({
|
||||
id: 'browser',
|
||||
operator: 'is',
|
||||
name: 'browser' as const,
|
||||
value: [hej.browser.get],
|
||||
});
|
||||
}
|
||||
|
||||
if (hej.browserVersion.get) {
|
||||
filters.push({
|
||||
id: 'browserVersion',
|
||||
operator: 'is',
|
||||
name: 'browser_version' as const,
|
||||
value: [hej.browserVersion.get],
|
||||
});
|
||||
}
|
||||
|
||||
if (hej.os.get) {
|
||||
filters.push({
|
||||
id: 'os',
|
||||
operator: 'is',
|
||||
name: 'os' as const,
|
||||
value: [hej.os.get],
|
||||
});
|
||||
}
|
||||
|
||||
if (hej.osVersion.get) {
|
||||
filters.push({
|
||||
id: 'osVersion',
|
||||
operator: 'is',
|
||||
name: 'os_version' as const,
|
||||
value: [hej.osVersion.get],
|
||||
});
|
||||
}
|
||||
|
||||
return filters;
|
||||
return getEventFilters({
|
||||
path: eventQueryFilters.path.get,
|
||||
device: eventQueryFilters.device.get,
|
||||
referrer: eventQueryFilters.referrer.get,
|
||||
referrerName: eventQueryFilters.referrerName.get,
|
||||
referrerType: eventQueryFilters.referrerType.get,
|
||||
utmSource: eventQueryFilters.utmSource.get,
|
||||
utmMedium: eventQueryFilters.utmMedium.get,
|
||||
utmCampaign: eventQueryFilters.utmCampaign.get,
|
||||
utmContent: eventQueryFilters.utmContent.get,
|
||||
utmTerm: eventQueryFilters.utmTerm.get,
|
||||
continent: eventQueryFilters.continent.get,
|
||||
country: eventQueryFilters.country.get,
|
||||
region: eventQueryFilters.region.get,
|
||||
city: eventQueryFilters.city.get,
|
||||
browser: eventQueryFilters.browser.get,
|
||||
browserVersion: eventQueryFilters.browserVersion.get,
|
||||
os: eventQueryFilters.os.get,
|
||||
osVersion: eventQueryFilters.osVersion.get,
|
||||
brand: eventQueryFilters.brand.get,
|
||||
model: eventQueryFilters.model.get,
|
||||
});
|
||||
}, [
|
||||
hej.path,
|
||||
hej.device,
|
||||
hej.referrer,
|
||||
hej.referrerName,
|
||||
hej.referrerType,
|
||||
hej.utmSource,
|
||||
hej.utmMedium,
|
||||
hej.utmCampaign,
|
||||
hej.utmContent,
|
||||
hej.utmTerm,
|
||||
hej.country,
|
||||
hej.region,
|
||||
hej.city,
|
||||
hej.browser,
|
||||
hej.browserVersion,
|
||||
hej.os,
|
||||
hej.osVersion,
|
||||
eventQueryFilters.path.get,
|
||||
eventQueryFilters.device.get,
|
||||
eventQueryFilters.referrer.get,
|
||||
eventQueryFilters.referrerName.get,
|
||||
eventQueryFilters.referrerType.get,
|
||||
eventQueryFilters.utmSource.get,
|
||||
eventQueryFilters.utmMedium.get,
|
||||
eventQueryFilters.utmCampaign.get,
|
||||
eventQueryFilters.utmContent.get,
|
||||
eventQueryFilters.utmTerm.get,
|
||||
eventQueryFilters.continent.get,
|
||||
eventQueryFilters.country.get,
|
||||
eventQueryFilters.region.get,
|
||||
eventQueryFilters.city.get,
|
||||
eventQueryFilters.browser.get,
|
||||
eventQueryFilters.browserVersion.get,
|
||||
eventQueryFilters.os.get,
|
||||
eventQueryFilters.osVersion.get,
|
||||
eventQueryFilters.model.get,
|
||||
eventQueryFilters.brand.get,
|
||||
]);
|
||||
|
||||
return filters;
|
||||
}
|
||||
|
||||
export function getEventFilters({
|
||||
path,
|
||||
device,
|
||||
referrer,
|
||||
referrerName,
|
||||
referrerType,
|
||||
utmSource,
|
||||
utmMedium,
|
||||
utmCampaign,
|
||||
utmContent,
|
||||
utmTerm,
|
||||
continent,
|
||||
country,
|
||||
region,
|
||||
city,
|
||||
browser,
|
||||
browserVersion,
|
||||
os,
|
||||
osVersion,
|
||||
brand,
|
||||
model,
|
||||
}: {
|
||||
path: string | null;
|
||||
device: string | null;
|
||||
referrer: string | null;
|
||||
referrerName: string | null;
|
||||
referrerType: string | null;
|
||||
utmSource: string | null;
|
||||
utmMedium: string | null;
|
||||
utmCampaign: string | null;
|
||||
utmContent: string | null;
|
||||
utmTerm: string | null;
|
||||
continent: string | null;
|
||||
country: string | null;
|
||||
region: string | null;
|
||||
city: string | null;
|
||||
browser: string | null;
|
||||
browserVersion: string | null;
|
||||
os: string | null;
|
||||
osVersion: string | null;
|
||||
brand: string | null;
|
||||
model: string | null;
|
||||
}) {
|
||||
const filters: IChartInput['events'][number]['filters'] = [];
|
||||
|
||||
if (path) {
|
||||
filters.push({
|
||||
id: 'path',
|
||||
operator: 'is',
|
||||
name: 'path' as const,
|
||||
value: [path],
|
||||
});
|
||||
}
|
||||
|
||||
if (device) {
|
||||
filters.push({
|
||||
id: 'device',
|
||||
operator: 'is',
|
||||
name: 'device' as const,
|
||||
value: [device],
|
||||
});
|
||||
}
|
||||
|
||||
if (referrer) {
|
||||
filters.push({
|
||||
id: 'referrer',
|
||||
operator: 'is',
|
||||
name: 'referrer' as const,
|
||||
value: [referrer],
|
||||
});
|
||||
}
|
||||
|
||||
if (referrerName) {
|
||||
filters.push({
|
||||
id: 'referrerName',
|
||||
operator: 'is',
|
||||
name: 'referrer_name' as const,
|
||||
value: [referrerName],
|
||||
});
|
||||
}
|
||||
|
||||
if (referrerType) {
|
||||
filters.push({
|
||||
id: 'referrerType',
|
||||
operator: 'is',
|
||||
name: 'referrer_type' as const,
|
||||
value: [referrerType],
|
||||
});
|
||||
}
|
||||
|
||||
if (utmSource) {
|
||||
filters.push({
|
||||
id: 'utmSource',
|
||||
operator: 'is',
|
||||
name: 'properties.query.utm_source' as const,
|
||||
value: [utmSource],
|
||||
});
|
||||
}
|
||||
|
||||
if (utmMedium) {
|
||||
filters.push({
|
||||
id: 'utmMedium',
|
||||
operator: 'is',
|
||||
name: 'properties.query.utm_medium' as const,
|
||||
value: [utmMedium],
|
||||
});
|
||||
}
|
||||
|
||||
if (utmCampaign) {
|
||||
filters.push({
|
||||
id: 'utmCampaign',
|
||||
operator: 'is',
|
||||
name: 'properties.query.utm_campaign' as const,
|
||||
value: [utmCampaign],
|
||||
});
|
||||
}
|
||||
|
||||
if (utmContent) {
|
||||
filters.push({
|
||||
id: 'utmContent',
|
||||
operator: 'is',
|
||||
name: 'properties.query.utm_content' as const,
|
||||
value: [utmContent],
|
||||
});
|
||||
}
|
||||
|
||||
if (utmTerm) {
|
||||
filters.push({
|
||||
id: 'utmTerm',
|
||||
operator: 'is',
|
||||
name: 'properties.query.utm_term' as const,
|
||||
value: [utmTerm],
|
||||
});
|
||||
}
|
||||
|
||||
if (continent) {
|
||||
filters.push({
|
||||
id: 'continent',
|
||||
operator: 'is',
|
||||
name: 'continent' as const,
|
||||
value: [continent],
|
||||
});
|
||||
}
|
||||
|
||||
if (country) {
|
||||
filters.push({
|
||||
id: 'country',
|
||||
operator: 'is',
|
||||
name: 'country' as const,
|
||||
value: [country],
|
||||
});
|
||||
}
|
||||
|
||||
if (region) {
|
||||
filters.push({
|
||||
id: 'region',
|
||||
operator: 'is',
|
||||
name: 'region' as const,
|
||||
value: [region],
|
||||
});
|
||||
}
|
||||
|
||||
if (city) {
|
||||
filters.push({
|
||||
id: 'city',
|
||||
operator: 'is',
|
||||
name: 'city' as const,
|
||||
value: [city],
|
||||
});
|
||||
}
|
||||
|
||||
if (browser) {
|
||||
filters.push({
|
||||
id: 'browser',
|
||||
operator: 'is',
|
||||
name: 'browser' as const,
|
||||
value: [browser],
|
||||
});
|
||||
}
|
||||
|
||||
if (browserVersion) {
|
||||
filters.push({
|
||||
id: 'browserVersion',
|
||||
operator: 'is',
|
||||
name: 'browser_version' as const,
|
||||
value: [browserVersion],
|
||||
});
|
||||
}
|
||||
|
||||
if (os) {
|
||||
filters.push({
|
||||
id: 'os',
|
||||
operator: 'is',
|
||||
name: 'os' as const,
|
||||
value: [os],
|
||||
});
|
||||
}
|
||||
|
||||
if (osVersion) {
|
||||
filters.push({
|
||||
id: 'osVersion',
|
||||
operator: 'is',
|
||||
name: 'os_version' as const,
|
||||
value: [osVersion],
|
||||
});
|
||||
}
|
||||
|
||||
if (brand) {
|
||||
filters.push({
|
||||
id: 'brand',
|
||||
operator: 'is',
|
||||
name: 'brand' as const,
|
||||
value: [brand],
|
||||
});
|
||||
}
|
||||
|
||||
if (model) {
|
||||
filters.push({
|
||||
id: 'model',
|
||||
operator: 'is',
|
||||
name: 'model' as const,
|
||||
value: [model],
|
||||
});
|
||||
}
|
||||
|
||||
return filters;
|
||||
}
|
||||
|
||||
@@ -1,38 +1,29 @@
|
||||
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
|
||||
import { transformEvent } from '@/server/services/event.service';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { IDBEvent } from '@mixan/db';
|
||||
import { chQuery, createSqlBuilder, getEvents } from '@mixan/db';
|
||||
import { db } from '@mixan/db';
|
||||
|
||||
export const eventRouter = createTRPCRouter({
|
||||
list: protectedProcedure
|
||||
updateEventMeta: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
take: z.number().default(100),
|
||||
skip: z.number().default(0),
|
||||
profileId: z.string().optional(),
|
||||
events: z.array(z.string()).optional(),
|
||||
name: z.string(),
|
||||
icon: z.string().optional(),
|
||||
color: z.string().optional(),
|
||||
conversion: z.boolean().optional(),
|
||||
})
|
||||
)
|
||||
.query(async ({ input: { take, skip, projectId, profileId, events } }) => {
|
||||
const { sb, getSql } = createSqlBuilder();
|
||||
|
||||
sb.limit = take;
|
||||
sb.offset = skip;
|
||||
sb.where.projectId = `project_id = '${projectId}'`;
|
||||
if (profileId) {
|
||||
sb.where.profileId = `profile_id = '${profileId}'`;
|
||||
}
|
||||
if (events?.length) {
|
||||
sb.where.name = `name IN (${events.map((e) => `'${e}'`).join(',')})`;
|
||||
}
|
||||
|
||||
sb.orderBy.created_at = 'created_at DESC';
|
||||
|
||||
const res = await getEvents(getSql(), { profile: true });
|
||||
|
||||
return res;
|
||||
.mutation(({ input: { projectId, name, icon, color, conversion } }) => {
|
||||
return db.eventMeta.upsert({
|
||||
where: {
|
||||
name_project_id: {
|
||||
name,
|
||||
project_id: projectId,
|
||||
},
|
||||
},
|
||||
create: { project_id: projectId, name, icon, color, conversion },
|
||||
update: { icon, color, conversion },
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user