1- import { Alert , Badge , Button , Card , Container , Group , Stack , Text , TextInput , Title } from '@mantine/core'
1+ import { Alert , Badge , Button , Card , Container , Divider , Group , Stack , Text , TextInput , Title , Collapse } from '@mantine/core'
2+ import { notifications } from '@mantine/notifications'
23import { useEffect , useMemo , useState } from 'react'
34import {
45 CartesianGrid ,
@@ -80,9 +81,19 @@ export function Analytics({ onHome }: AnalyticsProps) {
8081 } ) )
8182 } , [ filteredAttempts ] )
8283
84+ const copyText = async ( label : string , value : string ) => {
85+ try {
86+ await navigator . clipboard . writeText ( value )
87+ notifications . show ( { color : 'green' , message : `Copied ${ label } ` } )
88+ } catch ( error ) {
89+ const message = error instanceof Error ? error . message : String ( error )
90+ notifications . show ( { color : 'red' , title : 'Copy failed' , message } )
91+ }
92+ }
93+
8394 return (
84- < Container size = "lg" py = "lg" className = "h-full" >
85- < Stack gap = "md" className = "h-full" >
95+ < Container size = "lg" py = "lg" >
96+ < Stack gap = "md" >
8697 < Group justify = "space-between" align = "flex-end" wrap = "wrap" >
8798 < Group >
8899 < Button variant = "subtle" onClick = { onHome } > Home</ Button >
@@ -108,13 +119,13 @@ export function Analytics({ onHome }: AnalyticsProps) {
108119 </ Alert >
109120 ) }
110121
111- < div className = "grid grid-cols-1 gap-3" >
122+ < div className = "grid grid-cols-1 gap-3 lg:grid-cols-2 " >
112123 < Card withBorder padding = "md" >
113124 < Group justify = "space-between" mb = "xs" >
114125 < Text fw = { 600 } > WPM over time</ Text >
115126 { state . status === 'loading' && < Text size = "xs" c = "dimmed" > Loading...</ Text > }
116127 </ Group >
117- < div style = { { height : 220 } } >
128+ < div style = { { height : 240 } } >
118129 < ResponsiveContainer width = "100%" height = "100%" >
119130 < LineChart data = { series } margin = { { top : 10 , right : 16 , bottom : 8 , left : 0 } } >
120131 < CartesianGrid strokeDasharray = "4 4" />
@@ -134,8 +145,11 @@ export function Analytics({ onHome }: AnalyticsProps) {
134145 </ Card >
135146
136147 < Card withBorder padding = "md" >
137- < Text fw = { 600 } mb = "xs" > Unproductive% over time</ Text >
138- < div style = { { height : 220 } } >
148+ < Group justify = "space-between" mb = "xs" >
149+ < Text fw = { 600 } > Unproductive% over time</ Text >
150+ { state . status === 'loading' && < Text size = "xs" c = "dimmed" > Loading...</ Text > }
151+ </ Group >
152+ < div style = { { height : 240 } } >
139153 < ResponsiveContainer width = "100%" height = "100%" >
140154 < LineChart data = { series } margin = { { top : 10 , right : 16 , bottom : 8 , left : 0 } } >
141155 < CartesianGrid strokeDasharray = "4 4" />
@@ -155,9 +169,16 @@ export function Analytics({ onHome }: AnalyticsProps) {
155169 </ Card >
156170 </ div >
157171
158- < div className = "tt-border min-h-0 overflow-auto border-t pt-3" >
159- < Group justify = "space-between" mb = "xs" >
160- < Text fw = { 600 } > Attempts</ Text >
172+ < Divider />
173+
174+ < Stack gap = "sm" >
175+ < Group justify = "space-between" align = "flex-end" wrap = "wrap" >
176+ < div >
177+ < Text fw = { 700 } > Attempts</ Text >
178+ < Text size = "sm" c = "dimmed" >
179+ Browse attempts below (page scroll). Use the filter above to narrow by file name.
180+ </ Text >
181+ </ div >
161182 { state . status === 'loading' && < Text size = "xs" c = "dimmed" > Loading...</ Text > }
162183 </ Group >
163184
@@ -167,55 +188,113 @@ export function Analytics({ onHome }: AnalyticsProps) {
167188 </ Text >
168189 ) }
169190
170- < Stack gap = "xs " >
191+ < Stack gap = "md " >
171192 { filteredAttempts . map ( ( a ) => {
172193 const expanded = expandedId === a . id
194+ const detailsJson = JSON . stringify ( a , null , 2 )
173195 return (
174- < Card key = { a . id } withBorder padding = "sm" >
175- < button
176- type = "button"
177- onClick = { ( ) => setExpandedId ( expanded ? null : a . id ) }
178- className = "w-full text-left"
179- >
180- < Group justify = "space-between" align = "flex-start" wrap = "wrap" >
181- < div >
182- < Text fw = { 600 } > { a . fileName } </ Text >
183- < Text size = "xs" c = "dimmed" > { formatDateTime ( a . endAtMs ) } </ Text >
184- </ div >
185-
186- < Group gap = "md" wrap = "wrap" >
187- < Text size = "sm" > < strong > WPM</ strong > { a . wpm . toFixed ( 1 ) } </ Text >
188- < Text size = "sm" > < strong > Unprod%</ strong > { a . unproductivePercent . toFixed ( 1 ) } </ Text >
189- < Text size = "sm" > < strong > Seg</ strong > { a . segmentIndex + 1 } </ Text >
196+ < Card
197+ key = { a . id }
198+ withBorder
199+ padding = "md"
200+ style = { { cursor : 'pointer' } }
201+ onClick = { ( ) => setExpandedId ( expanded ? null : a . id ) }
202+ >
203+ < Group justify = "space-between" align = "flex-start" wrap = "wrap" gap = "md" >
204+ < div className = "min-w-0" >
205+ < Group gap = "sm" wrap = "wrap" >
206+ < Text fw = { 700 } > { a . fileName } </ Text >
207+ < Badge variant = "light" > Seg { a . segmentIndex + 1 } </ Badge >
208+ < Badge variant = "light" > Lines { a . segmentStartLine } -{ a . segmentEndLine } </ Badge >
209+ < Badge variant = "light" > { formatDateTime ( a . endAtMs ) } </ Badge >
190210 </ Group >
211+ < Text
212+ size = "xs"
213+ c = "dimmed"
214+ className = "truncate"
215+ title = { a . filePath }
216+ mt = { 6 }
217+ >
218+ { a . filePath }
219+ </ Text >
220+ </ div >
221+
222+ < Group gap = "md" wrap = "wrap" justify = "flex-end" >
223+ < Text size = "sm" > < strong > WPM</ strong > { a . wpm . toFixed ( 1 ) } </ Text >
224+ < Text size = "sm" > < strong > Unprod%</ strong > { a . unproductivePercent . toFixed ( 1 ) } </ Text >
225+ < Text size = "sm" > < strong > Duration</ strong > { ( a . durationMs / 1000 ) . toFixed ( 1 ) } s</ Text >
226+ < Button
227+ size = "xs"
228+ variant = "light"
229+ onClick = { ( e ) => {
230+ e . stopPropagation ( )
231+ setExpandedId ( expanded ? null : a . id )
232+ } }
233+ >
234+ { expanded ? 'Hide details' : 'Details' }
235+ </ Button >
191236 </ Group >
192- </ button >
193-
194- { expanded && (
195- < Stack gap = { 4 } mt = "sm" >
196- < Text size = "sm" > < strong > filePath</ strong > : < span className = "tt-muted" > { a . filePath } </ span > </ Text >
197- < Text size = "sm" > < strong > lines</ strong > : { a . segmentStartLine } -{ a . segmentEndLine } </ Text >
198- < Text size = "sm" > < strong > durationMs</ strong > : { a . durationMs } </ Text >
199- < Group gap = "md" wrap = "wrap" mt = { 4 } >
237+ </ Group >
238+
239+ < Collapse in = { expanded } >
240+ < Divider my = "sm" />
241+ < Stack gap = "xs" >
242+ < Group gap = "sm" wrap = "wrap" >
243+ < Button
244+ size = "xs"
245+ variant = "default"
246+ onClick = { ( e ) => {
247+ e . stopPropagation ( )
248+ void copyText ( 'file path' , a . filePath )
249+ } }
250+ >
251+ Copy path
252+ </ Button >
253+ < Button
254+ size = "xs"
255+ variant = "default"
256+ onClick = { ( e ) => {
257+ e . stopPropagation ( )
258+ void copyText ( 'JSON' , detailsJson )
259+ } }
260+ >
261+ Copy JSON
262+ </ Button >
263+ </ Group >
264+
265+ < Text size = "sm" style = { { overflowWrap : 'anywhere' } } >
266+ < strong > filePath</ strong > : < span className = "tt-muted" > { a . filePath } </ span >
267+ </ Text >
268+
269+ < Group gap = "md" wrap = "wrap" >
200270 < Text size = "sm" > < strong > typeableChars</ strong > : { a . typeableChars } </ Text >
201271 < Text size = "sm" > < strong > correctChars</ strong > : { a . correctChars } </ Text >
202272 < Text size = "sm" > < strong > typedKeystrokes</ strong > : { a . typedKeystrokes } </ Text >
203273 </ Group >
204- < Group gap = "md" wrap = "wrap" mt = { 4 } >
274+ < Group gap = "md" wrap = "wrap" >
205275 < Text size = "sm" > < strong > incorrect</ strong > : { a . incorrect } </ Text >
206276 < Text size = "sm" > < strong > collateral</ strong > : { a . collateral } </ Text >
207277 < Text size = "sm" > < strong > backspaces</ strong > : { a . backspaces } </ Text >
208278 </ Group >
209- < Text size = "sm" mt = { 4 } >
279+ < Text size = "sm" >
210280 < strong > settings</ strong > : linesPerSegment={ a . linesPerSegment } , tabWidth={ a . tabWidth } , slackN={ a . slackN }
211281 </ Text >
282+
283+ < Text fw = { 600 } size = "sm" mt = "xs" > Raw JSON</ Text >
284+ < pre
285+ className = "tt-panel tt-border rounded-md border px-3 py-2 text-xs"
286+ style = { { margin : 0 , whiteSpace : 'pre-wrap' , overflowWrap : 'anywhere' } }
287+ onClick = { ( e ) => e . stopPropagation ( ) }
288+ >
289+ { detailsJson }
290+ </ pre >
212291 </ Stack >
213- ) }
292+ </ Collapse >
214293 </ Card >
215294 )
216295 } ) }
217296 </ Stack >
218- </ div >
297+ </ Stack >
219298 </ Stack >
220299 </ Container >
221300 )
0 commit comments