diff --git a/IMPROVEMENTS.md b/IMPROVEMENTS.md index 7e53510..64b0225 100644 --- a/IMPROVEMENTS.md +++ b/IMPROVEMENTS.md @@ -1,5 +1,33 @@ # PingDiff Improvement Log +## 2026-03-20 — Bug Fix: Per-page metadata for dashboard, download, and community + +All three main pages used `"use client"` at the top level, which blocks Next.js from reading the +`metadata` export. Every page fell back to the generic homepage title and description — so sharing +any page on Discord, Twitter, or iMessage showed "PingDiff - Test Your Game Server Connection" +regardless of which page it was. The `template: "%s | PingDiff"` in `layout.tsx` was dead code. + +Fixed by splitting each page into a thin server wrapper (`page.tsx`, exports metadata) and a +client component (`*Client.tsx`, holds all interactive state). No logic changed — pure structural +refactor following the standard App Router pattern. Each page now has a distinct title, +description, and og:title/og:description for accurate social previews. + +**Files changed:** `web/src/app/dashboard/page.tsx`, `web/src/app/dashboard/DashboardClient.tsx` (new), `web/src/app/download/page.tsx`, `web/src/app/download/DownloadClient.tsx` (new), `web/src/app/community/page.tsx`, `web/src/app/community/CommunityClient.tsx` (new) +**Lines:** +934 / -890 + +## 2026-03-20 — Accessibility: ARIA labels, table semantics, and skip navigation + +Comprehensive a11y pass across the dashboard, navbar, and secondary pages. The site had no named navigation landmark, no skip links on 3 of 4 pages, tables without column scope attributes, charts completely invisible to assistive technology, and stat cards that conveyed quality purely through color (WCAG 1.4.1 violation). All fixed without new dependencies. + +Dashboard: skip link + main-content anchor, role="region" on stats grid, aria-label on each stat card with text description, aria-hidden on decorative icons, role="img" + aria-label on both charts, aria-label on table element, scope="col" on all th elements, time element for timestamps, aria-label on ping/loss cells so quality is communicated in text not just color. + +Navbar: aria-label="Main navigation" on nav element, aria-haspopup on mobile toggle, role="menu" on mobile menu container. + +Community and Download pages: both were missing skip-to-content links and main-content anchor targets entirely. + +**Files changed:** `web/src/app/dashboard/page.tsx`, `web/src/components/Navbar.tsx`, `web/src/app/community/page.tsx`, `web/src/app/download/page.tsx` +**Lines:** +64 / -30 + ## 2026-03-19 — Performance: Memoize dashboard derived state All derived values on the dashboard (filteredResults, avgPing, avgPacketLoss, avgJitter, regions, chartData, serverChartData) were being recomputed inline on every React render — including renders triggered by unrelated state changes like the loading flag toggling off. Wrapped each value in useMemo with the tightest possible dependency array, eliminating 5 O(n) reduce passes and 2 groupBy passes on every extraneous render. At current scale the savings are modest; at the 500-1000 result range the dashboard would hit without this change the difference is measurable. The memoized structure also makes data dependencies explicit and auditable at a glance. diff --git a/web/src/app/community/CommunityClient.tsx b/web/src/app/community/CommunityClient.tsx new file mode 100644 index 0000000..d2d5d54 --- /dev/null +++ b/web/src/app/community/CommunityClient.tsx @@ -0,0 +1,83 @@ +"use client"; + +import { MessageSquare, ThumbsUp, Users, Construction } from "lucide-react"; +import { Navbar } from "@/components/Navbar"; +import { Footer } from "@/components/Footer"; + +export default function CommunityClient() { + return ( +
+ + Skip to main content + + + +
+ {/* Coming Soon Banner */} +
+
+ +
+

Community Hub

+

+ Share tips, compare results, and help other players find the best servers. +

+
+ Coming Soon +
+
+ + {/* Preview Features */} +
+
+
+ +
+

ISP Tips

+

+ Share and discover tips for optimizing your connection based on your ISP and region. +

+
+ +
+
+ +
+

