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