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 ac4cc563f1ea2fc0c4680862fd6c7e837b0fb242
parent f6b4efe3e361b492a1e5492f7de9c46063988a8c
Author: Andreas Gruhler <andreas.gruhler@adfinis.com>
Date:   Sun, 11 Aug 2024 11:57:41 +0200

feat(leaderboard): export to CSV

Diffstat:
Msrc/Leaderboard.jsx | 55+++++++++++++++++++------------------------------------
Asrc/utils.js | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 101 insertions(+), 36 deletions(-)

diff --git a/src/Leaderboard.jsx b/src/Leaderboard.jsx @@ -1,4 +1,5 @@ import { supabase } from './supabaseClient' +import { exportCSV, rankByHeat, getScores } from './utils' import { Fragment, useEffect, useState, useRef } from 'react' import Select from 'react-select' @@ -61,42 +62,6 @@ async function getScoreSummary(heatIds) { return startListWithScores } -function rankByHeat(rankingComp) { - return function(a, b) { - // rank by chosen heat or ranking comparator - for (const r of rankingComp) { - switch(r.value) { - case 'start': - // rank by start number - return b.nr < a.nr - case 'best': - if (b.bestHeat - a.bestHeat !== 0) { - // rank by best heat first - return b.bestHeat - a.bestHeat - } - // rank by least worst heat for identical best heats - return b.worstHeat - a.worstHeat - case 'worst': - // rank by worst heat - return b.worstHeat - a.worstHeat - case 'total': - // rank by total sum across heats - 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 - } - } - } - } -} - -function getScores(i, h) { - const scores = i.heats.find(heat => heat.heatId === h.value)?.scores?.map(s => s.score).join(" + ") - return scores === "" ? 0 : scores -} - async function newHeatFromLeaderboard(e, {leaderboard, rankingComp, selectHeatRef, selectRankRef}) { e.preventDefault() @@ -143,6 +108,19 @@ async function newHeatFromLeaderboard(e, {leaderboard, rankingComp, selectHeatRe window.location.reload() } +function ExportForm({leaderboard, heatSelection, rankingComp}) { + return ( + // export leaderboard with current ranking + <form method='post' onSubmit={e => exportCSV( + e, + leaderboard, + heatSelection, + rankingComp)}> + <button type='submit'>&#9663; export scores</button> + </form> + ) +} + function NewHeatForm(leaderboard, rankingComp, selectHeatRef, selectRankRef) { return ( <div className='newHeatForm'> @@ -359,6 +337,7 @@ function Leaderboard({session}) { <td data-title='School'>{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}>{getScores(i, h)} = {i.heats.find(heat => heat.heatId === h.value)?.summary}</td> </Fragment> ))} @@ -370,6 +349,10 @@ function Leaderboard({session}) { </tbody> </table> </div> + <ExportForm + leaderboard={leaderboard} + heatSelection={heatSelection} + rankingComp={rankingComp} /> {session ? <NewHeatForm leaderboard={leaderboard} rankingComp={rankingComp} diff --git a/src/utils.js b/src/utils.js @@ -0,0 +1,81 @@ +export const exportCSV = async function(e, leaderboard, heatSelection, rankingComp) { + e.preventDefault() + + // concatenante heat labels + const heatNames = heatSelection.map(h => h.label) + + // rank leaderboard by selected comparator + const heatSummaries = leaderboard.sort(rankByHeat(rankingComp)).map(i => + // for each entry i in the board, for every heat h + heatSelection.map(h => + // get individual scores of the heat + getScores(i, h) + " = " + // get heat score sum + + i.heats.find(heat => heat.heatId === h.value)?.summary + ) + ) + + // csv header + let csv = "rank,start_nr,firstname,lastname,birthday,school," + + heatNames + "," + "best,worst,total\n" + + // append leaderboard score results + for (let i = 0; i < leaderboard.length; i++) { + csv += i+1 + "," + leaderboard[i].nr + "," + leaderboard[i].firstname + "," + leaderboard[i].lastname + "," + + leaderboard[i].birthday + "," + leaderboard[i].school + "," + + heatSummaries[i] + "," + + leaderboard[i].bestHeat + "," + leaderboard[i].worstHeat + "," + leaderboard[i].sum + "\n" + } + + // create blob from csv + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }) + + // download blob + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = "scores-" + new Date().toISOString() + ".csv" + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + // let browser know not to keep the reference to the file + URL.revokeObjectURL(url) +} + +// define the ranking logic +export const rankByHeat = function(rankingComp) { + return function(a, b) { + // rank by chosen heat or ranking comparator + for (const r of rankingComp) { + switch(r.value) { + case 'start': + // rank by start number + return b.nr < a.nr + case 'best': + if (b.bestHeat - a.bestHeat !== 0) { + // rank by best heat first + return b.bestHeat - a.bestHeat + } + // rank by least worst heat for identical best heats + return b.worstHeat - a.worstHeat + case 'worst': + // rank by worst heat + return b.worstHeat - a.worstHeat + case 'total': + // rank by total sum across heats + 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 + } + } + } + } +} + +// Scores concat with "+" for leaderboard entry i and heat h +export const getScores = function(i, h) { + const scores = i.heats.find(heat => heat.heatId === h.value)?.scores?.map(s => s.score).join(" + ") + return scores === "" ? 0 : scores +} +\ No newline at end of file