diff --git a/app/controllers/results_controller.rb b/app/controllers/results_controller.rb index 48d5f85fcf..5001a3e778 100644 --- a/app/controllers/results_controller.rb +++ b/app/controllers/results_controller.rb @@ -156,7 +156,27 @@ def rankings else flash[:danger] = t(".unknown_show") - redirect_to rankings_path + return redirect_to rankings_path + end + + @ranking_timestamp = ComputeAuxiliaryData.successful_start_date || Date.current + + respond_to do |format| + format.html + format.json do + cached_data = Rails.cache.fetch [*@cache_params, @ranking_timestamp] do + rows = DbHelper.execute_cached_query(@cache_params, @ranking_timestamp, @query) + comp_ids = rows.map { |r| r["competitionId"] }.uniq + if @is_by_region + rows = compute_rankings_by_region(rows, @continent, @country) + end + competitions_by_id = Competition.where(id: comp_ids).to_h { |c| [c.id, c] } + { + rows: rows.as_json, competitionsById: competitions_by_id.as_json({ methods: %w[country cellName id], includes: [] }) + } + end + render json: cached_data + end end end @@ -356,4 +376,43 @@ def records params[:show] = nil end end + + private def compute_rankings_by_region(rows, continent, country) + if rows.empty? + return [[], 0, 0] + end + best_value_of_world = rows.first["value"] + best_values_of_continents = {} + best_values_of_countries = {} + world_rows = [] + continents_rows = [] + countries_rows = [] + rows.each do |row| + result = LightResult.new(row) + value = row["value"] + + world_rows << row if value == best_value_of_world + + if best_values_of_continents[result.country.continent.id].nil? || value == best_values_of_continents[result.country.continent.id] + best_values_of_continents[result.country.continent.id] = value + + if (country.present? && country.continent.id == result.country.continent.id) || (continent.present? && continent.id == result.country.continent.id) || params[:region] == "world" + continents_rows << row + end + end + + if best_values_of_countries[result.country.id].nil? || value == best_values_of_countries[result.country.id] + best_values_of_countries[result.country.id] = value + + if (country.present? && country.id == result.country.id) || params[:region] == "world" + countries_rows << row + end + end + end + + first_continent_index = world_rows.length + first_country_index = first_continent_index + continents_rows.length + rows_to_display = world_rows + continents_rows + countries_rows + [rows_to_display, first_continent_index, first_country_index] + end end diff --git a/app/helpers/results_helper.rb b/app/helpers/results_helper.rb index dbdc746e87..44f6f75705 100644 --- a/app/helpers/results_helper.rb +++ b/app/helpers/results_helper.rb @@ -29,45 +29,6 @@ def historical_pb_markers(results) end end - def compute_rankings_by_region(rows, continent, country) - if rows.empty? - return [[], 0, 0] - end - best_value_of_world = rows.first["value"] - best_values_of_continents = {} - best_values_of_countries = {} - world_rows = [] - continents_rows = [] - countries_rows = [] - rows.each do |row| - result = LightResult.new(row) - value = row["value"] - - world_rows << row if value == best_value_of_world - - if best_values_of_continents[result.country.continent.id].nil? || value == best_values_of_continents[result.country.continent.id] - best_values_of_continents[result.country.continent.id] = value - - if (country.present? && country.continent.id == result.country.continent.id) || (continent.present? && continent.id == result.country.continent.id) || params[:region] == "world" - continents_rows << row - end - end - - if best_values_of_countries[result.country.id].nil? || value == best_values_of_countries[result.country.id] - best_values_of_countries[result.country.id] = value - - if (country.present? && country.id == result.country.id) || params[:region] == "world" - countries_rows << row - end - end - end - - first_continent_index = world_rows.length - first_country_index = first_continent_index + continents_rows.length - rows_to_display = world_rows + continents_rows + countries_rows - [rows_to_display, first_continent_index, first_country_index] - end - def compute_slim_or_separate_records(rows) single_rows = [] average_rows = [] diff --git a/app/views/results/rankings.html.erb b/app/views/results/rankings.html.erb index eea83d9a46..c9e36b7025 100644 --- a/app/views/results/rankings.html.erb +++ b/app/views/results/rankings.html.erb @@ -1,29 +1,16 @@ <% provide(:title, t(".title")) %> -<% ranking_timestamp = ComputeAuxiliaryData.successful_start_date || Date.current %> -<% @rows = DbHelper.execute_cached_query(@cache_params, ranking_timestamp, @query) %> -

