@@ -7,8 +7,10 @@ import { LEVEL_COLORS, levelLabel } from '@/features/lobby/format';
77import type { GameLevel } from '@/features/lobby/types' ;
88import { formatSeconds } from '@/features/tasks/shared' ;
99import { LangIcon } from '@/shared/LangIcon' ;
10+ import { useSharedProps } from '@/shared/useSharedProps' ;
1011import { Markdown } from '@/shared/Markdown' ;
1112
13+ import { useTournamentStore } from '../store' ;
1214import type { Tournament } from '../types' ;
1315import type { TournamentApi } from '../useTournament' ;
1416
@@ -315,6 +317,8 @@ interface TaskInfo {
315317 name : string ;
316318 description_en : string | null ;
317319 description_ru : string | null ;
320+ time_to_solve_sec : number | null ;
321+ base_score : number | null ;
318322}
319323
320324interface TopTaskUser {
@@ -335,6 +339,56 @@ interface DurationBucket {
335339 wins_count : number ;
336340}
337341
342+ /**
343+ * Winners' solve times on a fixed 0 … time-to-solve scale (the server's buckets), with
344+ * quarter ticks and the viewer's own solve marked.
345+ */
346+ function DurationChart ( { buckets, myDuration } : { buckets : DurationBucket [ ] ; myDuration : number | null } ) {
347+ const { t } = useTranslation ( ) ;
348+ const scale = buckets [ buckets . length - 1 ] . end || 1 ;
349+ const maxWins = Math . max ( 1 , ...buckets . map ( ( bucket ) => bucket . wins_count ) ) ;
350+ const mine = myDuration != null ? Math . min ( 100 , ( myDuration / scale ) * 100 ) : null ;
351+ const inBucket = ( bucket : DurationBucket , index : number ) =>
352+ myDuration != null && myDuration >= bucket . start && ( myDuration < bucket . end || index === buckets . length - 1 ) ;
353+
354+ return (
355+ < div className = { classes . durationChart } >
356+ < div className = { classes . histogram } >
357+ { buckets . map ( ( bucket , index ) => (
358+ < Tooltip
359+ key = { bucket . start }
360+ label = { t ( '%{from}–%{to}: %{count} solved' , {
361+ from : formatSeconds ( bucket . start ) ,
362+ to : formatSeconds ( bucket . end ) ,
363+ count : bucket . wins_count ,
364+ } ) }
365+ >
366+ < div className = { classes . histogramCol } >
367+ < div
368+ className = { classes . histogramBar }
369+ data-mine = { inBucket ( bucket , index ) || undefined }
370+ style = { { height : bucket . wins_count ? `${ ( bucket . wins_count / maxWins ) * 100 } %` : undefined } }
371+ />
372+ </ div >
373+ </ Tooltip >
374+ ) ) }
375+ { mine != null && (
376+ < div className = { classes . myMarker } style = { { left : `${ mine } %` } } >
377+ < span className = { classes . myLabel } > { t ( 'You %{time}' , { time : formatSeconds ( myDuration ! ) } ) } </ span >
378+ </ div >
379+ ) }
380+ </ div >
381+ < div className = { classes . axis } >
382+ { [ 0 , 0.25 , 0.5 , 0.75 , 1 ] . map ( ( share ) => (
383+ < span key = { share } style = { { left : `${ share * 100 } %` } } data-edge = { share === 0 ? 'start' : share === 1 ? 'end' : undefined } >
384+ { formatSeconds ( Math . round ( scale * share ) ) }
385+ </ span >
386+ ) ) }
387+ </ div >
388+ </ div >
389+ ) ;
390+ }
391+
338392/** One task: how fast winners solved it and who was fastest (legacy TaskRankingAdvancedPanel). */
339393export function TaskAdvancedPanel ( {
340394 tournament,
@@ -350,6 +404,8 @@ export function TaskAdvancedPanel({
350404 onSelectUser ?: ( id : number ) => void ;
351405} ) {
352406 const { t } = useTranslation ( ) ;
407+ const { current_user : me } = useSharedProps ( ) ;
408+ const matches = useTournamentStore ( ( state ) => state . matches ) ;
353409 const [ task , setTask ] = useState < TaskInfo | null > ( null ) ;
354410 const [ showDescription , setShowDescription ] = useState ( false ) ;
355411 const top = usePanelResults < TopTaskUser > ( api , tournament . state , { type : 'top_user_by_task_ranking' , task_id : taskId } ) ;
@@ -364,7 +420,16 @@ export function TaskAdvancedPanel({
364420 } , [ api , taskId ] ) ;
365421
366422 const buckets = distribution . rows ?? [ ] ;
367- const maxWins = Math . max ( 1 , ...buckets . map ( ( bucket ) => bucket . wins_count ) ) ;
423+ // The viewer's own winning solve on this task, if any (top list, else their matches).
424+ const myDuration =
425+ top . rows ?. find ( ( row ) => row . user_id === me . id ) ?. duration_sec ??
426+ Object . values ( matches ) . find (
427+ ( match ) =>
428+ match . task_id === taskId &&
429+ match . duration_sec != null &&
430+ ( match . player_results ?. [ me . id ] ?. result_percent ?? 0 ) >= 100 ,
431+ ) ?. duration_sec ??
432+ null ;
368433
369434 return (
370435 < Stack gap = "md" >
@@ -379,6 +444,16 @@ export function TaskAdvancedPanel({
379444 { levelLabel ( task . level ) }
380445 </ Badge >
381446 ) }
447+ { task ?. base_score != null && (
448+ < Badge size = "xs" variant = "default" tt = "none" >
449+ { t ( 'Base score %{score}' , { score : task . base_score } ) }
450+ </ Badge >
451+ ) }
452+ { task ?. time_to_solve_sec != null && (
453+ < Badge size = "xs" variant = "default" tt = "none" >
454+ { t ( 'Time limit %{time}' , { time : formatSeconds ( task . time_to_solve_sec ) } ) }
455+ </ Badge >
456+ ) }
382457 </ Group >
383458 < Switch size = "xs" label = { t ( 'Show task description' ) } checked = { showDescription } onChange = { ( event ) => setShowDescription ( event . currentTarget . checked ) } />
384459 </ Group >
@@ -392,25 +467,7 @@ export function TaskAdvancedPanel({
392467 ) : buckets . length === 0 ? (
393468 < PanelMessage > { t ( 'No results yet' ) } </ PanelMessage >
394469 ) : (
395- < >
396- < div className = { classes . histogram } >
397- { buckets . map ( ( bucket ) => (
398- < Tooltip key = { bucket . start } label = { `${ formatSeconds ( bucket . start ) } –${ formatSeconds ( bucket . end ) } : ${ bucket . wins_count } ` } >
399- < div className = { classes . histogramCol } >
400- < div className = { classes . histogramBar } style = { { height : `${ ( bucket . wins_count / maxWins ) * 100 } %` } } />
401- </ div >
402- </ Tooltip >
403- ) ) }
404- </ div >
405- < Group justify = "space-between" mt = { 4 } >
406- < Text size = "xs" c = "dimmed" ff = "monospace" >
407- { formatSeconds ( buckets [ 0 ] . start ) }
408- </ Text >
409- < Text size = "xs" c = "dimmed" ff = "monospace" >
410- { formatSeconds ( buckets [ buckets . length - 1 ] . end ) }
411- </ Text >
412- </ Group >
413- </ >
470+ < DurationChart buckets = { buckets } myDuration = { myDuration } />
414471 ) }
415472 </ Paper >
416473
0 commit comments