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:
M | src/Leaderboard.jsx | | | 55 | +++++++++++++++++++------------------------------------ |
A | src/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'>▿ 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