This commit is contained in:
Carl-Gerhard Lindesvärd
2026-03-11 21:27:41 +01:00
parent e6d0b6544b
commit fa78e63bc8
12 changed files with 178 additions and 108 deletions

View File

@@ -1,11 +1,3 @@
import {
useXAxisProps,
useYAxisProps,
} from '@/components/report-chart/common/axis';
import { Widget, WidgetBody } from '@/components/widget';
import { WidgetHead, WidgetTitle } from '../overview/overview-widget';
import { useNumber } from '@/hooks/use-numer-formatter';
import { useFormatDateInterval } from '@/hooks/use-format-date-interval';
import { TrendingUpIcon } from 'lucide-react';
import {
Area,
@@ -16,6 +8,14 @@ import {
XAxis,
YAxis,
} from 'recharts';
import { WidgetHead, WidgetTitle } from '../overview/overview-widget';
import {
useXAxisProps,
useYAxisProps,
} from '@/components/report-chart/common/axis';
import { Widget, WidgetBody } from '@/components/widget';
import { useFormatDateInterval } from '@/hooks/use-format-date-interval';
import { useNumber } from '@/hooks/use-numer-formatter';
import { getChartColor } from '@/utils/theme';
type Props = {
@@ -37,10 +37,16 @@ function Tooltip(props: any) {
{formatDate(new Date(payload.timestamp))}
</div>
<div className="flex items-center gap-2">
<div className="h-10 w-1 rounded-full" style={{ background: getChartColor(0) }} />
<div
className="h-10 w-1 rounded-full"
style={{ background: getChartColor(0) }}
/>
<div className="col gap-1">
<div className="text-muted-foreground text-sm">Total members</div>
<div className="font-semibold text-lg" style={{ color: getChartColor(0) }}>
<div
className="font-semibold text-lg"
style={{ color: getChartColor(0) }}
>
{number.format(payload.cumulative)}
</div>
</div>
@@ -87,7 +93,7 @@ export function GroupMemberGrowth({ data }: Props) {
<ResponsiveContainer>
<AreaChart data={chartData}>
<defs>
<linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
<linearGradient id={gradientId} x1="0" x2="0" y1="0" y2="1">
<stop offset="5%" stopColor={color} stopOpacity={0.3} />
<stop offset="95%" stopColor={color} stopOpacity={0.02} />
</linearGradient>
@@ -97,17 +103,18 @@ export function GroupMemberGrowth({ data }: Props) {
cursor={{ stroke: color, strokeOpacity: 0.3 }}
/>
<Area
type="monotone"
dataKey="cumulative"
dot={false}
fill={`url(#${gradientId})`}
isAnimationActive={false}
stroke={color}
strokeWidth={2}
fill={`url(#${gradientId})`}
dot={false}
isAnimationActive={false}
type="monotone"
/>
<XAxis {...xAxisProps} />
<YAxis {...yAxisProps} domain={[0, 'dataMax']} />
<YAxis {...yAxisProps} />
<CartesianGrid
className="stroke-border"
horizontal={true}
strokeDasharray="3 3"
strokeOpacity={0.5}

View File

@@ -242,6 +242,7 @@ export default function BillingUsage({ organization }: Props) {
<XAxis {...xAxisProps} dataKey="date" />
<YAxis {...yAxisProps} domain={[0, 'dataMax']} />
<CartesianGrid
className="stroke-border"
horizontal={true}
strokeDasharray="3 3"
strokeOpacity={0.5}

View File

@@ -1,4 +1,5 @@
import { Widget } from '@/components/widget';
import { ZapIcon } from 'lucide-react';
import { Widget, WidgetEmptyState } from '@/components/widget';
import { WidgetHead, WidgetTitle } from '../overview/overview-widget';
type Props = {
@@ -6,28 +7,32 @@ type Props = {
};
export const MostEvents = ({ data }: Props) => {
const max = Math.max(...data.map((item) => item.count));
const max = data.length > 0 ? Math.max(...data.map((item) => item.count)) : 0;
return (
<Widget className="w-full">
<WidgetHead>
<WidgetTitle>Popular events</WidgetTitle>
</WidgetHead>
<div className="flex flex-col gap-1 p-1">
{data.slice(0, 5).map((item) => (
<div key={item.name} className="relative px-3 py-2">
<div
className="absolute bottom-0 left-0 top-0 rounded bg-def-200"
style={{
width: `${(item.count / max) * 100}%`,
}}
/>
<div className="relative flex justify-between ">
<div>{item.name}</div>
<div>{item.count}</div>
{data.length === 0 ? (
<WidgetEmptyState icon={ZapIcon} text="No events yet" />
) : (
<div className="flex flex-col gap-1 p-1">
{data.slice(0, 5).map((item) => (
<div key={item.name} className="relative px-3 py-2">
<div
className="absolute bottom-0 left-0 top-0 rounded bg-def-200"
style={{
width: `${(item.count / max) * 100}%`,
}}
/>
<div className="relative flex justify-between ">
<div>{item.name}</div>
<div>{item.count}</div>
</div>
</div>
</div>
))}
</div>
))}
</div>
)}
</Widget>
);
};

View File

@@ -1,4 +1,5 @@
import { Widget } from '@/components/widget';
import { RouteIcon } from 'lucide-react';
import { Widget, WidgetEmptyState } from '@/components/widget';
import { WidgetHead, WidgetTitle } from '../overview/overview-widget';
type Props = {
@@ -6,28 +7,32 @@ type Props = {
};
export const PopularRoutes = ({ data }: Props) => {
const max = Math.max(...data.map((item) => item.count));
const max = data.length > 0 ? Math.max(...data.map((item) => item.count)) : 0;
return (
<Widget className="w-full">
<WidgetHead>
<WidgetTitle>Most visted pages</WidgetTitle>
</WidgetHead>
<div className="flex flex-col gap-1 p-1">
{data.slice(0, 5).map((item) => (
<div key={item.path} className="relative px-3 py-2">
<div
className="absolute bottom-0 left-0 top-0 rounded bg-def-200"
style={{
width: `${(item.count / max) * 100}%`,
}}
/>
<div className="relative flex justify-between ">
<div>{item.path}</div>
<div>{item.count}</div>
{data.length === 0 ? (
<WidgetEmptyState icon={RouteIcon} text="No pages visited yet" />
) : (
<div className="flex flex-col gap-1 p-1">
{data.slice(0, 5).map((item) => (
<div key={item.path} className="relative px-3 py-2">
<div
className="absolute bottom-0 left-0 top-0 rounded bg-def-200"
style={{
width: `${(item.count / max) * 100}%`,
}}
/>
<div className="relative flex justify-between ">
<div>{item.path}</div>
<div>{item.count}</div>
</div>
</div>
</div>
))}
</div>
))}
</div>
)}
</Widget>
);
};

View File

@@ -54,6 +54,19 @@ export function WidgetBody({ children, className }: WidgetBodyProps) {
return <div className={cn('p-4', className)}>{children}</div>;
}
export interface WidgetEmptyStateProps {
icon: LucideIcon;
text: string;
}
export function WidgetEmptyState({ icon: Icon, text }: WidgetEmptyStateProps) {
return (
<div className="flex flex-col items-center justify-center gap-2 py-8 text-muted-foreground">
<Icon size={28} strokeWidth={1.5} />
<p className="text-sm">{text}</p>
</div>
);
}
export interface WidgetProps {
children: React.ReactNode;
className?: string;

View File

@@ -9,7 +9,7 @@ import { MostEvents } from '@/components/profiles/most-events';
import { PopularRoutes } from '@/components/profiles/popular-routes';
import { ProfileActivity } from '@/components/profiles/profile-activity';
import { KeyValueGrid } from '@/components/ui/key-value-grid';
import { Widget, WidgetBody } from '@/components/widget';
import { Widget, WidgetBody, WidgetEmptyState } from '@/components/widget';
import { WidgetTable } from '@/components/widget-table';
import { useTRPC } from '@/integrations/trpc/react';
import { formatDateTime, formatTimeAgoOrDateTime } from '@/utils/date';
@@ -64,7 +64,7 @@ function Component() {
);
const g = group.data;
const m = metrics.data?.[0];
const m = metrics.data;
if (!g) {
return null;
@@ -177,9 +177,7 @@ function Component() {
</WidgetHead>
<WidgetBody className="p-0">
{members.data.length === 0 ? (
<p className="py-4 text-center text-muted-foreground text-sm">
No members found
</p>
<WidgetEmptyState icon={UsersIcon} text="No members yet" />
) : (
<WidgetTable
columnClassName="px-2"