<%= yield(:title) %>

-

<%= t("results.last_updated_html", timestamp: wca_local_time(ranking_timestamp)) %>

+

<%= t("results.last_updated_html", timestamp: wca_local_time(@ranking_timestamp)) %>

<%= t('results.filters_fixes_underway') %>

-
- <%= render 'results_selector', show_rankings_options: true %> -
- <% cache [*@cache_params, ranking_timestamp, I18n.locale] do %> - <% - comp_ids = @rows.map { |r| r["competitionId"] }.uniq - @competitions_by_id = Hash[Competition.where(id: comp_ids).map { |c| [c.id, c] }] - %> -
-
- <% if @is_by_region %> - <%= render 'rankings_by_region_table' %> - <% else %> - <%= render 'rankings_table' %> - <% end %> -
-
- <% end %> + <%= react_component("Results/Rankings", { + event: params[:event_id], + region: params[:region], + rankingType: params[:type], + year: params[:year], + gender: params[:gender], + show: params[:show], + }) %>
diff --git a/app/webpacker/components/Results/Rankings/RankingsTable.jsx b/app/webpacker/components/Results/Rankings/RankingsTable.jsx new file mode 100644 index 0000000000..1bafa38cea --- /dev/null +++ b/app/webpacker/components/Results/Rankings/RankingsTable.jsx @@ -0,0 +1,125 @@ +import React, { useMemo } from 'react'; +import { Table } from 'semantic-ui-react'; +import _ from 'lodash'; +import I18n from '../../../lib/i18n'; +import { formatAttemptResult } from '../../../lib/wca-live/attempts'; +import CountryFlag from '../../wca/CountryFlag'; +import { continents, countries } from '../../../lib/wca-data.js.erb'; +import { personUrl } from '../../../lib/requests/routes.js.erb'; + +function CountryCell({ country }) { + return ( + + {country.iso2 && } + {' '} + {country.name} + + ); +} + +function ResultRow({ + result, competition, rank, isAverage, show, country, +}) { + const attempts = [result.value1, result.value2, result.value3, result.value4, result.value5]; + const bestResult = _.max(attempts); + const worstResult = _.min(attempts); + const bestResultIndex = attempts.findIndex((a) => a === bestResult); + const worstResultIndex = attempts.findIndex((a) => a === worstResult); + return ( + + {show === 'by region' ? + : {rank} } + + {result.personName} + + + {formatAttemptResult(result.value, result.eventId)} + + {show !== 'by region' + && } + + + {' '} + {competition.cellName} + + {isAverage && (attempts.map((a, i) => ( + + { attempts.length === 5 + && (i === bestResultIndex || i === worstResultIndex) + ? `(${formatAttemptResult(a, result.eventId)})` : formatAttemptResult(a, result.eventId)} + + )) + )} + + ); +} + +export default function RankingsTable({ + rows, competitionsById, isAverage, show, +}) { + const r = useMemo(() => { + let rowsToMap = rows; + let firstContinentIndex = 0; + let firstCountryIndex = 0; + if (show === 'by region') { + [rowsToMap, firstContinentIndex, firstCountryIndex] = rows; + } + + let previousValue = 0; + let previousRank = 0; + return rowsToMap.map((result, index) => { + const competition = competitionsById[result.competitionId]; + const { value } = result; + const rank = value === previousValue ? previousRank : index + 1; + const tiedPrevious = rank === previousRank; + let country = countries.real.find((c) => c.id === result.countryId); + + if (index < firstContinentIndex) { + country = { name: I18n.t('results.table_elements.world') }; + } else if (index >= firstContinentIndex && index < firstCountryIndex) { + country = continents.real.find((c) => c.id === country.continentId); + } + + previousValue = value; + previousRank = rank; + + return ( + + ); + }); + }, [competitionsById, isAverage, rows, show]); + + return ( + + + {show !== 'by region' ? # + : {I18n.t('results.table_elements.region')}} + {I18n.t('results.table_elements.name')} + {I18n.t('results.table_elements.result')} + {show !== 'by region' && {I18n.t('results.table_elements.representing')}} + {I18n.t('results.table_elements.competition')} + {isAverage && ( + <> + {I18n.t('results.table_elements.solves')} + + + + + + )} + + + {r} + +
+ ); +} diff --git a/app/webpacker/components/Results/Rankings/index.jsx b/app/webpacker/components/Results/Rankings/index.jsx new file mode 100644 index 0000000000..1454b7f697 --- /dev/null +++ b/app/webpacker/components/Results/Rankings/index.jsx @@ -0,0 +1,74 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { Container } from 'semantic-ui-react'; +import { useQuery } from '@tanstack/react-query'; +import RankingsTable from './RankingsTable'; +import WCAQueryClientProvider from '../../../lib/providers/WCAQueryClientProvider'; +import { getRankings } from '../api/rankings'; +import Loading from '../../Requests/Loading'; +import { rankingsUrl } from '../../../lib/requests/routes.js.erb'; +import ResultsFilter from '../resultsFilter'; + +export default function Wrapper({ + event, region, year, rankingType, gender, show, +}) { + return ( + + + + ); +} + +export function Rankings({ + initialEvent, initialRegion, initialRankingType, initialGender, initialShow, +}) { + const [event, setEvent] = useState(initialEvent); + const [region, setRegion] = useState(initialRegion ?? 'all'); + const [rankingType, setRankingType] = useState(initialRankingType); + const [gender, setGender] = useState(initialGender); + const [show, setShow] = useState(initialShow ?? 'Persons'); + + const filterState = useMemo(() => ({ + event, + setEvent, + region, + setRegion, + rankingType, + setRankingType, + gender, + setGender, + show, + setShow, + }), [event, gender, rankingType, region, show]); + + const { data, isFetching } = useQuery({ + queryKey: ['rankings', event, region, rankingType, gender, show], + queryFn: () => getRankings(event, rankingType, region, gender, show), + }); + + useEffect(() => { + window.history.replaceState(null, '', rankingsUrl(event, rankingType, region, gender, show)); + }, [event, region, rankingType, gender, show]); + + if (isFetching) { + return ; + } + + return ( + + + + + ); +} diff --git a/app/webpacker/components/Results/api/rankings.js b/app/webpacker/components/Results/api/rankings.js new file mode 100644 index 0000000000..0ff4957cc7 --- /dev/null +++ b/app/webpacker/components/Results/api/rankings.js @@ -0,0 +1,8 @@ +import { fetchJsonOrError } from '../../../lib/requests/fetchWithAuthenticityToken'; +import { rankingsUrl } from '../../../lib/requests/routes.js.erb'; + +// eslint-disable-next-line import/prefer-default-export +export async function getRankings(eventId, rankingType, region, gender, show) { + const { data } = await fetchJsonOrError(rankingsUrl(eventId, rankingType, region, gender, show), { headers: { Accept: 'application/json' } }); + return data; +} diff --git a/app/webpacker/components/Results/resultsFilter.jsx b/app/webpacker/components/Results/resultsFilter.jsx new file mode 100644 index 0000000000..409d55803d --- /dev/null +++ b/app/webpacker/components/Results/resultsFilter.jsx @@ -0,0 +1,86 @@ +import React, { useMemo } from 'react'; +import { + Button, ButtonGroup, Form, Segment, +} from 'semantic-ui-react'; +import { EventSelector } from '../wca/EventSelector'; +import { RegionSelector } from '../CompetitionsOverview/CompetitionsFilters'; +import { countries } from '../../lib/wca-data.js.erb'; +import I18n from '../../lib/i18n'; + +export default function ResultsFilter({ filterState }) { + const { + event, + setEvent, + region, + setRegion, + rankingType, + setRankingType, + gender, + setGender, + show, + setShow, + } = filterState; + const regionIso2 = useMemo(() => { + if (region === 'world') { + return 'all'; + } + const iso2 = countries.real.find((country) => country.id === region)?.iso2; + if (iso2) { + return iso2; + } + return region; + }, [region]); + return ( + +
+ + setEvent(eventId)} + hideAllButton + hideClearButton + /> + setRegion(countries.byIso2[r]?.id ?? r)} + /> + + + + + + + + + + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + + + + + + + + + + + + + + + + + +
+
+ ); +} diff --git a/app/webpacker/components/wca/EventSelector.jsx b/app/webpacker/components/wca/EventSelector.jsx index 095540658b..40377338e8 100644 --- a/app/webpacker/components/wca/EventSelector.jsx +++ b/app/webpacker/components/wca/EventSelector.jsx @@ -29,16 +29,16 @@ export function EventSelector({ {`${I18n.t('competitions.competition_form.events')}`} {showBreakBeforeButtons ? (
) : (' ')} {hideAllButton || ( - - } - > - {I18n.t('competitions.registration_v2.register.event_limit', { - max_events: maxEvents, - })} - + + } + > + {I18n.t('competitions.registration_v2.register.event_limit', { + max_events: maxEvents, + })} + )} {hideClearButton || } diff --git a/app/webpacker/lib/requests/routes.js.erb b/app/webpacker/lib/requests/routes.js.erb index 4dc754e43d..e0cb28af65 100644 --- a/app/webpacker/lib/requests/routes.js.erb +++ b/app/webpacker/lib/requests/routes.js.erb @@ -1,3 +1,4 @@ +import {countries} from "../wca-data.js.erb"; function jsonToQueryString(json) { const jsonAfterRemovingUndefinedAndNull = Object.fromEntries( Object.entries(json).filter(([key, value]) => value !== null && value !== undefined) @@ -291,3 +292,20 @@ export const updateRegistrationUrl = `<%= CGI.unescape(Rails.application.routes. export const bulkUpdateRegistrationUrl = `<%= CGI.unescape(Rails.application.routes.url_helpers.api_v1_registrations_bulk_update_path) %>`; export const paymentTicketUrl = (competitionId, donationIso) => `<%= CGI.unescape(Rails.application.routes.url_helpers.api_v1_registrations_payment_ticket_path(competition_id: "${competitionId}", donation_iso: "${donationIso}")) %>`; + +export const rankingsUrl = (eventId, rankingType, region, gender, show) => { + const url = `<%= CGI.unescape(Rails.application.routes.url_helpers.rankings_path(event_id: "${eventId}", type: "${rankingType}")) %>` + const queryParams = new URLSearchParams(); + + queryParams.append('region', region); + + if (gender !== 'All') { + queryParams.append('gender', gender); + } + + if (show !== '100 persons') { + queryParams.append('show', show); + } + + return `${url}?${queryParams.toString()}` +}; diff --git a/config/i18n.yml b/config/i18n.yml index 5f2ebbbc48..4425a6670a 100644 --- a/config/i18n.yml +++ b/config/i18n.yml @@ -75,3 +75,5 @@ translations: - "*.time_limit.*" - "*.users.edit.*" - "*.persons.index.*" + - "*.results.table_elements.*" + - "*.results.selector_elements.*"