From 7c579f15efbcc705e54e4e015610e473a97eb4ba Mon Sep 17 00:00:00 2001 From: FinnIckler Date: Thu, 13 Feb 2025 08:53:10 +0100 Subject: [PATCH] WCA Live: Podiums and Competitors Views (#10819) * add new routes * add new controller methods * add new components * add live_urls to routes.js.erb * fix merge whoopsie * Backend cleanup * Data bindings fixes * Rename results vars in ByPerson view * Move route definition back to original place * Update app/webpacker/components/Live/ByPerson/index.jsx Co-authored-by: Kevin Matthews <49137025+kr-matthews@users.noreply.github.com> * Destructure more heavily --------- Co-authored-by: Gregor Billing Co-authored-by: Gregor Billing Co-authored-by: Kevin Matthews <49137025+kr-matthews@users.noreply.github.com> --- app/controllers/live_controller.rb | 22 +++++ app/models/round.rb | 8 ++ app/views/live/by_person.erb | 5 ++ app/views/live/competitors.erb | 5 ++ app/views/live/podiums.erb | 5 ++ .../components/Live/ByPerson/index.jsx | 85 +++++++++++++++++++ .../components/Live/Competitors/index.jsx | 46 ++++++++++ .../components/Live/Podiums/index.jsx | 43 ++++++++++ .../Live/components/ResultsTable.jsx | 7 +- app/webpacker/lib/requests/routes.js.erb | 1 + config/routes.rb | 3 + 11 files changed, 225 insertions(+), 5 deletions(-) create mode 100644 app/views/live/by_person.erb create mode 100644 app/views/live/competitors.erb create mode 100644 app/views/live/podiums.erb create mode 100644 app/webpacker/components/Live/ByPerson/index.jsx create mode 100644 app/webpacker/components/Live/Competitors/index.jsx create mode 100644 app/webpacker/components/Live/Podiums/index.jsx diff --git a/app/controllers/live_controller.rb b/app/controllers/live_controller.rb index d5ad55f1c7..43ba394eba 100644 --- a/app/controllers/live_controller.rb +++ b/app/controllers/live_controller.rb @@ -77,4 +77,26 @@ def schedule_admin @rounds = Round.joins(:competition_event).where(competition_event: { competition_id: @competition_id }) end + + def podiums + @competition = Competition.find(params[:competition_id]) + @competitors = @competition.registrations.includes(:user).accepted + @final_rounds = @competition.rounds.select(&:final_round?) + end + + def competitors + @competition = Competition.find(params[:competition_id]) + @competitors = @competition.registrations.includes(:user).accepted + end + + def by_person + registration_id = params.require(:registration_id) + registration = Registration.find(registration_id) + + @competition_id = params[:competition_id] + @competition = Competition.find(@competition_id) + + @user = registration.user + @results = registration.live_results.includes(:live_attempts) + end end diff --git a/app/models/round.rb b/app/models/round.rb index fd20178504..4f120ae6bf 100644 --- a/app/models/round.rb +++ b/app/models/round.rb @@ -92,6 +92,10 @@ def round_type_id end end + def event_id + event.id + end + def formats_used cutoff_format = Format.c_find!(cutoff.number_of_attempts.to_s) if cutoff [cutoff_format, format].compact @@ -137,6 +141,10 @@ def advancement_condition_to_s(short: false) advancement_condition ? advancement_condition.to_s(self, short: short) : "" end + def live_podium + live_results.where(ranking: 1..3) + end + def previous_round return nil if number == 1 Round.joins(:competition_event).find_by(competition_event: competition_event, number: number - 1) diff --git a/app/views/live/by_person.erb b/app/views/live/by_person.erb new file mode 100644 index 0000000000..8927e268ed --- /dev/null +++ b/app/views/live/by_person.erb @@ -0,0 +1,5 @@ +<% provide(:title, 'WCA Live Integration Test Page') %> + +<%= render layout: 'competitions/nav' do %> + <%= react_component('Live/ByPerson', { allResults: @results, user: @user, competitionId: @competition_id, }) %> +<% end %> diff --git a/app/views/live/competitors.erb b/app/views/live/competitors.erb new file mode 100644 index 0000000000..10c7f3a3f3 --- /dev/null +++ b/app/views/live/competitors.erb @@ -0,0 +1,5 @@ +<% provide(:title, 'WCA Live Integration Test Page') %> + +<%= render layout: 'competitions/nav' do %> + <%= react_component('Live/Competitors', { competitors: @competitors.as_json({ methods: [:user]}), competitionId: @competition.id, }) %> +<% end %> diff --git a/app/views/live/podiums.erb b/app/views/live/podiums.erb new file mode 100644 index 0000000000..4040faec80 --- /dev/null +++ b/app/views/live/podiums.erb @@ -0,0 +1,5 @@ +<% provide(:title, 'WCA Live Integration Test Page') %> + +<%= render layout: 'competitions/nav' do %> + <%= react_component('Live/Podiums', { podiums: @final_rounds.as_json({ methods: [:event_id, :live_podium]}), competitors: @competitors.as_json({ methods: [:user]}), competitionId: @competition.id, }) %> +<% end %> diff --git a/app/webpacker/components/Live/ByPerson/index.jsx b/app/webpacker/components/Live/ByPerson/index.jsx new file mode 100644 index 0000000000..5f32192d5a --- /dev/null +++ b/app/webpacker/components/Live/ByPerson/index.jsx @@ -0,0 +1,85 @@ +import React from 'react'; +import { + Header, Segment, Table, +} from 'semantic-ui-react'; +import _ from 'lodash'; +import { events } from '../../../lib/wca-data.js.erb'; +import WCAQueryClientProvider from '../../../lib/providers/WCAQueryClientProvider'; +import { formatAttemptResult } from '../../../lib/wca-live/attempts'; +import { liveUrls } from '../../../lib/requests/routes.js.erb'; +import { rankingCellStyle } from '../components/ResultsTable'; + +export default function Wrapper({ + results, user, competitionId, +}) { + return ( + + + + ); +} + +function PersonResults({ + results, user, competitionId, +}) { + const resultsByEvent = _.groupBy(results, 'event_id'); + + return ( + +
+ {user.name} +
+ {_.map(resultsByEvent, (eventResults, key) => ( + <> +
{events.byId[key].name}
+ + + + Round + Rank + {_.times(events.byId[key].recommendedFormat().expectedSolveCount).map((num) => ( + + {num + 1} + + ))} + Average + Best + + + + {eventResults.map((result) => { + const { + round, + attempts, + ranking, + average, + best, + } = result; + + return ( + + + + Round + {' '} + {round.number} + + + {ranking} + {attempts.map((a) => ( + + {formatAttemptResult(a.result, events.byId[key].id)} + + ))} + {formatAttemptResult(average, events.byId[key].id)} + {formatAttemptResult(best, events.byId[key].id)} + + ); + })} + +
+ + ))} +
+ ); +} diff --git a/app/webpacker/components/Live/Competitors/index.jsx b/app/webpacker/components/Live/Competitors/index.jsx new file mode 100644 index 0000000000..fe2de995c3 --- /dev/null +++ b/app/webpacker/components/Live/Competitors/index.jsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { + Input, Segment, Table, +} from 'semantic-ui-react'; +import WCAQueryClientProvider from '../../../lib/providers/WCAQueryClientProvider'; +import { liveUrls } from '../../../lib/requests/routes.js.erb'; +import useInputState from '../../../lib/hooks/useInputState'; +import CountryFlag from '../../wca/CountryFlag'; + +export default function Wrapper({ + competitionId, competitors, +}) { + return ( + + + + ); +} + +function Competitors({ + competitionId, competitors, +}) { + const [searchInput, setSearchInput] = useInputState(''); + + return ( + + + + + + + + + {competitors.filter((c) => c.user.name.includes(searchInput)).map((c) => ( + + + + {c.user.name} + + + ))} + +
+
+ ); +} diff --git a/app/webpacker/components/Live/Podiums/index.jsx b/app/webpacker/components/Live/Podiums/index.jsx new file mode 100644 index 0000000000..26878035ca --- /dev/null +++ b/app/webpacker/components/Live/Podiums/index.jsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { + Container, + Header, +} from 'semantic-ui-react'; +import { events } from '../../../lib/wca-data.js.erb'; +import WCAQueryClientProvider from '../../../lib/providers/WCAQueryClientProvider'; +import ResultsTable from '../components/ResultsTable'; + +export default function Wrapper({ + podiums, competitionId, competitors, +}) { + return ( + + + + ); +} + +function Podiums({ + podiums, competitionId, competitors, +}) { + return ( + +
+ Podiums +
+ {podiums.map((finalRound) => ( + <> +
{events.byId[finalRound.event_id].name}
+ {finalRound.live_podium.length > 0 ? ( + + ) : 'Podiums to be determined'} + + ))} +
+ ); +} diff --git a/app/webpacker/components/Live/components/ResultsTable.jsx b/app/webpacker/components/Live/components/ResultsTable.jsx index 4c7689a881..90d3b00090 100644 --- a/app/webpacker/components/Live/components/ResultsTable.jsx +++ b/app/webpacker/components/Live/components/ResultsTable.jsx @@ -17,14 +17,11 @@ const customOrderBy = (competitor, resultsByRegistrationId, sortBy) => { }; export const rankingCellStyle = (result) => { - if (!result) { - return {}; - } - if (result.advancing) { + if (result?.advancing) { return { backgroundColor: `rgb(${advancingColor})` }; } - if (result.advancing_questionable) { + if (result?.advancing_questionable) { return { backgroundColor: `rgba(${advancingColor}, 0.5)` }; } diff --git a/app/webpacker/lib/requests/routes.js.erb b/app/webpacker/lib/requests/routes.js.erb index c419c43bdf..4e68ad1f50 100644 --- a/app/webpacker/lib/requests/routes.js.erb +++ b/app/webpacker/lib/requests/routes.js.erb @@ -284,6 +284,7 @@ export const actionUrls = { } export const liveUrls = { + personResults: (competitionId, registrationId) => `<%= CGI.unescape(Rails.application.routes.url_helpers.live_person_results_path(competition_id: "${competitionId}", registration_id: "${registrationId}")) %>`, roundResultsAdmin: (competitionId, roundId) => `<%= CGI.unescape(Rails.application.routes.url_helpers.live_admin_round_results_path(competition_id: "${competitionId}", round_id: "${roundId}")) %>`, checkRoundResultsAdmin: (competitionId, roundId) => `<%= CGI.unescape(Rails.application.routes.url_helpers.live_admin_check_round_results_path(competition_id: "${competitionId}", round_id: "${roundId}")) %>`, api: { diff --git a/config/routes.rb b/config/routes.rb index 698d255807..a2a35475b4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -151,6 +151,9 @@ get 'competitions/edit/series-eligible-competitions-json' => 'competitions#series_eligible_competitions_json', as: :series_eligible_competitions_json if WcaLive.enabled? + get 'competitions/:competition_id/live/competitors/:registration_id' => 'live#by_person', as: :live_person_results + get 'competitions/:competition_id/live/podiums' => 'live#podiums', as: :live_podiums + get 'competitions/:competition_id/live/competitors' => 'live#competitors', as: :live_competitors get 'competitions/:competition_id/live/rounds/:round_id/admin' => 'live#admin', as: :live_admin_round_results get 'competitions/:competition_id/live/rounds/:round_id/admin/check' => 'live#double_check', as: :live_admin_check_round_results get 'competitions/:competition_id/live/admin' => 'live#schedule_admin', as: :live_schedule_admin