Upvote System

+

+ Vote on the most helpful tips to surface the best advice for each region. +

+
+ +
+
+ +
+

Leaderboards

+

+ See the best ping results by region, ISP, and server location. +

+
+
+ + {/* CTA */} +
+

Want to be notified when Community launches?

+

+ Star our GitHub repo to get updates on new features. +

+ + Star on GitHub + +
+
+ +
+ ); +} diff --git a/web/src/app/community/page.tsx b/web/src/app/community/page.tsx index cd6b1ca..b7105b7 100644 --- a/web/src/app/community/page.tsx +++ b/web/src/app/community/page.tsx @@ -1,83 +1,15 @@ -"use client"; - -import { MessageSquare, ThumbsUp, Users, Construction } from "lucide-react"; -import { Navbar } from "@/components/Navbar"; -import { Footer } from "@/components/Footer"; +import type { Metadata } from "next"; +import CommunityClient from "./CommunityClient"; + +export const metadata: Metadata = { + title: "Community", + description: "Share connection tips, compare ping results with other players, and find the best game servers for your region.", + openGraph: { + title: "PingDiff Community Hub", + description: "Share connection tips, compare ping results with other players, and find the best game servers for your region.", + }, +}; export default function CommunityPage() { - return ( -
- - Skip to main content - - - -
- {/* Coming Soon Banner */} -
-
- -
-

Community Hub

-

- Share tips, compare results, and help other players find the best servers. -

-
- Coming Soon -
-
- - {/* Preview Features */} -
-
-
- -
-

ISP Tips

-

- Share and discover tips for optimizing your connection based on your ISP and region. -

-
- -
-
- -
-

Upvote System

-

- Vote on the most helpful tips to surface the best advice for each region. -

-
- -
-
- -
-

Leaderboards

-

- See the best ping results by region, ISP, and server location. -

-
-
- - {/* CTA */} -
-

Want to be notified when Community launches?

-

- Star our GitHub repo to get updates on new features. -

- - Star on GitHub - -
-
- -
- ); + return ; } diff --git a/web/src/app/dashboard/DashboardClient.tsx b/web/src/app/dashboard/DashboardClient.tsx new file mode 100644 index 0000000..b75ef54 --- /dev/null +++ b/web/src/app/dashboard/DashboardClient.tsx @@ -0,0 +1,547 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import Link from "next/link"; +import { + Download, + Clock, + Server, + Wifi, + TrendingDown, + AlertTriangle, + RefreshCw, + AlertCircle, + FileDown, +} from "lucide-react"; +import { Navbar } from "@/components/Navbar"; +import { Footer } from "@/components/Footer"; +import { DashboardSkeleton } from "@/components/DashboardSkeleton"; +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + BarChart, + Bar, +} from "recharts"; + +interface TestResult { + id: string; + ping_avg: number; + ping_min: number; + ping_max: number; + jitter: number; + packet_loss: number; + isp: string; + country: string; + city: string; + created_at: string; + game_servers: { + location: string; + region: string; + }; +} + +type DateRange = "7" | "30" | "90" | "all"; + +const DATE_RANGE_OPTIONS: { value: DateRange; label: string }[] = [ + { value: "7", label: "Last 7 days" }, + { value: "30", label: "Last 30 days" }, + { value: "90", label: "Last 90 days" }, + { value: "all", label: "All time" }, +]; + +function exportToCSV(results: TestResult[]) { + const headers = [ + "Date", + "Server", + "Region", + "Avg Ping (ms)", + "Min Ping (ms)", + "Max Ping (ms)", + "Jitter (ms)", + "Packet Loss (%)", + "ISP", + "Country", + "City", + ]; + + const rows = results.map((r) => [ + new Date(r.created_at).toISOString(), + r.game_servers?.location ?? "Unknown", + r.game_servers?.region ?? "", + r.ping_avg, + r.ping_min, + r.ping_max, + r.jitter?.toFixed(2) ?? "0", + r.packet_loss, + r.isp ?? "Unknown", + r.country ?? "", + r.city ?? "", + ]); + + const csvContent = [headers, ...rows] + .map((row) => + row + .map((cell) => { + const str = String(cell); + // Wrap in quotes if contains comma, quote, or newline + if (str.includes(",") || str.includes('"') || str.includes("\n")) { + return `"${str.replace(/"/g, '""')}"`; + } + return str; + }) + .join(",") + ) + .join("\n"); + + const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = `pingdiff-results-${new Date().toISOString().slice(0, 10)}.csv`; + link.click(); + URL.revokeObjectURL(url); +} + +export default function DashboardClient() { + const [results, setResults] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [selectedRegion, setSelectedRegion] = useState("all"); + const [dateRange, setDateRange] = useState("30"); + + useEffect(() => { + fetchResults(); + }, []); + + const fetchResults = async () => { + setLoading(true); + setError(null); + try { + const response = await fetch("/api/results?limit=100"); + if (!response.ok) { + throw new Error(`Failed to load results (${response.status})`); + } + const data = await response.json(); + setResults(data); + } catch (err) { + console.error("Failed to fetch results:", err); + setError(err instanceof Error ? err.message : "Failed to load results. Please try again."); + } finally { + setLoading(false); + } + }; + + // Apply date range filter + const applyDateFilter = useCallback( + (data: TestResult[]): TestResult[] => { + if (dateRange === "all") return data; + const cutoff = new Date(); + cutoff.setDate(cutoff.getDate() - parseInt(dateRange)); + return data.filter((r) => new Date(r.created_at) >= cutoff); + }, + [dateRange] + ); + + // Apply region filter on top of date filter + const dateFiltered = applyDateFilter(results); + const filteredResults = + selectedRegion === "all" + ? dateFiltered + : dateFiltered.filter((r) => r.game_servers?.region === selectedRegion); + + const avgPing = + filteredResults.length > 0 + ? Math.round( + filteredResults.reduce((sum, r) => sum + r.ping_avg, 0) / + filteredResults.length + ) + : 0; + + const avgPacketLoss = + filteredResults.length > 0 + ? ( + filteredResults.reduce((sum, r) => sum + r.packet_loss, 0) / + filteredResults.length + ).toFixed(1) + : "0"; + + const avgJitter = + filteredResults.length > 0 + ? ( + filteredResults.reduce((sum, r) => sum + (r.jitter || 0), 0) / + filteredResults.length + ).toFixed(1) + : "0"; + + // Get unique regions (from all results, not filtered) + const regions = [ + ...new Set(results.map((r) => r.game_servers?.region).filter(Boolean)), + ]; + + // Prepare chart data + const chartData = filteredResults.slice(0, 20).reverse().map((r, i) => ({ + name: `Test ${i + 1}`, + ping: r.ping_avg, + jitter: r.jitter || 0, + loss: r.packet_loss, + })); + + // Server comparison data + const serverData = filteredResults.reduce((acc, r) => { + const location = r.game_servers?.location || "Unknown"; + if (!acc[location]) { + acc[location] = { pings: [], location }; + } + acc[location].pings.push(r.ping_avg); + return acc; + }, {} as Record); + + const serverChartData = Object.values(serverData) + .map((s) => ({ + name: s.location.split(" ")[0], + ping: Math.round(s.pings.reduce((a, b) => a + b, 0) / s.pings.length), + })) + .sort((a, b) => a.ping - b.ping); + + const getQualityColor = (ping: number) => { + if (ping < 30) return "text-green-500"; + if (ping < 60) return "text-green-400"; + if (ping < 100) return "text-yellow-500"; + if (ping < 150) return "text-orange-500"; + return "text-red-500"; + }; + + const getQualityLabel = (ping: number) => { + if (ping < 30) return "Excellent"; + if (ping < 60) return "Good"; + if (ping < 100) return "Fair"; + if (ping < 150) return "Poor"; + return "Bad"; + }; + + return ( +
+ + Skip to main content + + + + {/* Main Content */} +
+
+
+

Dashboard

+

Your connection test results

+
+ + {/* Filters */} +
+ {/* Date range filter */} + + + {/* Region filter */} + + + {/* Export CSV button */} + {filteredResults.length > 0 && ( + + )} + + {/* Refresh */} + +
+
+ + {loading ? ( + + ) : error ? ( +
+ +

Failed to Load Results

+

{error}

+ +
+ ) : results.length === 0 ? ( +
+ +

No Results Yet

+

+ Download the app and run your first test to see results here. +

+ + + Download PingDiff + +
+ ) : filteredResults.length === 0 ? ( +
+ +

No Results in This Range

+

+ No tests found for the selected filters. Try a wider date range or different region. +

+ +
+ ) : ( + <> + {/* Stats Cards */} +
+
+
+
+ +
+ {getQualityLabel(avgPing)} +
+
+ +
+
+
+ +
+ {parseFloat(avgPacketLoss) === 0 ? "No loss" : "Some loss"} +
+
+ +
+
+
+ +
Variation
+
+ +
+
+
+ +
Total tests
+
+
+ + {/* Charts */} +
+ {/* Ping History Chart */} +
+

Ping History

+
+ + + + + + + + + +
+
+ + {/* Server Comparison Chart */} +
+

Server Comparison

+
+ + + + + + + + + +
+
+
+ + {/* Recent Results Table */} +
+
+

Recent Tests

+ + Showing {Math.min(filteredResults.length, 10)} of {filteredResults.length} + +
+
+ + + + + + + + + + + + + {filteredResults.slice(0, 10).map((result) => ( + + + + + + + + + ))} + +
ServerPingJitterLossISPTime
+
+ {result.game_servers?.location || "Unknown"} +
+
+ {result.game_servers?.region} +
+
+ {result.ping_avg}ms + + {result.jitter?.toFixed(1) || "0"}ms + + {result.packet_loss}% + + {result.isp || "Unknown"} + + +
+
+
+ + )} +
+ +
+
+ ); +} diff --git a/web/src/app/dashboard/page.tsx b/web/src/app/dashboard/page.tsx index 51850c6..1808b47 100644 --- a/web/src/app/dashboard/page.tsx +++ b/web/src/app/dashboard/page.tsx @@ -1,567 +1,15 @@ -"use client"; - -import { useState, useEffect, useCallback, useMemo } from "react"; -import Link from "next/link"; -import { - Download, - Clock, - Server, - Wifi, - TrendingDown, - AlertTriangle, - RefreshCw, - AlertCircle, - FileDown, -} from "lucide-react"; -import { Navbar } from "@/components/Navbar"; -import { Footer } from "@/components/Footer"; -import { DashboardSkeleton } from "@/components/DashboardSkeleton"; -import { - LineChart, - Line, - XAxis, - YAxis, - CartesianGrid, - Tooltip, - ResponsiveContainer, - BarChart, - Bar, -} from "recharts"; - -interface TestResult { - id: string; - ping_avg: number; - ping_min: number; - ping_max: number; - jitter: number; - packet_loss: number; - isp: string; - country: string; - city: string; - created_at: string; - game_servers: { - location: string; - region: string; - }; -} - -type DateRange = "7" | "30" | "90" | "all"; - -const DATE_RANGE_OPTIONS: { value: DateRange; label: string }[] = [ - { value: "7", label: "Last 7 days" }, - { value: "30", label: "Last 30 days" }, - { value: "90", label: "Last 90 days" }, - { value: "all", label: "All time" }, -]; - -function exportToCSV(results: TestResult[]) { - const headers = [ - "Date", - "Server", - "Region", - "Avg Ping (ms)", - "Min Ping (ms)", - "Max Ping (ms)", - "Jitter (ms)", - "Packet Loss (%)", - "ISP", - "Country", - "City", - ]; - - const rows = results.map((r) => [ - new Date(r.created_at).toISOString(), - r.game_servers?.location ?? "Unknown", - r.game_servers?.region ?? "", - r.ping_avg, - r.ping_min, - r.ping_max, - r.jitter?.toFixed(2) ?? "0", - r.packet_loss, - r.isp ?? "Unknown", - r.country ?? "", - r.city ?? "", - ]); - - const csvContent = [headers, ...rows] - .map((row) => - row - .map((cell) => { - const str = String(cell); - // Wrap in quotes if contains comma, quote, or newline - if (str.includes(",") || str.includes('"') || str.includes("\n")) { - return `"${str.replace(/"/g, '""')}"`; - } - return str; - }) - .join(",") - ) - .join("\n"); - - const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" }); - const url = URL.createObjectURL(blob); - const link = document.createElement("a"); - link.href = url; - link.download = `pingdiff-results-${new Date().toISOString().slice(0, 10)}.csv`; - link.click(); - URL.revokeObjectURL(url); -} +import type { Metadata } from "next"; +import DashboardClient from "./DashboardClient"; + +export const metadata: Metadata = { + title: "Dashboard", + description: "View your ping test results, packet loss trends, and server comparisons. Analyze your connection history across all game servers.", + openGraph: { + title: "PingDiff Dashboard — Your Connection Test Results", + description: "View your ping test results, packet loss trends, and server comparisons.", + }, +}; export default function DashboardPage() { - const [results, setResults] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [selectedRegion, setSelectedRegion] = useState("all"); - const [dateRange, setDateRange] = useState("30"); - - useEffect(() => { - fetchResults(); - }, []); - - const fetchResults = async () => { - setLoading(true); - setError(null); - try { - const response = await fetch("/api/results?limit=100"); - if (!response.ok) { - throw new Error(`Failed to load results (${response.status})`); - } - const data = await response.json(); - setResults(data); - } catch (err) { - console.error("Failed to fetch results:", err); - setError(err instanceof Error ? err.message : "Failed to load results. Please try again."); - } finally { - setLoading(false); - } - }; - - // Apply date range filter - const applyDateFilter = useCallback( - (data: TestResult[]): TestResult[] => { - if (dateRange === "all") return data; - const cutoff = new Date(); - cutoff.setDate(cutoff.getDate() - parseInt(dateRange)); - return data.filter((r) => new Date(r.created_at) >= cutoff); - }, - [dateRange] - ); - - // Apply date + region filters — memoized so they only recompute when deps change - const filteredResults = useMemo(() => { - const dateFiltered = applyDateFilter(results); - return selectedRegion === "all" - ? dateFiltered - : dateFiltered.filter((r) => r.game_servers?.region === selectedRegion); - }, [results, applyDateFilter, selectedRegion]); - - // Aggregate stats — memoized on filteredResults - const avgPing = useMemo( - () => - filteredResults.length > 0 - ? Math.round( - filteredResults.reduce((sum, r) => sum + r.ping_avg, 0) / - filteredResults.length - ) - : 0, - [filteredResults] - ); - - const avgPacketLoss = useMemo( - () => - filteredResults.length > 0 - ? ( - filteredResults.reduce((sum, r) => sum + r.packet_loss, 0) / - filteredResults.length - ).toFixed(1) - : "0", - [filteredResults] - ); - - const avgJitter = useMemo( - () => - filteredResults.length > 0 - ? ( - filteredResults.reduce((sum, r) => sum + (r.jitter || 0), 0) / - filteredResults.length - ).toFixed(1) - : "0", - [filteredResults] - ); - - // Unique regions derived from all results (unaffected by filters) - const regions = useMemo( - () => [ - ...new Set(results.map((r) => r.game_servers?.region).filter(Boolean)), - ], - [results] - ); - - // Ping history chart data - const chartData = useMemo( - () => - filteredResults.slice(0, 20).reverse().map((r, i) => ({ - name: `Test ${i + 1}`, - ping: r.ping_avg, - jitter: r.jitter || 0, - loss: r.packet_loss, - })), - [filteredResults] - ); - - // Server comparison chart data — group by location, sort by avg ping - const serverChartData = useMemo(() => { - const serverData = filteredResults.reduce((acc, r) => { - const location = r.game_servers?.location || "Unknown"; - if (!acc[location]) { - acc[location] = { pings: [], location }; - } - acc[location].pings.push(r.ping_avg); - return acc; - }, {} as Record); - - return Object.values(serverData) - .map((s) => ({ - name: s.location.split(" ")[0], - ping: Math.round(s.pings.reduce((a, b) => a + b, 0) / s.pings.length), - })) - .sort((a, b) => a.ping - b.ping); - }, [filteredResults]); - - const getQualityColor = (ping: number) => { - if (ping < 30) return "text-green-500"; - if (ping < 60) return "text-green-400"; - if (ping < 100) return "text-yellow-500"; - if (ping < 150) return "text-orange-500"; - return "text-red-500"; - }; - - const getQualityLabel = (ping: number) => { - if (ping < 30) return "Excellent"; - if (ping < 60) return "Good"; - if (ping < 100) return "Fair"; - if (ping < 150) return "Poor"; - return "Bad"; - }; - - return ( -
- - Skip to main content - - - - {/* Main Content */} -
-
-
-

Dashboard

-

Your connection test results

-
- - {/* Filters */} -
- {/* Date range filter */} - - - {/* Region filter */} - - - {/* Export CSV button */} - {filteredResults.length > 0 && ( - - )} - - {/* Refresh */} - -
-
- - {loading ? ( - - ) : error ? ( -
- -

Failed to Load Results

-

{error}

- -
- ) : results.length === 0 ? ( -
- -

No Results Yet

-

- Download the app and run your first test to see results here. -

- - - Download PingDiff - -
- ) : filteredResults.length === 0 ? ( -
- -

No Results in This Range

-

- No tests found for the selected filters. Try a wider date range or different region. -

- -
- ) : ( - <> - {/* Stats Cards */} -
-
-
-
- -
- {getQualityLabel(avgPing)} -
-
- -
-
-
- -
- {parseFloat(avgPacketLoss) === 0 ? "No loss" : "Some loss"} -
-
- -
-
-
- -
Variation
-
- -
-
-
- -
Total tests
-
-
- - {/* Charts */} -
- {/* Ping History Chart */} -
-

Ping History

-
- - - - - - - - - -
-
- - {/* Server Comparison Chart */} -
-

Server Comparison

-
- - - - - - - - - -
-
-
- - {/* Recent Results Table */} -
-
-

Recent Tests

- - Showing {Math.min(filteredResults.length, 10)} of {filteredResults.length} - -
-
- - - - - - - - - - - - - {filteredResults.slice(0, 10).map((result) => ( - - - - - - - - - ))} - -
ServerPingJitterLossISPTime
-
- {result.game_servers?.location || "Unknown"} -
-
- {result.game_servers?.region} -
-
- {result.ping_avg}ms - - {result.jitter?.toFixed(1) || "0"}ms - - {result.packet_loss}% - - {result.isp || "Unknown"} - - -
-
-
- - )} -
- -
-
- ); + return ; } diff --git a/web/src/app/download/DownloadClient.tsx b/web/src/app/download/DownloadClient.tsx new file mode 100644 index 0000000..5cf3a1e --- /dev/null +++ b/web/src/app/download/DownloadClient.tsx @@ -0,0 +1,268 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Activity, Download, Shield, Settings, CheckCircle, Loader2, FolderOpen, AlertCircle, Github } from "lucide-react"; +import { Navbar } from "@/components/Navbar"; +import { Footer } from "@/components/Footer"; + +interface ReleaseInfo { + version: string; + downloadUrl: string; + size: string; + date: string; + isInstaller: boolean; +} + +export default function DownloadClient() { + const [release, setRelease] = useState(null); + const [loading, setLoading] = useState(true); + const [usingFallback, setUsingFallback] = useState(false); + + useEffect(() => { + // Fetch latest release from GitHub API + fetch("https://api.github.com/repos/bokiko/pingdiff/releases/latest") + .then(res => { + if (!res.ok) throw new Error("API error"); + return res.json(); + }) + .then(data => { + const asset = data.assets?.find((a: { name: string }) => a.name.endsWith('.exe')); + if (asset) { + const isInstaller = asset.name.includes('Setup'); + const sizeInMB = (asset.size / (1024 * 1024)).toFixed(1); + setRelease({ + version: data.tag_name || "v1.17.1", + downloadUrl: asset.browser_download_url, + size: `${sizeInMB} MB`, + date: new Date(data.published_at).toLocaleDateString(), + isInstaller + }); + } else { + throw new Error("No exe found"); + } + setLoading(false); + }) + .catch(() => { + // Fallback - still provide download link + setRelease({ + version: "v1.17.1", + downloadUrl: "https://github.com/bokiko/pingdiff/releases/latest", + size: "~11 MB", + date: "", + isInstaller: true + }); + setUsingFallback(true); + setLoading(false); + }); + }, []); + + const getFileName = () => { + if (!release) return "PingDiff"; + const version = release.version.replace('v', ''); + return release.isInstaller ? `PingDiff-Setup-${version}.exe` : `PingDiff-${release.version}.exe`; + }; + + return ( +
+ + Skip to main content + + + + {/* Main Content */} +
+
+

Download PingDiff

+

+ Get the desktop app to test your connection to game servers +

+
+ + {/* Fallback Notice */} + {usingFallback && ( +
+ +

+ Couldn't fetch latest version info. Showing fallback download link. + Check GitHub Releases for the latest version. +

+
+ )} + + {/* Download Card */} +
+
+
+ +
+ +
+

PingDiff for Windows

+ {loading ? ( +
+ + Loading version info... +
+ ) : ( +

+ {release?.version} • {release?.size} • Windows 10/11 + {release?.date && • Released {release.date}} +

+ )} + + + + {loading ? "Loading..." : `Download ${getFileName()}`} + +
+
+
+ + {/* Features */} +
+
+
+ +
+

Safe & Open Source

+

+ 100% open source. No malware, no tracking, no ads. Check the code yourself on GitHub or GitLab. +

+
+ +
+
+ +
+

Your Privacy, Your Choice

+

+ Toggle result sharing on or off. Your settings are saved locally and persist across updates. +

+
+ +
+
+ +
+

Clean Install & Updates

+

+ Proper Windows installer. Updates automatically clean up old versions while preserving your data. +

+
+
+ + {/* System Requirements */} +
+

System Requirements

+
+
+ + Windows 10 or Windows 11 +
+
+ + Internet connection +
+
+ + 50MB free disk space +
+
+ + Admin rights for installation +
+
+
+ + {/* Data Storage Info */} +
+

+ + Where is my data stored? +

+
+

Program files: C:\Program Files\PingDiff

+

Settings & logs: %APPDATA%\PingDiff

+

Your settings and logs are preserved when you update to a new version.

+
+
+ + {/* Other Platforms */} +
+

Other Platforms

+

+ Mac and Linux versions coming soon. Want to be notified? +

+
+ + macOS - Coming Soon + + + Linux - Coming Soon + +
+
+ + {/* Instructions */} +
+

How to Use

+
+
+
+ 1 +
+

Download & run the installer

+
+
+
+ 2 +
+

Launch PingDiff from Start Menu

+
+
+
+ 3 +
+

Select your region

+
+
+
+ 4 +
+

Click Test and see results

+
+
+
+ + {/* All Releases Links */} + +
+ +
+
+ ); +} diff --git a/web/src/app/download/page.tsx b/web/src/app/download/page.tsx index 8c3d4b7..cb03c09 100644 --- a/web/src/app/download/page.tsx +++ b/web/src/app/download/page.tsx @@ -1,269 +1,15 @@ -"use client"; - -import { useState } from "react"; -import { Activity, Download, Shield, Settings, CheckCircle, Loader2, FolderOpen, AlertCircle, Github } from "lucide-react"; -import { useEffect } from "react"; -import { Navbar } from "@/components/Navbar"; -import { Footer } from "@/components/Footer"; - -interface ReleaseInfo { - version: string; - downloadUrl: string; - size: string; - date: string; - isInstaller: boolean; -} +import type { Metadata } from "next"; +import DownloadClient from "./DownloadClient"; + +export const metadata: Metadata = { + title: "Download", + description: "Download PingDiff for Windows — free, open source desktop app to test your ping and packet loss to game servers before you queue.", + openGraph: { + title: "Download PingDiff for Windows", + description: "Free, open source desktop app to test your ping and packet loss to game servers before you queue.", + }, +}; export default function DownloadPage() { - const [release, setRelease] = useState(null); - const [loading, setLoading] = useState(true); - const [usingFallback, setUsingFallback] = useState(false); - - useEffect(() => { - // Fetch latest release from GitHub API - fetch("https://api.github.com/repos/bokiko/pingdiff/releases/latest") - .then(res => { - if (!res.ok) throw new Error("API error"); - return res.json(); - }) - .then(data => { - const asset = data.assets?.find((a: { name: string }) => a.name.endsWith('.exe')); - if (asset) { - const isInstaller = asset.name.includes('Setup'); - const sizeInMB = (asset.size / (1024 * 1024)).toFixed(1); - setRelease({ - version: data.tag_name || "v1.17.1", - downloadUrl: asset.browser_download_url, - size: `${sizeInMB} MB`, - date: new Date(data.published_at).toLocaleDateString(), - isInstaller - }); - } else { - throw new Error("No exe found"); - } - setLoading(false); - }) - .catch(() => { - // Fallback - still provide download link - setRelease({ - version: "v1.17.1", - downloadUrl: "https://github.com/bokiko/pingdiff/releases/latest", - size: "~11 MB", - date: "", - isInstaller: true - }); - setUsingFallback(true); - setLoading(false); - }); - }, []); - - const getFileName = () => { - if (!release) return "PingDiff"; - const version = release.version.replace('v', ''); - return release.isInstaller ? `PingDiff-Setup-${version}.exe` : `PingDiff-${release.version}.exe`; - }; - - return ( -
- - Skip to main content - - - - {/* Main Content */} -
-
-

Download PingDiff

-

- Get the desktop app to test your connection to game servers -

-
- - {/* Fallback Notice */} - {usingFallback && ( -
- -

- Couldn't fetch latest version info. Showing fallback download link. - Check GitHub Releases for the latest version. -

-
- )} - - {/* Download Card */} -
-
-
- -
- -
-

PingDiff for Windows

- {loading ? ( -
- - Loading version info... -
- ) : ( -

- {release?.version} • {release?.size} • Windows 10/11 - {release?.date && • Released {release.date}} -

- )} - - - - {loading ? "Loading..." : `Download ${getFileName()}`} - -
-
-
- - {/* Features */} -
-
-
- -
-

Safe & Open Source

-

- 100% open source. No malware, no tracking, no ads. Check the code yourself on GitHub or GitLab. -

-
- -
-
- -
-

Your Privacy, Your Choice

-

- Toggle result sharing on or off. Your settings are saved locally and persist across updates. -

-
- -
-
- -
-

Clean Install & Updates

-

- Proper Windows installer. Updates automatically clean up old versions while preserving your data. -

-
-
- - {/* System Requirements */} -
-

System Requirements

-
-
- - Windows 10 or Windows 11 -
-
- - Internet connection -
-
- - 50MB free disk space -
-
- - Admin rights for installation -
-
-
- - {/* Data Storage Info */} -
-

- - Where is my data stored? -

-
-

Program files: C:\Program Files\PingDiff

-

Settings & logs: %APPDATA%\PingDiff

-

Your settings and logs are preserved when you update to a new version.

-
-
- - {/* Other Platforms */} -
-

Other Platforms

-

- Mac and Linux versions coming soon. Want to be notified? -

-
- - macOS - Coming Soon - - - Linux - Coming Soon - -
-
- - {/* Instructions */} -
-

How to Use

-
-
-
- 1 -
-

Download & run the installer

-
-
-
- 2 -
-

Launch PingDiff from Start Menu

-
-
-
- 3 -
-

Select your region

-
-
-
- 4 -
-

Click Test and see results

-
-
-
- - {/* All Releases Links */} - -
- -
-
- ); + return ; }