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 63e4f09ba41152a9646be4d0ed5dc3789d784020
parent e68968cac97aed5e53bd8f67af0785016aa743f6
Author: Andreas Gruhler <andreas.gruhler@adfinis.com>
Date:   Thu, 13 Apr 2023 23:01:51 +0200

feat: improve form new heat from top N

Diffstat:
Msrc/App.css | 9++++++++-
Msrc/App.js | 2+-
Msrc/Athletes.js | 2+-
Msrc/Heats.js | 4++--
Msrc/Leaderboard.js | 265++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Msrc/Rate.js | 2+-
Msrc/Startlist.js | 2+-
7 files changed, 176 insertions(+), 110 deletions(-)

diff --git a/src/App.css b/src/App.css @@ -2,7 +2,7 @@ table { border-collapse: collapse; } -tr, th,td { +tr, th, td { border: 1px solid black; padding: 5px; } @@ -45,3 +45,10 @@ button:disabled { .heatInfo li { list-style: none; } + +.newHeatForm table th { + text-align: left; +} +.newHeatForm tr, .newHeatForm th, .newHeatForm td { + border: none; +} diff --git a/src/App.js b/src/App.js @@ -62,7 +62,7 @@ function App() { <Suspense fallback={<div>Loading...</div>}> <Routes> <Route path="/" element={<Layout session={session} />}> - <Route path="/leaderboard" element={<Leaderboard />} /> + <Route path="/leaderboard" element={<Leaderboard session={session} />} /> <Route path="/rate" element={<Rate session={session} />} /> <Route path="/heats" element={<Heats session={session} />} /> <Route path="/athletes" element={<Athletes session={session} />} /> diff --git a/src/Athletes.js b/src/Athletes.js @@ -136,4 +136,4 @@ function Athletes({session}) { ) } - export default Athletes; +export default Athletes diff --git a/src/Heats.js b/src/Heats.js @@ -34,7 +34,7 @@ async function addHeat(e) { } } -function defaultsSet({name, location}) { +export function defaultsSet({name, location}) { return (name === 'Name' || location === 'Location') } @@ -132,4 +132,4 @@ function Heats({session}) { ) } - export default Heats; +export default Heats; diff --git a/src/Leaderboard.js b/src/Leaderboard.js @@ -2,6 +2,8 @@ import { supabase } from './supabaseClient' import { Fragment, useEffect, useState, useRef } from 'react' import Select from 'react-select' +import { defaultsSet } from './Heats.js' + export async function getStartlistForHeats(heatIds) { return supabase.rpc('distinct_startlist', { 'heat_ids': heatIds }) } @@ -92,23 +94,36 @@ function rankByHeat(rankingComp) { } } -async function newHeatFromLeaderboard(e, leaderboard, rankingComp, selectHeatRef, selectRankRef, newHeatSize, newHeatName) { +async function newHeatFromLeaderboard(e, {leaderboard, rankingComp, selectHeatRef, selectRankRef}) { e.preventDefault() if (leaderboard.length === 0) { - // cannot create new heat from empty leaderboard + alert("Cannot create new heat from empty leaderboard. Select heats to display.") return } - if (newHeatName === "Heat name") { - alert('Choose a better name for your new heat') + // Read the form data + const formData = new FormData(e.target); + const formJson = Object.fromEntries(formData.entries()); + + console.log(formJson) + console.log(defaultsSet(formJson)) + + if (defaultsSet(formJson)) { + alert('Check data of the new heat, seems like the defaults') return } // create new heat const { data, error } = await supabase .from('heats') - .insert({ name: newHeatName }) + .insert({ + name: formJson.name, + location: formJson.location, + // planned_start is an empty string if unset + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/time + planned_start: formJson.planned_start === '' ? null : formJson.planned_start + }) .select() if (error !== null) { @@ -117,7 +132,7 @@ async function newHeatFromLeaderboard(e, leaderboard, rankingComp, selectHeatRef } const sortedBoard = leaderboard.sort(rankByHeat(rankingComp)) - for (let i = 0; i < newHeatSize && i < sortedBoard.length; i++ ) { + for (let i = 0; i < formJson.size && i < sortedBoard.length; i++ ) { // add top N athletes from current leaderboard to new heat await supabase .from('startlist') @@ -127,23 +142,77 @@ async function newHeatFromLeaderboard(e, leaderboard, rankingComp, selectHeatRef // clear values in selects to refresh list of heats selectHeatRef.current.clearValue() selectRankRef.current.clearValue() - alert('Created new heat "' + newHeatName + '" with top ' + newHeatSize + ' athletes') + alert('Created new heat "' + formJson.name + '" with top ' + formJson.size + ' athletes') // todo: put heatOpts and rankOptions in state/useEffect again window.location.reload() } -function Leaderboard() { +function NewHeatForm(leaderboard, rankingComp, selectHeatRef, selectRankRef) { + return ( + <div className='newHeatForm'> + <h2>New Heat from Leaderboard</h2> + <p> + Create new heat from the sorted leaderboard above. + </p> + <p> + Includes favorite "top N" athletes in the next 🔥 heat. + </p> + <form method='post' onSubmit={e => newHeatFromLeaderboard( + e, + leaderboard, + rankingComp, + selectHeatRef, + selectRankRef + )}> + <table> + <tbody> + <tr> + <th>New heat name: *</th> + <td> + <input type='text' name='name' defaultValue='Name' /> + </td> + </tr> + <tr> + <th>Location:</th> + <td> + <input type='text' name='location' defaultValue='Location' /> + </td> + </tr> + <tr> + <th>Planned start:</th> + <td> + <input + type='time' + name='planned_start' /> + </td> + </tr> + <tr> + <th>Include top N:</th> + <td> + <input type='number' name='size' defaultValue='10' /> + </td> + </tr> + <tr> + <td>(<i>* required</i>)</td> + <td> + <button type='submit'>➕ new</button> + </td> + </tr> + </tbody> + </table> + </form> + </div> + ) +} + +function Leaderboard({session}) { const [loading, setLoading] = useState(false) const [leaderboard, setLeaderboard] = useState([]) const [heatSelection, setHeatSelection] = useState([]) const [heats, setHeats] = useState([]) const [rankingComp, setRankingComp] = useState([]) - // state for new heat from top N leaderboard - const [newHeatSize, setNewHeatSize] = useState(6) - const [newHeatName, setNewHeatName] = useState("Heat name") - const selectHeatRef = useRef(); const selectRankRef = useRef(); @@ -239,97 +308,87 @@ function Leaderboard() { }, [heatSelection]); return ( - <div className="Leaderboard"> - <header> - <div> - Heats to display: - <Select - closeMenuOnSelect={false} - isMulti - options={heatOpts} - onChange={h => setHeatSelection(h)} - ref={selectHeatRef} - /> - Rank by (in this order): - <Select - closeMenuOnSelect={false} - isMulti - options={rankOpts} - onChange={h => setRankingComp(h)} - ref={selectRankRef} - /> - <form> - New heat from top <input - type='number' - size='5' - value={newHeatSize} - onChange={(e) => setNewHeatSize(e.target.value)} - />: - <input type='text' value={newHeatName} onChange={(e) => setNewHeatName(e.target.value)} /> - <button onClick={e => newHeatFromLeaderboard( - e, - leaderboard, - rankingComp, - selectHeatRef, - selectRankRef, - newHeatSize, - newHeatName - )}>Create</button> - </form> - </div> - <h1>Leaderboard <button disabled={!loading}>{loading ? '🔄 loading' : ''}</button></h1> - </header> - <table className='leaderboard'> - <thead> - <tr> - <th>Rank</th> - <th>Start Nr.</th> - <th>Firstname</th> - <th>Lastname</th> - <th>Birthday</th> - <th>School</th> - <th colSpan={heatSelection.length * 2}>Heat Ratings & Sum</th> - <th colSpan={3}>Summary (all heats)</th> - </tr> - <tr> - <th></th> - <th></th> - <th></th> - <th></th> - <th></th> - <th></th> - {heatSelection.map(h => ( - <th key={h.value} colSpan={2}>{h.label}</th> - ))} - <th>👍 Best</th> - <th>👎 Worst</th> - <th>Total</th> - </tr> - </thead> - <tbody> - {leaderboard.sort(rankByHeat(rankingComp)).map(i => ( - <tr key={i.id}> - <td></td> - <td>{i.nr}</td> - <td>{i.firstname}</td> - <td>{i.lastname}</td> - <td>{i.birthday}</td> - <td>{i.school}</td> - {heatSelection.map(h => ( - <Fragment key={h.value}> - <td className='right'>{i.heats.find(heat => heat.heatId === h.value)?.ratings?.map(r => r.rating).join(" / ")}</td> - <td className='right'>{i.heats.find(heat => heat.heatId === h.value)?.summary}</td> - </Fragment> - ))} - <td>{i.bestHeat}</td> - <td>{i.worstHeat}</td> - <td>{i.sum}</td> - </tr> - ))} - </tbody> - </table> + <div> + <div className='Leaderboard'> + <header> + <div> + Heats to display: + <Select + closeMenuOnSelect={false} + isMulti + options={heatOpts} + onChange={h => setHeatSelection(h)} + ref={selectHeatRef} + /> + Rank by (in this order): + <Select + closeMenuOnSelect={false} + isMulti + options={rankOpts} + onChange={h => setRankingComp(h)} + ref={selectRankRef} + /> + </div> + <h1>Leaderboard <button disabled={!loading}>{loading ? '🔄 loading' : ''}</button></h1> + </header> + <table className='leaderboard'> + <thead> + <tr> + <th>Rank</th> + <th>Start Nr.</th> + <th>Firstname</th> + <th>Lastname</th> + <th>Birthday</th> + <th>School</th> + <th colSpan={heatSelection.length * 2}>Heat Ratings & Sum</th> + <th colSpan={3}>Summary (all heats)</th> + </tr> + <tr> + <th></th> + <th></th> + <th></th> + <th></th> + <th></th> + <th></th> + {heatSelection.map(h => ( + <th key={h.value} colSpan={2}>{h.label}</th> + ))} + <th>👍 Best</th> + <th>👎 Worst</th> + <th>Total</th> + </tr> + </thead> + <tbody> + {leaderboard.sort(rankByHeat(rankingComp)).map(i => ( + <tr key={i.id}> + <td></td> + <td>{i.nr}</td> + <td>{i.firstname}</td> + <td>{i.lastname}</td> + <td>{i.birthday}</td> + <td>{i.school}</td> + {heatSelection.map(h => ( + <Fragment key={h.value}> + <td className='right'>{i.heats.find(heat => heat.heatId === h.value)?.ratings?.map(r => r.rating).join(" / ")}</td> + <td className='right'>{i.heats.find(heat => heat.heatId === h.value)?.summary}</td> + </Fragment> + ))} + <td>{i.bestHeat}</td> + <td>{i.worstHeat}</td> + <td>{i.sum}</td> + </tr> + ))} + </tbody> + </table> + </div> + {session ? <NewHeatForm + leaderboard={leaderboard} + rankingComp={rankingComp} + selectHeatRef={selectHeatRef} + selectRankRef={selectRankRef} + /> : ''} </div> - ); + ) } -export default Leaderboard; +export default Leaderboard diff --git a/src/Rate.js b/src/Rate.js @@ -100,4 +100,4 @@ function Rate({session}) { ) } -export default Rate; +export default Rate diff --git a/src/Startlist.js b/src/Startlist.js @@ -156,4 +156,4 @@ function Startlist({session}) { ) } - export default Startlist; +export default Startlist