wip event list item

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-02-15 00:35:13 +01:00
parent a1d5104166
commit f6c6a403b4
6 changed files with 277 additions and 56 deletions

View File

@@ -1,15 +1,12 @@
'use client';
import { useMemo } from 'react';
import type { RouterOutputs } from '@/app/_trpc/client';
import { ListProperties } from '@/components/events/ListProperties';
import { ExpandableListItem } from '@/components/general/ExpandableListItem';
import { ProfileAvatar } from '@/components/profiles/ProfileAvatar';
import { KeyValue, KeyValueSubtle } from '@/components/ui/key-value';
import { useAppParams } from '@/hooks/useAppParams';
import { formatDateTime } from '@/utils/date';
import { getProfileName } from '@/utils/getters';
import { round } from '@/utils/math';
import Link from 'next/link';
import { useQueryState } from 'nuqs';
import { EventIcon } from './event-icon';
@@ -21,49 +18,202 @@ export function EventListItem({
name,
properties,
path,
duration,
referrer,
referrerName,
referrerType,
brand,
model,
browser,
browserVersion,
os,
osVersion,
city,
region,
country,
continent,
device,
}: EventListItemProps) {
const params = useAppParams();
const bullets = useMemo(() => {
const bullets: React.ReactNode[] = [
<span>{formatDateTime(createdAt)}</span>,
];
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');
if (profile) {
bullets.push(
<Link
href={`/${params.organizationId}/${params.projectId}/profiles/${profile.id}`}
className="flex items-center gap-1 text-black font-medium hover:underline"
>
<ProfileAvatar size="xs" {...(profile ?? {})}></ProfileAvatar>
{getProfileName(profile)}
</Link>
);
}
const keyValueList = [
{
name: 'Duration',
value: duration ? round(duration / 1000, 1) : undefined,
},
{
name: 'Referrer',
value: referrer,
onClick() {
setReferrer(referrer ?? null);
},
},
{
name: 'Referrer name',
value: referrerName,
onClick() {
setReferrerName(referrerName ?? null);
},
},
{
name: 'Referrer type',
value: referrerType,
onClick() {
setReferrerType(referrerType ?? null);
},
},
{
name: 'Brand',
value: brand,
onClick() {
setBrand(brand ?? null);
},
},
{
name: 'Model',
value: model,
onClick() {
setModel(model ?? null);
},
},
{
name: 'Browser',
value: browser,
onClick() {
setBrowser(browser ?? null);
},
},
{
name: 'Browser version',
value: browserVersion,
onClick() {
setBrowserVersion(browserVersion ?? null);
},
},
{
name: 'OS',
value: os,
onClick() {
setOs(os ?? null);
},
},
{
name: 'OS cersion',
value: osVersion,
onClick() {
setOsVersion(osVersion ?? null);
},
},
{
name: 'City',
value: city,
onClick() {
setCity(city ?? null);
},
},
{
name: 'Region',
value: region,
onClick() {
setRegion(region ?? null);
},
},
{
name: 'Country',
value: country,
onClick() {
setCountry(country ?? null);
},
},
{
name: 'Continent',
value: continent,
onClick() {
setContinent(continent ?? null);
},
},
{
name: 'Device',
value: device,
onClick() {
setDevice(device ?? null);
},
},
].filter((item) => typeof item.value === 'string' && item.value);
if (typeof properties.duration === 'number') {
bullets.push(`${round(properties.duration / 1000, 1)}s`);
}
switch (name) {
case 'screen_view': {
if (path) {
bullets.push(path);
}
break;
}
}
return bullets;
}, [name, createdAt, profile, properties, params, path]);
const propertiesList = Object.entries(properties)
.map(([name, value]) => ({
name,
value: value as string | number | undefined,
}))
.filter((item) => typeof item.value === 'string' && item.value);
return (
<ExpandableListItem
title={name.split('_').join(' ')}
bullets={bullets}
content={
<>
<KeyValueSubtle name="Time" value={createdAt.toLocaleString()} />
{profile && (
<KeyValueSubtle
name="Profile"
// icon={<ProfileAvatar size="xs" {...(profile ?? {})} />}
value={getProfileName(profile)}
href={`/${params.organizationId}/${params.projectId}/profiles/${profile.id}`}
/>
)}
{path && (
<KeyValueSubtle
name="Path"
value={path}
onClick={() => {
setPath(path);
}}
/>
)}
</>
}
image={<EventIcon name={name} />}
>
<ListProperties data={properties} className="rounded-none border-none" />
{propertiesList.length > 0 && (
<div className="p-4 flex flex-col gap-4">
<div className="font-medium">Your properties</div>
<div className="flex flex-wrap gap-x-4 gap-y-2">
{propertiesList.map((item) => (
<KeyValue key={item.name} name={item.name} value={item.value} />
))}
</div>
</div>
)}
<div className="p-4 flex flex-col gap-4">
<div className="font-medium">Properties</div>
<div className="flex flex-wrap gap-x-4 gap-y-2">
{keyValueList.map((item) => (
<KeyValue
onClick={item.onClick}
key={item.name}
name={item.name}
value={item.value}
/>
))}
</div>
</div>
</ExpandableListItem>
);
}

