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 6d58d104a4827c3fa9510f847d10d596c2286a75
parent e8f224a3169d1bb0de05c10c80012f045b4b8e71
Author: Andreas Gruhler <andreas.gruhler@adfinis.com>
Date:   Tue,  4 Apr 2023 00:59:59 +0200

fix: infinite useEffect dependency loop

Diffstat:
MREADME.md | 20++++++++++++++++++++
Msrc/Leaderboard.js | 128+++++++++++++++++++++++++++++++++++++++++--------------------------------------
2 files changed, 86 insertions(+), 62 deletions(-)

diff --git a/README.md b/README.md @@ -72,6 +72,26 @@ create trigger on_auth_user_created for each row execute procedure public.handle_new_judge(); ``` +Lastly, enable [PostgreSQL publication](https://www.postgresql.org/docs/current/logical-replication-publication.html) to enable [Supabase realtime feature for Postgres changes](https://supabase.com/docs/guides/realtime/extensions/postgres-changes): +```sql +begin; + +-- remove the supabase_realtime publication +drop + publication if exists supabase_realtime; + +-- re-create the supabase_realtime publication with no tables +create publication supabase_realtime; + +commit; + +-- add a table to the publication +alter + publication supabase_realtime add table ratings; +``` + +This is required for the leaderboard to automatically update scores when they are created or changed by judges. + ## Authentication with Magic Links Authentication of judges is performed using [Supabase Magic Links](https://supabase.com/docs/guides/auth/auth-magic-link). diff --git a/src/Leaderboard.js b/src/Leaderboard.js @@ -128,14 +128,15 @@ async function newHeatFromLeaderboard(e, leaderboard, rankingComp, selectHeatRef selectHeatRef.current.clearValue() selectRankRef.current.clearValue() alert('Created new heat "' + newHeatName + '" with top ' + newHeatSize + ' athletes') + + // todo: put heatOpts and rankOptions in state/useEffect again + window.location.reload(); } function Leaderboard() { const [leaderboard, setLeaderboard] = useState([]) const [heatSelection, setHeatSelection] = useState([]) const [heats, setHeats] = useState([]) - const [heatOpts, setHeatOpts] = useState([]) - const [rankOpts, setRankOpts] = useState([]) const [rankingComp, setRankingComp] = useState([]) // state for new heat from top N leaderboard @@ -145,78 +146,81 @@ function Leaderboard() { const selectHeatRef = useRef(); const selectRankRef = useRef(); - // subscribe to ratings from judges and - // reload all ratings to refresh leaderboard - const channel = supabase.channel('ratings') - channel.on( - 'postgres_changes', + // add options to select or rank by heat + const heatOpts = heats.map(h => { + return { + value: h.id, + label: h.name + } + }) + + // // add static ranking options + const rankOptions = [ + { + value: 'start', + label: 'Start Nr.' + }, { + value: 'best', + label: 'Best Heat' + }, { + value: 'worst', + label: 'Worst Heat' + }, { + value: 'total', + label: 'Total Sum (all heats)' + } + ] + + // // add dynamic options to rank by best/worst heat + const heatOptions = heatOpts.map(h => { + return { + value: h.value, + label: "Sum " + h.label + } + }) + + const rankOpts = [ { - event: '*', - table: 'ratings', + label: "Overall", + options: rankOptions }, - async (payload) => { - const ratingSummary = await getRatingSummary(heatSelection.map(h => h.value)) - setLeaderboard(ratingSummary) + { + label: "Heat Sum", + options: heatOptions } - ).subscribe() + ] useEffect(() => { (async () => { const heatList = await supabase.from('heats').select() setHeats(heatList.data) + })(); + }, []); - if (heats) { - // add options to select or rank by heat - setHeatOpts(heats.map(h => { - return { - value: h.id, - label: h.name - } - })) - - // add static ranking options - const rankOptions = [ - { - value: 'start', - label: 'Start Nr.' - }, { - value: 'best', - label: 'Best Heat' - }, { - value: 'worst', - label: 'Worst Heat' - }, { - value: 'total', - label: 'Total Sum (all heats)' - } - ] - - // add dynamic options to rank by best/worst heat - const heatOptions = heatOpts.map(h => { - return { - value: h.value, - label: "Sum " + h.label - } - }) - - const groupedOptions = [ - { - label: "Overall", - options: rankOptions - }, - { - label: "Heat Sum", - options: heatOptions - } - ]; - - setRankOpts(groupedOptions) - } - + useEffect(() => { + (async () => { + // reload entire leaderboard when heat selection is changed const ratingSummary = await getRatingSummary(heatSelection.map(h => h.value)) setLeaderboard(ratingSummary) })(); - }, [heatSelection, heatOpts, heats]); + }, [heatSelection]); + + useEffect(() => { + // subscribe to ratings from judges and + const channel = supabase.channel('ratings') + channel.on( + 'postgres_changes', + { + event: '*', + table: 'ratings', + }, + async (payload) => { + // todo: reload only required ratings + const ratingSummary = await getRatingSummary(heatSelection.map(h => h.value)) + setLeaderboard(ratingSummary) + } + ).subscribe() + }, [heatSelection]); return ( <div className="Leaderboard">