From cb53072bf79c5c6d15ffd149bc3eee7882df5016 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl-Gerhard=20Lindesva=CC=88rd?= Date: Fri, 8 Mar 2024 10:44:25 +0100 Subject: [PATCH] wip improve event list --- .../[projectId]/events/event-icon.tsx | 6 +- .../events/event-list-item copy.tsx | 275 ++++++++++++++++++ .../[projectId]/events/event-list-item.tsx | 228 ++++++++++----- .../[projectId]/events/event-list.tsx | 4 +- .../[projectId]/layout-project-selector.tsx | 1 + .../src/components/profiles/ProfileAvatar.tsx | 2 +- .../src/components/report/chart/SerieIcon.tsx | 4 +- apps/web/src/components/ui/combobox.tsx | 8 +- apps/web/src/components/ui/key-value.tsx | 2 +- apps/web/src/components/ui/popover.tsx | 42 ++- apps/web/tailwind.config.js | 2 +- 11 files changed, 480 insertions(+), 94 deletions(-) create mode 100644 apps/web/src/app/(app)/[organizationId]/[projectId]/events/event-list-item copy.tsx diff --git a/apps/web/src/app/(app)/[organizationId]/[projectId]/events/event-icon.tsx b/apps/web/src/app/(app)/[organizationId]/[projectId]/events/event-icon.tsx index fe457749..99b90806 100644 --- a/apps/web/src/app/(app)/[organizationId]/[projectId]/events/event-icon.tsx +++ b/apps/web/src/app/(app)/[organizationId]/[projectId]/events/event-icon.tsx @@ -21,11 +21,11 @@ import { toast } from 'sonner'; import type { EventMeta } from '@mixan/db'; -const variants = cva('flex items-center justify-center shrink-0', { +const variants = cva('flex items-center justify-center shrink-0 rounded-full', { variants: { size: { - sm: 'w-6 h-6 rounded', - default: 'w-12 h-12 rounded-xl', + sm: 'w-6 h-6', + default: 'w-10 h-10', }, }, defaultVariants: { diff --git a/apps/web/src/app/(app)/[organizationId]/[projectId]/events/event-list-item copy.tsx b/apps/web/src/app/(app)/[organizationId]/[projectId]/events/event-list-item copy.tsx new file mode 100644 index 00000000..9fac09a9 --- /dev/null +++ b/apps/web/src/app/(app)/[organizationId]/[projectId]/events/event-list-item copy.tsx @@ -0,0 +1,275 @@ +'use client'; + +import { ExpandableListItem } from '@/components/general/ExpandableListItem'; +import { ProfileAvatar } from '@/components/profiles/ProfileAvatar'; +import { SerieIcon } from '@/components/report/chart/SerieIcon'; +import { KeyValue, KeyValueSubtle } from '@/components/ui/key-value'; +import { useAppParams } from '@/hooks/useAppParams'; +import { + useEventQueryFilters, + useEventQueryNamesFilter, +} from '@/hooks/useEventQueryFilters'; +import { cn } from '@/utils/cn'; +import { getProfileName } from '@/utils/getters'; +import { round } from '@/utils/math'; +import Link from 'next/link'; +import { uniq } from 'ramda'; + +import type { IServiceCreateEventPayload } from '@mixan/db'; + +import { EventIcon } from './event-icon'; + +type EventListItemProps = IServiceCreateEventPayload; + +export function EventListItem(props: EventListItemProps) { + const { + profile, + createdAt, + name, + properties, + path, + duration, + referrer, + referrerName, + referrerType, + brand, + model, + browser, + browserVersion, + os, + osVersion, + city, + region, + country, + continent, + device, + projectId, + meta, + } = props; + const params = useAppParams(); + const [, setEvents] = useEventQueryNamesFilter({ shallow: false }); + const [, setFilter] = useEventQueryFilters({ shallow: false }); + const keyValueList = [ + { + name: 'Duration', + value: duration ? round(duration / 1000, 1) : undefined, + }, + { + name: 'Referrer', + value: referrer, + onClick() { + setFilter('referrer', referrer ?? ''); + }, + }, + { + name: 'Referrer name', + value: referrerName, + onClick() { + setFilter('referrer_name', referrerName ?? ''); + }, + }, + { + name: 'Referrer type', + value: referrerType, + onClick() { + setFilter('referrer_type', referrerType ?? ''); + }, + }, + { + name: 'Brand', + value: brand, + onClick() { + setFilter('brand', brand ?? ''); + }, + }, + { + name: 'Model', + value: model, + onClick() { + setFilter('model', model ?? ''); + }, + }, + { + name: 'Browser', + value: browser, + onClick() { + setFilter('browser', browser ?? ''); + }, + }, + { + name: 'Browser version', + value: browserVersion, + onClick() { + setFilter('browser_version', browserVersion ?? ''); + }, + }, + { + name: 'OS', + value: os, + onClick() { + setFilter('os', os ?? ''); + }, + }, + { + name: 'OS version', + value: osVersion, + onClick() { + setFilter('os_version', osVersion ?? ''); + }, + }, + { + name: 'City', + value: city, + onClick() { + setFilter('city', city ?? ''); + }, + }, + { + name: 'Region', + value: region, + onClick() { + setFilter('region', region ?? ''); + }, + }, + { + name: 'Country', + value: country, + onClick() { + setFilter('country', country ?? ''); + }, + }, + { + name: 'Continent', + value: continent, + onClick() { + setFilter('continent', continent ?? ''); + }, + }, + { + name: 'Device', + value: device, + onClick() { + setFilter('device', device ?? ''); + }, + }, + ].filter((item) => typeof item.value === 'string' && item.value); + + const propertiesList = Object.entries(properties) + .map(([name, value]) => ({ + name, + value: value as string | number | undefined, + })) + .filter((item) => typeof item.value === 'string' && item.value); + + return ( +
+ +
+ {!!profile && ( +
+ + + {getProfileName(profile)} + +
+ )} +
+ + {meta?.conversion && '⭐️ '} + {name} + + {' at '} + {path} + {' from '} + + {city || 'Unknown'}, {country} + + {' using '} + + {brand || device} + +
+
+
+ ); + // return ( + // setEvents((p) => uniq([...p, name]))}> + // {name.split('_').join(' ')} + // + // } + // content={ + // <> + // + // {profile?.id === props.deviceId && ( + // + // )} + // {profile && ( + // + // )} + // {path && ( + // { + // setFilter('path', path); + // }} + // /> + // )} + // + // } + // image={} + // > + //
+ // {propertiesList.length > 0 && ( + //
+ //
Your properties
+ //
+ // {propertiesList.map((item) => ( + // { + // setFilter( + // `properties.${item.name}`, + // item.value ? String(item.value) : '', + // 'is' + // ); + // }} + // /> + // ))} + //
+ //
+ // )} + //
+ //
Properties
+ //
+ // {keyValueList.map((item) => ( + // item.onClick?.()} + // key={item.name} + // name={item.name} + // value={item.value} + // /> + // ))} + //
+ //
+ //
+ //
+ // ); +} diff --git a/apps/web/src/app/(app)/[organizationId]/[projectId]/events/event-list-item.tsx b/apps/web/src/app/(app)/[organizationId]/[projectId]/events/event-list-item.tsx index 6ee378f6..17964aac 100644 --- a/apps/web/src/app/(app)/[organizationId]/[projectId]/events/event-list-item.tsx +++ b/apps/web/src/app/(app)/[organizationId]/[projectId]/events/event-list-item.tsx @@ -1,6 +1,8 @@ 'use client'; import { ExpandableListItem } from '@/components/general/ExpandableListItem'; +import { ProfileAvatar } from '@/components/profiles/ProfileAvatar'; +import { SerieIcon } from '@/components/report/chart/SerieIcon'; import { KeyValue, KeyValueSubtle } from '@/components/ui/key-value'; import { useAppParams } from '@/hooks/useAppParams'; import { @@ -10,6 +12,7 @@ import { import { cn } from '@/utils/cn'; import { getProfileName } from '@/utils/getters'; import { round } from '@/utils/math'; +import Link from 'next/link'; import { uniq } from 'ramda'; import type { IServiceCreateEventPayload } from '@mixan/db'; @@ -159,75 +162,164 @@ export function EventListItem(props: EventListItemProps) { .filter((item) => typeof item.value === 'string' && item.value); return ( - setEvents((p) => uniq([...p, name]))}> - {name.split('_').join(' ')} - - } - content={ - <> - - {profile?.id === props.deviceId && ( - - )} - {profile && ( - - )} - {path && ( - { - setFilter('path', path); - }} - /> - )} - - } - image={} - > -
- {propertiesList.length > 0 && ( -
-
Your properties
-
- {propertiesList.map((item) => ( - { - setFilter( - `properties.${item.name}`, - item.value ? String(item.value) : '', - 'is' - ); - }} - /> - ))} -
-
- )} -
-
Properties
-
- {keyValueList.map((item) => ( - item.onClick?.()} - key={item.name} - name={item.name} - value={item.value} - /> - ))} -
+
+
+
+ +
{name.replace(/_/g, ' ')}
+
+
+ {createdAt.toLocaleTimeString()}
- +
+ {path && } + {profile && ( + + {profile.avatar && } + {getProfileName(profile)} + + } + href={`/${params.organizationId}/${params.projectId}/profiles/${profile.id}`} + /> + )} + + + {city} + + } + /> + + + {brand} + + } + /> + {browser !== 'WebKit' && browser !== '' && ( + + + {browser} + + } + /> + )} + {/* {!!profile && ( +
+ + + {getProfileName(profile)} + +
+ )} +
+ + {meta?.conversion && '⭐️ '} + {name} + + {' at '} + {path} + {' from '} + + {city || 'Unknown'}, {country} + + {' using '} + + {brand || device} + +
*/} +
+
); + // return ( + // setEvents((p) => uniq([...p, name]))}> + // {name.split('_').join(' ')} + // + // } + // content={ + // <> + // + // {profile?.id === props.deviceId && ( + // + // )} + // {profile && ( + // + // )} + // {path && ( + // { + // setFilter('path', path); + // }} + // /> + // )} + // + // } + // image={} + // > + //
+ // {propertiesList.length > 0 && ( + //
+ //
Your properties
+ //
+ // {propertiesList.map((item) => ( + // { + // setFilter( + // `properties.${item.name}`, + // item.value ? String(item.value) : '', + // 'is' + // ); + // }} + // /> + // ))} + //
+ //
+ // )} + //
+ //
Properties
+ //
+ // {keyValueList.map((item) => ( + // item.onClick?.()} + // key={item.name} + // name={item.name} + // value={item.value} + // /> + // ))} + //
+ //
+ //
+ //
+ // ); } diff --git a/apps/web/src/app/(app)/[organizationId]/[projectId]/events/event-list.tsx b/apps/web/src/app/(app)/[organizationId]/[projectId]/events/event-list.tsx index 16d8ad51..1f01a0cf 100644 --- a/apps/web/src/app/(app)/[organizationId]/[projectId]/events/event-list.tsx +++ b/apps/web/src/app/(app)/[organizationId]/[projectId]/events/event-list.tsx @@ -60,14 +60,14 @@ export function EventList({ data, count }: EventListProps) { count={count} take={50} /> -
+
{data.map((item, index, list) => ( {showDateHeader( item.createdAt, list[index - 1]?.createdAt ) && ( -
+
{item.createdAt.toLocaleDateString()}
)} diff --git a/apps/web/src/app/(app)/[organizationId]/[projectId]/layout-project-selector.tsx b/apps/web/src/app/(app)/[organizationId]/[projectId]/layout-project-selector.tsx index 52a86986..8232925a 100644 --- a/apps/web/src/app/(app)/[organizationId]/[projectId]/layout-project-selector.tsx +++ b/apps/web/src/app/(app)/[organizationId]/[projectId]/layout-project-selector.tsx @@ -19,6 +19,7 @@ export default function LayoutProjectSelector({ return (
span]:rounded-xl', + default: 'h-12 w-12 rounded-full [&>span]:rounded-full', sm: 'h-6 w-6 rounded [&>span]:rounded', xs: 'h-4 w-4 rounded [&>span]:rounded', }, diff --git a/apps/web/src/components/report/chart/SerieIcon.tsx b/apps/web/src/components/report/chart/SerieIcon.tsx index a79160d3..68803599 100644 --- a/apps/web/src/components/report/chart/SerieIcon.tsx +++ b/apps/web/src/components/report/chart/SerieIcon.tsx @@ -32,7 +32,9 @@ const createImageIcon = (url: string) => { const createFlagIcon = (url: string) => { return function (props: LucideProps) { - return ; + return ( + + ); } as LucideIcon; }; diff --git a/apps/web/src/components/ui/combobox.tsx b/apps/web/src/components/ui/combobox.tsx index 4db8263a..1e88e49a 100644 --- a/apps/web/src/components/ui/combobox.tsx +++ b/apps/web/src/components/ui/combobox.tsx @@ -36,6 +36,7 @@ export interface ComboboxProps { size?: ButtonProps['size']; label?: string; align?: 'start' | 'end' | 'center'; + portal?: boolean; } export type ExtendedComboboxProps = Omit< @@ -57,6 +58,7 @@ export function Combobox({ icon: Icon, size, align = 'start', + portal, }: ComboboxProps) { const [open, setOpen] = React.useState(false); const [search, setSearch] = React.useState(''); @@ -87,7 +89,11 @@ export function Combobox({ )} - + {searchable === true && ( {name}
diff --git a/apps/web/src/components/ui/popover.tsx b/apps/web/src/components/ui/popover.tsx index 966a3175..68538cf5 100644 --- a/apps/web/src/components/ui/popover.tsx +++ b/apps/web/src/components/ui/popover.tsx @@ -8,22 +8,32 @@ const PopoverTrigger = PopoverPrimitive.Trigger; const PopoverContent = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, align = 'center', sideOffset = 4, ...props }, ref) => ( - -)); + React.ComponentPropsWithoutRef & { + portal?: boolean; + } +>(({ className, align = 'center', sideOffset = 4, portal, ...props }, ref) => { + const node = ( + + ); + + if (portal) { + return {node}; + } + + return node; +}); PopoverContent.displayName = PopoverPrimitive.Content.displayName; export { Popover, PopoverTrigger, PopoverContent }; diff --git a/apps/web/tailwind.config.js b/apps/web/tailwind.config.js index 655b7fcd..f4ceb134 100644 --- a/apps/web/tailwind.config.js +++ b/apps/web/tailwind.config.js @@ -38,7 +38,7 @@ const twColors = [ 'grey', 'slate', ]; -const twColorVariants = ['50', '100', '200', '700']; +const twColorVariants = ['50', '100', '200', '700', '800', '900']; /** @type {import('tailwindcss').Config} */ const config = {