show source per day
This commit is contained in:
parent
d1202a03fa
commit
26e0a4f92f
5 changed files with 165 additions and 111 deletions
|
@ -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
|
||||||
|
</>
|
||||||
|
),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -32,6 +32,7 @@ export interface ClickStats {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SourceStats {
|
export interface SourceStats {
|
||||||
|
date: string;
|
||||||
source: string;
|
source: string;
|
||||||
count: number;
|
count: number;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue