Skip to content

Commit 84b53d5

Browse files
committed
feat: update charts, make faster
1 parent 0cafb0c commit 84b53d5

File tree

7 files changed

+382
-228
lines changed

7 files changed

+382
-228
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
'use client';
2+
3+
import { memo } from 'react';
4+
import {
5+
Area,
6+
AreaChart,
7+
ResponsiveContainer,
8+
Tooltip,
9+
XAxis,
10+
YAxis,
11+
} from 'recharts';
12+
13+
interface MiniChartProps {
14+
data: { date: string; value: number }[];
15+
id: string;
16+
}
17+
18+
const formatNumber = (num: number) => {
19+
if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(1)}M`;
20+
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`;
21+
return num.toString();
22+
};
23+
24+
const MiniChart = memo(({ data, id }: MiniChartProps) => (
25+
<div className="chart-container">
26+
<ResponsiveContainer height={50} width="100%">
27+
<AreaChart
28+
data={data}
29+
margin={{ top: 5, right: 0, left: 0, bottom: 0 }}
30+
>
31+
<defs>
32+
<linearGradient id={`gradient-${id}`} x1="0" x2="0" y1="0" y2="1">
33+
<stop
34+
offset="5%"
35+
stopColor="var(--chart-color)"
36+
stopOpacity={0.8}
37+
/>
38+
<stop
39+
offset="95%"
40+
stopColor="var(--chart-color)"
41+
stopOpacity={0.1}
42+
/>
43+
</linearGradient>
44+
</defs>
45+
<XAxis dataKey="date" hide />
46+
<YAxis domain={['dataMin - 5', 'dataMax + 5']} hide />
47+
<Tooltip
48+
content={({ active, payload, label }) =>
49+
active && payload?.[0] && typeof payload[0].value === 'number' ? (
50+
<div className="rounded-lg border bg-background p-2 text-sm shadow-lg">
51+
<p className="font-medium">
52+
{new Date(label).toLocaleDateString('en-US', {
53+
month: 'short',
54+
day: 'numeric',
55+
})}
56+
</p>
57+
<p className="text-primary">
58+
{formatNumber(payload[0].value)} views
59+
</p>
60+
</div>
61+
) : null
62+
}
63+
/>
64+
<Area
65+
dataKey="value"
66+
dot={false}
67+
fill={`url(#gradient-${id})`}
68+
stroke="var(--chart-color)"
69+
strokeWidth={2.5}
70+
type="monotone"
71+
/>
72+
</AreaChart>
73+
</ResponsiveContainer>
74+
</div>
75+
));
76+
77+
MiniChart.displayName = 'MiniChart';
78+
79+
export default MiniChart;
Lines changed: 22 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,13 @@
1-
import type { MiniChartDataPoint, Website } from '@databuddy/shared';
1+
import type { Website, ProcessedMiniChartData } from '@databuddy/shared';
22
import {
33
ArrowRightIcon,
4-
GlobeIcon,
54
MinusIcon,
65
TrendDownIcon,
76
TrendUpIcon,
87
} from '@phosphor-icons/react';
8+
import dynamic from 'next/dynamic';
99
import Link from 'next/link';
10-
import { memo, useMemo } from 'react';
11-
import {
12-
Area,
13-
AreaChart,
14-
ResponsiveContainer,
15-
Tooltip,
16-
XAxis,
17-
YAxis,
18-
} from 'recharts';
10+
import { memo, Suspense } from 'react';
1911
import { FaviconImage } from '@/components/analytics/favicon-image';
2012
import {
2113
Card,
@@ -28,7 +20,7 @@ import { Skeleton } from '@/components/ui/skeleton';
2820

2921
interface WebsiteCardProps {
3022
website: Website;
31-
chartData?: MiniChartDataPoint[];
23+
chartData?: ProcessedMiniChartData;
3224
isLoadingChart?: boolean;
3325
}
3426

@@ -38,97 +30,17 @@ const formatNumber = (num: number) => {
3830
return num.toString();
3931
};
4032

41-
const getTrend = (data: MiniChartDataPoint[]) => {
42-
if (!data || data.length === 0) return null;
43-
44-
const mid = Math.floor(data.length / 2);
45-
const [first, second] = [data.slice(0, mid), data.slice(mid)];
46-
47-
const avg = (arr: MiniChartDataPoint[]) =>
48-
arr.length > 0 ? arr.reduce((sum, p) => sum + p.value, 0) / arr.length : 0;
49-
const [prevAvg, currAvg] = [avg(first), avg(second)];
50-
51-
if (prevAvg === 0)
52-
return currAvg > 0
53-
? { type: 'up', value: 100 }
54-
: { type: 'neutral', value: 0 };
55-
56-
const change = ((currAvg - prevAvg) / prevAvg) * 100;
57-
let type: 'up' | 'down' | 'neutral' = 'neutral';
58-
if (change > 5) type = 'up';
59-
else if (change < -5) type = 'down';
60-
return { type, value: Math.abs(change) };
61-
};
62-
63-
const Chart = memo(
64-
({ data, id }: { data: MiniChartDataPoint[]; id: string }) => (
65-
<div className="chart-container">
66-
<ResponsiveContainer height={50} width="100%">
67-
<AreaChart
68-
data={data}
69-
margin={{ top: 5, right: 0, left: 0, bottom: 0 }}
70-
>
71-
<defs>
72-
<linearGradient id={`gradient-${id}`} x1="0" x2="0" y1="0" y2="1">
73-
<stop
74-
offset="5%"
75-
stopColor="var(--chart-color)"
76-
stopOpacity={0.8}
77-
/>
78-
<stop
79-
offset="95%"
80-
stopColor="var(--chart-color)"
81-
stopOpacity={0.1}
82-
/>
83-
</linearGradient>
84-
</defs>
85-
<XAxis dataKey="date" hide />
86-
<YAxis domain={['dataMin - 5', 'dataMax + 5']} hide />
87-
<Tooltip
88-
content={({ active, payload, label }) =>
89-
active && payload?.[0] && typeof payload[0].value === 'number' ? (
90-
<div className="rounded-lg border bg-background p-2 text-sm shadow-lg">
91-
<p className="font-medium">
92-
{new Date(label).toLocaleDateString('en-US', {
93-
month: 'short',
94-
day: 'numeric',
95-
})}
96-
</p>
97-
<p className="text-primary">
98-
{formatNumber(payload[0].value)} views
99-
</p>
100-
</div>
101-
) : null
102-
}
103-
/>
104-
<Area
105-
dataKey="value"
106-
dot={false}
107-
fill={`url(#gradient-${id})`}
108-
stroke="var(--chart-color)"
109-
strokeWidth={2.5}
110-
type="monotone"
111-
/>
112-
</AreaChart>
113-
</ResponsiveContainer>
114-
</div>
115-
)
33+
// Lazy load the chart component to improve initial page load
34+
const MiniChart = dynamic(
35+
() => import('./mini-chart').then(mod => mod.default),
36+
{
37+
loading: () => <Skeleton className="h-12 w-full rounded" />,
38+
ssr: false,
39+
}
11640
);
11741

118-
Chart.displayName = 'Chart';
119-
12042
export const WebsiteCard = memo(
12143
({ website, chartData, isLoadingChart }: WebsiteCardProps) => {
122-
const data = chartData || [];
123-
124-
const { totalViews, trend } = useMemo(
125-
() => ({
126-
totalViews: data.reduce((sum, point) => sum + point.value, 0),
127-
trend: getTrend(data),
128-
}),
129-
[data]
130-
);
131-
13244
return (
13345
<Link
13446
className="group block"
@@ -173,15 +85,15 @@ export const WebsiteCard = memo(
17385
<Skeleton className="h-12 w-full rounded" />
17486
</div>
17587
) : chartData ? (
176-
data.length > 0 ? (
88+
chartData.data.length > 0 ? (
17789
<div className="space-y-2">
17890
<div className="flex items-center justify-between">
17991
<span className="font-medium text-muted-foreground text-xs">
180-
{formatNumber(totalViews)} views
92+
{formatNumber(chartData.totalViews)} views
18193
</span>
182-
{trend && (
94+
{chartData.trend && (
18395
<div className="flex items-center gap-1 font-medium text-xs">
184-
{trend.type === 'up' ? (
96+
{chartData.trend.type === 'up' ? (
18597
<>
18698
<TrendUpIcon
18799
aria-hidden="true"
@@ -193,10 +105,10 @@ export const WebsiteCard = memo(
193105
className="!text-success"
194106
style={{ color: 'var(--tw-success, #22c55e)' }}
195107
>
196-
+{trend.value.toFixed(0)}%
108+
+{chartData.trend.value.toFixed(0)}%
197109
</span>
198110
</>
199-
) : trend.type === 'down' ? (
111+
) : chartData.trend.type === 'down' ? (
200112
<>
201113
<TrendDownIcon
202114
aria-hidden="true"
@@ -212,7 +124,7 @@ export const WebsiteCard = memo(
212124
color: 'var(--tw-destructive, #ef4444)',
213125
}}
214126
>
215-
-{trend.value.toFixed(0)}%
127+
-{chartData.trend.value.toFixed(0)}%
216128
</span>
217129
</>
218130
) : (
@@ -229,7 +141,9 @@ export const WebsiteCard = memo(
229141
)}
230142
</div>
231143
<div className="transition-colors duration-300 [--chart-color:theme(colors.primary.DEFAULT)] group-hover:[--chart-color:theme(colors.primary.600)]">
232-
<Chart data={data} id={website.id} />
144+
<Suspense fallback={<Skeleton className="h-12 w-full rounded" />}>
145+
<MiniChart data={chartData.data} id={website.id} />
146+
</Suspense>
233147
</div>
234148
</div>
235149
) : (
@@ -263,4 +177,4 @@ export function WebsiteCardSkeleton() {
263177
</CardContent>
264178
</Card>
265179
);
266-
}
180+
}

apps/dashboard/app/(main)/websites/page.tsx

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { Card, CardContent, CardHeader } from '@/components/ui/card';
1414
import { Skeleton } from '@/components/ui/skeleton';
1515
import { WebsiteDialog } from '@/components/website-dialog';
1616
import { useWebsites } from '@/hooks/use-websites';
17+
1718
import { trpc } from '@/lib/trpc';
1819
import { cn } from '@/lib/utils';
1920
import { WebsiteCard } from './_components/website-card';
@@ -151,20 +152,9 @@ function ErrorState({ onRetry }: { onRetry: () => void }) {
151152
}
152153

153154
export default function WebsitesPage() {
154-
const { websites, isLoading, isError, refetch } = useWebsites();
155155
const [dialogOpen, setDialogOpen] = useState(false);
156156

157-
const websiteIds = websites.map((w) => w.id);
158-
159-
const { data: chartData, isLoading: isLoadingChart } =
160-
trpc.miniCharts.getMiniCharts.useQuery(
161-
{
162-
websiteIds,
163-
},
164-
{
165-
enabled: !isLoading && websiteIds.length > 0,
166-
}
167-
);
157+
const { websites, chartData, isLoading, isError, refetch } = useWebsites();
168158

169159
const handleRetry = () => {
170160
refetch();
@@ -266,8 +256,8 @@ export default function WebsitesPage() {
266256
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
267257
{websites.map((website) => (
268258
<WebsiteCard
269-
chartData={chartData?.[website.id] || []}
270-
isLoadingChart={isLoadingChart}
259+
chartData={chartData?.[website.id]}
260+
isLoadingChart={isLoading}
271261
key={website.id}
272262
website={website}
273263
/>

0 commit comments

Comments
 (0)