wip event list item
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
|
||||
51
apps/web/src/components/ui/key-value.tsx
Normal file
51
apps/web/src/components/ui/key-value.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user