Skip to content

Commit 26e0a4f

Browse files
committed
show source per day
1 parent d1202a0 commit 26e0a4f

File tree

5 files changed

+165
-111
lines changed

5 files changed

+165
-111
lines changed

frontend/src/components/LinkList.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,13 @@ export function LinkList({ refresh = 0 }: LinkListProps) {
8585
const baseUrl = window.location.origin
8686
navigator.clipboard.writeText(`${baseUrl}/${shortCode}`)
8787
toast({
88-
description: "Link copied to clipboard",
88+
description: (
89+
<>
90+
Link copied to clipboard
91+
<br />
92+
You can add ?source=TextHere to the end of the link to track the source of clicks
93+
</>
94+
),
8995
})
9096
}
9197

Lines changed: 148 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,121 +1,165 @@
11
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
22
import {
3-
LineChart,
4-
Line,
5-
XAxis,
6-
YAxis,
7-
CartesianGrid,
8-
Tooltip,
9-
ResponsiveContainer,
3+
LineChart,
4+
Line,
5+
XAxis,
6+
YAxis,
7+
CartesianGrid,
8+
Tooltip,
9+
ResponsiveContainer,
1010
} from "recharts";
1111
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
12-
import { toast } from "@/hooks/use-toast"
12+
import { toast } from "@/hooks/use-toast";
1313
import { useState, useEffect } from "react";
1414

15-
import { getLinkClickStats, getLinkSourceStats } from '../api/client';
16-
import { ClickStats, SourceStats } from '../types/api';
15+
import { getLinkClickStats, getLinkSourceStats } from "../api/client";
16+
import { ClickStats, SourceStats } from "../types/api";
1717

1818
interface StatisticsModalProps {
19-
isOpen: boolean;
20-
onClose: () => void;
21-
linkId: number;
19+
isOpen: boolean;
20+
onClose: () => void;
21+
linkId: number;
2222
}
2323

