dashboard: update event and profile list
This commit is contained in:
@@ -1,5 +1,12 @@
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
ChevronsLeftIcon,
|
||||
ChevronsRightIcon,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { Button } from './ui/button';
|
||||
|
||||
@@ -20,38 +27,72 @@ export function Pagination({
|
||||
count,
|
||||
cursor,
|
||||
setCursor,
|
||||
className,
|
||||
size = 'base',
|
||||
}: {
|
||||
take?: number;
|
||||
count?: number;
|
||||
take: number;
|
||||
count: number;
|
||||
cursor: number;
|
||||
setCursor: Dispatch<SetStateAction<number>>;
|
||||
className?: string;
|
||||
size?: 'sm' | 'base';
|
||||
}) {
|
||||
const isNextDisabled =
|
||||
count !== undefined && take !== undefined && cursor * take + take >= count;
|
||||
|
||||
const lastCursor = Math.floor(count / take) - 1;
|
||||
const isNextDisabled = count === 0 || lastCursor === cursor;
|
||||
return (
|
||||
<div className="flex select-none items-center justify-end gap-2">
|
||||
<div className="font-medium text-xs">Page: {cursor + 1}</div>
|
||||
{typeof count === 'number' && (
|
||||
<div className="font-medium text-xs">Total rows: {count}</div>
|
||||
<div
|
||||
className={cn(
|
||||
'flex select-none items-center justify-end gap-2',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{size === 'base' && (
|
||||
<>
|
||||
<div className="font-medium text-xs">Page: {cursor + 1}</div>
|
||||
{typeof count === 'number' && (
|
||||
<div className="font-medium text-xs">Total rows: {count}</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{size === 'base' && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setCursor(0)}
|
||||
disabled={cursor === 0}
|
||||
className="max-sm:hidden"
|
||||
>
|
||||
<ChevronsLeftIcon size={14} />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
size="icon"
|
||||
onClick={() => setCursor((p) => Math.max(0, p - 1))}
|
||||
disabled={cursor === 0}
|
||||
>
|
||||
Previous
|
||||
<ChevronLeftIcon size={14} />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCursor((p) => p + 1)}
|
||||
size="icon"
|
||||
onClick={() => setCursor((p) => Math.min(lastCursor, p + 1))}
|
||||
disabled={isNextDisabled}
|
||||
>
|
||||
Next
|
||||
<ChevronRightIcon size={14} />
|
||||
</Button>
|
||||
{size === 'base' && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setCursor(lastCursor)}
|
||||
disabled={isNextDisabled}
|
||||
className="max-sm:hidden"
|
||||
>
|
||||
<ChevronsRightIcon size={14} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { toDots } from '@openpanel/common';
|
||||
|
||||
import { Table, TableBody, TableCell, TableRow } from '../ui/table';
|
||||
|
||||
interface ListPropertiesProps {
|
||||
data: any;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ListProperties({
|
||||
data,
|
||||
className = 'mini',
|
||||
}: ListPropertiesProps) {
|
||||
const dots = toDots(data);
|
||||
return (
|
||||
<Table className={className}>
|
||||
<TableBody>
|
||||
{Object.keys(dots).map((key) => {
|
||||
return (
|
||||
<TableRow key={key}>
|
||||
<TableCell className="font-medium">{key}</TableCell>
|
||||
<TableCell>
|
||||
{typeof dots[key] === 'boolean'
|
||||
? dots[key]
|
||||
? 'true'
|
||||
: 'false'
|
||||
: dots[key]}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
69
apps/dashboard/src/components/events/ListPropertiesIcon.tsx
Normal file
69
apps/dashboard/src/components/events/ListPropertiesIcon.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import { SerieIcon } from '../report/chart/SerieIcon';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
|
||||
|
||||
interface Props {
|
||||
country?: string;
|
||||
city?: string;
|
||||
os?: string;
|
||||
os_version?: string;
|
||||
browser?: string;
|
||||
browser_version?: string;
|
||||
referrer_name?: string;
|
||||
referrer_type?: string;
|
||||
}
|
||||
|
||||
export function ListPropertiesIcon({
|
||||
country,
|
||||
city,
|
||||
os,
|
||||
os_version,
|
||||
browser,
|
||||
browser_version,
|
||||
referrer_name,
|
||||
referrer_type,
|
||||
}: Props) {
|
||||
return (
|
||||
<div className="flex gap-1">
|
||||
{country && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<SerieIcon name={country} />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{country}, {city}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{os && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<SerieIcon name={os} />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{os} ({os_version})
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{browser && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<SerieIcon name={browser} />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{browser} ({browser_version})
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{referrer_name && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<SerieIcon name={referrer_name} />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{referrer_name} ({referrer_type})
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { BarChartIcon, LineChartIcon } from 'lucide-react';
|
||||
|
||||
import { Button } from '../ui/button';
|
||||
import { useOverviewOptions } from './useOverviewOptions';
|
||||
|
||||
export function OverviewChartToggle() {
|
||||
const { chartType, setChartType } = useOverviewOptions();
|
||||
return (
|
||||
<Button
|
||||
size={'icon'}
|
||||
variant={'outline'}
|
||||
onClick={() => {
|
||||
setChartType((p) => (p === 'linear' ? 'bar' : 'linear'));
|
||||
}}
|
||||
>
|
||||
{chartType === 'bar' ? (
|
||||
<LineChartIcon size={16} />
|
||||
) : (
|
||||
<BarChartIcon size={16} />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { Widget, WidgetBody } from '../Widget';
|
||||
import { OverviewChartToggle } from './overview-chart-toggle';
|
||||
import { WidgetButtons, WidgetHead } from './overview-widget';
|
||||
import { useOverviewOptions } from './useOverviewOptions';
|
||||
import { useOverviewWidget } from './useOverviewWidget';
|
||||
@@ -15,7 +16,7 @@ interface OverviewTopDevicesProps {
|
||||
export default function OverviewTopDevices({
|
||||
projectId,
|
||||
}: OverviewTopDevicesProps) {
|
||||
const { interval, range, previous, startDate, endDate } =
|
||||
const { interval, range, previous, startDate, endDate, chartType } =
|
||||
useOverviewOptions();
|
||||
const [filters, setFilter] = useEventQueryFilters();
|
||||
const isPageFilter = filters.find((filter) => filter.name === 'path');
|
||||
@@ -41,7 +42,7 @@ export default function OverviewTopDevices({
|
||||
name: 'device',
|
||||
},
|
||||
],
|
||||
chartType: 'bar',
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
@@ -71,7 +72,7 @@ export default function OverviewTopDevices({
|
||||
name: 'browser',
|
||||
},
|
||||
],
|
||||
chartType: 'bar',
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
@@ -101,7 +102,7 @@ export default function OverviewTopDevices({
|
||||
name: 'browser_version',
|
||||
},
|
||||
],
|
||||
chartType: 'bar',
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
@@ -131,7 +132,7 @@ export default function OverviewTopDevices({
|
||||
name: 'os',
|
||||
},
|
||||
],
|
||||
chartType: 'bar',
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
@@ -161,7 +162,7 @@ export default function OverviewTopDevices({
|
||||
name: 'os_version',
|
||||
},
|
||||
],
|
||||
chartType: 'bar',
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
@@ -176,7 +177,10 @@ export default function OverviewTopDevices({
|
||||
<>
|
||||
<Widget className="col-span-6 md:col-span-3">
|
||||
<WidgetHead>
|
||||
<div className="title">{widget.title}</div>
|
||||
<div className="title">
|
||||
{widget.title}
|
||||
<OverviewChartToggle />
|
||||
</div>
|
||||
<WidgetButtons>
|
||||
{widgets.map((w) => (
|
||||
<button
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { ChartSwitch } from '@/components/report/chart';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { BarChartIcon, LineChart, LineChartIcon } from 'lucide-react';
|
||||
|
||||
import { Widget, WidgetBody } from '../../Widget';
|
||||
import { OverviewChartToggle } from '../overview-chart-toggle';
|
||||
import { WidgetButtons, WidgetHead } from '../overview-widget';
|
||||
import { useOverviewOptions } from '../useOverviewOptions';
|
||||
import { useOverviewWidget } from '../useOverviewWidget';
|
||||
@@ -17,8 +20,15 @@ export default function OverviewTopEvents({
|
||||
projectId,
|
||||
conversions,
|
||||
}: OverviewTopEventsProps) {
|
||||
const { interval, range, previous, startDate, endDate } =
|
||||
useOverviewOptions();
|
||||
const {
|
||||
interval,
|
||||
range,
|
||||
previous,
|
||||
startDate,
|
||||
endDate,
|
||||
chartType,
|
||||
setChartType,
|
||||
} = useOverviewOptions();
|
||||
const [filters] = useEventQueryFilters();
|
||||
const [widget, setWidget, widgets] = useOverviewWidget('ev', {
|
||||
all: {
|
||||
@@ -50,7 +60,7 @@ export default function OverviewTopEvents({
|
||||
name: 'name',
|
||||
},
|
||||
],
|
||||
chartType: 'bar',
|
||||
chartType: chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
@@ -89,7 +99,7 @@ export default function OverviewTopEvents({
|
||||
name: 'name',
|
||||
},
|
||||
],
|
||||
chartType: 'bar',
|
||||
chartType: chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
@@ -104,7 +114,10 @@ export default function OverviewTopEvents({
|
||||
<>
|
||||
<Widget className="col-span-6 md:col-span-3">
|
||||
<WidgetHead>
|
||||
<div className="title">{widget.title}</div>
|
||||
<div className="title">
|
||||
{widget.title}
|
||||
<OverviewChartToggle />
|
||||
</div>
|
||||
<WidgetButtons>
|
||||
{widgets
|
||||
.filter((item) => item.hide !== true)
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { Widget, WidgetBody } from '../Widget';
|
||||
import { OverviewChartToggle } from './overview-chart-toggle';
|
||||
import { WidgetButtons, WidgetHead } from './overview-widget';
|
||||
import { useOverviewOptions } from './useOverviewOptions';
|
||||
import { useOverviewWidget } from './useOverviewWidget';
|
||||
@@ -13,7 +14,7 @@ interface OverviewTopGeoProps {
|
||||
projectId: string;
|
||||
}
|
||||
export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
||||
const { interval, range, previous, startDate, endDate } =
|
||||
const { interval, range, previous, startDate, endDate, chartType } =
|
||||
useOverviewOptions();
|
||||
const [filters, setFilter] = useEventQueryFilters();
|
||||
const isPageFilter = filters.find((filter) => filter.name === 'path');
|
||||
@@ -39,7 +40,7 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
||||
name: 'country',
|
||||
},
|
||||
],
|
||||
chartType: 'bar',
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
@@ -69,7 +70,7 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
||||
name: 'region',
|
||||
},
|
||||
],
|
||||
chartType: 'bar',
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
@@ -99,7 +100,7 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
||||
name: 'city',
|
||||
},
|
||||
],
|
||||
chartType: 'bar',
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
@@ -114,7 +115,10 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
||||
<>
|
||||
<Widget className="col-span-6 md:col-span-3">
|
||||
<WidgetHead>
|
||||
<div className="title">{widget.title}</div>
|
||||
<div className="title">
|
||||
{widget.title}
|
||||
<OverviewChartToggle />
|
||||
</div>
|
||||
<WidgetButtons>
|
||||
{widgets.map((w) => (
|
||||
<button
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { Widget, WidgetBody } from '../Widget';
|
||||
import { OverviewChartToggle } from './overview-chart-toggle';
|
||||
import { WidgetButtons, WidgetHead } from './overview-widget';
|
||||
import { useOverviewOptions } from './useOverviewOptions';
|
||||
import { useOverviewWidget } from './useOverviewWidget';
|
||||
@@ -13,7 +14,7 @@ interface OverviewTopPagesProps {
|
||||
projectId: string;
|
||||
}
|
||||
export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
||||
const { interval, range, previous, startDate, endDate } =
|
||||
const { interval, range, previous, startDate, endDate, chartType } =
|
||||
useOverviewOptions();
|
||||
const [filters, setFilter] = useEventQueryFilters();
|
||||
const [widget, setWidget, widgets] = useOverviewWidget('pages', {
|
||||
@@ -38,7 +39,7 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
||||
name: 'path',
|
||||
},
|
||||
],
|
||||
chartType: 'bar',
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval,
|
||||
name: 'Top sources',
|
||||
@@ -68,7 +69,7 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
||||
name: 'path',
|
||||
},
|
||||
],
|
||||
chartType: 'bar',
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval,
|
||||
name: 'Top sources',
|
||||
@@ -98,7 +99,7 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
||||
name: 'path',
|
||||
},
|
||||
],
|
||||
chartType: 'bar',
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval,
|
||||
name: 'Top sources',
|
||||
@@ -113,7 +114,10 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
||||
<>
|
||||
<Widget className="col-span-6 md:col-span-3">
|
||||
<WidgetHead>
|
||||
<div className="title">{widget.title}</div>
|
||||
<div className="title">
|
||||
{widget.title}
|
||||
<OverviewChartToggle />
|
||||
</div>
|
||||
<WidgetButtons>
|
||||
{widgets.map((w) => (
|
||||
<button
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { Widget, WidgetBody } from '../Widget';
|
||||
import { OverviewChartToggle } from './overview-chart-toggle';
|
||||
import { WidgetButtons, WidgetHead } from './overview-widget';
|
||||
import { useOverviewOptions } from './useOverviewOptions';
|
||||
import { useOverviewWidget } from './useOverviewWidget';
|
||||
@@ -15,7 +16,7 @@ interface OverviewTopSourcesProps {
|
||||
export default function OverviewTopSources({
|
||||
projectId,
|
||||
}: OverviewTopSourcesProps) {
|
||||
const { interval, range, previous, startDate, endDate } =
|
||||
const { interval, range, previous, startDate, endDate, chartType } =
|
||||
useOverviewOptions();
|
||||
const [filters, setFilter] = useEventQueryFilters();
|
||||
const isPageFilter = filters.find((filter) => filter.name === 'path');
|
||||
@@ -41,7 +42,7 @@ export default function OverviewTopSources({
|
||||
name: 'referrer_name',
|
||||
},
|
||||
],
|
||||
chartType: 'bar',
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top groups',
|
||||
@@ -71,7 +72,7 @@ export default function OverviewTopSources({
|
||||
name: 'referrer',
|
||||
},
|
||||
],
|
||||
chartType: 'bar',
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
@@ -101,7 +102,7 @@ export default function OverviewTopSources({
|
||||
name: 'referrer_type',
|
||||
},
|
||||
],
|
||||
chartType: 'bar',
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top types',
|
||||
@@ -131,7 +132,7 @@ export default function OverviewTopSources({
|
||||
name: 'properties.query.utm_source',
|
||||
},
|
||||
],
|
||||
chartType: 'bar',
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
@@ -161,7 +162,7 @@ export default function OverviewTopSources({
|
||||
name: 'properties.query.utm_medium',
|
||||
},
|
||||
],
|
||||
chartType: 'bar',
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
@@ -191,7 +192,7 @@ export default function OverviewTopSources({
|
||||
name: 'properties.query.utm_campaign',
|
||||
},
|
||||
],
|
||||
chartType: 'bar',
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
@@ -221,7 +222,7 @@ export default function OverviewTopSources({
|
||||
name: 'properties.query.utm_term',
|
||||
},
|
||||
],
|
||||
chartType: 'bar',
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
@@ -251,7 +252,7 @@ export default function OverviewTopSources({
|
||||
name: 'properties.query.utm_content',
|
||||
},
|
||||
],
|
||||
chartType: 'bar',
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
@@ -266,7 +267,11 @@ export default function OverviewTopSources({
|
||||
<>
|
||||
<Widget className="col-span-6 md:col-span-3">
|
||||
<WidgetHead>
|
||||
<div className="title">{widget.title}</div>
|
||||
<div className="title">
|
||||
{widget.title}
|
||||
<OverviewChartToggle />
|
||||
</div>
|
||||
|
||||
<WidgetButtons>
|
||||
{widgets.map((w) => (
|
||||
<button
|
||||
|
||||
@@ -19,7 +19,10 @@ import { WidgetHead as WidgetHeadBase } from '../Widget';
|
||||
export function WidgetHead({ className, ...props }: WidgetHeadProps) {
|
||||
return (
|
||||
<WidgetHeadBase
|
||||
className={cn('flex flex-col p-0 [&_.title]:p-4', className)}
|
||||
className={cn(
|
||||
'flex flex-col p-0 [&_.title]:p-4 [&_.title]:flex [&_.title]:justify-between [&_.title]:items-center',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -16,6 +16,12 @@ import { mapKeys } from '@openpanel/validation';
|
||||
const nuqsOptions = { history: 'push' } as const;
|
||||
|
||||
export function useOverviewOptions() {
|
||||
const [chartType, setChartType] = useQueryState(
|
||||
'ct',
|
||||
parseAsStringEnum(['bar', 'linear'])
|
||||
.withDefault('bar')
|
||||
.withOptions(nuqsOptions)
|
||||
);
|
||||
const [previous, setPrevious] = useQueryState(
|
||||
'compare',
|
||||
parseAsBoolean.withDefault(true).withOptions(nuqsOptions)
|
||||
@@ -68,5 +74,9 @@ export function useOverviewOptions() {
|
||||
// Toggles
|
||||
liveHistogram,
|
||||
setLiveHistogram,
|
||||
|
||||
// Other
|
||||
chartType,
|
||||
setChartType,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,14 +11,14 @@ import { Avatar, AvatarFallback } from '../ui/avatar';
|
||||
|
||||
interface ProfileAvatarProps
|
||||
extends VariantProps<typeof variants>,
|
||||
Partial<Pick<IServiceProfile, 'avatar' | 'first_name'>> {
|
||||
Partial<Pick<IServiceProfile, 'avatar' | 'firstName'>> {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const variants = cva('', {
|
||||
variants: {
|
||||
size: {
|
||||
default: 'h-12 w-12 rounded-full [&>span]:rounded-full',
|
||||
default: 'h-8 w-8 rounded [&>span]:rounded',
|
||||
sm: 'h-6 w-6 rounded [&>span]:rounded',
|
||||
xs: 'h-4 w-4 rounded [&>span]:rounded',
|
||||
},
|
||||
@@ -30,7 +30,7 @@ const variants = cva('', {
|
||||
|
||||
export function ProfileAvatar({
|
||||
avatar,
|
||||
first_name,
|
||||
firstName,
|
||||
className,
|
||||
size,
|
||||
}: ProfileAvatarProps) {
|
||||
@@ -47,7 +47,7 @@ export function ProfileAvatar({
|
||||
'bg-slate-200 text-slate-800'
|
||||
)}
|
||||
>
|
||||
{first_name?.at(0) ?? '🫣'}
|
||||
{firstName?.at(0) ?? '🫣'}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
import { NOT_SET_VALUE } from '@openpanel/constants';
|
||||
|
||||
interface SerieIconProps extends LucideProps {
|
||||
name: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
function getProxyImage(url: string) {
|
||||
@@ -26,14 +26,16 @@ function getProxyImage(url: string) {
|
||||
|
||||
const createImageIcon = (url: string) => {
|
||||
return function (props: LucideProps) {
|
||||
return <img className="w-4 h-4 object-cover rounded" src={url} />;
|
||||
return <img className="h-4 object-contain rounded-[2px]" src={url} />;
|
||||
} as LucideIcon;
|
||||
};
|
||||
|
||||
const createFlagIcon = (url: string) => {
|
||||
return function (props: LucideProps) {
|
||||
return (
|
||||
<span className={`rounded !block !leading-[1rem] fi fi-${url}`}></span>
|
||||
<span
|
||||
className={`rounded-[2px] overflow-hidden !block !leading-[1rem] fi fi-${url}`}
|
||||
></span>
|
||||
);
|
||||
} as LucideIcon;
|
||||
};
|
||||
@@ -92,6 +94,20 @@ const mapper: Record<string, LucideIcon> = {
|
||||
),
|
||||
snapchat: createImageIcon(getProxyImage('https://snapchat.com')),
|
||||
|
||||
// OS
|
||||
'mac os': createImageIcon(
|
||||
'https://upload.wikimedia.org/wikipedia/commons/c/c9/Finder_Icon_macOS_Big_Sur.png'
|
||||
),
|
||||
windows: createImageIcon(
|
||||
'https://upload.wikimedia.org/wikipedia/commons/c/c7/Windows_logo_-_2012.png'
|
||||
),
|
||||
ios: createImageIcon(
|
||||
'https://upload.wikimedia.org/wikipedia/commons/9/96/IOS_17_logo.png'
|
||||
),
|
||||
android: createImageIcon(
|
||||
'https://image.similarpng.com/very-thumbnail/2020/08/Android-icon-on-transparent--background-PNG.png'
|
||||
),
|
||||
|
||||
// Misc
|
||||
mobile: SmartphoneIcon,
|
||||
desktop: MonitorIcon,
|
||||
@@ -220,6 +236,10 @@ const mapper: Record<string, LucideIcon> = {
|
||||
|
||||
export function SerieIcon({ name, ...props }: SerieIconProps) {
|
||||
const Icon = useMemo(() => {
|
||||
if (!name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const mapped = mapper[name.toLowerCase()] ?? null;
|
||||
|
||||
if (mapped) {
|
||||
@@ -234,7 +254,7 @@ export function SerieIcon({ name, ...props }: SerieIconProps) {
|
||||
}, [name]);
|
||||
|
||||
return Icon ? (
|
||||
<div className="w-4 h-4 flex-shrink-0 relative [&_a]:!w-4 [&_a]:!h-4 [&_svg]:!rounded">
|
||||
<div className="h-4 flex-shrink-0 relative [&_a]:![&_a]:!h-4 [&_svg]:!rounded-[2px]">
|
||||
<Icon size={16} {...props} />
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
@@ -101,7 +101,7 @@ const SheetFooter = ({
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
|
||||
'sticky bottom-0 left-0 right-0 mt-auto',
|
||||
'sticky bottom-0 left-0 right-0 mt-auto bg-white',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -27,3 +27,17 @@ const TooltipContent = React.forwardRef<
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
|
||||
interface TooltiperProps {
|
||||
asChild: boolean;
|
||||
content: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
export function Tooltiper({ asChild, content, children }: TooltiperProps) {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild={asChild}>{children}</TooltipTrigger>
|
||||
<TooltipContent>{content}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
34
apps/dashboard/src/components/widget-table.tsx
Normal file
34
apps/dashboard/src/components/widget-table.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
interface Props<T> {
|
||||
columns: {
|
||||
name: string;
|
||||
render: (item: T) => React.ReactNode;
|
||||
}[];
|
||||
keyExtractor: (item: T) => string;
|
||||
data: T[];
|
||||
}
|
||||
|
||||
export function WidgetTable<T>({ columns, data, keyExtractor }: Props<T>) {
|
||||
return (
|
||||
<table className="w-full">
|
||||
<thead className="bg-slate-50 border-b border-border text-slate-500 [&_th]:font-medium text-sm [&_th]:p-4 [&_th]:py-2 [&_th]:text-left [&_th:last-child]:text-right [&_th]:whitespace-nowrap">
|
||||
<tr>
|
||||
{columns.map((column) => (
|
||||
<th key={column.name}>{column.name}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((item) => (
|
||||
<tr
|
||||
key={keyExtractor(item)}
|
||||
className="text-sm border-b border-border last:border-0 [&_td]:p-4 [&_td:first-child]:text-left text-right"
|
||||
>
|
||||
{columns.map((column) => (
|
||||
<td key={column.name}>{column.render(item)}</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user