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 3827101ca35316504f31a8770643852546abf714
parent a067af77aff2c3ccf9ded1a72fc8e453d65b2424
Author: Andreas Gruhler <andreas.gruhler@adfinis.com>
Date:   Thu, 13 Apr 2023 23:25:27 +0200

feat: replace rating w/ scoring

Diffstat:
MREADME.md | 6++----
Msrc/App.js | 6+++---
Msrc/Heats.js | 2+-
Msrc/Leaderboard.js | 2+-
Dsrc/Rate.js | 103-------------------------------------------------------------------------------
Asrc/Score.js | 103+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 110 insertions(+), 112 deletions(-)

diff --git a/README.md b/README.md @@ -100,7 +100,7 @@ The basic code for the authentication flow was taken from the [official React Ge Sign up process for new judges: * The judge is required to have a valid email address -* Judges can be invied to the rating backend by invite only (Click ["Invite"](https://app.supabase.com/project/_/auth/users) in Supabase backend) +* Judges can be invied to the scoring backend by invite only (Click ["Invite"](https://app.supabase.com/project/_/auth/users) in Supabase backend) * The UUID is referenced automatically in the `judges` table, row can be amended with name of the judge Sign in process: @@ -125,9 +125,7 @@ To export data to local csv: ``` ## TODO - -* Rating UI -* Delete/Edit heats +* Bulk import/export in UI ## Contributors All source code is available in this repository. Contributions are always welcome! diff --git a/src/App.js b/src/App.js @@ -4,7 +4,7 @@ import { BrowserRouter as Router, Routes, Route, Outlet, Link } from 'react-rout import { supabase } from './supabaseClient' -const Rate = lazy(() => import('./Rate')) +const Score = lazy(() => import('./Score')) const Heats = lazy(() => import('./Heats')) const Athletes = lazy(() => import('./Athletes')) const Startlist = lazy(() => import('./Startlist')) @@ -19,7 +19,7 @@ function Layout({session}) { <nav> <ul> <li><Link to="/">Leaderboard</Link></li> - {session ? <li><Link to="/rate">Rate</Link></li> : ''} + {session ? <li><Link to="/score">Scoring</Link></li> : ''} {session ? <li><Link to="/heats">Heats and Startlists</Link></li> : ''} {session ? <li> <Link to="/athletes">Athletes</Link></li> : ''} <li> @@ -66,7 +66,7 @@ function App() { <Routes> <Route path="/" element={<Layout session={session} />}> <Route path="/" element={<Leaderboard session={session} />} /> - <Route path="/rate" element={<Rate session={session} />} /> + <Route path="/score" element={<Score session={session} />} /> <Route path="/heats" element={<Heats session={session} />} /> <Route path="/athletes" element={<Athletes session={session} />} /> <Route path="/startlist/:heatId" element={<Startlist session={session} />} /> diff --git a/src/Heats.js b/src/Heats.js @@ -73,7 +73,7 @@ function HeatForm({session}) { return ( <div> - <h1>Heats <button disabled={!loading}>{loading ? '🔄 loading' : ''}</button></h1> + <h1>Heats and Startlists <button disabled={!loading}>{loading ? '🔄 loading' : ''}</button></h1> <form method='post' onSubmit={addHeat}> <table> <thead> diff --git a/src/Leaderboard.js b/src/Leaderboard.js @@ -337,7 +337,7 @@ function Leaderboard({session}) { <th>Lastname</th> <th>Birthday</th> <th>School</th> - <th colSpan={heatSelection.length * 2}>Heat Ratings & Sum</th> + <th colSpan={heatSelection.length * 2}>Heat Scores & Sum</th> <th colSpan={3}>Summary (all heats)</th> </tr> <tr> diff --git a/src/Rate.js b/src/Rate.js @@ -1,103 +0,0 @@ -import { lazy, useEffect, useState } from 'react' -import { getStartlistForHeats } from './Leaderboard' -import Select from 'react-select' -import { supabase } from './supabaseClient' - -const Auth = lazy(() => import('./Auth')) - -async function updateRating(rating, heatId, athleteId, userId) { - await supabase.from('ratings').upsert({ - rating: rating, - heat: heatId, - athlete: athleteId, - judge: userId - }) -} - -function RatingForm({session}) { - const [heats, setHeats] = useState([]) - const [heatSelection, setHeatSelection] = useState(0) - const [athleteOpts, setAthleteOpts] = useState([]) - const [athleteSelection, setAthleteSelection] = useState(0) - const [rating, setRating] = useState(0) - - // add options to select or rank by heat - const heatOpts = heats.map(h => { - return { - value: h.id, - label: h.name - } - }) - - useEffect(() => { - (async () => { - const heatList = await supabase.from('heats').select() - setHeats(heatList.data) - - const startlist = await getStartlistForHeats([heatSelection.value]) - - if (startlist.error) - return - - setAthleteOpts(startlist.data.map(s => { - return { - value: s.athlete, - label: s.nr + " " + s.firstname + " " + (s.lastname ? s.lastname : "") - } - })) - - if (heatSelection.value === undefined || athleteSelection.value === undefined) - return - - // check if existing rating for heat and athlete exists - const currentRating = await supabase.from('ratings').select() - .eq('heat', heatSelection.value) - .eq('athlete', athleteSelection.value) - .eq('judge', session.user.id) - - if (rating === 0 && currentRating.data?.length > 0) { - // fallback to current rating when no rating was given - setRating(currentRating.data[0].rating) - } else { - // store new rating - updateRating(rating, - heatSelection.value, - athleteSelection.value, - session.user.id) - } - })(); - }, [heatSelection, athleteSelection, session.user.id, rating]); - - return ( - <div> - <h1>Rate Athletes</h1> - Heat: - <Select - options={heatOpts} - onChange={h => { setHeatSelection(h); setRating(0) }} - /> - Athlete: - <Select - options={athleteOpts} - onChange={a => { setAthleteSelection(a); setRating(0) }} - /> - Rating: - <input - type="number" - size="5" - value={rating} - onChange={(e) => setRating(e.target.value)} - /> - </div> - ) -} - -function Rate({session}) { - return ( - <div> - {!session ? <Auth /> : <RatingForm session={session} />} - </div> - ) -} - -export default Rate diff --git a/src/Score.js b/src/Score.js @@ -0,0 +1,103 @@ +import { lazy, useEffect, useState } from 'react' +import { getStartlistForHeats } from './Leaderboard' +import Select from 'react-select' +import { supabase } from './supabaseClient' + +const Auth = lazy(() => import('./Auth')) + +async function updateRating(rating, heatId, athleteId, userId) { + await supabase.from('ratings').upsert({ + rating: rating, + heat: heatId, + athlete: athleteId, + judge: userId + }) +} + +function ScoringForm({session}) { + const [heats, setHeats] = useState([]) + const [heatSelection, setHeatSelection] = useState(0) + const [athleteOpts, setAthleteOpts] = useState([]) + const [athleteSelection, setAthleteSelection] = useState(0) + const [rating, setRating] = useState(0) + + // add options to select or rank by heat + const heatOpts = heats.map(h => { + return { + value: h.id, + label: h.name + } + }) + + useEffect(() => { + (async () => { + const heatList = await supabase.from('heats').select() + setHeats(heatList.data) + + const startlist = await getStartlistForHeats([heatSelection.value]) + + if (startlist.error) + return + + setAthleteOpts(startlist.data.map(s => { + return { + value: s.athlete, + label: s.nr + " " + s.firstname + " " + (s.lastname ? s.lastname : "") + } + })) + + if (heatSelection.value === undefined || athleteSelection.value === undefined) + return + + // check if existing rating for heat and athlete exists + const currentRating = await supabase.from('ratings').select() + .eq('heat', heatSelection.value) + .eq('athlete', athleteSelection.value) + .eq('judge', session.user.id) + + if (rating === 0 && currentRating.data?.length > 0) { + // fallback to current rating when no rating was given + setRating(currentRating.data[0].rating) + } else { + // store new rating + updateRating(rating, + heatSelection.value, + athleteSelection.value, + session.user.id) + } + })(); + }, [heatSelection, athleteSelection, session.user.id, rating]); + + return ( + <div> + <h1>Score Athletes</h1> + Heat: + <Select + options={heatOpts} + onChange={h => { setHeatSelection(h); setRating(0) }} + /> + Athlete: + <Select + options={athleteOpts} + onChange={a => { setAthleteSelection(a); setRating(0) }} + /> + Score: + <input + type="number" + size="5" + value={rating} + onChange={(e) => setRating(e.target.value)} + /> + </div> + ) +} + +function Score({session}) { + return ( + <div> + {!session ? <Auth /> : <ScoringForm session={session} />} + </div> + ) +} + +export default Score