myheats

Live heats, scoring and leaderboard for sport events
git clone https://git.in0rdr.ch/myheats.git
Log | Files | Refs | Pull requests | README | LICENSE

commit 7f4057404dd0d56226e2073ce9bbf2e58abcaef1
parent b1dc6bcad26e903cd64fb6770d62325ccca346be
Author: Andreas Gruhler <andreas.gruhler@adfinis.com>
Date:   Sun,  6 Oct 2024 13:28:43 +0200

feat: hide details

Diffstat:
Msrc/frontend/Leaderboard.jsx | 102++++++++++++++++++++++++++++++++++++++-----------------------------------------
Msrc/frontend/css/App.css | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Msrc/frontend/utils.js | 8+++++---
3 files changed, 124 insertions(+), 64 deletions(-)

diff --git a/src/frontend/Leaderboard.jsx b/src/frontend/Leaderboard.jsx @@ -125,8 +125,8 @@ async function getScoreSummary(heatIds) { } // find best/worst heat, use 'n/a' if no score yet - i.bestHeat = i.heats.length > 0 ? Math.max(...i.heats.map(h => h.summary.toFixed(1))) : 'n/a' - i.worstHeat = i.heats.length > 0 ? Math.min(...i.heats.map(h => h.summary.toFixed(1))) : 'n/a' + i.bestHeat = i.heats.length > 0 ? Math.max(...i.heats.map(h => h.summary.toFixed(1))) : NaN.toString() + i.worstHeat = i.heats.length > 0 ? Math.min(...i.heats.map(h => h.summary.toFixed(1))) : NaN.toString() // sum up all totals across heats i.sum = i.heats.map(h => h.summary).reduce((a, b) => a + b, 0).toFixed(1) @@ -255,6 +255,7 @@ function Leaderboard({session}) { const [heatSelection, setHeatSelection] = useState([]) const [heats, setHeats] = useState([]) const [rankingComp, setRankingComp] = useState([]) + const [details, showDetails] = useState(false) const selectHeatRef = useRef(); const selectRankRef = useRef(); @@ -320,6 +321,9 @@ function Leaderboard({session}) { console.error(error) } setHeats(data) + + // default to rank by total + setRankingComp([rankOptions[3]]) setLoading(false) })(); }, []); @@ -348,74 +352,66 @@ function Leaderboard({session}) { return ( <div> - <button disabled={!loading}>{loading ? '↺ loading' : ''}</button> <div className='Leaderboard'> <header> - <table> - <thead> - <tr> - <th>Heats to display</th> - <th>Rank by</th> - </tr> - </thead> - <tbody> - <tr> - <td data-title='Heats to display'> - <Select - closeMenuOnSelect={false} - isMulti - options={heatOpts} - onChange={h => setHeatSelection(h)} - ref={selectHeatRef} - /> - </td> - <td data-title='Rank by'> - <Select - closeMenuOnSelect={false} - isMulti - options={rankOpts} - onChange={h => setRankingComp(h)} - ref={selectRankRef} - /> - </td> - </tr> - </tbody> - </table> + <button className='loading' disabled={!loading}>↺ loading</button> + <button className={`show-details ${details ? 'toggled' : ''}`} onClick={() => showDetails(!details)}> + <div className='thumb'></div> + <span>{details ? 'less' : 'more'}</span> + </button> + <label htmlFor='heat'>Heats to display</label> + <Select + closeMenuOnSelect={false} + isMulti + options={heatOpts} + onChange={h => setHeatSelection(h)} + ref={selectHeatRef} + id='heat' /> + <label htmlFor='rank' className={details ? '' : 'hidden'}>Rank by</label> + <Select + closeMenuOnSelect={false} + isMulti + options={rankOpts} + defaultValue={rankOpts[0].options[3]} + onChange={h => setRankingComp(h)} + ref={selectRankRef} + className={details ? '' : 'hidden'} + id='rank' /> </header> - <table className='leaderboard'> + <table className={details ? 'leaderboard' : 'hide-rank'}> <thead> <tr> - <th>Rank</th> - <th>Start Nr.</th> - <th>Firstname</th> - <th>Lastname</th> - <th>Birthday</th> - <th>School</th> + <th className={details ? 'right' : 'hidden'}>Rank</th> + <th className='right'>Start Nr.</th> + <th>Name</th> + <th className={details ? '' : 'hidden'}>Birthday</th> + <th className={details ? '' : 'hidden'}>School</th> {heatSelection.map(h => ( - <th key={h.value}>{h.label}</th> + <th className={details ? 'right' : 'hidden'} key={h.value}>{h.label}</th> ))} - <th>Best</th> - <th>Worst</th> - <th>Total</th> + <th className={details ? 'right' : 'hidden'}>Best</th> + <th className={details ? 'right' : 'hidden'}>Worst</th> + <th className='right'>Total</th> </tr> </thead> <tbody> {leaderboard.sort(rankByHeat(rankingComp)).map(i => ( <tr key={i.id}> - <td></td> - <td data-title='Start Nr.'>{i.nr}</td> - <td data-title='Firstname'>{i.firstname}</td> - <td data-title='Lastname'>{i.lastname}</td> - <td data-title='Birthday'>{i.birthday ? new Date(i.birthday).toLocaleDateString(locale, dateOptions) : ''}</td> - <td data-title='School'>{i.school}</td> + <td className={details ? 'right' : 'hidden'}></td> + <td data-title='Start Nr.' className='right'>{i.nr}</td> + <td data-title='Name'>{i.firstname} {i.lastname}</td> + <td data-title='Birthday' className={details ? '' : 'hidden'}> + {i.birthday ? new Date(i.birthday).toLocaleDateString(locale, dateOptions) : ''} + </td> + <td data-title='School' className={details ? '' : 'hidden'}>{i.school}</td> {heatSelection.map(h => ( <Fragment key={h.value}> {/* list all scores from the judges seperated with '+' signs, show sum on right side */} - <td className='right' data-title={h.label}>{formatScores(i, h)}</td> + <td className={details ? 'right' : 'hidden'} data-title={h.label}>{formatScores(i, h)}</td> </Fragment> ))} - <td className='right' data-title='Best'>{i.bestHeat}</td> - <td className='right' data-title='Worst'>{i.worstHeat}</td> + <td className={details ? 'right' : 'hidden'} data-title='Best'>{i.bestHeat}</td> + <td className={details ? 'right' : 'hidden'} data-title='Worst'>{i.worstHeat}</td> <td className='right' data-title='Total'>{i.sum}</td> </tr> ))} diff --git a/src/frontend/css/App.css b/src/frontend/css/App.css @@ -57,7 +57,7 @@ th, td { text-align: left; } -th { +th, label { font-weight: normal; font-size: 0.8em; text-transform: uppercase; @@ -113,11 +113,73 @@ footer span button { list-style: none; } +.hidden { + display: none +} + +.Leaderboard { + position: relative; +} + +.Leaderboard header { + padding: 0 20px; +} + +.loading { + width: 80px; + position: absolute; + right: 120px; + top: -29px; + font-size: 0.8em; + text-transform: uppercase; + color: #b0b0b6; +} + +/* Show details toggle button */ +.show-details { + width: 50px; + height: 20px; + border-radius: 5px; + box-shadow: 0 1px #b1b0b6; + position: absolute; + right: 20px; + top: -15px; + transition: background 0.1s ease, box-shadow 0.1s ease; +} +.show-details .thumb { + text-transform: uppercase; + height: 12px; + width: 22px; + border-radius: 5px; + background: white; + border: 1px solid #cfced3; + position: absolute; + left: 3px; + transform: translateX(0); + transform: translateY(-50%); + transition: left 0.15s ease; +} +.show-details.toggled { + background: #f3f2f7; + box-shadow: 0 -1px #b1b0b6; +} +.show-details.toggled .thumb { + left: calc(50px - 29px); +} +.show-details span { + font-size: 0.8em; + text-transform: uppercase; + color: #b0b0b6; + right: 53px; + top: 1px; + position: absolute; +} + /* increment rank row number */ -table.leaderboard tr{ +table.leaderboard:not(.hide-rank) tr { counter-increment: rowNumber; } -table.leaderboard tr td:first-child::before { +table.leaderboard:not(.hide-rank) tr td:first-child::before { content: counter(rowNumber); min-width: 1em; margin-right: 0.5em; @@ -137,24 +199,24 @@ table.leaderboard td:last-child { /* https://css-tricks.com/making-tables-responsive-with-minimal-css */ @media(max-width: 1100px) { - table thead { + table.leaderboard thead { left: -9999px; position: absolute; visibility: hidden; } - table tr { + table.leaderboard tr { display: flex; flex-direction: row; flex-wrap: wrap; padding: 20px 0; } - table tr:not(:last-child) { + table.leaderboard tr:not(:last-child) { border-bottom: 1px solid #e1e1e7; } - table td { + table.leaderboard td { margin: 0 -1px -1px 0; padding-top: 35px; margin-bottom: 25px; @@ -163,7 +225,7 @@ table.leaderboard td:last-child { text-align: left !important; } - table td:before { + table.leaderboard td:before { content: attr(data-title); position: absolute; top: 3px; diff --git a/src/frontend/utils.js b/src/frontend/utils.js @@ -87,8 +87,10 @@ export const rankByHeat = function(rankingComp) { return b.sum - a.sum default: // rank by heat totals - if (b.heats.find(h => h.heatId === r.value)?.summary - a.heats.find(h => h.heatId === r.value)?.summary !== 0) { - return b.heats.find(h => h.heatId === r.value)?.summary - a.heats.find(h => h.heatId === r.value)?.summary + let aHeatTotal = a.heats.find(h => h.heatId === r.value)?.summary + let bHeatTotal = b.heats.find(h => h.heatId === r.value)?.summary + if (bHeatTotal - aHeatTotal !== 0) { + return bHeatTotal - aHeatTotal } } } @@ -108,7 +110,7 @@ export const formatScores = function(i, h) { // get individual scores of the heat and score sum return getScores(i, h) + " = " + i.heats.find(heat => heat.heatId === h.value)?.summary.toFixed(1) } else { - return "n/a" + return NaN.toString() } }