wip event list

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-02-16 23:06:36 +01:00
parent a74acda707
commit 02d52d5da8
27 changed files with 1178 additions and 465 deletions

View File

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

View File

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

View File

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

View File

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