show source per day

This commit is contained in:
Wavering Ana 2025-01-31 20:25:44 -05:00
parent d1202a03fa
commit 26e0a4f92f
5 changed files with 165 additions and 111 deletions

View file

@ -85,7 +85,13 @@ export function LinkList({ refresh = 0 }: LinkListProps) {
const baseUrl = window.location.origin const baseUrl = window.location.origin
navigator.clipboard.writeText(`${baseUrl}/${shortCode}`) navigator.clipboard.writeText(`${baseUrl}/${shortCode}`)
toast({ toast({
description: "Link copied to clipboard", description: (
<>
Link copied to clipboard
<br />
You can add ?source=TextHere to the end of the link to track the source of clicks
</>
),
}) })
} }

View file

@ -9,11 +9,11 @@ import {
ResponsiveContainer, ResponsiveContainer,
} from "recharts"; } from "recharts";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 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 { useState, useEffect } from "react";
import { getLinkClickStats, getLinkSourceStats } from '../api/client'; import { getLinkClickStats, getLinkSourceStats } from "../api/client";
import { ClickStats, SourceStats } from '../types/api'; import { ClickStats, SourceStats } from "../types/api";
interface StatisticsModalProps { interface StatisticsModalProps {
isOpen: boolean; isOpen: boolean;
@ -21,8 +21,45 @@ interface StatisticsModalProps {
linkId: number; 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 (
<div className="bg-background text-foreground p-4 rounded-lg shadow-lg border">
<p className="font-medium">{label}</p>
<p className="text-sm">Clicks: {data.clicks}</p>
{data.sources && data.sources.length > 0 && (
<div className="mt-2">
<p className="font-medium text-sm">Sources:</p>
<ul className="text-sm">
{data.sources.map((source: { source: string; count: number }) => (
<li key={source.source}>
{source.source}: {source.count}
</li>
))}
</ul>
</div>
)}
</div>
);
}
return null;
};
export function StatisticsModal({ isOpen, onClose, linkId }: StatisticsModalProps) { export function StatisticsModal({ isOpen, onClose, linkId }: StatisticsModalProps) {
const [clicksOverTime, setClicksOverTime] = useState<ClickStats[]>([]); const [clicksOverTime, setClicksOverTime] = useState<EnhancedClickStats[]>([]);
const [sourcesData, setSourcesData] = useState<SourceStats[]>([]); const [sourcesData, setSourcesData] = useState<SourceStats[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@ -35,7 +72,14 @@ export function StatisticsModal({ isOpen, onClose, linkId }: StatisticsModalProp
getLinkClickStats(linkId), getLinkClickStats(linkId),
getLinkSourceStats(linkId), getLinkSourceStats(linkId),
]); ]);
setClicksOverTime(clicksData);
// Enhance clicks data with source information
const enhancedClicksData = clicksData.map((clickData) => ({
...clickData,
sources: sourcesData.filter((source) => source.date === clickData.date),
}));
setClicksOverTime(enhancedClicksData);
setSourcesData(sourcesData); setSourcesData(sourcesData);
} catch (error: any) { } catch (error: any) {
console.error("Failed to fetch statistics:", error); console.error("Failed to fetch statistics:", error);
@ -75,7 +119,7 @@ export function StatisticsModal({ isOpen, onClose, linkId }: StatisticsModalProp
<CartesianGrid strokeDasharray="3 3" /> <CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" /> <XAxis dataKey="date" />
<YAxis /> <YAxis />
<Tooltip /> <Tooltip content={<CustomTooltip />} />
<Line <Line
type="monotone" type="monotone"
dataKey="clicks" dataKey="clicks"

View file

@ -32,6 +32,7 @@ export interface ClickStats {
} }
export interface SourceStats { export interface SourceStats {
date: string;
source: string; source: string;
count: number; count: number;
} }

View file

@ -643,15 +643,16 @@ pub async fn get_link_sources(
sqlx::query_as::<_, SourceStats>( sqlx::query_as::<_, SourceStats>(
r#" r#"
SELECT SELECT
DATE(created_at)::text as date,
query_source as source, query_source as source,
COUNT(*)::bigint as count COUNT(*)::bigint as count
FROM clicks FROM clicks
WHERE link_id = $1 WHERE link_id = $1
AND query_source IS NOT NULL AND query_source IS NOT NULL
AND query_source != '' AND query_source != ''
GROUP BY query_source GROUP BY DATE(created_at), query_source
ORDER BY COUNT(*) DESC ORDER BY DATE(created_at) ASC, COUNT(*) DESC
LIMIT 10 LIMIT 300
"#, "#,
) )
.bind(link_id) .bind(link_id)
@ -662,15 +663,16 @@ pub async fn get_link_sources(
sqlx::query_as::<_, SourceStats>( sqlx::query_as::<_, SourceStats>(
r#" r#"
SELECT SELECT
DATE(created_at) as date,
query_source as source, query_source as source,
COUNT(*) as count COUNT(*) as count
FROM clicks FROM clicks
WHERE link_id = ? WHERE link_id = ?
AND query_source IS NOT NULL AND query_source IS NOT NULL
AND query_source != '' AND query_source != ''
GROUP BY query_source GROUP BY DATE(created_at), query_source
ORDER BY COUNT(*) DESC ORDER BY DATE(created_at) ASC, COUNT(*) DESC
LIMIT 10 LIMIT 300
"#, "#,
) )
.bind(link_id) .bind(link_id)

View file

@ -150,6 +150,7 @@ pub struct ClickStats {
#[derive(sqlx::FromRow, Serialize)] #[derive(sqlx::FromRow, Serialize)]
pub struct SourceStats { pub struct SourceStats {
pub date: String,
pub source: String, pub source: String,
pub count: i64, pub count: i64,
} }