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:
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