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.*"