Skip to content

Commit 03207f6

Browse files
committed
refactor: website card, mini chart route caching, database indexing / keys
1 parent 2d0f95d commit 03207f6

File tree

12 files changed

+187
-2558
lines changed

12 files changed

+187
-2558
lines changed

apps/dashboard/app/(main)/websites/[id]/_components/tabs/tracking-setup-tab.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import {
44
ActivityIcon,
55
ArrowClockwiseIcon,
6-
ArrowSquareOutIcon,
76
BookOpenIcon,
87
ChatCircleIcon,
98
CheckIcon,

apps/dashboard/app/(main)/websites/_components/mini-chart.tsx

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use client';
22

3+
import dayjs from 'dayjs';
34
import { memo } from 'react';
45
import {
56
Area,
@@ -13,20 +14,37 @@ import {
1314
interface MiniChartProps {
1415
data: { date: string; value: number }[];
1516
id: string;
17+
days?: number;
1618
}
1719

1820
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+
if (num >= 1_000_000) {
22+
return `${(num / 1_000_000).toFixed(1)}M`;
23+
}
24+
if (num >= 1000) {
25+
return `${(num / 1000).toFixed(1)}K`;
26+
}
2127
return num.toString();
2228
};
2329

24-
const MiniChart = memo(({ data, id }: MiniChartProps) => (
25-
<div className="chart-container">
26-
<ResponsiveContainer height={50} width="100%">
27-
<AreaChart data={data} margin={{ top: 5, right: 0, left: 0, bottom: 0 }}>
30+
const MiniChart = memo(({ data, id, days = 7 }: MiniChartProps) => (
31+
<div className="chart-container rounded">
32+
<ResponsiveContainer height={days > 14 ? 56 : 50} width="100%">
33+
<AreaChart
34+
aria-label={`Mini chart showing views for the last ${days} days`}
35+
data={data}
36+
margin={{ top: 5, right: 0, left: 0, bottom: 0 }}
37+
role="img"
38+
>
39+
<title>{`Views over time (last ${days} days)`}</title>
2840
<defs>
29-
<linearGradient id={`gradient-${id}`} x1="0" x2="0" y1="0" y2="1">
41+
<linearGradient
42+
id={`gradient-${id}-${days}`}
43+
x1="0"
44+
x2="0"
45+
y1="0"
46+
y2="1"
47+
>
3048
<stop
3149
offset="5%"
3250
stopColor="var(--chart-color)"
@@ -44,12 +62,9 @@ const MiniChart = memo(({ data, id }: MiniChartProps) => (
4462
<Tooltip
4563
content={({ active, payload, label }) =>
4664
active && payload?.[0] && typeof payload[0].value === 'number' ? (
47-
<div className="rounded-lg border bg-background p-2 text-sm shadow-lg">
65+
<div className="rounded border bg-background p-2 text-xs shadow-md">
4866
<p className="font-medium">
49-
{new Date(label).toLocaleDateString('en-US', {
50-
month: 'short',
51-
day: 'numeric',
52-
})}
67+
{dayjs(label as string).format('MMM D')}
5368
</p>
5469
<p className="text-primary">
5570
{formatNumber(payload[0].value)} views
@@ -59,10 +74,16 @@ const MiniChart = memo(({ data, id }: MiniChartProps) => (
5974
}
6075
/>
6176
<Area
77+
activeDot={{ r: 3 }}
78+
animationDuration={600}
79+
animationEasing="ease-out"
6280
dataKey="value"
6381
dot={false}
64-
fill={`url(#gradient-${id})`}
82+
fill={`url(#gradient-${id}-${days})`}
83+
isAnimationActive
6584
stroke="var(--chart-color)"
85+
strokeLinecap="round"
86+
strokeLinejoin="round"
6687
strokeWidth={2.5}
6788
type="monotone"
6889
/>

apps/dashboard/app/(main)/websites/_components/website-card.tsx

Lines changed: 81 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,64 @@ interface WebsiteCardProps {
2424
isLoadingChart?: boolean;
2525
}
2626

27+
function TrendStat({
28+
trend,
29+
className = 'flex items-center gap-1 font-medium text-xs sm:text-sm',
30+
}: {
31+
trend: ProcessedMiniChartData['trend'] | undefined;
32+
className?: string;
33+
}) {
34+
if (!trend) {
35+
return null;
36+
}
37+
if (trend.type === 'up') {
38+
return (
39+
<div className={className}>
40+
<TrendUpIcon
41+
aria-hidden="true"
42+
className="!text-success h-4 w-4"
43+
style={{ color: 'var(--tw-success, #22c55e)' }}
44+
weight="duotone"
45+
/>
46+
<span
47+
className="!text-success"
48+
style={{ color: 'var(--tw-success, #22c55e)' }}
49+
>
50+
+{trend.value.toFixed(0)}%
51+
</span>
52+
</div>
53+
);
54+
}
55+
if (trend.type === 'down') {
56+
return (
57+
<div className={className}>
58+
<TrendDownIcon
59+
aria-hidden
60+
className="!text-destructive h-4 w-4"
61+
style={{ color: 'var(--tw-destructive, #ef4444)' }}
62+
weight="duotone"
63+
/>
64+
<span
65+
className="!text-destructive"
66+
style={{ color: 'var(--tw-destructive, #ef4444)' }}
67+
>
68+
-{trend.value.toFixed(0)}%
69+
</span>
70+
</div>
71+
);
72+
}
73+
return (
74+
<div className={className}>
75+
<MinusIcon
76+
aria-hidden
77+
className="h-4 w-4 text-muted-foreground"
78+
weight="fill"
79+
/>
80+
<span className="text-muted-foreground">0%</span>
81+
</div>
82+
);
83+
}
84+
2785
const formatNumber = (num: number) => {
2886
if (num >= 1_000_000) {
2987
return `${(num / 1_000_000).toFixed(1)}M`;
@@ -47,18 +105,19 @@ export const WebsiteCard = memo(
47105
({ website, chartData, isLoadingChart }: WebsiteCardProps) => {
48106
return (
49107
<Link
50-
className="group block"
108+
aria-label={`Open ${website.name} analytics`}
109+
className="group block rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background"
51110
data-section="website-grid"
52111
data-track="website-card-click"
53112
data-website-id={website.id}
54113
data-website-name={website.name}
55114
href={`/websites/${website.id}`}
56115
>
57-
<Card className="flex h-full select-none flex-col bg-background transition-all duration-300 ease-in-out group-hover:border-primary/60 group-hover:shadow-primary/5 group-hover:shadow-xl">
116+
<Card className="flex h-full select-none flex-col overflow-hidden bg-background transition-all duration-300 ease-in-out group-hover:border-primary/60 group-hover:shadow-primary/5 group-hover:shadow-xl motion-reduce:transform-none motion-reduce:transition-none">
58117
<CardHeader className="pb-2">
59-
<div className="flex items-center justify-between">
118+
<div className="flex items-center justify-between gap-2">
60119
<div className="min-w-0 flex-1">
61-
<CardTitle className="truncate font-bold text-base transition-colors group-hover:text-primary">
120+
<CardTitle className="truncate font-bold text-base leading-tight transition-colors group-hover:text-primary sm:text-lg">
62121
{website.name}
63122
</CardTitle>
64123
<CardDescription className="flex items-center gap-1 pt-0.5">
@@ -68,7 +127,9 @@ export const WebsiteCard = memo(
68127
domain={website.domain}
69128
size={24}
70129
/>
71-
<span className="truncate text-xs">{website.domain}</span>
130+
<span className="truncate text-xs sm:text-sm">
131+
{website.domain}
132+
</span>
72133
</CardDescription>
73134
</div>
74135
<ArrowRightIcon
@@ -86,69 +147,28 @@ export const WebsiteCard = memo(
86147
<Skeleton className="h-3 w-12 rounded" />
87148
<Skeleton className="h-3 w-8 rounded" />
88149
</div>
89-
<Skeleton className="h-12 w-full rounded" />
150+
<Skeleton className="h-12 w-full rounded sm:h-16" />
90151
</div>
91152
) : chartData ? (
92153
chartData.data.length > 0 ? (
93154
<div className="space-y-2">
94155
<div className="flex items-center justify-between">
95-
<span className="font-medium text-muted-foreground text-xs">
156+
<span className="font-medium text-muted-foreground text-xs sm:text-sm">
96157
{formatNumber(chartData.totalViews)} views
97158
</span>
98-
{chartData.trend && (
99-
<div className="flex items-center gap-1 font-medium text-xs">
100-
{chartData.trend.type === 'up' ? (
101-
<>
102-
<TrendUpIcon
103-
aria-hidden="true"
104-
className="!text-success h-4 w-4"
105-
style={{ color: 'var(--tw-success, #22c55e)' }}
106-
weight="fill"
107-
/>
108-
<span
109-
className="!text-success"
110-
style={{ color: 'var(--tw-success, #22c55e)' }}
111-
>
112-
+{chartData.trend.value.toFixed(0)}%
113-
</span>
114-
</>
115-
) : chartData.trend.type === 'down' ? (
116-
<>
117-
<TrendDownIcon
118-
aria-hidden="true"
119-
className="!text-destructive h-4 w-4"
120-
style={{
121-
color: 'var(--tw-destructive, #ef4444)',
122-
}}
123-
weight="fill"
124-
/>
125-
<span
126-
className="!text-destructive"
127-
style={{
128-
color: 'var(--tw-destructive, #ef4444)',
129-
}}
130-
>
131-
-{chartData.trend.value.toFixed(0)}%
132-
</span>
133-
</>
134-
) : (
135-
<>
136-
<MinusIcon
137-
aria-hidden="true"
138-
className="h-4 w-4 text-muted-foreground"
139-
weight="fill"
140-
/>
141-
<span className="text-muted-foreground">0%</span>
142-
</>
143-
)}
144-
</div>
145-
)}
159+
<TrendStat trend={chartData.trend} />
146160
</div>
147-
<div className="transition-colors duration-300 [--chart-color:theme(colors.primary.DEFAULT)] group-hover:[--chart-color:theme(colors.primary.600)]">
161+
<div className="transition-colors duration-300 [--chart-color:theme(colors.primary.DEFAULT)] motion-reduce:transition-none group-hover:[--chart-color:theme(colors.primary.600)]">
148162
<Suspense
149-
fallback={<Skeleton className="h-12 w-full rounded" />}
163+
fallback={
164+
<Skeleton className="h-12 w-full rounded sm:h-16" />
165+
}
150166
>
151-
<MiniChart data={chartData.data} id={website.id} />
167+
<MiniChart
168+
data={chartData.data}
169+
days={chartData.data.length}
170+
id={website.id}
171+
/>
152172
</Suspense>
153173
</div>
154174
</div>
@@ -175,11 +195,11 @@ export function WebsiteCardSkeleton() {
175195
return (
176196
<Card className="h-full">
177197
<CardHeader>
178-
<Skeleton className="h-6 w-3/4 rounded-md" />
179-
<Skeleton className="mt-1 h-4 w-1/2 rounded-md" />
198+
<Skeleton className="h-6 w-3/4 rounded" />
199+
<Skeleton className="mt-1 h-4 w-1/2 rounded" />
180200
</CardHeader>
181201
<CardContent>
182-
<Skeleton className="h-20 w-full rounded-md" />
202+
<Skeleton className="h-20 w-full rounded sm:h-24" />
183203
</CardContent>
184204
</Card>
185205
);

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

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,8 @@ function ErrorState({ onRetry }: { onRetry: () => void }) {
154154
export default function WebsitesPage() {
155155
const [dialogOpen, setDialogOpen] = useState(false);
156156

157-
const { websites, chartData, isLoading, isError, refetch } = useWebsites();
157+
const { websites, chartData, isLoading, isError, isFetching, refetch } =
158+
useWebsites();
158159

159160
const handleRetry = () => {
160161
refetch();
@@ -188,13 +189,14 @@ export default function WebsitesPage() {
188189
<div className="flex items-center gap-2">
189190
<Button
190191
aria-label="Refresh websites"
191-
disabled={isLoading}
192+
disabled={isLoading || isFetching}
192193
onClick={() => refetch()}
193194
size="icon"
194195
variant="outline"
195196
>
196197
<ArrowClockwiseIcon
197-
className={`h-4 w-4 ${isLoading ? 'animate-spin' : ''}`}
198+
aria-hidden
199+
className={`h-4 w-4 ${isLoading || isFetching ? 'animate-spin' : ''}`}
198200
/>
199201
</Button>
200202
<Button
@@ -218,7 +220,10 @@ export default function WebsitesPage() {
218220
</div>
219221

220222
{/* Content area */}
221-
<div className="flex-1 overflow-y-auto p-3 sm:p-4 lg:p-6">
223+
<div
224+
aria-busy={isFetching}
225+
className="flex-1 overflow-y-auto p-3 sm:p-4 lg:p-6"
226+
>
222227
{/* Website count indicator */}
223228
{!isLoading && websites && websites.length > 0 && (
224229
<div className="mb-6">
@@ -253,11 +258,16 @@ export default function WebsitesPage() {
253258

254259
{/* Show website grid */}
255260
{!(isLoading || isError) && websites && websites.length > 0 && (
256-
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
261+
<div
262+
aria-label="Websites list"
263+
aria-live="polite"
264+
className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
265+
role="region"
266+
>
257267
{websites.map((website) => (
258268
<WebsiteCard
259269
chartData={chartData?.[website.id]}
260-
isLoadingChart={isLoading}
270+
isLoadingChart={isLoading || isFetching}
261271
key={website.id}
262272
website={website}
263273
/>

0 commit comments

Comments
 (0)