wip improve event list
This commit is contained in:
@@ -21,11 +21,11 @@ import { toast } from 'sonner';
|
|||||||
|
|
||||||
import type { EventMeta } from '@mixan/db';
|
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: {
|
variants: {
|
||||||
size: {
|
size: {
|
||||||
sm: 'w-6 h-6 rounded',
|
sm: 'w-6 h-6',
|
||||||
default: 'w-12 h-12 rounded-xl',
|
default: 'w-10 h-10',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<div className="p-4 flex gap-4">
|
||||||
|
<EventIcon name={name} meta={meta} projectId={projectId} />
|
||||||
|
<div>
|
||||||
|
{!!profile && (
|
||||||
|
<div className="flex gap-2 items-center mb-1">
|
||||||
|
<ProfileAvatar size="xs" {...profile} />
|
||||||
|
<Link
|
||||||
|
className="font-medium text-sm text-muted-foreground"
|
||||||
|
href={`/${params.organizationId}/${params.projectId}/profiles/${profile.id}`}
|
||||||
|
>
|
||||||
|
{getProfileName(profile)}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="[&>span]:inline text-slate-700">
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'font-medium bg-muted p-0.5 px-1 leading-none rounded-md',
|
||||||
|
meta?.conversion && `bg-${meta.color}-100 text-${meta.color}-800`
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{meta?.conversion && '⭐️ '}
|
||||||
|
{name}
|
||||||
|
</span>
|
||||||
|
<span className="text-muted-foreground">{' at '}</span>
|
||||||
|
<span>{path}</span>
|
||||||
|
<span className="text-muted-foreground">{' from '}</span>
|
||||||
|
<span>
|
||||||
|
{city || 'Unknown'}, {country}
|
||||||
|
</span>
|
||||||
|
<span className="text-muted-foreground">{' using '}</span>
|
||||||
|
<span className="!inline-flex items-center gap-1">
|
||||||
|
{brand || device} <SerieIcon name={device} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
// return (
|
||||||
|
// <ExpandableListItem
|
||||||
|
// className={cn(meta?.conversion && `bg-${meta.color}-50`)}
|
||||||
|
// title={
|
||||||
|
// <button onClick={() => setEvents((p) => uniq([...p, name]))}>
|
||||||
|
// {name.split('_').join(' ')}
|
||||||
|
// </button>
|
||||||
|
// }
|
||||||
|
// content={
|
||||||
|
// <>
|
||||||
|
// <KeyValueSubtle name="Time" value={createdAt.toLocaleString()} />
|
||||||
|
// {profile?.id === props.deviceId && (
|
||||||
|
// <KeyValueSubtle name="Anonymous" value={'Yes'} />
|
||||||
|
// )}
|
||||||
|
// {profile && (
|
||||||
|
// <KeyValueSubtle
|
||||||
|
// name="Profile"
|
||||||
|
// value={getProfileName(profile)}
|
||||||
|
// href={`/${params.organizationId}/${params.projectId}/profiles/${profile.id}`}
|
||||||
|
// />
|
||||||
|
// )}
|
||||||
|
// {path && (
|
||||||
|
// <KeyValueSubtle
|
||||||
|
// name="Path"
|
||||||
|
// value={path}
|
||||||
|
// onClick={() => {
|
||||||
|
// setFilter('path', path);
|
||||||
|
// }}
|
||||||
|
// />
|
||||||
|
// )}
|
||||||
|
// </>
|
||||||
|
// }
|
||||||
|
// image={<EventIcon name={name} meta={meta} projectId={projectId} />}
|
||||||
|
// >
|
||||||
|
// <div className="bg-white p-4">
|
||||||
|
// {propertiesList.length > 0 && (
|
||||||
|
// <div className="flex flex-col gap-4 mb-6">
|
||||||
|
// <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}
|
||||||
|
// onClick={() => {
|
||||||
|
// setFilter(
|
||||||
|
// `properties.${item.name}`,
|
||||||
|
// item.value ? String(item.value) : '',
|
||||||
|
// 'is'
|
||||||
|
// );
|
||||||
|
// }}
|
||||||
|
// />
|
||||||
|
// ))}
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// )}
|
||||||
|
// <div className="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>
|
||||||
|
// </div>
|
||||||
|
// </ExpandableListItem>
|
||||||
|
// );
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ExpandableListItem } from '@/components/general/ExpandableListItem';
|
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 { KeyValue, KeyValueSubtle } from '@/components/ui/key-value';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import {
|
import {
|
||||||
@@ -10,6 +12,7 @@ import {
|
|||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import { getProfileName } from '@/utils/getters';
|
import { getProfileName } from '@/utils/getters';
|
||||||
import { round } from '@/utils/math';
|
import { round } from '@/utils/math';
|
||||||
|
import Link from 'next/link';
|
||||||
import { uniq } from 'ramda';
|
import { uniq } from 'ramda';
|
||||||
|
|
||||||
import type { IServiceCreateEventPayload } from '@mixan/db';
|
import type { IServiceCreateEventPayload } from '@mixan/db';
|
||||||
@@ -159,75 +162,164 @@ export function EventListItem(props: EventListItemProps) {
|
|||||||
.filter((item) => typeof item.value === 'string' && item.value);
|
.filter((item) => typeof item.value === 'string' && item.value);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ExpandableListItem
|
<div className="p-4 flex flex-col gap-2 hover:bg-slate-50 rounded-lg transition-colors">
|
||||||
className={cn(meta?.conversion && `bg-${meta.color}-50`)}
|
<div className="flex justify-between items-center">
|
||||||
title={
|
<div className="flex gap-4 items-center">
|
||||||
<button onClick={() => setEvents((p) => uniq([...p, name]))}>
|
<EventIcon name={name} meta={meta} projectId={projectId} />
|
||||||
{name.split('_').join(' ')}
|
<div className="font-semibold">{name.replace(/_/g, ' ')}</div>
|
||||||
</button>
|
</div>
|
||||||
}
|
<div className="text-muted-foreground">
|
||||||
content={
|
{createdAt.toLocaleTimeString()}
|
||||||
<>
|
</div>
|
||||||
<KeyValueSubtle name="Time" value={createdAt.toLocaleString()} />
|
</div>
|
||||||
{profile?.id === props.deviceId && (
|
<div className="flex flex-wrap gap-2">
|
||||||
<KeyValueSubtle name="Anonymous" value={'Yes'} />
|
{path && <KeyValueSubtle name={'Path'} value={path} />}
|
||||||
)}
|
|
||||||
{profile && (
|
{profile && (
|
||||||
<KeyValueSubtle
|
<KeyValueSubtle
|
||||||
name="Profile"
|
name={'Profile'}
|
||||||
value={getProfileName(profile)}
|
value={
|
||||||
|
<>
|
||||||
|
{profile.avatar && <ProfileAvatar size="xs" {...profile} />}
|
||||||
|
{getProfileName(profile)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
href={`/${params.organizationId}/${params.projectId}/profiles/${profile.id}`}
|
href={`/${params.organizationId}/${params.projectId}/profiles/${profile.id}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{path && (
|
|
||||||
<KeyValueSubtle
|
<KeyValueSubtle
|
||||||
name="Path"
|
name={'From'}
|
||||||
value={path}
|
value={
|
||||||
onClick={() => {
|
<>
|
||||||
setFilter('path', path);
|
<SerieIcon name={country} />
|
||||||
}}
|
{city}
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
image={<EventIcon name={name} meta={meta} projectId={projectId} />}
|
|
||||||
>
|
|
||||||
<div className="bg-white p-4">
|
|
||||||
{propertiesList.length > 0 && (
|
|
||||||
<div className="flex flex-col gap-4 mb-6">
|
|
||||||
<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}
|
|
||||||
onClick={() => {
|
|
||||||
setFilter(
|
|
||||||
`properties.${item.name}`,
|
|
||||||
item.value ? String(item.value) : '',
|
|
||||||
'is'
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
))}
|
<KeyValueSubtle
|
||||||
</div>
|
name={'Device'}
|
||||||
|
value={
|
||||||
|
<>
|
||||||
|
<SerieIcon name={device} />
|
||||||
|
{brand}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{browser !== 'WebKit' && browser !== '' && (
|
||||||
|
<KeyValueSubtle
|
||||||
|
name={'Browser'}
|
||||||
|
value={
|
||||||
|
<>
|
||||||
|
<SerieIcon name={browser} />
|
||||||
|
{browser}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{/* {!!profile && (
|
||||||
|
<div className="flex gap-2 items-center mb-1">
|
||||||
|
<ProfileAvatar size="xs" {...profile} />
|
||||||
|
<Link
|
||||||
|
className="font-medium text-sm text-muted-foreground"
|
||||||
|
href={`/${params.organizationId}/${params.projectId}/profiles/${profile.id}`}
|
||||||
|
>
|
||||||
|
{getProfileName(profile)}
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col gap-4">
|
<div className="[&>span]:inline text-slate-700">
|
||||||
<div className="font-medium">Properties</div>
|
<span
|
||||||
<div className="flex flex-wrap gap-x-4 gap-y-2">
|
className={cn(
|
||||||
{keyValueList.map((item) => (
|
'font-medium bg-muted p-0.5 px-1 leading-none rounded-md',
|
||||||
<KeyValue
|
meta?.conversion && `bg-${meta.color}-100 text-${meta.color}-800`
|
||||||
onClick={() => item.onClick?.()}
|
)}
|
||||||
key={item.name}
|
>
|
||||||
name={item.name}
|
{meta?.conversion && '⭐️ '}
|
||||||
value={item.value}
|
{name}
|
||||||
/>
|
</span>
|
||||||
))}
|
<span className="text-muted-foreground">{' at '}</span>
|
||||||
|
<span>{path}</span>
|
||||||
|
<span className="text-muted-foreground">{' from '}</span>
|
||||||
|
<span>
|
||||||
|
{city || 'Unknown'}, {country}
|
||||||
|
</span>
|
||||||
|
<span className="text-muted-foreground">{' using '}</span>
|
||||||
|
<span className="!inline-flex items-center gap-1">
|
||||||
|
{brand || device} <SerieIcon name={device} />
|
||||||
|
</span>
|
||||||
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</ExpandableListItem>
|
|
||||||
);
|
);
|
||||||
|
// return (
|
||||||
|
// <ExpandableListItem
|
||||||
|
// className={cn(meta?.conversion && `bg-${meta.color}-50`)}
|
||||||
|
// title={
|
||||||
|
// <button onClick={() => setEvents((p) => uniq([...p, name]))}>
|
||||||
|
// {name.split('_').join(' ')}
|
||||||
|
// </button>
|
||||||
|
// }
|
||||||
|
// content={
|
||||||
|
// <>
|
||||||
|
// <KeyValueSubtle name="Time" value={createdAt.toLocaleString()} />
|
||||||
|
// {profile?.id === props.deviceId && (
|
||||||
|
// <KeyValueSubtle name="Anonymous" value={'Yes'} />
|
||||||
|
// )}
|
||||||
|
// {profile && (
|
||||||
|
// <KeyValueSubtle
|
||||||
|
// name="Profile"
|
||||||
|
// value={getProfileName(profile)}
|
||||||
|
// href={`/${params.organizationId}/${params.projectId}/profiles/${profile.id}`}
|
||||||
|
// />
|
||||||
|
// )}
|
||||||
|
// {path && (
|
||||||
|
// <KeyValueSubtle
|
||||||
|
// name="Path"
|
||||||
|
// value={path}
|
||||||
|
// onClick={() => {
|
||||||
|
// setFilter('path', path);
|
||||||
|
// }}
|
||||||
|
// />
|
||||||
|
// )}
|
||||||
|
// </>
|
||||||
|
// }
|
||||||
|
// image={<EventIcon name={name} meta={meta} projectId={projectId} />}
|
||||||
|
// >
|
||||||
|
// <div className="bg-white p-4">
|
||||||
|
// {propertiesList.length > 0 && (
|
||||||
|
// <div className="flex flex-col gap-4 mb-6">
|
||||||
|
// <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}
|
||||||
|
// onClick={() => {
|
||||||
|
// setFilter(
|
||||||
|
// `properties.${item.name}`,
|
||||||
|
// item.value ? String(item.value) : '',
|
||||||
|
// 'is'
|
||||||
|
// );
|
||||||
|
// }}
|
||||||
|
// />
|
||||||
|
// ))}
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// )}
|
||||||
|
// <div className="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>
|
||||||
|
// </div>
|
||||||
|
// </ExpandableListItem>
|
||||||
|
// );
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,14 +60,14 @@ export function EventList({ data, count }: EventListProps) {
|
|||||||
count={count}
|
count={count}
|
||||||
take={50}
|
take={50}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col gap-4 my-4">
|
<div className="flex flex-col my-4 card p-4">
|
||||||
{data.map((item, index, list) => (
|
{data.map((item, index, list) => (
|
||||||
<Fragment key={item.id}>
|
<Fragment key={item.id}>
|
||||||
{showDateHeader(
|
{showDateHeader(
|
||||||
item.createdAt,
|
item.createdAt,
|
||||||
list[index - 1]?.createdAt
|
list[index - 1]?.createdAt
|
||||||
) && (
|
) && (
|
||||||
<div className="font-medium text-xs [&:not(:first-child)]:mt-12">
|
<div className="text-muted-foreground font-medium text-sm [&:not(:first-child)]:mt-12 text-center">
|
||||||
{item.createdAt.toLocaleDateString()}
|
{item.createdAt.toLocaleDateString()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export default function LayoutProjectSelector({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Combobox
|
<Combobox
|
||||||
|
portal
|
||||||
align="end"
|
align="end"
|
||||||
className="w-auto min-w-0 max-sm:max-w-[100px]"
|
className="w-auto min-w-0 max-sm:max-w-[100px]"
|
||||||
placeholder={'Select project'}
|
placeholder={'Select project'}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ interface ProfileAvatarProps
|
|||||||
const variants = cva('', {
|
const variants = cva('', {
|
||||||
variants: {
|
variants: {
|
||||||
size: {
|
size: {
|
||||||
default: 'h-12 w-12 rounded-xl [&>span]:rounded-xl',
|
default: 'h-12 w-12 rounded-full [&>span]:rounded-full',
|
||||||
sm: 'h-6 w-6 rounded [&>span]:rounded',
|
sm: 'h-6 w-6 rounded [&>span]:rounded',
|
||||||
xs: 'h-4 w-4 rounded [&>span]:rounded',
|
xs: 'h-4 w-4 rounded [&>span]:rounded',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ const createImageIcon = (url: string) => {
|
|||||||
|
|
||||||
const createFlagIcon = (url: string) => {
|
const createFlagIcon = (url: string) => {
|
||||||
return function (props: LucideProps) {
|
return function (props: LucideProps) {
|
||||||
return <span className={`rounded fi fi-${url}`}></span>;
|
return (
|
||||||
|
<span className={`rounded !block !leading-[1rem] fi fi-${url}`}></span>
|
||||||
|
);
|
||||||
} as LucideIcon;
|
} as LucideIcon;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export interface ComboboxProps<T> {
|
|||||||
size?: ButtonProps['size'];
|
size?: ButtonProps['size'];
|
||||||
label?: string;
|
label?: string;
|
||||||
align?: 'start' | 'end' | 'center';
|
align?: 'start' | 'end' | 'center';
|
||||||
|
portal?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ExtendedComboboxProps<T> = Omit<
|
export type ExtendedComboboxProps<T> = Omit<
|
||||||
@@ -57,6 +58,7 @@ export function Combobox<T extends string>({
|
|||||||
icon: Icon,
|
icon: Icon,
|
||||||
size,
|
size,
|
||||||
align = 'start',
|
align = 'start',
|
||||||
|
portal,
|
||||||
}: ComboboxProps<T>) {
|
}: ComboboxProps<T>) {
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
const [search, setSearch] = React.useState('');
|
const [search, setSearch] = React.useState('');
|
||||||
@@ -87,7 +89,11 @@ export function Combobox<T extends string>({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-full max-w-md p-0" align={align}>
|
<PopoverContent
|
||||||
|
className="w-full max-w-md p-0"
|
||||||
|
align={align}
|
||||||
|
portal={portal}
|
||||||
|
>
|
||||||
<Command>
|
<Command>
|
||||||
{searchable === true && (
|
{searchable === true && (
|
||||||
<CommandInput
|
<CommandInput
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export function KeyValueSubtle({ href, onClick, name, value }: KeyValueProps) {
|
|||||||
<div className="text-gray-400">{name}</div>
|
<div className="text-gray-400">{name}</div>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-black/5 rounded p-0.5 px-1 sm:p-1 sm:px-2 text-gray-600 whitespace-nowrap overflow-hidden text-ellipsis',
|
'bg-black/5 rounded p-0.5 px-1 sm:p-1 sm:px-2 text-gray-600 whitespace-nowrap text-ellipsis flex items-center gap-1',
|
||||||
clickable && 'group-hover:underline'
|
clickable && 'group-hover:underline'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -8,8 +8,11 @@ const PopoverTrigger = PopoverPrimitive.Trigger;
|
|||||||
|
|
||||||
const PopoverContent = React.forwardRef<
|
const PopoverContent = React.forwardRef<
|
||||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & {
|
||||||
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
|
portal?: boolean;
|
||||||
|
}
|
||||||
|
>(({ className, align = 'center', sideOffset = 4, portal, ...props }, ref) => {
|
||||||
|
const node = (
|
||||||
<PopoverPrimitive.Content
|
<PopoverPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
align={align}
|
align={align}
|
||||||
@@ -23,7 +26,14 @@ const PopoverContent = React.forwardRef<
|
|||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
);
|
||||||
|
|
||||||
|
if (portal) {
|
||||||
|
return <PopoverPrimitive.Portal>{node}</PopoverPrimitive.Portal>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return node;
|
||||||
|
});
|
||||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||||
|
|
||||||
export { Popover, PopoverTrigger, PopoverContent };
|
export { Popover, PopoverTrigger, PopoverContent };
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const twColors = [
|
|||||||
'grey',
|
'grey',
|
||||||
'slate',
|
'slate',
|
||||||
];
|
];
|
||||||
const twColorVariants = ['50', '100', '200', '700'];
|
const twColorVariants = ['50', '100', '200', '700', '800', '900'];
|
||||||
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
|
|||||||
Reference in New Issue
Block a user