11"use client" ;
22
3- import { useState , useEffect } from "react" ;
3+ import { useState , useEffect , useCallback } from "react" ;
44import Link from "next/link" ;
55import {
66 Download ,
@@ -11,6 +11,7 @@ import {
1111 AlertTriangle ,
1212 RefreshCw ,
1313 AlertCircle ,
14+ FileDown ,
1415} from "lucide-react" ;
1516import { Navbar } from "@/components/Navbar" ;
1617import { Footer } from "@/components/Footer" ;
@@ -43,11 +44,75 @@ interface TestResult {
4344 } ;
4445}
4546
47+ type DateRange = "7" | "30" | "90" | "all" ;
48+
49+ const DATE_RANGE_OPTIONS : { value : DateRange ; label : string } [ ] = [
50+ { value : "7" , label : "Last 7 days" } ,
51+ { value : "30" , label : "Last 30 days" } ,
52+ { value : "90" , label : "Last 90 days" } ,
53+ { value : "all" , label : "All time" } ,
54+ ] ;
55+
56+ function exportToCSV ( results : TestResult [ ] ) {
57+ const headers = [
58+ "Date" ,
59+ "Server" ,
60+ "Region" ,
61+ "Avg Ping (ms)" ,
62+ "Min Ping (ms)" ,
63+ "Max Ping (ms)" ,
64+ "Jitter (ms)" ,
65+ "Packet Loss (%)" ,
66+ "ISP" ,
67+ "Country" ,
68+ "City" ,
69+ ] ;
70+
71+ const rows = results . map ( ( r ) => [
72+ new Date ( r . created_at ) . toISOString ( ) ,
73+ r . game_servers ?. location ?? "Unknown" ,
74+ r . game_servers ?. region ?? "" ,
75+ r . ping_avg ,
76+ r . ping_min ,
77+ r . ping_max ,
78+ r . jitter ?. toFixed ( 2 ) ?? "0" ,
79+ r . packet_loss ,
80+ r . isp ?? "Unknown" ,
81+ r . country ?? "" ,
82+ r . city ?? "" ,
83+ ] ) ;
84+
85+ const csvContent = [ headers , ...rows ]
86+ . map ( ( row ) =>
87+ row
88+ . map ( ( cell ) => {
89+ const str = String ( cell ) ;
90+ // Wrap in quotes if contains comma, quote, or newline
91+ if ( str . includes ( "," ) || str . includes ( '"' ) || str . includes ( "\n" ) ) {
92+ return `"${ str . replace ( / " / g, '""' ) } "` ;
93+ }
94+ return str ;
95+ } )
96+ . join ( "," )
97+ )
98+ . join ( "\n" ) ;
99+
100+ const blob = new Blob ( [ csvContent ] , { type : "text/csv;charset=utf-8;" } ) ;
101+ const url = URL . createObjectURL ( blob ) ;
102+ const link = document . createElement ( "a" ) ;
103+ link . href = url ;
104+ link . download = `pingdiff-results-${ new Date ( ) . toISOString ( ) . slice ( 0 , 10 ) } .csv` ;
105+ link . click ( ) ;
106+ URL . revokeObjectURL ( url ) ;
107+ }
108+
46109export default function DashboardPage ( ) {
47110 const [ results , setResults ] = useState < TestResult [ ] > ( [ ] ) ;
48111 const [ loading , setLoading ] = useState ( true ) ;
49112 const [ error , setError ] = useState < string | null > ( null ) ;
50113 const [ selectedRegion , setSelectedRegion ] = useState < string > ( "all" ) ;
114+ const [ dateRange , setDateRange ] = useState < DateRange > ( "30" ) ;
115+
51116 useEffect ( ( ) => {
52117 fetchResults ( ) ;
53118 } , [ ] ) ;
@@ -70,11 +135,23 @@ export default function DashboardPage() {
70135 }
71136 } ;
72137
73- // Calculate stats
138+ // Apply date range filter
139+ const applyDateFilter = useCallback (
140+ ( data : TestResult [ ] ) : TestResult [ ] => {
141+ if ( dateRange === "all" ) return data ;
142+ const cutoff = new Date ( ) ;
143+ cutoff . setDate ( cutoff . getDate ( ) - parseInt ( dateRange ) ) ;
144+ return data . filter ( ( r ) => new Date ( r . created_at ) >= cutoff ) ;
145+ } ,
146+ [ dateRange ]
147+ ) ;
148+
149+ // Apply region filter on top of date filter
150+ const dateFiltered = applyDateFilter ( results ) ;
74151 const filteredResults =
75152 selectedRegion === "all"
76- ? results
77- : results . filter ( ( r ) => r . game_servers ?. region === selectedRegion ) ;
153+ ? dateFiltered
154+ : dateFiltered . filter ( ( r ) => r . game_servers ?. region === selectedRegion ) ;
78155
79156 const avgPing =
80157 filteredResults . length > 0
@@ -100,7 +177,7 @@ export default function DashboardPage() {
100177 ) . toFixed ( 1 )
101178 : "0" ;
102179
103- // Get unique regions
180+ // Get unique regions (from all results, not filtered)
104181 const regions = [
105182 ...new Set ( results . map ( ( r ) => r . game_servers ?. region ) . filter ( Boolean ) ) ,
106183 ] ;
@@ -152,22 +229,34 @@ export default function DashboardPage() {
152229
153230 { /* Main Content */ }
154231 < main className = "max-w-6xl mx-auto px-4 py-8" >
155- < div className = "flex justify-between items-center mb-8" >
232+ < div className = "flex flex-col sm:flex-row sm: justify-between sm: items-center gap-4 mb-8" >
156233 < div >
157234 < h1 className = "text-3xl font-bold" > Dashboard</ h1 >
158235 < p className = "text-zinc-400" > Your connection test results</ p >
159236 </ div >
160237
161- { /* Region Filter */ }
162- < div className = "flex items-center gap-2" >
163- < label htmlFor = "region-filter" className = "text-zinc-400 text-sm sr-only md:not-sr-only" >
164- Filter by:
165- </ label >
238+ { /* Filters */ }
239+ < div className = "flex flex-wrap items-center gap-2" >
240+ { /* Date range filter */ }
241+ < select
242+ value = { dateRange }
243+ onChange = { ( e ) => setDateRange ( e . target . value as DateRange ) }
244+ className = "bg-zinc-800 border border-zinc-700 rounded-lg px-3 py-2 text-sm focus-ring"
245+ aria-label = "Filter results by date range"
246+ >
247+ { DATE_RANGE_OPTIONS . map ( ( opt ) => (
248+ < option key = { opt . value } value = { opt . value } >
249+ { opt . label }
250+ </ option >
251+ ) ) }
252+ </ select >
253+
254+ { /* Region filter */ }
166255 < select
167256 id = "region-filter"
168257 value = { selectedRegion }
169258 onChange = { ( e ) => setSelectedRegion ( e . target . value ) }
170- className = "bg-zinc-800 border border-zinc-700 rounded-lg px-4 py-2 focus-ring"
259+ className = "bg-zinc-800 border border-zinc-700 rounded-lg px-3 py-2 text-sm focus-ring"
171260 aria-label = "Filter results by region"
172261 >
173262 < option value = "all" > All Regions</ option >
@@ -177,6 +266,30 @@ export default function DashboardPage() {
177266 </ option >
178267 ) ) }
179268 </ select >
269+
270+ { /* Export CSV button */ }
271+ { filteredResults . length > 0 && (
272+ < button
273+ onClick = { ( ) => exportToCSV ( filteredResults ) }
274+ className = "inline-flex items-center gap-2 bg-zinc-800 hover:bg-zinc-700 border border-zinc-700 px-3 py-2 rounded-lg text-sm font-medium transition focus-ring"
275+ aria-label = { `Export ${ filteredResults . length } results to CSV` }
276+ title = "Export filtered results as CSV"
277+ >
278+ < FileDown className = "w-4 h-4" />
279+ Export CSV
280+ </ button >
281+ ) }
282+
283+ { /* Refresh */ }
284+ < button
285+ onClick = { fetchResults }
286+ disabled = { loading }
287+ className = "inline-flex items-center gap-2 bg-zinc-800 hover:bg-zinc-700 border border-zinc-700 px-3 py-2 rounded-lg text-sm font-medium transition focus-ring disabled:opacity-50"
288+ aria-label = "Refresh results"
289+ >
290+ < RefreshCw className = { `w-4 h-4 ${ loading ? "animate-spin" : "" } ` } />
291+ Refresh
292+ </ button >
180293 </ div >
181294 </ div >
182295
@@ -213,6 +326,20 @@ export default function DashboardPage() {
213326 Download PingDiff
214327 </ Link >
215328 </ div >
329+ ) : filteredResults . length === 0 ? (
330+ < div className = "text-center py-20" >
331+ < Clock className = "w-16 h-16 text-zinc-600 mx-auto mb-4" />
332+ < h2 className = "text-xl font-semibold mb-2" > No Results in This Range</ h2 >
333+ < p className = "text-zinc-400 mb-6" >
334+ No tests found for the selected filters. Try a wider date range or different region.
335+ </ p >
336+ < button
337+ onClick = { ( ) => { setDateRange ( "all" ) ; setSelectedRegion ( "all" ) ; } }
338+ className = "inline-flex items-center gap-2 bg-zinc-700 hover:bg-zinc-600 px-6 py-3 rounded-lg font-medium transition"
339+ >
340+ Clear Filters
341+ </ button >
342+ </ div >
216343 ) : (
217344 < >
218345 { /* Stats Cards */ }
@@ -327,7 +454,12 @@ export default function DashboardPage() {
327454
328455 { /* Recent Results Table */ }
329456 < div className = "bg-zinc-900 border border-zinc-800 rounded-xl p-6" >
330- < h3 className = "text-lg font-semibold mb-4" > Recent Tests</ h3 >
457+ < div className = "flex items-center justify-between mb-4" >
458+ < h3 className = "text-lg font-semibold" > Recent Tests</ h3 >
459+ < span className = "text-sm text-zinc-500" >
460+ Showing { Math . min ( filteredResults . length , 10 ) } of { filteredResults . length }
461+ </ span >
462+ </ div >
331463 < div className = "overflow-x-auto" >
332464 < table className = "w-full" >
333465 < thead >
0 commit comments