redesign overview
This commit is contained in:
@@ -58,10 +58,7 @@ export function ListReports({ reports }: ListReportsProps) {
|
||||
{reports.map((report) => {
|
||||
const chartRange = report.range; // timeRanges[report.range];
|
||||
return (
|
||||
<div
|
||||
className="rounded-md border border-border bg-white shadow"
|
||||
key={report.id}
|
||||
>
|
||||
<div className="card" key={report.id}>
|
||||
<Link
|
||||
href={`/${params.organizationId}/${params.projectId}/reports/${report.id}`}
|
||||
className="flex border-b border-border p-4 leading-none [&_svg]:hover:opacity-100 items-center justify-between"
|
||||
|
||||
@@ -13,9 +13,9 @@ import { getExists } from '@/server/pageExists';
|
||||
|
||||
import { db } from '@mixan/db';
|
||||
|
||||
import OverviewMetrics from '../../../../components/overview/overview-metrics';
|
||||
import { CreateClient } from './create-client';
|
||||
import { StickyBelowHeader } from './layout-sticky-below-header';
|
||||
import OverviewMetrics from './overview-metrics';
|
||||
import { OverviewReportRange } from './overview-sticky-header';
|
||||
|
||||
interface PageProps {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { StickyBelowHeader } from '@/app/(app)/[organizationId]/[projectId]/layout-sticky-below-header';
|
||||
import OverviewMetrics from '@/app/(app)/[organizationId]/[projectId]/overview-metrics';
|
||||
import { OverviewReportRange } from '@/app/(app)/[organizationId]/[projectId]/overview-sticky-header';
|
||||
import { Logo } from '@/components/Logo';
|
||||
import { OverviewFiltersButtons } from '@/components/overview/filters/overview-filters-buttons';
|
||||
import { OverviewFiltersDrawer } from '@/components/overview/filters/overview-filters-drawer';
|
||||
import ServerLiveCounter from '@/components/overview/live-counter';
|
||||
import { OverviewLiveHistogram } from '@/components/overview/overview-live-histogram';
|
||||
import OverviewMetrics from '@/components/overview/overview-metrics';
|
||||
import OverviewTopDevices from '@/components/overview/overview-top-devices';
|
||||
import OverviewTopEvents from '@/components/overview/overview-top-events/overview-top-events';
|
||||
import OverviewTopEvents from '@/components/overview/overview-top-events';
|
||||
import OverviewTopGeo from '@/components/overview/overview-top-geo';
|
||||
import OverviewTopPages from '@/components/overview/overview-top-pages';
|
||||
import OverviewTopSources from '@/components/overview/overview-top-sources';
|
||||
@@ -43,7 +43,7 @@ export default async function Page({ params: { id } }: PageProps) {
|
||||
<Logo className="text-white" />
|
||||
</a>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg shadow ring-8 ring-blue-600/50">
|
||||
<div className="bg-slate-100 rounded-lg shadow ring-8 ring-blue-600/50">
|
||||
<StickyBelowHeader>
|
||||
<div className="p-4 flex gap-2 justify-between">
|
||||
<div className="flex gap-2">
|
||||
@@ -65,9 +65,7 @@ export default async function Page({ params: { id } }: PageProps) {
|
||||
<OverviewTopPages projectId={projectId} />
|
||||
<OverviewTopDevices projectId={projectId} />
|
||||
<OverviewTopEvents projectId={projectId} />
|
||||
<div className="col-span-6">
|
||||
<OverviewTopGeo projectId={projectId} />
|
||||
</div>
|
||||
<OverviewTopGeo projectId={projectId} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@ export default function RootLayout({
|
||||
return (
|
||||
<html lang="en" className="light">
|
||||
<body
|
||||
className={cn('min-h-screen font-sans antialiased grainy bg-slate-50')}
|
||||
className={cn('min-h-screen font-sans antialiased grainy bg-slate-100')}
|
||||
>
|
||||
<Providers>{children}</Providers>
|
||||
</body>
|
||||
|
||||
@@ -19,8 +19,8 @@ export function Card({ children, hover, className }: CardProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'border border-border rounded relative bg-white',
|
||||
hover && 'transition-all hover:shadow hover:border-black',
|
||||
'card relative',
|
||||
hover && 'transition-all hover:border-black',
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -30,14 +30,5 @@ export interface WidgetProps {
|
||||
className?: string;
|
||||
}
|
||||
export function Widget({ children, className }: WidgetProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'border border-border rounded-md bg-white self-start',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
return <div className={cn('card self-start', className)}>{children}</div>;
|
||||
}
|
||||
|
||||
@@ -23,9 +23,7 @@ export function ExpandableListItem({
|
||||
}: ExpandableListItemProps) {
|
||||
const [open, setOpen] = useState(initialOpen ?? false);
|
||||
return (
|
||||
<div
|
||||
className={cn('bg-white shadow rounded-xl overflow-hidden', className)}
|
||||
>
|
||||
<div className={cn('card overflow-hidden', className)}>
|
||||
<div className="p-2 sm:p-4 flex gap-4">
|
||||
<div className="flex gap-1">{image}</div>
|
||||
<div className="flex flex-col flex-1 gap-1 min-w-0">
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { getConversionEventNames } from '@mixan/db';
|
||||
|
||||
import type { OverviewLatestEventsProps } from './overview-latest-events';
|
||||
import OverviewLatestEvents from './overview-latest-events';
|
||||
|
||||
export default async function OverviewLatestEventsServer({
|
||||
projectId,
|
||||
}: Omit<OverviewLatestEventsProps, 'conversions'>) {
|
||||
const eventNames = await getConversionEventNames(projectId);
|
||||
return (
|
||||
<OverviewLatestEvents
|
||||
projectId={projectId}
|
||||
conversions={eventNames.map((item) => item.name)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
'use client';
|
||||
|
||||
import { ChartSwitch } from '@/components/report/chart';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { Widget, WidgetBody } from '../../Widget';
|
||||
import { WidgetButtons, WidgetHead } from '../overview-widget';
|
||||
import { useOverviewOptions } from '../useOverviewOptions';
|
||||
import { useOverviewWidget } from '../useOverviewWidget';
|
||||
|
||||
export interface OverviewLatestEventsProps {
|
||||
projectId: string;
|
||||
conversions: string[];
|
||||
}
|
||||
export default function OverviewLatestEvents({
|
||||
projectId,
|
||||
conversions,
|
||||
}: OverviewLatestEventsProps) {
|
||||
const { interval, range, previous, startDate, endDate } =
|
||||
useOverviewOptions();
|
||||
const [filters] = useEventQueryFilters();
|
||||
const [widget, setWidget, widgets] = useOverviewWidget('ev', {
|
||||
all: {
|
||||
title: 'Top events',
|
||||
btn: 'All',
|
||||
chart: {
|
||||
projectId,
|
||||
startDate,
|
||||
endDate,
|
||||
events: [
|
||||
{
|
||||
segment: 'event',
|
||||
filters: [
|
||||
...filters,
|
||||
{
|
||||
id: 'ex_session',
|
||||
name: 'name',
|
||||
operator: 'isNot',
|
||||
value: ['session_start', 'session_end'],
|
||||
},
|
||||
],
|
||||
id: 'A',
|
||||
name: '*',
|
||||
},
|
||||
],
|
||||
breakdowns: [
|
||||
{
|
||||
id: 'A',
|
||||
name: 'name',
|
||||
},
|
||||
],
|
||||
chartType: 'bar',
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
},
|
||||
},
|
||||
conversions: {
|
||||
title: 'Conversions',
|
||||
btn: 'Conversions',
|
||||
hide: conversions.length === 0,
|
||||
chart: {
|
||||
projectId,
|
||||
startDate,
|
||||
endDate,
|
||||
events: [
|
||||
{
|
||||
segment: 'event',
|
||||
filters: [
|
||||
...filters,
|
||||
{
|
||||
id: 'conversion',
|
||||
name: 'name',
|
||||
operator: 'is',
|
||||
value: conversions,
|
||||
},
|
||||
],
|
||||
id: 'A',
|
||||
name: '*',
|
||||
},
|
||||
],
|
||||
breakdowns: [
|
||||
{
|
||||
id: 'A',
|
||||
name: 'name',
|
||||
},
|
||||
],
|
||||
chartType: 'bar',
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Widget className="col-span-6 md:col-span-3">
|
||||
<WidgetHead>
|
||||
<div className="title">{widget.title}</div>
|
||||
<WidgetButtons>
|
||||
{widgets
|
||||
.filter((item) => item.hide !== true)
|
||||
.map((w) => (
|
||||
<button
|
||||
key={w.key}
|
||||
onClick={() => setWidget(w.key)}
|
||||
className={cn(w.key === widget.key && 'active')}
|
||||
>
|
||||
{w.btn}
|
||||
</button>
|
||||
))}
|
||||
</WidgetButtons>
|
||||
</WidgetHead>
|
||||
<WidgetBody>
|
||||
<ChartSwitch hideID {...widget.chart} previous={false} />
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,22 +1,25 @@
|
||||
'use client';
|
||||
|
||||
import { Fragment } from 'react';
|
||||
import { api } from '@/app/_trpc/client';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { ChevronsUpDownIcon } from 'lucide-react';
|
||||
import AnimateHeight from 'react-animate-height';
|
||||
|
||||
import type { IChartInput } from '@mixan/validation';
|
||||
|
||||
import { ChartSwitch } from '../report/chart';
|
||||
import { Widget, WidgetBody, WidgetHead } from '../Widget';
|
||||
import { redisSub } from '../../../../../packages/redis';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
|
||||
import { useOverviewOptions } from './useOverviewOptions';
|
||||
|
||||
interface OverviewLiveHistogramProps {
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
export function OverviewLiveHistogram({
|
||||
projectId,
|
||||
}: OverviewLiveHistogramProps) {
|
||||
const { liveHistogram, setLiveHistogram } = useOverviewOptions();
|
||||
const { liveHistogram } = useOverviewOptions();
|
||||
const report: IChartInput = {
|
||||
projectId,
|
||||
events: [
|
||||
@@ -44,26 +47,112 @@ export function OverviewLiveHistogram({
|
||||
lineType: 'monotone',
|
||||
previous: false,
|
||||
};
|
||||
const countReport: IChartInput = {
|
||||
name: '',
|
||||
projectId,
|
||||
events: [
|
||||
{
|
||||
segment: 'user',
|
||||
filters: [],
|
||||
id: 'A',
|
||||
name: 'session_start',
|
||||
},
|
||||
],
|
||||
breakdowns: [],
|
||||
chartType: 'metric',
|
||||
lineType: 'monotone',
|
||||
interval: 'minute',
|
||||
range: '30min',
|
||||
previous: false,
|
||||
metric: 'sum',
|
||||
};
|
||||
|
||||
const res = api.chart.chart.useQuery(report);
|
||||
const countRes = api.chart.chart.useQuery(countReport);
|
||||
|
||||
const metrics = res.data?.series[0]?.metrics;
|
||||
const minutes = (res.data?.series[0]?.data || []).slice(-30);
|
||||
const liveCount = countRes.data?.series[0]?.metrics?.sum ?? 0;
|
||||
|
||||
if (res.isInitialLoading || countRes.isInitialLoading) {
|
||||
// prettier-ignore
|
||||
const staticArray = [
|
||||
10, 25, 30, 45, 20, 5, 55, 18, 40, 12,
|
||||
50, 35, 8, 22, 38, 42, 15, 28, 52, 5,
|
||||
48, 14, 32, 58, 7, 19, 33, 56, 24, 5
|
||||
];
|
||||
|
||||
return (
|
||||
<Wrapper count={0} open={liveHistogram}>
|
||||
{staticArray.map((percent, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex-1 rounded-md bg-slate-200 animate-pulse"
|
||||
style={{ height: `${percent}%` }}
|
||||
/>
|
||||
))}
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
if (!res.isSuccess && !countRes.isSuccess) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<button onClick={() => setLiveHistogram((p) => !p)} className="w-full">
|
||||
<WidgetHead
|
||||
className={cn(
|
||||
'flex justify-between items-center',
|
||||
!liveHistogram && 'border-b-0'
|
||||
)}
|
||||
>
|
||||
<div className="title">Active users last 30 minutes</div>
|
||||
<ChevronsUpDownIcon size={16} />
|
||||
</WidgetHead>
|
||||
</button>
|
||||
|
||||
<AnimateHeight duration={500} height={liveHistogram ? 'auto' : 0}>
|
||||
<WidgetBody>
|
||||
<ChartSwitch {...report} />
|
||||
</WidgetBody>
|
||||
</AnimateHeight>
|
||||
</Widget>
|
||||
<Wrapper open={liveHistogram} count={liveCount}>
|
||||
{minutes.map((minute) => {
|
||||
return (
|
||||
<Tooltip key={minute.date}>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className={cn(
|
||||
'flex-1 rounded-md hover:scale-110 transition-all ease-in-out',
|
||||
minute.count === 0 ? 'bg-slate-200' : 'bg-blue-600'
|
||||
)}
|
||||
style={{
|
||||
height:
|
||||
minute.count === 0
|
||||
? '5%'
|
||||
: `${(minute.count / metrics!.max) * 100}%`,
|
||||
}}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
<div>{minute.count} active users</div>
|
||||
<div>@ {new Date(minute.date).toLocaleTimeString()}</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
interface WrapperProps {
|
||||
open: boolean;
|
||||
children: React.ReactNode;
|
||||
count: number;
|
||||
}
|
||||
|
||||
function Wrapper({ open, children, count }: WrapperProps) {
|
||||
return (
|
||||
<AnimateHeight duration={500} height={open ? 'auto' : 0}>
|
||||
<div className="flex items-end flex-col md:flex-row">
|
||||
<div className="md:mr-2 flex md:flex-col max-md:justify-between items-end max-md:w-full max-md:mb-2 md:card md:p-4">
|
||||
<div className="text-sm max-md:mb-1">Last 30 minutes</div>
|
||||
<div className="text-2xl font-bold text-ellipsis overflow-hidden whitespace-nowrap">
|
||||
{count}
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-h-[150px] aspect-[5/1] flex flex-1 gap-0.5 md:gap-2 items-end w-full relative">
|
||||
<div className="absolute top-0 right-0 text-xs text-muted-foreground">
|
||||
NOW
|
||||
</div>
|
||||
{/* <div className="md:absolute top-0 left-0 md:card md:p-4 mr-2 md:bg-white/90 z-50"> */}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</AnimateHeight>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -202,12 +202,6 @@ export default function OverviewMetrics({ projectId }: OverviewMetricsProps) {
|
||||
}}
|
||||
>
|
||||
<ChartSwitch hideID {...report} />
|
||||
<div
|
||||
className={cn(
|
||||
'transition-opacity top-0 left-0 right-0 bottom-0 absolute rounded-md w-full h-full border ring-1 border-black ring-black',
|
||||
metric === index ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
{/* add active border */}
|
||||
</button>
|
||||
))}
|
||||
@@ -18,6 +18,7 @@ export default function OverviewTopDevices({
|
||||
const { interval, range, previous, startDate, endDate } =
|
||||
useOverviewOptions();
|
||||
const [filters, setFilter] = useEventQueryFilters();
|
||||
const isPageFilter = filters.find((filter) => filter.name === 'path');
|
||||
const [widget, setWidget, widgets] = useOverviewWidget('tech', {
|
||||
devices: {
|
||||
title: 'Top devices',
|
||||
@@ -31,7 +32,7 @@ export default function OverviewTopDevices({
|
||||
segment: 'user',
|
||||
filters,
|
||||
id: 'A',
|
||||
name: '*',
|
||||
name: isPageFilter ? 'screen_view' : 'session_start',
|
||||
},
|
||||
],
|
||||
breakdowns: [
|
||||
@@ -61,7 +62,7 @@ export default function OverviewTopDevices({
|
||||
segment: 'user',
|
||||
filters,
|
||||
id: 'A',
|
||||
name: '*',
|
||||
name: isPageFilter ? 'screen_view' : 'session_start',
|
||||
},
|
||||
],
|
||||
breakdowns: [
|
||||
@@ -91,7 +92,7 @@ export default function OverviewTopDevices({
|
||||
segment: 'user',
|
||||
filters,
|
||||
id: 'A',
|
||||
name: '*',
|
||||
name: isPageFilter ? 'screen_view' : 'session_start',
|
||||
},
|
||||
],
|
||||
breakdowns: [
|
||||
@@ -121,7 +122,7 @@ export default function OverviewTopDevices({
|
||||
segment: 'user',
|
||||
filters,
|
||||
id: 'A',
|
||||
name: '*',
|
||||
name: isPageFilter ? 'screen_view' : 'session_start',
|
||||
},
|
||||
],
|
||||
breakdowns: [
|
||||
@@ -151,7 +152,7 @@ export default function OverviewTopDevices({
|
||||
segment: 'user',
|
||||
filters,
|
||||
id: 'A',
|
||||
name: '*',
|
||||
name: isPageFilter ? 'screen_view' : 'session_start',
|
||||
},
|
||||
],
|
||||
breakdowns: [
|
||||
|
||||
@@ -16,6 +16,7 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
||||
const { interval, range, previous, startDate, endDate } =
|
||||
useOverviewOptions();
|
||||
const [filters, setFilter] = useEventQueryFilters();
|
||||
const isPageFilter = filters.find((filter) => filter.name === 'path');
|
||||
const [widget, setWidget, widgets] = useOverviewWidget('geo', {
|
||||
countries: {
|
||||
title: 'Top countries',
|
||||
@@ -29,7 +30,7 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
||||
segment: 'event',
|
||||
filters,
|
||||
id: 'A',
|
||||
name: '*',
|
||||
name: isPageFilter ? 'screen_view' : 'session_start',
|
||||
},
|
||||
],
|
||||
breakdowns: [
|
||||
@@ -59,7 +60,7 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
||||
segment: 'event',
|
||||
filters,
|
||||
id: 'A',
|
||||
name: '*',
|
||||
name: isPageFilter ? 'screen_view' : 'session_start',
|
||||
},
|
||||
],
|
||||
breakdowns: [
|
||||
@@ -89,7 +90,7 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
||||
segment: 'event',
|
||||
filters,
|
||||
id: 'A',
|
||||
name: '*',
|
||||
name: isPageFilter ? 'screen_view' : 'session_start',
|
||||
},
|
||||
],
|
||||
breakdowns: [
|
||||
@@ -165,7 +166,7 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
||||
segment: 'event',
|
||||
filters,
|
||||
id: 'A',
|
||||
name: '*',
|
||||
name: isPageFilter ? 'screen_view' : 'session_start',
|
||||
},
|
||||
],
|
||||
breakdowns: [
|
||||
|
||||
@@ -19,10 +19,7 @@ import { WidgetHead as WidgetHeadBase } from '../Widget';
|
||||
export function WidgetHead({ className, ...props }: WidgetHeadProps) {
|
||||
return (
|
||||
<WidgetHeadBase
|
||||
className={cn(
|
||||
'flex flex-col p-0 [&_.title]:text-sm [&_.title]:px-4 [&_.title]:py-2',
|
||||
className
|
||||
)}
|
||||
className={cn('flex flex-col p-0 [&_.title]:p-4', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useEffect } from 'react';
|
||||
import {
|
||||
parseAsBoolean,
|
||||
parseAsInteger,
|
||||
@@ -48,7 +47,7 @@ export function useOverviewOptions() {
|
||||
// Toggles
|
||||
const [liveHistogram, setLiveHistogram] = useQueryState(
|
||||
'live',
|
||||
parseAsBoolean.withDefault(false).withOptions(nuqsOptions)
|
||||
parseAsBoolean.withDefault(true).withOptions(nuqsOptions)
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -107,7 +107,7 @@ export function PreviousDiffIndicatorText({
|
||||
])}
|
||||
>
|
||||
{renderIcon()}
|
||||
{number.format(diff)}%
|
||||
{number.short(diff)}%
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ export function MetricCard({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group relative border border-border p-2 rounded-md bg-white overflow-hidden h-24"
|
||||
className="group relative card p-4 overflow-hidden h-24"
|
||||
key={serie.name}
|
||||
>
|
||||
<div className="absolute -top-2 -left-2 -right-2 -bottom-2 z-0 opacity-20 transition-opacity duration-300 group-hover:opacity-50 rounded-md">
|
||||
@@ -64,28 +64,14 @@ export function MetricCard({
|
||||
{({ width, height }) => (
|
||||
<AreaChart
|
||||
width={width}
|
||||
height={height / 3}
|
||||
height={height / 4}
|
||||
data={serie.data}
|
||||
style={{ marginTop: (height / 3) * 2 }}
|
||||
style={{ marginTop: (height / 4) * 3 }}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="red" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={'red'} stopOpacity={0.5} />
|
||||
<stop offset="95%" stopColor={'red'} stopOpacity={0.2} />
|
||||
</linearGradient>
|
||||
<linearGradient id="green" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={'green'} stopOpacity={0.5} />
|
||||
<stop offset="95%" stopColor={'green'} stopOpacity={0.2} />
|
||||
</linearGradient>
|
||||
<linearGradient id="blue" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={'blue'} stopOpacity={0.5} />
|
||||
<stop offset="95%" stopColor={'blue'} stopOpacity={0.2} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Area
|
||||
dataKey="count"
|
||||
type="monotone"
|
||||
fill={`url(#${graphColors})`}
|
||||
fill={`transparent`}
|
||||
fillOpacity={1}
|
||||
stroke={graphColors}
|
||||
strokeWidth={2}
|
||||
@@ -120,7 +106,7 @@ export function MetricCard({
|
||||
|
||||
export function MetricCardEmpty() {
|
||||
return (
|
||||
<div className="border border-border p-4 rounded-md bg-white h-24">
|
||||
<div className="card p-4 h-24">
|
||||
<div className="flex items-center justify-center h-full text-slate-600">
|
||||
No data
|
||||
</div>
|
||||
@@ -130,7 +116,7 @@ export function MetricCardEmpty() {
|
||||
|
||||
export function MetricCardLoading() {
|
||||
return (
|
||||
<div className="h-24 p-4 py-5 flex flex-col bg-white border border-border rounded-md">
|
||||
<div className="h-24 p-4 py-5 flex flex-col card">
|
||||
<div className="bg-slate-200 rounded animate-pulse h-4 w-1/2"></div>
|
||||
<div className="bg-slate-200 rounded animate-pulse h-6 w-1/5 mt-auto"></div>
|
||||
</div>
|
||||
|
||||
@@ -30,16 +30,17 @@ export function ReportBarChart({ data }: ReportBarChartProps) {
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col w-full text-xs -mx-2',
|
||||
editMode && 'text-base bg-white border border-border rounded-md p-4'
|
||||
editMode && 'text-base card p-4'
|
||||
)}
|
||||
>
|
||||
{series.map((serie) => {
|
||||
{series.map((serie, index) => {
|
||||
const isClickable = serie.name !== NOT_SET_VALUE && onClick;
|
||||
return (
|
||||
<div
|
||||
key={serie.name}
|
||||
className={cn(
|
||||
'relative py-3 px-2 flex flex-1 w-full gap-4 items-center even:bg-slate-50 [&_[role=progressbar]]:even:bg-white [&_[role=progressbar]]:shadow-sm rounded overflow-hidden',
|
||||
'relative py-3 px-2 flex flex-1 w-full gap-4 items-center even:bg-slate-50 rounded overflow-hidden',
|
||||
'[&_[role=progressbar]]:even:bg-white [&_[role=progressbar]]:shadow-sm',
|
||||
isClickable && 'cursor-pointer hover:!bg-slate-100'
|
||||
)}
|
||||
{...(isClickable ? { onClick: () => onClick(serie) } : {})}
|
||||
@@ -57,14 +58,12 @@ export function ReportBarChart({ data }: ReportBarChartProps) {
|
||||
<div className="font-bold">
|
||||
{number.format(serie.metrics.sum)}
|
||||
</div>
|
||||
<Progress
|
||||
color={getChartColor(index)}
|
||||
className={cn('w-1/2', editMode ? 'h-5' : 'h-2')}
|
||||
value={(serie.metrics.sum / maxCount) * 100}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="absolute left-0 bottom-0 h-0.5 rounded-full min-w-2 z-10 bg-blue-600"
|
||||
style={{
|
||||
width: `${(serie.metrics.sum / maxCount) * 100}%`,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -31,12 +31,7 @@ export function ReportPieChart({ data }: ReportPieChartProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
'max-sm:-mx-3',
|
||||
editMode && 'border border-border bg-white rounded-md p-4'
|
||||
)}
|
||||
>
|
||||
<div className={cn('max-sm:-mx-3', editMode && 'card p-4')}>
|
||||
<AutoSizer disableHeight>
|
||||
{({ width }) => {
|
||||
const height = Math.min(Math.max(width * 0.5625, 250), 400);
|
||||
|
||||
@@ -17,10 +17,7 @@ export function ResponsiveContainer({ children }: ResponsiveContainerProps) {
|
||||
maxHeight,
|
||||
minHeight,
|
||||
}}
|
||||
className={cn(
|
||||
'max-sm:-mx-3 aspect-video w-full',
|
||||
editMode && 'border border-border bg-white rounded-md p-4'
|
||||
)}
|
||||
className={cn('max-sm:-mx-3 aspect-video w-full', editMode && 'card p-4')}
|
||||
>
|
||||
<AutoSizer disableHeight>
|
||||
{({ width }) =>
|
||||
|
||||
@@ -44,6 +44,11 @@ const mapper: Record<string, LucideIcon> = {
|
||||
link_out: ExternalLinkIcon,
|
||||
|
||||
// Websites
|
||||
linkedin: createImageIcon(getProxyImage('https://linkedin.com')),
|
||||
slack: createImageIcon(getProxyImage('https://slack.com')),
|
||||
pinterest: createImageIcon(getProxyImage('https://www.pinterest.se')),
|
||||
ecosia: createImageIcon(getProxyImage('https://ecosia.com')),
|
||||
yandex: createImageIcon(getProxyImage('https://yandex.com')),
|
||||
google: createImageIcon(getProxyImage('https://google.com')),
|
||||
facebook: createImageIcon(getProxyImage('https://facebook.com')),
|
||||
bing: createImageIcon(getProxyImage('https://bing.com')),
|
||||
|
||||
@@ -99,7 +99,7 @@ export function FunnelSteps({
|
||||
)}
|
||||
key={step.event.id}
|
||||
>
|
||||
<div className="border border-border divide-y divide-border bg-white">
|
||||
<div className="card divide-y divide-border bg-white">
|
||||
<div className="p-4">
|
||||
<p className="text-muted-foreground">Step {index + 1}</p>
|
||||
<h3 className="font-bold">
|
||||
|
||||
@@ -10,7 +10,7 @@ const Table = React.forwardRef<
|
||||
overflow?: boolean;
|
||||
}
|
||||
>(({ className, wrapper, overflow = true, ...props }, ref) => (
|
||||
<div className={cn('border border-border rounded-md bg-white', className)}>
|
||||
<div className={cn('card', className)}>
|
||||
<div className={cn('relative w-full', overflow && 'overflow-auto')}>
|
||||
<table
|
||||
ref={ref}
|
||||
|
||||
@@ -118,6 +118,12 @@
|
||||
0 0;
|
||||
transition-duration: 0.5s;
|
||||
}
|
||||
|
||||
.card {
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
|
||||
border: 0 !important;
|
||||
@apply bg-white rounded-xl;
|
||||
}
|
||||
}
|
||||
|
||||
.resizer {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"js:codegen": "pnpm -r --filter sdk-web run build-for-openpanel",
|
||||
"migrate": "pnpm -r --filter db run migrate",
|
||||
"migrate:deploy": "pnpm -r --filter db run migrate:deploy",
|
||||
"dev": "pnpm -r dev",
|
||||
"dev": "pnpm -r --parallel testing",
|
||||
"format": "pnpm -r format --cache --cache-location=\"node_modules/.cache/.prettiercache\"",
|
||||
"format:fix": "pnpm -r format --write --cache --cache-location=\"node_modules/.cache/.prettiercache\"",
|
||||
"lint": "pnpm -r lint",
|
||||
|
||||
Reference in New Issue
Block a user