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 52ea1bcf0aecaba35259cf03a69c82118e14b60b
parent 5b452279c155f41a94395f4858d8378437149bed
Author: Andreas Gruhler <andreas.gruhler@adfinis.com>
Date:   Tue, 14 Mar 2023 18:06:10 +0100

feat: rating ui for judges

Diffstat:
MREADME.md | 9+++++++--
Mschema/ratings.sql | 24+++++++++++++++++++-----
Msrc/Leaderboard.js | 9+++++----
Msrc/Rate.js | 87++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
4 files changed, 117 insertions(+), 12 deletions(-)

diff --git a/README.md b/README.md @@ -18,6 +18,11 @@ The Supabase schema is stored in the `schema` folder and can be created using pl ![supabase-schema](supabase-schema.png) +To update the schema from the current database, use (example for table `startlist`): +```bash +pg_dump -h db.aaxkgqazjhwumoljibld.supabase.co -U postgres -t 'public.startlist' --schema-only > schema/startlist.sql +``` + Additionally, following views and functions are required: ```sql @@ -101,4 +106,4 @@ To export data to local csv: * Magic link TTL * Rating UI -* New start list, create from existing runs -\ No newline at end of file +* Delete/Edit heats +\ No newline at end of file diff --git a/schema/ratings.sql b/schema/ratings.sql @@ -27,10 +27,10 @@ SET default_table_access_method = heap; CREATE TABLE public.ratings ( id bigint NOT NULL, created_at timestamp with time zone DEFAULT now(), - athlete bigint, - judge uuid, + athlete bigint NOT NULL, + judge uuid NOT NULL, rating double precision, - heat bigint + heat bigint NOT NULL ); @@ -58,11 +58,11 @@ ALTER TABLE public.ratings ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY -- --- Name: ratings rating_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- Name: ratings ratings_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres -- ALTER TABLE ONLY public.ratings - ADD CONSTRAINT rating_pkey PRIMARY KEY (id); + ADD CONSTRAINT ratings_pkey PRIMARY KEY (athlete, judge, heat); -- @@ -90,6 +90,20 @@ ALTER TABLE ONLY public.ratings -- +-- Name: ratings Enable insert for authenticated users only; Type: POLICY; Schema: public; Owner: postgres +-- + +CREATE POLICY "Enable insert for authenticated users only" ON public.ratings FOR INSERT TO authenticated WITH CHECK (true); + + +-- +-- Name: ratings Enable judges to update their own ratings; Type: POLICY; Schema: public; Owner: postgres +-- + +CREATE POLICY "Enable judges to update their own ratings" ON public.ratings FOR UPDATE USING ((auth.uid() = judge)); + + +-- -- Name: ratings Enable read access for all users; Type: POLICY; Schema: public; Owner: postgres -- diff --git a/src/Leaderboard.js b/src/Leaderboard.js @@ -2,7 +2,7 @@ import { supabase } from './supabaseClient' import { Fragment, useEffect, useState, useRef } from 'react' import Select from 'react-select' -async function getStartlistForHeats(heatIds) { +export async function getStartlistForHeats(heatIds) { return supabase.rpc('distinct_startlist', { 'heat_ids': heatIds }) } @@ -134,7 +134,7 @@ function Leaderboard() { const selectHeatRef = useRef(); // add options to select or rank by heat - let heatOpts = heats.map(h => { + const heatOpts = heats.map(h => { return { value: h.id, label: h.name @@ -142,7 +142,7 @@ function Leaderboard() { }) // add options to rank by best/worst heat - let rankOpts = heatOpts.concat([ + const rankOpts = heatOpts.concat([ { value: 'best', label: 'Best Heat' @@ -273,4 +273,4 @@ function Leaderboard() { ); } -export default Leaderboard; +export default Leaderboard; +\ No newline at end of file diff --git a/src/Rate.js b/src/Rate.js @@ -1,11 +1,96 @@ -import { lazy } from 'react' +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 () => { + let heatList = {data: []} + heatList = await supabase.from('heats').select() + setHeats(heatList.data) + + let startlist = {data: []} + startlist = await getStartlistForHeats([heatSelection.value]) + + if (startlist.error !== null) { + return {data: []} + } + + setAthleteOpts(startlist.data.map(s => { + return { + value: s.athlete, + label: s.id + " " + 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.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.session.user.id) + } + })(); + }, [heatSelection, athleteSelection, session.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> ) }