diff --git a/app/assets/javascripts/persons.js b/app/assets/javascripts/persons.js index 7b9b097550..2a033d0991 100644 --- a/app/assets/javascripts/persons.js +++ b/app/assets/javascripts/persons.js @@ -1,53 +1,3 @@ - -// Add params from the search fields to the bootstrap-table for on Ajax request. -var personsTableAjax = { - queryParams: function(params) { - if (personsTableAjax.queriesOK !== true) { - return false; // Stop Bootstrap Table's uncontrollable initial load - } - - return $.extend(params || {}, { - region: $('#region').val(), - search: $('#search').val(), - }); - }, - doAjax: function(options) { - $('.pagination li').addClass('disabled'); - return window.wca.cancelPendingAjaxAndAjax('persons-index', options); - }, -}; - -onPage('persons#index', function() { - var $table = $('.persons-table'); - var options = $table.bootstrapTable('getOptions'); - - function reloadPersons() { - $('#search-box i').removeClass('fa-search').addClass('fa-spinner fa-spin'); - options.pageNumber = 1; - $table.bootstrapTable('refresh'); - } - - // Set the table options from the url params. - options.pageNumber = parseInt(window.wca.getUrlParams().page) || options.pageNumber; - // Load the data using the options set above. - personsTableAjax.queriesOK = true; - $table.bootstrapTable('refresh'); - - $('#region').on('change', reloadPersons); - $('#search').on('input', window.wca.lodashDebounce(reloadPersons, window.wca.TEXT_INPUT_DEBOUNCE_MS)); - - $table.on('load-success.bs.table', function(e, data) { - $('#search-box i').removeClass('fa-spinner fa-spin').addClass('fa-search'); - - // Update params in the url. - var params = personsTableAjax.queryParams({ - // Extended with region and search params. - page: options.pageNumber - }); - window.wca.setUrlParams(params); - }); -}); - onPage('persons#show', function() { function scrollToTabs() { var top = $('.nav.nav-tabs').offset().top; diff --git a/app/controllers/persons_controller.rb b/app/controllers/persons_controller.rb index 32dd865a8f..eb43204df2 100644 --- a/app/controllers/persons_controller.rb +++ b/app/controllers/persons_controller.rb @@ -4,7 +4,7 @@ class PersonsController < ApplicationController def index respond_to do |format| format.html - format.js do + format.json do persons = Person.in_region(params[:region]).order(:name) params[:search]&.split&.each do |part| persons = persons.where("MATCH(Persons.name) AGAINST (:name_match IN BOOLEAN MODE) OR wca_id LIKE :wca_id_part", name_match: "#{part}*", wca_id_part: "#{part}%") @@ -14,9 +14,9 @@ def index total: persons.count, rows: persons.limit(params[:limit]).offset(params[:offset]).map do |person| { - name: view_context.link_to(person.name, person_path(person.wca_id)), + name: person.name, wca_id: person.wca_id, - country: person.country.name, + country: person.country.iso2, competitions_count: person.competitions.count, podiums_count: person.results.podium.count, } diff --git a/app/views/persons/index.html.erb b/app/views/persons/index.html.erb index a80ef986a1..01c4e7b95b 100644 --- a/app/views/persons/index.html.erb +++ b/app/views/persons/index.html.erb @@ -1,39 +1,5 @@ <% provide(:title, t('layouts.navigation.persons')) %> <div class="container" id="persons-index"> - <h2><%= yield(:title) %></h2> - - <div id="persons-query-fields" class="form-inline bs-table-query-fields"> - <div class="form-group"> - <%= select_tag(:region, region_option_tags(selected_id: params[:region], real_only: true), class: "form-control") %> - </div> - <div id="search-box" class="form-group"> - <div class="input-group"> - <%= content_tag :span, ui_icon("search"), class: "input-group-addon" %> - <%= text_field_tag :search, params[:search], placeholder: t('.name_or_wca_id'), class: "form-control" %> - </div> - </div> - </div> - - <%= wca_table table_class: "persons-table", floatThead: false, - data: { toggle: "table", pagination: true, side_pagination: "server", url: persons_path, - query_params: "personsTableAjax.queryParams", mobile_responsive: true, ajax: "personsTableAjax.doAjax" } do %> - <thead> - <tr> - <th class="name" data-field="name"><%=t '.name' %></th> - <th class="wca-id" data-field="wca_id"><%=t 'common.user.wca_id' %></th> - <th class="country" data-field="country"><%=t '.country' %></th> - <th class="competitions-count" data-field="competitions_count"><%=t 'layouts.navigation.competitions' %></th> - <th class="podiums-count" data-field="podiums_count"><%=t '.podiums' %></th> - </tr> - </thead> - <% end %> + <%= react_component("Persons/List") %> </div> - -<script> - $(function() { - $('.persons-table').bootstrapTable('getOptions').formatNoMatches = function() { - return '<%=j t('.no_persons_found') %>'; - } - }); -</script> diff --git a/app/webpacker/components/Persons/List/index.jsx b/app/webpacker/components/Persons/List/index.jsx new file mode 100644 index 0000000000..7d46f5117f --- /dev/null +++ b/app/webpacker/components/Persons/List/index.jsx @@ -0,0 +1,100 @@ +import React, { useState } from 'react'; +import { + Header, Input, Pagination, Table, +} from 'semantic-ui-react'; +import { useQuery } from '@tanstack/react-query'; +import I18n from '../../../lib/i18n'; +import useDebounce from '../../../lib/hooks/useDebounce'; +import { getPersons } from '../api/getPersons'; +import Loading from '../../Requests/Loading'; +import WCAQueryClientProvider from '../../../lib/providers/WCAQueryClientProvider'; +import { RegionSelector } from '../../CompetitionsOverview/CompetitionsFilters'; +import { personUrl } from '../../../lib/requests/routes.js.erb'; +import { countries } from '../../../lib/wca-data.js.erb'; + +export default function Wrapper() { + return ( + <WCAQueryClientProvider> + <PersonList /> + </WCAQueryClientProvider> + ); +} + +function PersonList() { + const [query, setQuery] = useState(''); + const [page, setPage] = useState(1); + const [region, setRegion] = useState('all'); + + const debouncedSearch = useDebounce(query, 600); + + const { data, isLoading } = useQuery({ + queryKey: ['persons', debouncedSearch, region, page], + queryFn: () => getPersons(page, countries.byIso2[region]?.id ?? region, debouncedSearch), + }); + + if (isLoading) { + return <Loading />; + } + + return ( + <> + <Header> + {I18n.t('layouts.navigation.persons')} + </Header> + <RegionSelector region={region} dispatchFilter={({ region: r }) => setRegion(r)} /> + <Input type="text" placeholder={I18n.t('persons.index.name_or_wca_id')} value={query} onChange={(d) => setQuery(d.target.value)} /> + <Table striped> + <Table.Header> + <Table.Row> + <Table.HeaderCell> + {I18n.t('persons.index.name')} + </Table.HeaderCell> + <Table.HeaderCell> + {I18n.t('common.user.wca_id')} + </Table.HeaderCell> + <Table.HeaderCell> + {I18n.t('persons.index.country')} + </Table.HeaderCell> + <Table.HeaderCell> + {I18n.t('layouts.navigation.competitions')} + </Table.HeaderCell> + <Table.HeaderCell> + {I18n.t('persons.index.podiums')} + </Table.HeaderCell> + </Table.Row> + </Table.Header> + <Table.Body> + {data.rows.length === 0 ? ( + <Table.Row> + <Table.Cell>{I18n.t('persons.index.no_persons_found')}</Table.Cell> + </Table.Row> + ) : data.rows.map((row) => ( + <Table.Row key={row.wca_id}> + <Table.Cell> + <a href={personUrl(row.wca_id)}>{row.name}</a> + </Table.Cell> + <Table.Cell> + {row.wca_id} + </Table.Cell> + <Table.Cell> + {countries.byIso2[row.country].name} + </Table.Cell> + + <Table.Cell> + {row.competitions_count} + </Table.Cell> + <Table.Cell> + {row.podiums_count} + </Table.Cell> + </Table.Row> + ))} + </Table.Body> + </Table> + <Pagination + defaultActivePage={page} + totalPages={Math.ceil(data.total / 10)} + onPageChange={(e, p) => setPage(p.activePage)} + /> + </> + ); +} diff --git a/app/webpacker/components/Persons/api/getPersons.js b/app/webpacker/components/Persons/api/getPersons.js new file mode 100644 index 0000000000..3bf2d26b32 --- /dev/null +++ b/app/webpacker/components/Persons/api/getPersons.js @@ -0,0 +1,14 @@ +import { fetchJsonOrError } from '../../../lib/requests/fetchWithAuthenticityToken'; +import { personsUrl } from '../../../lib/requests/routes.js.erb'; +// eslint-disable-next-line import/prefer-default-export +export async function getPersons(page, region, query) { + const { data } = await fetchJsonOrError( + `${personsUrl}?search=${query}&order=asc&offset=${(page - 1) * 10}&limit=10®ion=${region ?? 'all'}`, + { + headers: { + Accept: 'application/json', + }, + }, + ); + return data; +} diff --git a/config/i18n.yml b/config/i18n.yml index d43221c507..5f2ebbbc48 100644 --- a/config/i18n.yml +++ b/config/i18n.yml @@ -74,3 +74,4 @@ translations: - "*.attempts.*" - "*.time_limit.*" - "*.users.edit.*" + - "*.persons.index.*" diff --git a/spec/controllers/persons_controller_spec.rb b/spec/controllers/persons_controller_spec.rb index 06a993ad06..cfa32c8589 100644 --- a/spec/controllers/persons_controller_spec.rb +++ b/spec/controllers/persons_controller_spec.rb @@ -20,33 +20,33 @@ let!(:result) { FactoryBot.create(:result, pos: 1, roundTypeId: "f", competitionId: competition.id, person: person1) } it "responds with correct JSON when region and search are specified" do - get :index, params: { search: "Jennifer", region: "USA" }, xhr: true + get :index, params: { search: "Jennifer", region: "USA" }, format: :json json = JSON.parse(response.body) expect(json['total']).to eq 1 json_person = json['rows'][0] expect(json_person['name']).to include "Jennifer Lawrence" expect(json_person['wca_id']).to eq "2016LAWR01" - expect(json_person['country']).to eq "United States" + expect(json_person['country']).to eq "US" expect(json_person['competitions_count']).to eq 1 expect(json_person['podiums_count']).to eq 1 end it "selecting continent works" do - get :index, params: { region: "_Europe" }, xhr: true + get :index, params: { region: "_Europe" }, format: :json json = JSON.parse(response.body) expect(json['total']).to eq 1 expect(json['rows'].count).to eq 1 end it "searching by WCA ID works" do - get :index, params: { search: "2016" }, xhr: true + get :index, params: { search: "2016" }, format: :json json = JSON.parse(response.body) expect(json['total']).to eq 2 expect(json['rows'].count).to eq 2 end it "works well when parts of the name are given" do - get :index, params: { search: "Law Jenn" }, xhr: true + get :index, params: { search: "Law Jenn" }, format: :json json = JSON.parse(response.body) expect(json['total']).to eq 1 expect(json['rows'].count).to eq 1