import { useState, useEffect, useRef, type FC } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { toast } from "sonner"; import { Loader2 } from "lucide-react"; interface PingResult { seq: number; time: number; success: boolean; error?: string; } interface PingStats { sent: number; received: number; lost: number; min: number; max: number; avg: number; } const Tool: FC = () => { const [url, setUrl] = useState(""); const [running, setRunning] = useState(false); const [results, setResults] = useState([]); const [stats, setStats] = useState({ sent: 0, received: 0, lost: 0, min: 0, max: 0, avg: 0, }); const intervalRef = useRef(null); const seqRef = useRef(0); const resultsContainerRef = useRef(null); const ping = async () => { if (!url.trim()) { toast.error("Please enter a URL"); return; } const seq = ++seqRef.current; let targetUrl = url.trim(); // If no protocol prefix, default to https:// if (!targetUrl.startsWith("http://") && !targetUrl.startsWith("https://")) { targetUrl = `https://${targetUrl}`; } const startTime = performance.now(); try { await fetch(targetUrl, { method: "HEAD", mode: "no-cors", cache: "no-cache", }); const endTime = performance.now(); const time = endTime - startTime; const newResult: PingResult = { seq, time, success: true, }; setResults((prev) => [...prev, newResult]); updateStats(newResult); } catch (error: unknown) { const endTime = performance.now(); const time = endTime - startTime; const errorMessage = error instanceof Error ? error.message : "Request failed"; const newResult: PingResult = { seq, time, success: false, error: errorMessage, }; setResults((prev) => [...prev, newResult]); updateStats(newResult); } }; const updateStats = (newResult: PingResult) => { setStats((prev) => { const sent = prev.sent + 1; const received = newResult.success ? prev.received + 1 : prev.received; const lost = sent - received; let min = prev.min; let max = prev.max; let avg = prev.avg; if (newResult.success) { if (received === 1) { min = newResult.time; max = newResult.time; avg = newResult.time; } else { min = Math.min(min, newResult.time); max = Math.max(max, newResult.time); avg = (prev.avg * (received - 1) + newResult.time) / received; } } return { sent, received, lost, min, max, avg }; }); }; const startPing = () => { if (!url.trim()) { toast.error("Please enter a URL"); return; } setRunning(true); setResults([]); setStats({ sent: 0, received: 0, lost: 0, min: 0, max: 0, avg: 0, }); seqRef.current = 0; // Execute first ping immediately ping(); // Then execute every second intervalRef.current = window.setInterval(ping, 1000); }; const stopPing = () => { setRunning(false); if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } }; useEffect(() => { // Auto-scroll to bottom if (resultsContainerRef.current) { resultsContainerRef.current.scrollTop = resultsContainerRef.current.scrollHeight; } }, [results]); useEffect(() => { // Cleanup timer return () => { if (intervalRef.current) { clearInterval(intervalRef.current); } }; }, []); const lossRate = stats.sent > 0 ? ((stats.lost / stats.sent) * 100).toFixed(1) : "0.0"; return (
setUrl(e.target.value)} disabled={running} />
{!running ? ( ) : ( )}
{stats.sent > 0 && (
Statistics
Sent:
{stats.sent} packets
Received:
{stats.received} packets
Lost:
{stats.lost} packets ({lossRate}%)
{stats.received > 0 && ( <>
Min Latency:
{stats.min.toFixed(2)} ms
Max Latency:
{stats.max.toFixed(2)} ms
Avg Latency:
{stats.avg.toFixed(2)} ms
)}
)} {results.length > 0 && (
Ping Results:
{results.map((result) => (
{result.success ? ( <> seq={result.seq} time={result.time.toFixed(2)}ms ) : ( <> seq={result.seq} Request timeout {result.error && ` (${result.error})`} )}
))} {running && (
Running...
)}
)}
); }; export default Tool;