From 26e0a4f92f9498c599fbe03f55acbf038ba718ef Mon Sep 17 00:00:00 2001 From: Wavering Ana Date: Fri, 31 Jan 2025 20:25:44 -0500 Subject: [PATCH] show source per day --- frontend/src/components/LinkList.tsx | 8 +- frontend/src/components/StatisticsModal.tsx | 252 ++++++++++++-------- frontend/src/types/api.ts | 1 + src/handlers.rs | 14 +- src/models.rs | 1 + 5 files changed, 165 insertions(+), 111 deletions(-) diff --git a/frontend/src/components/LinkList.tsx b/frontend/src/components/LinkList.tsx index 1911889..6f768cf 100644 --- a/frontend/src/components/LinkList.tsx +++ b/frontend/src/components/LinkList.tsx @@ -85,7 +85,13 @@ export function LinkList({ refresh = 0 }: LinkListProps) { const baseUrl = window.location.origin navigator.clipboard.writeText(`${baseUrl}/${shortCode}`) toast({ - description: "Link copied to clipboard", + description: ( + <> + Link copied to clipboard +
+ You can add ?source=TextHere to the end of the link to track the source of clicks + + ), }) } diff --git a/frontend/src/components/StatisticsModal.tsx b/frontend/src/components/StatisticsModal.tsx index 1df2bc4..db936dd 100644 --- a/frontend/src/components/StatisticsModal.tsx +++ b/frontend/src/components/StatisticsModal.tsx @@ -1,121 +1,165 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { - LineChart, - Line, - XAxis, - YAxis, - CartesianGrid, - Tooltip, - ResponsiveContainer, + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, } from "recharts"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { toast } from "@/hooks/use-toast" +import { toast } from "@/hooks/use-toast"; import { useState, useEffect } from "react"; -import { getLinkClickStats, getLinkSourceStats } from '../api/client'; -import { ClickStats, SourceStats } from '../types/api'; +import { getLinkClickStats, getLinkSourceStats } from "../api/client"; +import { ClickStats, SourceStats } from "../types/api"; interface StatisticsModalProps { - isOpen: boolean; - onClose: () => void; - linkId: number; + isOpen: boolean; + onClose: () => void; + linkId: number; } +interface EnhancedClickStats extends ClickStats { + sources?: { source: string; count: number }[]; +} + +const CustomTooltip = ({ + active, + payload, + label, +}: { + active?: boolean; + payload?: any[]; + label?: string; +}) => { + if (active && payload && payload.length > 0) { + const data = payload[0].payload; + return ( +
+

{label}

+

Clicks: {data.clicks}

+ {data.sources && data.sources.length > 0 && ( +
+

Sources:

+
    + {data.sources.map((source: { source: string; count: number }) => ( +
  • + {source.source}: {source.count} +
  • + ))} +
+
+ )} +
+ ); + } + return null; +}; + export function StatisticsModal({ isOpen, onClose, linkId }: StatisticsModalProps) { - const [clicksOverTime, setClicksOverTime] = useState([]); - const [sourcesData, setSourcesData] = useState([]); - const [loading, setLoading] = useState(true); + const [clicksOverTime, setClicksOverTime] = useState([]); + const [sourcesData, setSourcesData] = useState([]); + const [loading, setLoading] = useState(true); - useEffect(() => { - if (isOpen && linkId) { - const fetchData = async () => { - try { - setLoading(true); - const [clicksData, sourcesData] = await Promise.all([ - getLinkClickStats(linkId), - getLinkSourceStats(linkId), - ]); - setClicksOverTime(clicksData); - setSourcesData(sourcesData); - } catch (error: any) { - console.error("Failed to fetch statistics:", error); - toast({ - variant: "destructive", - title: "Error", - description: error.response?.data || "Failed to load statistics", - }); - } finally { - setLoading(false); - } - }; - - fetchData(); + useEffect(() => { + if (isOpen && linkId) { + const fetchData = async () => { + try { + setLoading(true); + const [clicksData, sourcesData] = await Promise.all([ + getLinkClickStats(linkId), + getLinkSourceStats(linkId), + ]); + + // Enhance clicks data with source information + const enhancedClicksData = clicksData.map((clickData) => ({ + ...clickData, + sources: sourcesData.filter((source) => source.date === clickData.date), + })); + + setClicksOverTime(enhancedClicksData); + setSourcesData(sourcesData); + } catch (error: any) { + console.error("Failed to fetch statistics:", error); + toast({ + variant: "destructive", + title: "Error", + description: error.response?.data || "Failed to load statistics", + }); + } finally { + setLoading(false); } - }, [isOpen, linkId]); + }; - return ( - - - - Link Statistics - + fetchData(); + } + }, [isOpen, linkId]); - {loading ? ( -
Loading...
- ) : ( -
- - - Clicks Over Time - - -
- - - - - - - - - -
-
-
+ return ( + + + + Link Statistics + - - - Top Sources - - -
    - {sourcesData.map((source, index) => ( -
  • - - - {index + 1}. - - {source.source} - - - {source.count} clicks - -
  • - ))} -
-
-
-
- )} -
-
- ); + {loading ? ( +
Loading...
+ ) : ( +
+ + + Clicks Over Time + + +
+ + + + + + } /> + + + +
+
+
+ + + + Top Sources + + +
    + {sourcesData.map((source, index) => ( +
  • + + + {index + 1}. + + {source.source} + + + {source.count} clicks + +
  • + ))} +
+
+
+
+ )} + + + ); } diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts index 2d30756..a2b5322 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -32,6 +32,7 @@ export interface ClickStats { } export interface SourceStats { + date: string; source: string; count: number; } diff --git a/src/handlers.rs b/src/handlers.rs index 7d505c3..6f0a488 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -643,15 +643,16 @@ pub async fn get_link_sources( sqlx::query_as::<_, SourceStats>( r#" SELECT + DATE(created_at)::text as date, query_source as source, COUNT(*)::bigint as count FROM clicks WHERE link_id = $1 AND query_source IS NOT NULL AND query_source != '' - GROUP BY query_source - ORDER BY COUNT(*) DESC - LIMIT 10 + GROUP BY DATE(created_at), query_source + ORDER BY DATE(created_at) ASC, COUNT(*) DESC + LIMIT 300 "#, ) .bind(link_id) @@ -662,15 +663,16 @@ pub async fn get_link_sources( sqlx::query_as::<_, SourceStats>( r#" SELECT + DATE(created_at) as date, query_source as source, COUNT(*) as count FROM clicks WHERE link_id = ? AND query_source IS NOT NULL AND query_source != '' - GROUP BY query_source - ORDER BY COUNT(*) DESC - LIMIT 10 + GROUP BY DATE(created_at), query_source + ORDER BY DATE(created_at) ASC, COUNT(*) DESC + LIMIT 300 "#, ) .bind(link_id) diff --git a/src/models.rs b/src/models.rs index 9e97dcf..1af84da 100644 --- a/src/models.rs +++ b/src/models.rs @@ -150,6 +150,7 @@ pub struct ClickStats { #[derive(sqlx::FromRow, Serialize)] pub struct SourceStats { + pub date: String, pub source: String, pub count: i64, }