24+
interface EnhancedClickStats extends ClickStats {
25+
sources?: { source: string; count: number }[];
26+
}
27+
28+
const CustomTooltip = ({
29+
active,
30+
payload,
31+
label,
32+
}: {
33+
active?: boolean;
34+
payload?: any[];
35+
label?: string;
36+
}) => {
37+
if (active && payload && payload.length > 0) {
38+
const data = payload[0].payload;
39+
return (
40+
<div className="bg-background text-foreground p-4 rounded-lg shadow-lg border">
41+
<p className="font-medium">{label}</p>
42+
<p className="text-sm">Clicks: {data.clicks}</p>
43+
{data.sources && data.sources.length > 0 && (
44+
<div className="mt-2">
45+
<p className="font-medium text-sm">Sources:</p>
46+
<ul className="text-sm">
47+
{data.sources.map((source: { source: string; count: number }) => (
48+
<li key={source.source}>
49+
{source.source}: {source.count}
50+
</li>
51+
))}
52+
</ul>
53+
</div>
54+
)}
55+
</div>
56+
);
57+
}
58+
return null;
59+
};
60+
2461
export function StatisticsModal({ isOpen, onClose, linkId }: StatisticsModalProps) {
25-
const [clicksOverTime, setClicksOverTime] = useState<ClickStats[]>([]);
26-
const [sourcesData, setSourcesData] = useState<SourceStats[]>([]);
27-
const [loading, setLoading] = useState(true);
62+
const [clicksOverTime, setClicksOverTime] = useState<EnhancedClickStats[]>([]);
63+
const [sourcesData, setSourcesData] = useState<SourceStats[]>([]);
64+
const [loading, setLoading] = useState(true);
2865

29-
useEffect(() => {
30-
if (isOpen && linkId) {
31-
const fetchData = async () => {
32-
try {
33-
setLoading(true);
34-
const [clicksData, sourcesData] = await Promise.all([
35-
getLinkClickStats(linkId),
36-
getLinkSourceStats(linkId),
37-
]);
38-
setClicksOverTime(clicksData);
39-
setSourcesData(sourcesData);
40-
} catch (error: any) {
41-
console.error("Failed to fetch statistics:", error);
42-
toast({
43-
variant: "destructive",
44-
title: "Error",
45-
description: error.response?.data || "Failed to load statistics",
46-
});
47-
} finally {
48-
setLoading(false);
49-
}
50-
};
51-
52-
fetchData();
66+
useEffect(() => {
67+
if (isOpen && linkId) {
68+
const fetchData = async () => {
69+
try {
70+
setLoading(true);
71+
const [clicksData, sourcesData] = await Promise.all([
72+
getLinkClickStats(linkId),
73+
getLinkSourceStats(linkId),
74+
]);
75+
76+
// Enhance clicks data with source information
77+
const enhancedClicksData = clicksData.map((clickData) => ({
78+
...clickData,
79+
sources: sourcesData.filter((source) => source.date === clickData.date),
80+
}));
81+
82+
setClicksOverTime(enhancedClicksData);
83+
setSourcesData(sourcesData);
84+
} catch (error: any) {
85+
console.error("Failed to fetch statistics:", error);
86+
toast({
87+
variant: "destructive",
88+
title: "Error",
89+
description: error.response?.data || "Failed to load statistics",
90+
});
91+
} finally {
92+
setLoading(false);
5393
}
54-
}, [isOpen, linkId]);
94+
};
5595

56-
return (
57-
<Dialog open={isOpen} onOpenChange={onClose}>
58-
<DialogContent className="max-w-3xl">
59-
<DialogHeader>
60-
<DialogTitle>Link Statistics</DialogTitle>
61-
</DialogHeader>
96+
fetchData();
97+
}
98+
}, [isOpen, linkId]);
6299

63-
{loading ? (
64-
<div className="flex items-center justify-center h-64">Loading...</div>
65-
) : (
66-
<div className="grid gap-4">
67-
<Card>
68-
<CardHeader>
69-
<CardTitle>Clicks Over Time</CardTitle>
70-
</CardHeader>
71-
<CardContent>
72-
<div className="h-[300px]">
73-
<ResponsiveContainer width="100%" height="100%">
74-
<LineChart data={clicksOverTime}>
75-
<CartesianGrid strokeDasharray="3 3" />
76-
<XAxis dataKey="date" />
77-
<YAxis />
78-
<Tooltip />
79-
<Line
80-
type="monotone"
81-
dataKey="clicks"
82-
stroke="#8884d8"
83-
strokeWidth={2}
84-
/>
85-
</LineChart>
86-
</ResponsiveContainer>
87-
</div>
88-
</CardContent>
89-
</Card>
100+
return (
101+
<Dialog open={isOpen} onOpenChange={onClose}>
102+
<DialogContent className="max-w-3xl">
103+
<DialogHeader>
104+
<DialogTitle>Link Statistics</DialogTitle>
105+
</DialogHeader>
90106

91-
<Card>
92-
<CardHeader>
93-
<CardTitle>Top Sources</CardTitle>
94-
</CardHeader>
95-
<CardContent>
96-
<ul className="space-y-2">
97-
{sourcesData.map((source, index) => (
98-
<li
99-
key={source.source}
100-
className="flex items-center justify-between py-2 border-b last:border-0"
101-
>
102-
<span className="text-sm">
103-
<span className="font-medium text-muted-foreground mr-2">
104-
{index + 1}.
105-
</span>
106-
{source.source}
107-
</span>
108-
<span className="text-sm font-medium">
109-
{source.count} clicks
110-
</span>
111-
</li>
112-
))}
113-
</ul>
114-
</CardContent>
115-
</Card>
116-
</div>
117-
)}
118-
</DialogContent>
119-
</Dialog>
120-
);
107+
{loading ? (
108+
<div className="flex items-center justify-center h-64">Loading...</div>
109+
) : (
110+
<div className="grid gap-4">
111+
<Card>
112+
<CardHeader>
113+
<CardTitle>Clicks Over Time</CardTitle>
114+
</CardHeader>
115+
<CardContent>
116+
<div className="h-[300px]">
117+
<ResponsiveContainer width="100%" height="100%">
118+
<LineChart data={clicksOverTime}>
119+
<CartesianGrid strokeDasharray="3 3" />
120+
<XAxis dataKey="date" />
121+
<YAxis />
122+
<Tooltip content={<CustomTooltip />} />
123+
<Line
124+
type="monotone"
125+
dataKey="clicks"
126+
stroke="#8884d8"
127+
strokeWidth={2}
128+
/>
129+
</LineChart>
130+
</ResponsiveContainer>
131+
</div>
132+
</CardContent>
133+
</Card>
134+
135+
<Card>
136+
<CardHeader>
137+
<CardTitle>Top Sources</CardTitle>
138+
</CardHeader>
139+
<CardContent>
140+
<ul className="space-y-2">
141+
{sourcesData.map((source, index) => (
142+
<li
143+
key={source.source}
144+
className="flex items-center justify-between py-2 border-b last:border-0"
145+
>
146+
<span className="text-sm">
147+
<span className="font-medium text-muted-foreground mr-2">
148+
{index + 1}.
149+
</span>
150+
{source.source}
151+
</span>
152+
<span className="text-sm font-medium">
153+
{source.count} clicks
154+
</span>
155+
</li>
156+
))}
157+
</ul>
158+
</CardContent>
159+
</Card>
160+
</div>
161+
)}
162+
</DialogContent>
163+
</Dialog>
164+
);
121165
}

frontend/src/types/api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export interface ClickStats {
3232
}
3333

3434
export interface SourceStats {
35+
date: string;
3536
source: string;
3637
count: number;
3738
}

src/handlers.rs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -643,15 +643,16 @@ pub async fn get_link_sources(
643643
sqlx::query_as::<_, SourceStats>(
644644
r#"
645645
SELECT
646+
DATE(created_at)::text as date,
646647
query_source as source,
647648
COUNT(*)::bigint as count
648649
FROM clicks
649650
WHERE link_id = $1
650651
AND query_source IS NOT NULL
651652
AND query_source != ''
652-
GROUP BY query_source
653-
ORDER BY COUNT(*) DESC
654-
LIMIT 10
653+
GROUP BY DATE(created_at), query_source
654+
ORDER BY DATE(created_at) ASC, COUNT(*) DESC
655+
LIMIT 300
655656
"#,
656657
)
657658
.bind(link_id)
@@ -662,15 +663,16 @@ pub async fn get_link_sources(
662663
sqlx::query_as::<_, SourceStats>(
663664
r#"
664665
SELECT
666+
DATE(created_at) as date,
665667
query_source as source,
666668
COUNT(*) as count
667669
FROM clicks
668670
WHERE link_id = ?
669671
AND query_source IS NOT NULL
670672
AND query_source != ''
671-
GROUP BY query_source
672-
ORDER BY COUNT(*) DESC
673-
LIMIT 10
673+
GROUP BY DATE(created_at), query_source
674+
ORDER BY DATE(created_at) ASC, COUNT(*) DESC
675+
LIMIT 300
674676
"#,
675677
)
676678
.bind(link_id)

src/models.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ pub struct ClickStats {
150150

151151
#[derive(sqlx::FromRow, Serialize)]
152152
pub struct SourceStats {
153+
pub date: String,
153154
pub source: String,
154155
pub count: i64,
155156
}

0 commit comments

Comments
 (0)