|
| 1 | +import classNames from 'classnames'; |
| 2 | +import { useEffect, useMemo, useState } from 'react'; |
| 3 | +import { Popover } from 'react-tiny-popover'; |
| 4 | +import { Container } from '@/components/Container'; |
| 5 | +import { useWcaCompetitionResults } from '@/hooks/queries/useWcaResults'; |
| 6 | +import { AnchorLink, LinkRenderer } from '@/lib/linkRenderer'; |
| 7 | +import { useWCIF } from '@/providers/WCIFProvider'; |
| 8 | +import { SumOfRanksPersonRanking, getSumOfRanks } from './rankings'; |
| 9 | + |
| 10 | +export interface CompetitionSumOfRanksContainerProps { |
| 11 | + competitionId: string; |
| 12 | + LinkComponent?: LinkRenderer; |
| 13 | +} |
| 14 | + |
| 15 | +const formatKinch = (kinch: number) => (kinch * 100).toFixed(2); |
| 16 | + |
| 17 | +const formatMedals = (ranking: SumOfRanksPersonRanking) => |
| 18 | + ranking.medals.length > 0 |
| 19 | + ? `${ranking.medals.length} (${ranking.medals.map((medal) => medal.eventId).join(', ')})` |
| 20 | + : ''; |
| 21 | + |
| 22 | +function KinchTooltip() { |
| 23 | + const [open, setOpen] = useState(false); |
| 24 | + |
| 25 | + return ( |
| 26 | + <Popover |
| 27 | + isOpen={open} |
| 28 | + positions={['bottom', 'left']} |
| 29 | + padding={8} |
| 30 | + onClickOutside={() => setOpen(false)} |
| 31 | + content={ |
| 32 | + <div |
| 33 | + className={classNames( |
| 34 | + 'z-1000 max-w-xs space-y-2 rounded border border-tertiary-weak bg-panel p-3 text-left type-body-sm text-default shadow-md transition-opacity duration-300 opacity-0', |
| 35 | + { |
| 36 | + 'opacity-100': open, |
| 37 | + }, |
| 38 | + )}> |
| 39 | + <p>Kinch is averaged across counted rounds.</p> |
| 40 | + <p> |
| 41 | + Each round compares the winning result to the competitor's result. Missed or |
| 42 | + invalid results count as 0. |
| 43 | + </p> |
| 44 | + </div> |
| 45 | + }> |
| 46 | + <button |
| 47 | + type="button" |
| 48 | + className="inline-flex h-5 w-5 items-center justify-center rounded-full text-gray-500 hover:bg-gray-200 hover:text-gray-700" |
| 49 | + aria-label="Explain Kinch" |
| 50 | + onClick={() => setOpen((current) => !current)}> |
| 51 | + ? |
| 52 | + </button> |
| 53 | + </Popover> |
| 54 | + ); |
| 55 | +} |
| 56 | + |
| 57 | +export function CompetitionSumOfRanksContainer({ |
| 58 | + competitionId, |
| 59 | + LinkComponent = AnchorLink, |
| 60 | +}: CompetitionSumOfRanksContainerProps) { |
| 61 | + const { wcif, setTitle } = useWCIF(); |
| 62 | + const { data: wcaApiResults, status: wcaApiResultsStatus } = useWcaCompetitionResults( |
| 63 | + competitionId, |
| 64 | + { |
| 65 | + enabled: Boolean(wcif), |
| 66 | + }, |
| 67 | + ); |
| 68 | + |
| 69 | + useEffect(() => { |
| 70 | + setTitle(`${wcif?.name ?? competitionId} Sum of Rankings`); |
| 71 | + }, [competitionId, setTitle, wcif?.name]); |
| 72 | + |
| 73 | + const rankings = useMemo( |
| 74 | + () => (wcif ? getSumOfRanks(wcif, wcaApiResults) : []), |
| 75 | + [wcaApiResults, wcif], |
| 76 | + ); |
| 77 | + const isLoadingResults = wcaApiResultsStatus === 'pending' && rankings.length === 0; |
| 78 | + const roundsCounted = rankings[0]?.roundsCounted ?? 0; |
| 79 | + const summary = |
| 80 | + rankings.length > 0 |
| 81 | + ? `${rankings.length} competitors across ${roundsCounted} rounds` |
| 82 | + : 'No ranked rounds yet.'; |
| 83 | + |
| 84 | + return ( |
| 85 | + <Container className="mx-auto max-w-screen-lg space-y-4 p-2" fullWidth> |
| 86 | + <h1 className="type-heading">Sum of Rankings</h1> |
| 87 | + |
| 88 | + {!isLoadingResults && <div className="type-body-sm text-gray-600">{summary}</div>} |
| 89 | + |
| 90 | + {rankings.length > 0 && ( |
| 91 | + <div className="overflow-x-auto rounded border border-gray-200 shadow-sm"> |
| 92 | + <table className="w-full table-auto whitespace-nowrap type-body"> |
| 93 | + <thead className="bg-gray-100 text-left"> |
| 94 | + <tr> |
| 95 | + <th className="px-3 py-2 text-right font-semibold">#</th> |
| 96 | + <th className="px-3 py-2 font-semibold">Competitor</th> |
| 97 | + <th className="px-3 py-2 text-right font-semibold">Sum</th> |
| 98 | + <th className="px-3 py-2 text-right font-semibold"> |
| 99 | + <span className="inline-flex items-center justify-end gap-1"> |
| 100 | + Kinch |
| 101 | + <KinchTooltip /> |
| 102 | + </span> |
| 103 | + </th> |
| 104 | + <th className="px-3 py-2 text-right font-semibold">Medals</th> |
| 105 | + </tr> |
| 106 | + </thead> |
| 107 | + <tbody> |
| 108 | + {rankings.map((ranking) => ( |
| 109 | + <tr key={ranking.person.registrantId} className="border-t border-gray-200"> |
| 110 | + <td className="px-3 py-2 text-right">{ranking.position}</td> |
| 111 | + <td className="px-3 py-2"> |
| 112 | + <LinkComponent |
| 113 | + className="text-blue-600 hover:underline" |
| 114 | + to={`/competitions/${competitionId}/persons/${ranking.person.registrantId}/results`}> |
| 115 | + {ranking.person.name} |
| 116 | + </LinkComponent> |
| 117 | + </td> |
| 118 | + <td className="px-3 py-2 text-right font-semibold">{ranking.sumOfRanks}</td> |
| 119 | + <td className="px-3 py-2 text-right">{formatKinch(ranking.kinch)}</td> |
| 120 | + <td className="px-3 py-2 text-right">{formatMedals(ranking)}</td> |
| 121 | + </tr> |
| 122 | + ))} |
| 123 | + </tbody> |
| 124 | + </table> |
| 125 | + </div> |
| 126 | + )} |
| 127 | + |
| 128 | + {isLoadingResults && <div className="type-body text-gray-600">Loading results...</div>} |
| 129 | + </Container> |
| 130 | + ); |
| 131 | +} |
0 commit comments