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
navigator.clipboard.writeText(`${baseUrl}/${shortCode}`)
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

@ -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 (
<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) {
const [clicksOverTime, setClicksOverTime] = useState<ClickStats[]>([]);
const [sourcesData, setSourcesData] = useState<SourceStats[]>([]);
const [loading, setLoading] = useState(true);
const [clicksOverTime, setClicksOverTime] = useState<EnhancedClickStats[]>([]);
const [sourcesData, setSourcesData] = useState<SourceStats[]>([]);
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 (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle>Link Statistics</DialogTitle>
</DialogHeader>
fetchData();
}
}, [isOpen, linkId]);
{loading ? (
<div className="flex items-center justify-center h-64">Loading...</div>
) : (
<div className="grid gap-4">
<Card>
<CardHeader>
<CardTitle>Clicks Over Time</CardTitle>
</CardHeader>
<CardContent>
<div className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={clicksOverTime}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis />
<Tooltip />
<Line
type="monotone"
dataKey="clicks"
stroke="#8884d8"
strokeWidth={2}
/>
</LineChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle>Link Statistics</DialogTitle>
</DialogHeader>
<Card>
<CardHeader>
<CardTitle>Top Sources</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-2">
{sourcesData.map((source, index) => (
<li
key={source.source}
className="flex items-center justify-between py-2 border-b last:border-0"
>
<span className="text-sm">
<span className="font-medium text-muted-foreground mr-2">
{index + 1}.
</span>
{source.source}
</span>
<span className="text-sm font-medium">
{source.count} clicks
</span>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
)}
</DialogContent>
</Dialog>
);
{loading ? (
<div className="flex items-center justify-center h-64">Loading...</div>
) : (
<div className="grid gap-4">
<Card>
<CardHeader>
<CardTitle>Clicks Over Time</CardTitle>
</CardHeader>
<CardContent>
<div className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={clicksOverTime}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis />
<Tooltip content={<CustomTooltip />} />
<Line
type="monotone"
dataKey="clicks"
stroke="#8884d8"
strokeWidth={2}
/>
</LineChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Top Sources</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-2">
{sourcesData.map((source, index) => (
<li
key={source.source}
className="flex items-center justify-between py-2 border-b last:border-0"
>
<span className="text-sm">
<span className="font-medium text-muted-foreground mr-2">
{index + 1}.
</span>
{source.source}
</span>
<span className="text-sm font-medium">
{source.count} clicks
</span>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
)}
</DialogContent>
</Dialog>
);
}