update dashboard metrics and move away from round corners
This commit is contained in:
@@ -12,7 +12,7 @@ export function StickyBelowHeader({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'top-0 z-20 rounded-lg border-b border-border bg-background md:sticky [[id=dashboard]_&]:top-16 [[id=dashboard]_&]:rounded-none',
|
||||
'top-0 z-20 border-b border-border bg-background md:sticky [[id=dashboard]_&]:top-16 [[id=dashboard]_&]:rounded-none',
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -41,9 +41,6 @@ export default function Page({
|
||||
<OverviewFiltersButtons />
|
||||
</StickyBelowHeader>
|
||||
<div className="grid grid-cols-6 gap-4 p-4">
|
||||
<div className="col-span-6">
|
||||
<OverviewLiveHistogram projectId={projectId} />
|
||||
</div>
|
||||
<OverviewMetrics projectId={projectId} />
|
||||
<OverviewTopSources projectId={projectId} />
|
||||
<OverviewTopPages projectId={projectId} />
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
eachDayOfInterval,
|
||||
endOfMonth,
|
||||
format,
|
||||
formatISO,
|
||||
startOfMonth,
|
||||
subMonths,
|
||||
} from 'date-fns';
|
||||
@@ -60,7 +61,9 @@ const ProfileActivity = ({ data }: Props) => {
|
||||
end: endOfMonth(subMonths(startDate, 1)),
|
||||
}).map((date) => {
|
||||
const hit = data.find((item) =>
|
||||
item.date.includes(date.toISOString().split('T')[0])
|
||||
item.date.includes(
|
||||
formatISO(date, { representation: 'date' })
|
||||
)
|
||||
);
|
||||
return (
|
||||
<div
|
||||
@@ -82,7 +85,9 @@ const ProfileActivity = ({ data }: Props) => {
|
||||
end: endDate,
|
||||
}).map((date) => {
|
||||
const hit = data.find((item) =>
|
||||
item.date.includes(date.toISOString().split('T')[0])
|
||||
item.date.includes(
|
||||
formatISO(date, { representation: 'date' })
|
||||
)
|
||||
);
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -18,9 +18,15 @@ interface PageProps {
|
||||
params: {
|
||||
id: string;
|
||||
};
|
||||
searchParams: {
|
||||
header: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default async function Page({ params: { id } }: PageProps) {
|
||||
export default async function Page({
|
||||
params: { id },
|
||||
searchParams,
|
||||
}: PageProps) {
|
||||
const share = await getShareOverviewById(id);
|
||||
if (!share) {
|
||||
return notFound();
|
||||
@@ -32,43 +38,42 @@ export default async function Page({ params: { id } }: PageProps) {
|
||||
const organization = await getOrganizationBySlug(share.organizationSlug);
|
||||
|
||||
return (
|
||||
<div className="bg-gradient-to-tl from-blue-950 to-blue-600 p-4 md:p-16">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<div className="mb-4 flex items-end justify-between">
|
||||
<div>
|
||||
{searchParams.header !== '0' && (
|
||||
<div className="flex items-center justify-between border-b border-border bg-white p-4">
|
||||
<div className="leading-none">
|
||||
<span className="mb-4 text-white">{organization?.name}</span>
|
||||
<h1 className="text-xl font-medium text-white">
|
||||
{share.project?.name}
|
||||
</h1>
|
||||
<span className="mb-4">{organization?.name}</span>
|
||||
<h1 className="text-xl font-medium">{share.project?.name}</h1>
|
||||
</div>
|
||||
<a href="https://openpanel.dev?utm_source=openpanel.dev&utm_medium=share">
|
||||
<Logo className="text-white max-sm:[&_span]:hidden" />
|
||||
<a
|
||||
href="https://openpanel.dev?utm_source=openpanel.dev&utm_medium=share"
|
||||
className="flex flex-col items-end text-lg font-medium"
|
||||
>
|
||||
<span className="text-xs">POWERED BY</span>
|
||||
<span>openpanel.dev</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="rounded-lg bg-slate-100 shadow ring-8 ring-blue-600/50 max-sm:-mx-3">
|
||||
<StickyBelowHeader>
|
||||
<div className="flex justify-between gap-2 p-4">
|
||||
<div className="flex gap-2">
|
||||
<OverviewReportRange />
|
||||
{/* <OverviewFiltersDrawer projectId={projectId} mode="events" /> */}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<ServerLiveCounter projectId={projectId} />
|
||||
</div>
|
||||
)}
|
||||
<div className="">
|
||||
<StickyBelowHeader>
|
||||
<div className="flex justify-between gap-2 p-4">
|
||||
<div className="flex gap-2">
|
||||
<OverviewReportRange />
|
||||
{/* <OverviewFiltersDrawer projectId={projectId} mode="events" /> */}
|
||||
</div>
|
||||
<OverviewFiltersButtons />
|
||||
</StickyBelowHeader>
|
||||
<div className="grid grid-cols-6 gap-4 p-4">
|
||||
<div className="col-span-6">
|
||||
<OverviewLiveHistogram projectId={projectId} />
|
||||
<div className="flex gap-2">
|
||||
<ServerLiveCounter projectId={projectId} />
|
||||
</div>
|
||||
<OverviewMetrics projectId={projectId} />
|
||||
<OverviewTopSources projectId={projectId} />
|
||||
<OverviewTopPages projectId={projectId} />
|
||||
<OverviewTopDevices projectId={projectId} />
|
||||
<OverviewTopEvents projectId={projectId} />
|
||||
<OverviewTopGeo projectId={projectId} />
|
||||
</div>
|
||||
<OverviewFiltersButtons />
|
||||
</StickyBelowHeader>
|
||||
<div className="mx-auto grid max-w-7xl grid-cols-6 gap-4 p-4">
|
||||
<OverviewMetrics projectId={projectId} />
|
||||
<OverviewTopSources projectId={projectId} />
|
||||
<OverviewTopPages projectId={projectId} />
|
||||
<OverviewTopDevices projectId={projectId} />
|
||||
<OverviewTopEvents projectId={projectId} />
|
||||
<OverviewTopGeo projectId={projectId} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,8 +12,6 @@ import { useQueryClient } from '@tanstack/react-query';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { useOverviewOptions } from '../useOverviewOptions';
|
||||
|
||||
export interface LiveCounterProps {
|
||||
data: number;
|
||||
projectId: string;
|
||||
@@ -27,7 +25,6 @@ const AnimatedNumbers = dynamic(() => import('react-animated-numbers'), {
|
||||
const FIFTEEN_SECONDS = 1000 * 15;
|
||||
|
||||
export default function LiveCounter({ data = 0, projectId }: LiveCounterProps) {
|
||||
const { setLiveHistogram } = useOverviewOptions();
|
||||
const client = useQueryClient();
|
||||
const [counter, setCounter] = useState(data);
|
||||
const lastRefresh = useRef(Date.now());
|
||||
@@ -47,38 +44,33 @@ export default function LiveCounter({ data = 0, projectId }: LiveCounterProps) {
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
onClick={() => setLiveHistogram((p) => !p)}
|
||||
className="flex h-8 items-center gap-2 rounded border border-border px-3 font-medium leading-none"
|
||||
>
|
||||
<div className="relative">
|
||||
<div
|
||||
className={cn(
|
||||
'h-3 w-3 animate-ping rounded-full bg-emerald-500 opacity-100 transition-all',
|
||||
counter === 0 && 'bg-destructive opacity-0'
|
||||
)}
|
||||
></div>
|
||||
<div
|
||||
className={cn(
|
||||
'absolute left-0 top-0 h-3 w-3 rounded-full bg-emerald-500 transition-all',
|
||||
counter === 0 && 'bg-destructive'
|
||||
)}
|
||||
></div>
|
||||
</div>
|
||||
<AnimatedNumbers
|
||||
includeComma
|
||||
transitions={(index) => ({
|
||||
type: 'spring',
|
||||
duration: index + 0.3,
|
||||
|
||||
damping: 10,
|
||||
stiffness: 200,
|
||||
})}
|
||||
animateToNumber={counter}
|
||||
locale="en"
|
||||
<TooltipTrigger className="flex h-8 items-center gap-2 rounded border border-border px-3 font-medium leading-none">
|
||||
<div className="relative">
|
||||
<div
|
||||
className={cn(
|
||||
'h-3 w-3 animate-ping rounded-full bg-emerald-500 opacity-100 transition-all',
|
||||
counter === 0 && 'bg-destructive opacity-0'
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
className={cn(
|
||||
'absolute left-0 top-0 h-3 w-3 rounded-full bg-emerald-500 transition-all',
|
||||
counter === 0 && 'bg-destructive'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<AnimatedNumbers
|
||||
includeComma
|
||||
transitions={(index) => ({
|
||||
type: 'spring',
|
||||
duration: index + 0.3,
|
||||
|
||||
damping: 10,
|
||||
stiffness: 200,
|
||||
})}
|
||||
animateToNumber={counter}
|
||||
locale="en"
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
<p>{counter} unique visitors last 5 minutes</p>
|
||||
|
||||
@@ -5,9 +5,7 @@ import { cn } from '@/utils/cn';
|
||||
|
||||
import type { IChartInput } from '@openpanel/validation';
|
||||
|
||||
import AnimateHeight from '../animate-height';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
|
||||
import { useOverviewOptions } from './useOverviewOptions';
|
||||
|
||||
interface OverviewLiveHistogramProps {
|
||||
projectId: string;
|
||||
@@ -16,7 +14,6 @@ interface OverviewLiveHistogramProps {
|
||||
export function OverviewLiveHistogram({
|
||||
projectId,
|
||||
}: OverviewLiveHistogramProps) {
|
||||
const { liveHistogram } = useOverviewOptions();
|
||||
const report: IChartInput = {
|
||||
projectId,
|
||||
events: [
|
||||
@@ -80,11 +77,11 @@ export function OverviewLiveHistogram({
|
||||
];
|
||||
|
||||
return (
|
||||
<Wrapper count={0} open={liveHistogram}>
|
||||
<Wrapper count={0}>
|
||||
{staticArray.map((percent, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex-1 animate-pulse rounded-md bg-slate-200 dark:bg-slate-800"
|
||||
className="flex-1 animate-pulse rounded bg-slate-200 dark:bg-slate-800"
|
||||
style={{ height: `${percent}%` }}
|
||||
/>
|
||||
))}
|
||||
@@ -97,14 +94,14 @@ export function OverviewLiveHistogram({
|
||||
}
|
||||
|
||||
return (
|
||||
<Wrapper open={liveHistogram} count={liveCount}>
|
||||
<Wrapper count={liveCount}>
|
||||
{minutes.map((minute) => {
|
||||
return (
|
||||
<Tooltip key={minute.date}>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className={cn(
|
||||
'flex-1 rounded-md transition-all ease-in-out hover:scale-110',
|
||||
'flex-1 rounded transition-all ease-in-out hover:scale-110',
|
||||
minute.count === 0 ? 'bg-slate-200' : 'bg-blue-600'
|
||||
)}
|
||||
style={{
|
||||
@@ -127,22 +124,19 @@ export function OverviewLiveHistogram({
|
||||
}
|
||||
|
||||
interface WrapperProps {
|
||||
open: boolean;
|
||||
children: React.ReactNode;
|
||||
count: number;
|
||||
}
|
||||
|
||||
function Wrapper({ open, children, count }: WrapperProps) {
|
||||
function Wrapper({ children, count }: WrapperProps) {
|
||||
return (
|
||||
<AnimateHeight open={open}>
|
||||
<div className="flex flex-col">
|
||||
<div className="relative -top-2 text-center text-xs font-medium text-muted-foreground">
|
||||
{count} unique vistors last 30 minutes
|
||||
</div>
|
||||
<div className="relative flex aspect-[6/1] max-h-[150px] w-full flex-1 items-end gap-0.5 md:aspect-[10/1] md:gap-2">
|
||||
{children}
|
||||
</div>
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="relative mb-1 text-xs font-medium text-muted-foreground">
|
||||
{count} unique vistors last 30 minutes
|
||||
</div>
|
||||
</AnimateHeight>
|
||||
<div className="relative flex h-full w-full flex-1 items-end gap-1">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { WidgetHead } from '@/components/overview/overview-widget';
|
||||
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
||||
import { ChartSwitch } from '@/components/report/chart';
|
||||
import { Widget, WidgetBody } from '@/components/widget';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import type { IChartInput } from '@openpanel/validation';
|
||||
|
||||
import { OverviewLiveHistogram } from './overview-live-histogram';
|
||||
|
||||
interface OverviewMetricsProps {
|
||||
projectId: string;
|
||||
}
|
||||
@@ -191,30 +191,40 @@ export default function OverviewMetrics({ projectId }: OverviewMetricsProps) {
|
||||
const selectedMetric = reports[metric]!;
|
||||
|
||||
return (
|
||||
<div className="card col-span-6 p-4">
|
||||
<div className="-mx-2 -mt-2 mb-2 grid grid-cols-6 gap-2">
|
||||
{reports.map((report, index) => (
|
||||
<button
|
||||
key={index}
|
||||
<>
|
||||
<div className="relative -top-0.5 col-span-6 -m-4 mb-0 md:m-0">
|
||||
<div className="card mb-2 grid grid-cols-4">
|
||||
{reports.map((report, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className={cn(
|
||||
'col-span-2 flex-1 p-4 shadow-[0_0_0_0.5px] shadow-border md:col-span-1',
|
||||
index === metric && 'bg-slate-50'
|
||||
)}
|
||||
onClick={() => {
|
||||
setMetric(index);
|
||||
}}
|
||||
>
|
||||
<ChartSwitch hideID {...report} />
|
||||
</button>
|
||||
))}
|
||||
<div
|
||||
className={cn(
|
||||
'col-span-3 rounded-lg border border-background p-2 transition-all max-md:flex-1 md:col-span-2 lg:col-span-1 [&_[role="heading"]]:text-xs',
|
||||
index === metric && 'border-border'
|
||||
'col-span-4 min-h-28 flex-1 p-4 shadow-[0_0_0_0.5px] shadow-border max-md:row-start-1 md:col-span-2'
|
||||
)}
|
||||
onClick={() => {
|
||||
setMetric(index);
|
||||
}}
|
||||
>
|
||||
<ChartSwitch hideID {...report} />
|
||||
{/* add active border */}
|
||||
</button>
|
||||
))}
|
||||
<OverviewLiveHistogram projectId={projectId} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="card col-span-6 p-4">
|
||||
<ChartSwitch
|
||||
key={selectedMetric.id}
|
||||
hideID
|
||||
{...selectedMetric}
|
||||
chartType="linear"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ChartSwitch
|
||||
key={selectedMetric.id}
|
||||
hideID
|
||||
{...selectedMetric}
|
||||
chartType="linear"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -46,12 +46,6 @@ export function useOverviewOptions() {
|
||||
parseAsInteger.withDefault(0).withOptions(nuqsOptions)
|
||||
);
|
||||
|
||||
// Toggles
|
||||
const [liveHistogram, setLiveHistogram] = useQueryState(
|
||||
'live',
|
||||
parseAsBoolean.withDefault(true).withOptions(nuqsOptions)
|
||||
);
|
||||
|
||||
return {
|
||||
previous,
|
||||
setPrevious,
|
||||
@@ -72,9 +66,5 @@ export function useOverviewOptions() {
|
||||
|
||||
// Computed
|
||||
interval,
|
||||
|
||||
// Toggles
|
||||
liveHistogram,
|
||||
setLiveHistogram,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -71,9 +71,9 @@ export function MetricCard({
|
||||
{({ width, height }) => (
|
||||
<AreaChart
|
||||
width={width}
|
||||
height={height / 2.5}
|
||||
height={height / 3}
|
||||
data={serie.data}
|
||||
style={{ marginTop: (height / 2.5) * 1.5 }}
|
||||
style={{ marginTop: (height / 3) * 2 }}
|
||||
>
|
||||
<Area
|
||||
dataKey="count"
|
||||
@@ -89,12 +89,9 @@ export function MetricCard({
|
||||
</div>
|
||||
<div className="relative">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex min-w-0 items-center gap-2 text-left font-medium">
|
||||
<div className="flex min-w-0 items-center gap-2 text-left font-semibold">
|
||||
<ColorSquare>{serie.event.id}</ColorSquare>
|
||||
<span
|
||||
role="heading"
|
||||
className="overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
>
|
||||
<span className="overflow-hidden text-ellipsis whitespace-nowrap text-muted-foreground">
|
||||
{serie.name || serie.event.displayName || serie.event.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -25,7 +25,7 @@ function BarHover({ x, y, width, height, top, left, right, bottom }: any) {
|
||||
return (
|
||||
<rect
|
||||
{...{ x, y, width, height, top, left, right, bottom }}
|
||||
rx="8"
|
||||
rx="3"
|
||||
fill={bg}
|
||||
fillOpacity={0.5}
|
||||
/>
|
||||
@@ -79,7 +79,7 @@ export function ReportHistogramChart({
|
||||
dataKey={`${serie.id}:prev:count`}
|
||||
fill={getChartColor(serie.index)}
|
||||
fillOpacity={0.2}
|
||||
radius={8}
|
||||
radius={3}
|
||||
/>
|
||||
)}
|
||||
<Bar
|
||||
@@ -87,7 +87,7 @@ export function ReportHistogramChart({
|
||||
name={serie.name}
|
||||
dataKey={`${serie.id}:count`}
|
||||
fill={getChartColor(serie.index)}
|
||||
radius={8}
|
||||
radius={3}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
@@ -144,9 +144,7 @@
|
||||
}
|
||||
|
||||
.card {
|
||||
box-shadow: 0 1px 2px 0.5px rgba(0, 0, 0, 0.08);
|
||||
border: 0 !important;
|
||||
@apply rounded-xl bg-background;
|
||||
@apply border border-border bg-background;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user