View File

@@ -7,6 +7,7 @@ import { FullPageEmptyState } from '@/components/FullPageEmptyState';
import { Pagination, usePagination } from '@/components/Pagination';
import { ComboboxAdvanced } from '@/components/ui/combobox-advanced';
import { GanttChartIcon } from 'lucide-react';
import { parseAsArrayOf, parseAsString, useQueryState } from 'nuqs';
import { EventListItem } from './event-list-item';
@@ -15,22 +16,21 @@ interface ListEventsProps {
}
export function ListEvents({ projectId }: ListEventsProps) {
const pagination = usePagination();
const [eventFilters, setEventFilters] = useState<string[]>([]);
const eventsQuery = api.event.list.useQuery(
{
events: eventFilters,
projectId: projectId,
...pagination,
},
{
keepPreviousData: true,
}
const [eventFilters, setEventFilters] = useQueryState(
'events',
parseAsArrayOf(parseAsString).withDefault([])
);
const eventsQuery = api.event.list.useQuery({
events: eventFilters,
projectId: projectId,
...pagination,
});
const events = useMemo(() => eventsQuery.data ?? [], [eventsQuery]);
const filterEventsQuery = api.chart.events.useQuery({
projectId: projectId,
});
const filterEvents = (filterEventsQuery.data ?? []).map((item) => ({
value: item.name,
label: item.name,
@@ -61,7 +61,7 @@ export function ListEvents({ projectId }: ListEventsProps) {
<>
<div className="flex flex-col gap-4">
{events.map((item) => (
<EventListItem key={item.id} {...item} />
<EventListItem key={item.createdAt.toString()} {...item} />
))}
</div>
<div className="mt-2">

View File

@@ -7,14 +7,14 @@ import { Button } from '../ui/button';
interface ExpandableListItemProps {
children: React.ReactNode;
bullets: React.ReactNode[];
content: React.ReactNode;
title: string;
image?: React.ReactNode;
initialOpen?: boolean;
}
export function ExpandableListItem({
title,
bullets,
content,
image,
initialOpen = false,
children,
@@ -22,15 +22,15 @@ export function ExpandableListItem({
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-start">
<div className="p-3 sm:p-6 flex gap-4 items-center">
<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>
<div className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-4 text-sm text-muted-foreground">
{bullets.map((bullet, index) => (
<span key={index}>{bullet}</span>
))}
</div>
{!!content && (
<div className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-4">
{content}
</div>
)}
</div>
<Button
variant="secondary"

View File

@@ -0,0 +1,51 @@
import { cn } from '@/utils/cn';
import Link from 'next/link';
interface KeyValueProps {
name: string;
value: string | number | undefined;
onClick?: () => void;
href?: string;
}
export function KeyValue({ href, onClick, name, value }: KeyValueProps) {
const clickable = href || onClick;
const Component = href ? (Link as any) : onClick ? 'button' : 'div';
return (
<Component
className="group flex border border-border rounded-md text-xs divide-x font-medium self-start min-w-0 max-w-full"
{...{ href, onClick }}
>
<div className="p-1 px-2">{name}</div>
<div
className={cn(
'p-1 px-2 font-mono text-blue-700 bg-slate-50 whitespace-nowrap overflow-hidden text-ellipsis',
clickable && 'group-hover:underline'
)}
>
{value}
</div>
</Component>
);
}
export function KeyValueSubtle({ href, onClick, name, value }: KeyValueProps) {
const clickable = href || onClick;
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"
{...{ 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',
clickable && 'group-hover:underline'
)}
>
{value}
</div>
</Component>
);
}