almost production ready
This commit is contained in:
parent
3932b48a64
commit
937b3fc811
21 changed files with 1071 additions and 55 deletions
|
@ -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 };
|
|
@ -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!}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
115
frontend/src/components/StatisticsModal.tsx
Normal file
115
frontend/src/components/StatisticsModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue