almost production ready

This commit is contained in:
WaveringAna 2025-01-26 01:30:51 -05:00
parent 3932b48a64
commit 937b3fc811
21 changed files with 1071 additions and 55 deletions

View file

@ -1,5 +1,5 @@
import axios from 'axios';
import { CreateLinkRequest, Link, AuthResponse } from '../types/api';
import { CreateLinkRequest, Link, AuthResponse, ClickStats, SourceStats } from '../types/api';
// Create axios instance with default config
const api = axios.create({
@ -45,4 +45,16 @@ export const getAllLinks = async () => {
export const deleteLink = async (id: number) => {
await api.delete(`/links/${id}`);
};
};
export const getLinkClickStats = async (id: number) => {
const response = await api.get<ClickStats[]>(`/links/${id}/clicks`);
return response.data;
};
export const getLinkSourceStats = async (id: number) => {
const response = await api.get<SourceStats[]>(`/links/${id}/sources`);
return response.data;
};
export { api };

View file

@ -12,7 +12,7 @@ import {
} from "@/components/ui/table"
import { Button } from "@/components/ui/button"
import { useToast } from "@/hooks/use-toast"
import { Copy, Trash2 } from "lucide-react"
import { Copy, Trash2, BarChart2 } from "lucide-react"
import {
Dialog,
DialogContent,
@ -22,6 +22,8 @@ import {
DialogFooter,
} from "@/components/ui/dialog"
import { StatisticsModal } from "./StatisticsModal"
interface LinkListProps {
refresh?: number;
}
@ -33,6 +35,10 @@ export function LinkList({ refresh = 0 }: LinkListProps) {
isOpen: false,
linkId: null,
})
const [statsModal, setStatsModal] = useState<{ isOpen: boolean; linkId: number | null }>({
isOpen: false,
linkId: null,
});
const { toast } = useToast()
const fetchLinks = async () => {
@ -145,6 +151,15 @@ export function LinkList({ refresh = 0 }: LinkListProps) {
<Copy className="h-4 w-4" />
<span className="sr-only">Copy link</span>
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => setStatsModal({ isOpen: true, linkId: link.id })}
>
<BarChart2 className="h-4 w-4" />
<span className="sr-only">View statistics</span>
</Button>
<Button
variant="ghost"
size="icon"
@ -163,6 +178,11 @@ export function LinkList({ refresh = 0 }: LinkListProps) {
</div>
</CardContent>
</Card>
<StatisticsModal
isOpen={statsModal.isOpen}
onClose={() => setStatsModal({ isOpen: false, linkId: null })}
linkId={statsModal.linkId!}
/>
</>
)
}

View file

@ -0,0 +1,115 @@
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from "recharts";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { useState, useEffect } from "react";
import { getLinkClickStats, getLinkSourceStats } from '../api/client';
import { ClickStats, SourceStats } from '../types/api';
interface StatisticsModalProps {
isOpen: boolean;
onClose: () => void;
linkId: number;
}
export function StatisticsModal({ isOpen, onClose, linkId }: StatisticsModalProps) {
const [clicksOverTime, setClicksOverTime] = useState<ClickStats[]>([]);
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) {
console.error("Failed to fetch statistics:", error);
} finally {
setLoading(false);
}
};
fetchData();
}
}, [isOpen, linkId]);
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle>Link Statistics</DialogTitle>
</DialogHeader>
{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>
<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>
);
}

View file

@ -24,4 +24,14 @@ export interface AuthResponse {
export interface ApiError {
error: string;
}
}
export interface ClickStats {
date: string;
clicks: number;
}
export interface SourceStats {
source: string;
count: number;
}