From c5237cacb7c144911263ff088585fb329f3dec8d Mon Sep 17 00:00:00 2001 From: FinnIckler Date: Tue, 7 May 2024 11:43:30 +0200 Subject: [PATCH] Use Event and Schedule React Component from Registrations (#9262) * add schedule * use new schedule component * add events * use new event component * Pass only relevant slices of WCIF * Only allow passing timezone when 'custom' is selected * Respect timezone setting in the Add To Calendar link * Hide new-tab icon for Google Calendar link * Reinstate events picker * Remove obsolete FullCalendar files * Localize FullCalendar depending on backend * Render 'other' activities in gray * Make ESLint happy * Hack around FC client-side width computation * Fix tests * translate events (except round name) * remove console.log * Add roundType information in WCIF events output * Set selectable and rowSpan for events * use roundTypeId instead of wcif extension * localize rest of schedule * use two dropdowns for timezones * Localize ActivityCode in calendar view * Localize ActivityCode in table view * ignore missing keys for schedule * testing if updates to show will get test to pass * correcting test failure * fixed path * added anchor to event test * added missing value * Make whole room panel clickable * Source timezones from Rails backend * Simplify timezone selection * Render rooms as panel * Add timezone clicker below room button * Fix (kinda) menue width issue * Review i18n changes * Only show 'follow' checkbox if there is more than one venue * Fix table view translation interpolation quirk by introducing new key --------- Co-authored-by: Gregor Billing Co-authored-by: Duncan --- app/assets/javascripts/competition_tabs.js | 8 +- .../fullcalendar/fullcalendar_wca.js | 2 - .../javascripts/fullcalendar/locales/ca.js | 1 - .../javascripts/fullcalendar/locales/cs.js | 1 - .../javascripts/fullcalendar/locales/da.js | 1 - .../javascripts/fullcalendar/locales/de.js | 1 - .../javascripts/fullcalendar/locales/eo.js | 1 - .../javascripts/fullcalendar/locales/es.js | 1 - .../javascripts/fullcalendar/locales/fi.js | 1 - .../javascripts/fullcalendar/locales/fr.js | 1 - .../javascripts/fullcalendar/locales/hr.js | 1 - .../javascripts/fullcalendar/locales/hu.js | 1 - .../javascripts/fullcalendar/locales/id.js | 1 - .../javascripts/fullcalendar/locales/it.js | 1 - .../javascripts/fullcalendar/locales/ja.js | 1 - .../javascripts/fullcalendar/locales/kk.js | 1 - .../javascripts/fullcalendar/locales/ko.js | 1 - .../javascripts/fullcalendar/locales/nl.js | 1 - .../javascripts/fullcalendar/locales/pl.js | 1 - .../javascripts/fullcalendar/locales/pt-br.js | 1 - .../javascripts/fullcalendar/locales/pt.js | 1 - .../javascripts/fullcalendar/locales/ro.js | 1 - .../javascripts/fullcalendar/locales/ru.js | 1 - .../javascripts/fullcalendar/locales/sk.js | 1 - .../javascripts/fullcalendar/locales/sl.js | 1 - .../javascripts/fullcalendar/locales/sv.js | 1 - .../javascripts/fullcalendar/locales/th.js | 1 - .../javascripts/fullcalendar/locales/uk.js | 1 - .../javascripts/fullcalendar/locales/vi.js | 1 - .../javascripts/fullcalendar/locales/zh-cn.js | 1 - .../javascripts/fullcalendar/locales/zh-tw.js | 1 - app/assets/stylesheets/fullcalendar_wca.scss | 1 - app/helpers/application_helper.rb | 8 - app/models/round.rb | 2 +- ...petition_schedule_for_venue_table.html.erb | 84 ------ .../_competition_schedule_tab.html.erb | 139 +--------- app/views/competitions/_events_tab.html.erb | 58 +---- .../_schedule_table_responsive_cell.html.erb | 11 - .../components/EventsTable/index.jsx | 97 +++++++ .../Register/CompetingStep.jsx | 2 +- .../RegistrationAdministrationList.jsx | 2 +- .../RegistrationEdit/RegistrationEditor.jsx | 2 +- .../components/Schedule/AddToCalendar.jsx | 31 +++ .../components/Schedule/CalendarView.jsx | 99 ++++++++ .../components/Schedule/TableView.jsx | 240 ++++++++++++++++++ .../components/Schedule/TimeZone.jsx | 67 +++++ .../components/Schedule/VenuesAndRooms.jsx | 194 ++++++++++++++ .../components/Schedule/ViewSelector.jsx | 20 ++ app/webpacker/components/Schedule/index.jsx | 185 ++++++++++++++ app/webpacker/lib/fullcalendar.js | 33 --- app/webpacker/lib/hooks/useStoredState.js | 30 +++ app/webpacker/lib/show-schedule.js | 225 ---------------- app/webpacker/lib/utils/activities.js | 144 +++++++++++ app/webpacker/lib/utils/calendar.js | 8 +- .../lib => lib/utils}/dates.js | 0 app/webpacker/lib/utils/edit-events.js | 2 +- app/webpacker/lib/utils/wcif.js | 15 ++ app/webpacker/lib/wca-live/attempts.js | 15 ++ app/webpacker/packs/show_schedule.js | 1 - config/i18n-tasks.yml.erb | 1 + config/i18n.yml | 4 + config/initializers/assets.rb | 3 - config/locales/en.yml | 28 +- .../competition_page_spec.rb} | 15 +- spec/i18n_locale_files_spec.rb | 4 +- spec/requests/registrations_spec.rb | 2 +- vendor/assets/javascripts/fc_locales/af.js | 1 - vendor/assets/javascripts/fc_locales/ar-dz.js | 1 - vendor/assets/javascripts/fc_locales/ar-kw.js | 1 - vendor/assets/javascripts/fc_locales/ar-ly.js | 1 - vendor/assets/javascripts/fc_locales/ar-ma.js | 1 - vendor/assets/javascripts/fc_locales/ar-sa.js | 1 - vendor/assets/javascripts/fc_locales/ar-tn.js | 1 - vendor/assets/javascripts/fc_locales/ar.js | 1 - vendor/assets/javascripts/fc_locales/bg.js | 1 - vendor/assets/javascripts/fc_locales/bs.js | 1 - vendor/assets/javascripts/fc_locales/ca.js | 1 - vendor/assets/javascripts/fc_locales/cs.js | 1 - vendor/assets/javascripts/fc_locales/da.js | 1 - vendor/assets/javascripts/fc_locales/de-at.js | 1 - vendor/assets/javascripts/fc_locales/de-ch.js | 1 - vendor/assets/javascripts/fc_locales/de.js | 1 - vendor/assets/javascripts/fc_locales/el.js | 1 - vendor/assets/javascripts/fc_locales/en-au.js | 1 - vendor/assets/javascripts/fc_locales/en-ca.js | 1 - vendor/assets/javascripts/fc_locales/en-gb.js | 1 - vendor/assets/javascripts/fc_locales/en-ie.js | 1 - vendor/assets/javascripts/fc_locales/en-nz.js | 1 - vendor/assets/javascripts/fc_locales/eo.js | 0 vendor/assets/javascripts/fc_locales/es-do.js | 1 - vendor/assets/javascripts/fc_locales/es-us.js | 1 - vendor/assets/javascripts/fc_locales/es.js | 1 - vendor/assets/javascripts/fc_locales/et.js | 1 - vendor/assets/javascripts/fc_locales/eu.js | 1 - vendor/assets/javascripts/fc_locales/fa.js | 1 - vendor/assets/javascripts/fc_locales/fi.js | 1 - vendor/assets/javascripts/fc_locales/fr-ca.js | 1 - vendor/assets/javascripts/fc_locales/fr-ch.js | 1 - vendor/assets/javascripts/fc_locales/fr.js | 1 - vendor/assets/javascripts/fc_locales/gl.js | 1 - vendor/assets/javascripts/fc_locales/he.js | 1 - vendor/assets/javascripts/fc_locales/hi.js | 1 - vendor/assets/javascripts/fc_locales/hr.js | 1 - vendor/assets/javascripts/fc_locales/hu.js | 1 - vendor/assets/javascripts/fc_locales/id.js | 1 - vendor/assets/javascripts/fc_locales/is.js | 1 - vendor/assets/javascripts/fc_locales/it.js | 1 - vendor/assets/javascripts/fc_locales/ja.js | 1 - vendor/assets/javascripts/fc_locales/ka.js | 1 - vendor/assets/javascripts/fc_locales/kk.js | 1 - vendor/assets/javascripts/fc_locales/ko.js | 1 - vendor/assets/javascripts/fc_locales/lb.js | 1 - vendor/assets/javascripts/fc_locales/lt.js | 1 - vendor/assets/javascripts/fc_locales/lv.js | 1 - vendor/assets/javascripts/fc_locales/mk.js | 1 - vendor/assets/javascripts/fc_locales/ms-my.js | 1 - vendor/assets/javascripts/fc_locales/ms.js | 1 - vendor/assets/javascripts/fc_locales/nb.js | 1 - vendor/assets/javascripts/fc_locales/nl-be.js | 1 - vendor/assets/javascripts/fc_locales/nl.js | 1 - vendor/assets/javascripts/fc_locales/nn.js | 1 - vendor/assets/javascripts/fc_locales/pl.js | 1 - vendor/assets/javascripts/fc_locales/pt-br.js | 1 - vendor/assets/javascripts/fc_locales/pt.js | 1 - vendor/assets/javascripts/fc_locales/ro.js | 1 - vendor/assets/javascripts/fc_locales/ru.js | 1 - vendor/assets/javascripts/fc_locales/sk.js | 1 - vendor/assets/javascripts/fc_locales/sl.js | 1 - vendor/assets/javascripts/fc_locales/sq.js | 1 - .../assets/javascripts/fc_locales/sr-cyrl.js | 1 - vendor/assets/javascripts/fc_locales/sr.js | 1 - vendor/assets/javascripts/fc_locales/sv.js | 1 - vendor/assets/javascripts/fc_locales/th.js | 1 - vendor/assets/javascripts/fc_locales/tr.js | 1 - vendor/assets/javascripts/fc_locales/uk.js | 1 - vendor/assets/javascripts/fc_locales/vi.js | 1 - vendor/assets/javascripts/fc_locales/zh-cn.js | 1 - vendor/assets/javascripts/fc_locales/zh-tw.js | 1 - vendor/assets/javascripts/fullcalendar.min.js | 12 - vendor/assets/javascripts/jquery-ui.min.js | 6 - 140 files changed, 1200 insertions(+), 700 deletions(-) delete mode 100644 app/assets/javascripts/fullcalendar/fullcalendar_wca.js delete mode 100644 app/assets/javascripts/fullcalendar/locales/ca.js delete mode 100644 app/assets/javascripts/fullcalendar/locales/cs.js delete mode 100644 app/assets/javascripts/fullcalendar/locales/da.js delete mode 100644 app/assets/javascripts/fullcalendar/locales/de.js delete mode 100644 app/assets/javascripts/fullcalendar/locales/eo.js delete mode 100644 app/assets/javascripts/fullcalendar/locales/es.js delete mode 100644 app/assets/javascripts/fullcalendar/locales/fi.js delete mode 100644 app/assets/javascripts/fullcalendar/locales/fr.js delete mode 100644 app/assets/javascripts/fullcalendar/locales/hr.js delete mode 100644 app/assets/javascripts/fullcalendar/locales/hu.js delete mode 100644 app/assets/javascripts/fullcalendar/locales/id.js delete mode 100644 app/assets/javascripts/fullcalendar/locales/it.js delete mode 100644 app/assets/javascripts/fullcalendar/locales/ja.js delete mode 100644 app/assets/javascripts/fullcalendar/locales/kk.js delete mode 100644 app/assets/javascripts/fullcalendar/locales/ko.js delete mode 100644 app/assets/javascripts/fullcalendar/locales/nl.js delete mode 100644 app/assets/javascripts/fullcalendar/locales/pl.js delete mode 100644 app/assets/javascripts/fullcalendar/locales/pt-br.js delete mode 100644 app/assets/javascripts/fullcalendar/locales/pt.js delete mode 100644 app/assets/javascripts/fullcalendar/locales/ro.js delete mode 100644 app/assets/javascripts/fullcalendar/locales/ru.js delete mode 100644 app/assets/javascripts/fullcalendar/locales/sk.js delete mode 100644 app/assets/javascripts/fullcalendar/locales/sl.js delete mode 100644 app/assets/javascripts/fullcalendar/locales/sv.js delete mode 100644 app/assets/javascripts/fullcalendar/locales/th.js delete mode 100644 app/assets/javascripts/fullcalendar/locales/uk.js delete mode 100644 app/assets/javascripts/fullcalendar/locales/vi.js delete mode 100644 app/assets/javascripts/fullcalendar/locales/zh-cn.js delete mode 100644 app/assets/javascripts/fullcalendar/locales/zh-tw.js delete mode 100644 app/assets/stylesheets/fullcalendar_wca.scss delete mode 100644 app/views/competitions/_competition_schedule_for_venue_table.html.erb delete mode 100644 app/views/competitions/_schedule_table_responsive_cell.html.erb create mode 100644 app/webpacker/components/EventsTable/index.jsx create mode 100644 app/webpacker/components/Schedule/AddToCalendar.jsx create mode 100644 app/webpacker/components/Schedule/CalendarView.jsx create mode 100644 app/webpacker/components/Schedule/TableView.jsx create mode 100644 app/webpacker/components/Schedule/TimeZone.jsx create mode 100644 app/webpacker/components/Schedule/VenuesAndRooms.jsx create mode 100644 app/webpacker/components/Schedule/ViewSelector.jsx create mode 100644 app/webpacker/components/Schedule/index.jsx delete mode 100644 app/webpacker/lib/fullcalendar.js create mode 100644 app/webpacker/lib/hooks/useStoredState.js delete mode 100644 app/webpacker/lib/show-schedule.js create mode 100644 app/webpacker/lib/utils/activities.js rename app/webpacker/{components/RegistrationsV2/lib => lib/utils}/dates.js (100%) delete mode 100644 app/webpacker/packs/show_schedule.js rename spec/{views/competitions/show.html.erb_spec.rb => features/competition_page_spec.rb} (77%) delete mode 100644 vendor/assets/javascripts/fc_locales/af.js delete mode 100644 vendor/assets/javascripts/fc_locales/ar-dz.js delete mode 100644 vendor/assets/javascripts/fc_locales/ar-kw.js delete mode 100644 vendor/assets/javascripts/fc_locales/ar-ly.js delete mode 100644 vendor/assets/javascripts/fc_locales/ar-ma.js delete mode 100644 vendor/assets/javascripts/fc_locales/ar-sa.js delete mode 100644 vendor/assets/javascripts/fc_locales/ar-tn.js delete mode 100644 vendor/assets/javascripts/fc_locales/ar.js delete mode 100644 vendor/assets/javascripts/fc_locales/bg.js delete mode 100644 vendor/assets/javascripts/fc_locales/bs.js delete mode 100644 vendor/assets/javascripts/fc_locales/ca.js delete mode 100644 vendor/assets/javascripts/fc_locales/cs.js delete mode 100644 vendor/assets/javascripts/fc_locales/da.js delete mode 100644 vendor/assets/javascripts/fc_locales/de-at.js delete mode 100644 vendor/assets/javascripts/fc_locales/de-ch.js delete mode 100644 vendor/assets/javascripts/fc_locales/de.js delete mode 100644 vendor/assets/javascripts/fc_locales/el.js delete mode 100644 vendor/assets/javascripts/fc_locales/en-au.js delete mode 100644 vendor/assets/javascripts/fc_locales/en-ca.js delete mode 100644 vendor/assets/javascripts/fc_locales/en-gb.js delete mode 100644 vendor/assets/javascripts/fc_locales/en-ie.js delete mode 100644 vendor/assets/javascripts/fc_locales/en-nz.js delete mode 100644 vendor/assets/javascripts/fc_locales/eo.js delete mode 100644 vendor/assets/javascripts/fc_locales/es-do.js delete mode 100644 vendor/assets/javascripts/fc_locales/es-us.js delete mode 100644 vendor/assets/javascripts/fc_locales/es.js delete mode 100644 vendor/assets/javascripts/fc_locales/et.js delete mode 100644 vendor/assets/javascripts/fc_locales/eu.js delete mode 100644 vendor/assets/javascripts/fc_locales/fa.js delete mode 100644 vendor/assets/javascripts/fc_locales/fi.js delete mode 100644 vendor/assets/javascripts/fc_locales/fr-ca.js delete mode 100644 vendor/assets/javascripts/fc_locales/fr-ch.js delete mode 100644 vendor/assets/javascripts/fc_locales/fr.js delete mode 100644 vendor/assets/javascripts/fc_locales/gl.js delete mode 100644 vendor/assets/javascripts/fc_locales/he.js delete mode 100644 vendor/assets/javascripts/fc_locales/hi.js delete mode 100644 vendor/assets/javascripts/fc_locales/hr.js delete mode 100644 vendor/assets/javascripts/fc_locales/hu.js delete mode 100644 vendor/assets/javascripts/fc_locales/id.js delete mode 100644 vendor/assets/javascripts/fc_locales/is.js delete mode 100644 vendor/assets/javascripts/fc_locales/it.js delete mode 100644 vendor/assets/javascripts/fc_locales/ja.js delete mode 100644 vendor/assets/javascripts/fc_locales/ka.js delete mode 100644 vendor/assets/javascripts/fc_locales/kk.js delete mode 100644 vendor/assets/javascripts/fc_locales/ko.js delete mode 100644 vendor/assets/javascripts/fc_locales/lb.js delete mode 100644 vendor/assets/javascripts/fc_locales/lt.js delete mode 100644 vendor/assets/javascripts/fc_locales/lv.js delete mode 100644 vendor/assets/javascripts/fc_locales/mk.js delete mode 100644 vendor/assets/javascripts/fc_locales/ms-my.js delete mode 100644 vendor/assets/javascripts/fc_locales/ms.js delete mode 100644 vendor/assets/javascripts/fc_locales/nb.js delete mode 100644 vendor/assets/javascripts/fc_locales/nl-be.js delete mode 100644 vendor/assets/javascripts/fc_locales/nl.js delete mode 100644 vendor/assets/javascripts/fc_locales/nn.js delete mode 100644 vendor/assets/javascripts/fc_locales/pl.js delete mode 100644 vendor/assets/javascripts/fc_locales/pt-br.js delete mode 100644 vendor/assets/javascripts/fc_locales/pt.js delete mode 100644 vendor/assets/javascripts/fc_locales/ro.js delete mode 100644 vendor/assets/javascripts/fc_locales/ru.js delete mode 100644 vendor/assets/javascripts/fc_locales/sk.js delete mode 100644 vendor/assets/javascripts/fc_locales/sl.js delete mode 100644 vendor/assets/javascripts/fc_locales/sq.js delete mode 100644 vendor/assets/javascripts/fc_locales/sr-cyrl.js delete mode 100644 vendor/assets/javascripts/fc_locales/sr.js delete mode 100644 vendor/assets/javascripts/fc_locales/sv.js delete mode 100644 vendor/assets/javascripts/fc_locales/th.js delete mode 100644 vendor/assets/javascripts/fc_locales/tr.js delete mode 100644 vendor/assets/javascripts/fc_locales/uk.js delete mode 100644 vendor/assets/javascripts/fc_locales/vi.js delete mode 100644 vendor/assets/javascripts/fc_locales/zh-cn.js delete mode 100644 vendor/assets/javascripts/fc_locales/zh-tw.js delete mode 100644 vendor/assets/javascripts/fullcalendar.min.js delete mode 100644 vendor/assets/javascripts/jquery-ui.min.js diff --git a/app/assets/javascripts/competition_tabs.js b/app/assets/javascripts/competition_tabs.js index b0ffc02a1e..a10d49a559 100644 --- a/app/assets/javascripts/competition_tabs.js +++ b/app/assets/javascripts/competition_tabs.js @@ -9,7 +9,7 @@ onPage("competitions#show", function() { $(window).on('hashchange', showTabFromHash); function showTabFromHash() { - id = window.location.hash || '#general-info'; + var id = window.location.hash || '#general-info'; $('a[href="' + id + '"]').tab('show'); $(id).find("iframe").each(function () { $iframe = $(this); @@ -17,6 +17,12 @@ onPage("competitions#show", function() { $iframe.attr("src", $iframe.data("src")); } }); + // The FullCalendar implementation that lives inside React (rendered client-side!) + // does not go well with the Bootstrap Tabs that are rendered server-side :( + // So we "fake" a resize fo React FullCalendar to compute its own dimensions correctly. + if (id === '#competition-schedule') { + window.dispatchEvent(new Event('resize')); + } } }); diff --git a/app/assets/javascripts/fullcalendar/fullcalendar_wca.js b/app/assets/javascripts/fullcalendar/fullcalendar_wca.js deleted file mode 100644 index 796e5e846d..0000000000 --- a/app/assets/javascripts/fullcalendar/fullcalendar_wca.js +++ /dev/null @@ -1,2 +0,0 @@ -//= require jquery-ui.min -//= require fullcalendar.min diff --git a/app/assets/javascripts/fullcalendar/locales/ca.js b/app/assets/javascripts/fullcalendar/locales/ca.js deleted file mode 100644 index de708a3ad4..0000000000 --- a/app/assets/javascripts/fullcalendar/locales/ca.js +++ /dev/null @@ -1 +0,0 @@ -//= require fc_locales/ca.js diff --git a/app/assets/javascripts/fullcalendar/locales/cs.js b/app/assets/javascripts/fullcalendar/locales/cs.js deleted file mode 100644 index d58bf804b1..0000000000 --- a/app/assets/javascripts/fullcalendar/locales/cs.js +++ /dev/null @@ -1 +0,0 @@ -//= require fc_locales/cs.js diff --git a/app/assets/javascripts/fullcalendar/locales/da.js b/app/assets/javascripts/fullcalendar/locales/da.js deleted file mode 100644 index a960175402..0000000000 --- a/app/assets/javascripts/fullcalendar/locales/da.js +++ /dev/null @@ -1 +0,0 @@ -//= require fc_locales/da.js diff --git a/app/assets/javascripts/fullcalendar/locales/de.js b/app/assets/javascripts/fullcalendar/locales/de.js deleted file mode 100644 index 8e05ea8040..0000000000 --- a/app/assets/javascripts/fullcalendar/locales/de.js +++ /dev/null @@ -1 +0,0 @@ -//= require fc_locales/de.js diff --git a/app/assets/javascripts/fullcalendar/locales/eo.js b/app/assets/javascripts/fullcalendar/locales/eo.js deleted file mode 100644 index 30bce7f877..0000000000 --- a/app/assets/javascripts/fullcalendar/locales/eo.js +++ /dev/null @@ -1 +0,0 @@ -//= require fc_locales/eo.js diff --git a/app/assets/javascripts/fullcalendar/locales/es.js b/app/assets/javascripts/fullcalendar/locales/es.js deleted file mode 100644 index eb5894fca9..0000000000 --- a/app/assets/javascripts/fullcalendar/locales/es.js +++ /dev/null @@ -1 +0,0 @@ -//= require fc_locales/es.js diff --git a/app/assets/javascripts/fullcalendar/locales/fi.js b/app/assets/javascripts/fullcalendar/locales/fi.js deleted file mode 100644 index 9f46ac81b5..0000000000 --- a/app/assets/javascripts/fullcalendar/locales/fi.js +++ /dev/null @@ -1 +0,0 @@ -//= require fc_locales/fi.js diff --git a/app/assets/javascripts/fullcalendar/locales/fr.js b/app/assets/javascripts/fullcalendar/locales/fr.js deleted file mode 100644 index c12511854a..0000000000 --- a/app/assets/javascripts/fullcalendar/locales/fr.js +++ /dev/null @@ -1 +0,0 @@ -//= require fc_locales/fr.js diff --git a/app/assets/javascripts/fullcalendar/locales/hr.js b/app/assets/javascripts/fullcalendar/locales/hr.js deleted file mode 100644 index b6216adf9a..0000000000 --- a/app/assets/javascripts/fullcalendar/locales/hr.js +++ /dev/null @@ -1 +0,0 @@ -//= require fc_locales/hr.js diff --git a/app/assets/javascripts/fullcalendar/locales/hu.js b/app/assets/javascripts/fullcalendar/locales/hu.js deleted file mode 100644 index 84ce790113..0000000000 --- a/app/assets/javascripts/fullcalendar/locales/hu.js +++ /dev/null @@ -1 +0,0 @@ -//= require fc_locales/hu.js diff --git a/app/assets/javascripts/fullcalendar/locales/id.js b/app/assets/javascripts/fullcalendar/locales/id.js deleted file mode 100644 index 060ce84db7..0000000000 --- a/app/assets/javascripts/fullcalendar/locales/id.js +++ /dev/null @@ -1 +0,0 @@ -//= require fc_locales/id.js diff --git a/app/assets/javascripts/fullcalendar/locales/it.js b/app/assets/javascripts/fullcalendar/locales/it.js deleted file mode 100644 index 1fba5b202a..0000000000 --- a/app/assets/javascripts/fullcalendar/locales/it.js +++ /dev/null @@ -1 +0,0 @@ -//= require fc_locales/it.js diff --git a/app/assets/javascripts/fullcalendar/locales/ja.js b/app/assets/javascripts/fullcalendar/locales/ja.js deleted file mode 100644 index c277a3ed61..0000000000 --- a/app/assets/javascripts/fullcalendar/locales/ja.js +++ /dev/null @@ -1 +0,0 @@ -//= require fc_locales/ja.js diff --git a/app/assets/javascripts/fullcalendar/locales/kk.js b/app/assets/javascripts/fullcalendar/locales/kk.js deleted file mode 100644 index 2ca5286064..0000000000 --- a/app/assets/javascripts/fullcalendar/locales/kk.js +++ /dev/null @@ -1 +0,0 @@ -//= require fc_locales/kk.js diff --git a/app/assets/javascripts/fullcalendar/locales/ko.js b/app/assets/javascripts/fullcalendar/locales/ko.js deleted file mode 100644 index 47f4f182c1..0000000000 --- a/app/assets/javascripts/fullcalendar/locales/ko.js +++ /dev/null @@ -1 +0,0 @@ -//= require fc_locales/ko.js diff --git a/app/assets/javascripts/fullcalendar/locales/nl.js b/app/assets/javascripts/fullcalendar/locales/nl.js deleted file mode 100644 index d57faf356a..0000000000 --- a/app/assets/javascripts/fullcalendar/locales/nl.js +++ /dev/null @@ -1 +0,0 @@ -//= require fc_locales/nl.js diff --git a/app/assets/javascripts/fullcalendar/locales/pl.js b/app/assets/javascripts/fullcalendar/locales/pl.js deleted file mode 100644 index bb08a71775..0000000000 --- a/app/assets/javascripts/fullcalendar/locales/pl.js +++ /dev/null @@ -1 +0,0 @@ -//= require fc_locales/pl.js diff --git a/app/assets/javascripts/fullcalendar/locales/pt-br.js b/app/assets/javascripts/fullcalendar/locales/pt-br.js deleted file mode 100644 index 3434194d02..0000000000 --- a/app/assets/javascripts/fullcalendar/locales/pt-br.js +++ /dev/null @@ -1 +0,0 @@ -//= require fc_locales/pt-br.js diff --git a/app/assets/javascripts/fullcalendar/locales/pt.js b/app/assets/javascripts/fullcalendar/locales/pt.js deleted file mode 100644 index 783694a727..0000000000 --- a/app/assets/javascripts/fullcalendar/locales/pt.js +++ /dev/null @@ -1 +0,0 @@ -//= require fc_locales/pt.js diff --git a/app/assets/javascripts/fullcalendar/locales/ro.js b/app/assets/javascripts/fullcalendar/locales/ro.js deleted file mode 100644 index d33180da4b..0000000000 --- a/app/assets/javascripts/fullcalendar/locales/ro.js +++ /dev/null @@ -1 +0,0 @@ -//= require fc_locales/ro.js diff --git a/app/assets/javascripts/fullcalendar/locales/ru.js b/app/assets/javascripts/fullcalendar/locales/ru.js deleted file mode 100644 index 883cf01458..0000000000 --- a/app/assets/javascripts/fullcalendar/locales/ru.js +++ /dev/null @@ -1 +0,0 @@ -//= require fc_locales/ru.js diff --git a/app/assets/javascripts/fullcalendar/locales/sk.js b/app/assets/javascripts/fullcalendar/locales/sk.js deleted file mode 100644 index d5c9561be6..0000000000 --- a/app/assets/javascripts/fullcalendar/locales/sk.js +++ /dev/null @@ -1 +0,0 @@ -//= require fc_locales/sk.js diff --git a/app/assets/javascripts/fullcalendar/locales/sl.js b/app/assets/javascripts/fullcalendar/locales/sl.js deleted file mode 100644 index e2d8fc0352..0000000000 --- a/app/assets/javascripts/fullcalendar/locales/sl.js +++ /dev/null @@ -1 +0,0 @@ -//= require fc_locales/sl.js diff --git a/app/assets/javascripts/fullcalendar/locales/sv.js b/app/assets/javascripts/fullcalendar/locales/sv.js deleted file mode 100644 index 2b5ede9241..0000000000 --- a/app/assets/javascripts/fullcalendar/locales/sv.js +++ /dev/null @@ -1 +0,0 @@ -//= require fc_locales/sv.js \ No newline at end of file diff --git a/app/assets/javascripts/fullcalendar/locales/th.js b/app/assets/javascripts/fullcalendar/locales/th.js deleted file mode 100644 index 5932d7dd79..0000000000 --- a/app/assets/javascripts/fullcalendar/locales/th.js +++ /dev/null @@ -1 +0,0 @@ -//= require fc_locales/th.js diff --git a/app/assets/javascripts/fullcalendar/locales/uk.js b/app/assets/javascripts/fullcalendar/locales/uk.js deleted file mode 100644 index 8387b2f42f..0000000000 --- a/app/assets/javascripts/fullcalendar/locales/uk.js +++ /dev/null @@ -1 +0,0 @@ -//= require fc_locales/uk.js diff --git a/app/assets/javascripts/fullcalendar/locales/vi.js b/app/assets/javascripts/fullcalendar/locales/vi.js deleted file mode 100644 index 6158a7fc2e..0000000000 --- a/app/assets/javascripts/fullcalendar/locales/vi.js +++ /dev/null @@ -1 +0,0 @@ -//= require fc_locales/vi.js diff --git a/app/assets/javascripts/fullcalendar/locales/zh-cn.js b/app/assets/javascripts/fullcalendar/locales/zh-cn.js deleted file mode 100644 index 091172dda2..0000000000 --- a/app/assets/javascripts/fullcalendar/locales/zh-cn.js +++ /dev/null @@ -1 +0,0 @@ -//= require fc_locales/zh-cn.js diff --git a/app/assets/javascripts/fullcalendar/locales/zh-tw.js b/app/assets/javascripts/fullcalendar/locales/zh-tw.js deleted file mode 100644 index 18f971e62b..0000000000 --- a/app/assets/javascripts/fullcalendar/locales/zh-tw.js +++ /dev/null @@ -1 +0,0 @@ -//= require fc_locales/zh-tw.js diff --git a/app/assets/stylesheets/fullcalendar_wca.scss b/app/assets/stylesheets/fullcalendar_wca.scss deleted file mode 100644 index e0b51c5ba9..0000000000 --- a/app/assets/stylesheets/fullcalendar_wca.scss +++ /dev/null @@ -1 +0,0 @@ -@import "fullcalendar.min"; /* stylelint-disable-line scss/at-import-partial-extension */ diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index aba4838b2c..5c0ae6907b 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -250,12 +250,4 @@ def add_to_js_assets(*names) [@all_js_assets, *names].compact.join(",") end end - - def add_fullcalendar_to_packs - add_to_js_assets('fullcalendar/fullcalendar_wca') - add_to_css_assets('fullcalendar_wca') - if I18n.locale != :en - add_to_js_assets("fullcalendar/locales/#{I18n.locale.downcase}.js") - end - end end diff --git a/app/models/round.rb b/app/models/round.rb index 65c6baaaf4..05c8de20cd 100644 --- a/app/models/round.rb +++ b/app/models/round.rb @@ -219,7 +219,7 @@ def self.wcif_json_schema "type" => "object", "properties" => { "id" => { "type" => "string" }, - "format" => { "type" => "string", "enum" => Format.pluck(:id) }, + "format" => { "type" => "string", "enum" => Format.ids }, "timeLimit" => TimeLimit.wcif_json_schema, "cutoff" => Cutoff.wcif_json_schema, "advancementCondition" => AdvancementConditions::AdvancementCondition.wcif_json_schema, diff --git a/app/views/competitions/_competition_schedule_for_venue_table.html.erb b/app/views/competitions/_competition_schedule_for_venue_table.html.erb deleted file mode 100644 index 363e2bffca..0000000000 --- a/app/views/competitions/_competition_schedule_for_venue_table.html.erb +++ /dev/null @@ -1,84 +0,0 @@ -<%# Local variables that must be passed: %> -<%# - competition %> -<%# - activities %> -<%# - rounds_by_wcif_id %> - -<% activities_by_day = activities.group_by { |a| a[:start].strftime("%Y-%m-%d") } %> - -<% competition.start_date.upto(competition.end_date) do |date| %> - <% activities_for_day = activities_by_day[date.to_fs] %> - <% grouped_activities = activities_for_day&.group_by { |activity| [activity[:title], activity[:start], activity[:end]] } %> - <% if activities_for_day&.any? %> -

<%= t("competitions.schedule.schedule_for_date", day_name: l(date, format: '%A'), full_date: l(date, format: :long)) %>

- - -
- <% grouped_activities.each do |title, activities| %> - <% a = activities.first %> - <% activity_code_without_attempt = "#{a[:activityDetails][:event_id]}-r#{a[:activityDetails][:round_number]}" %> - <% round_data = rounds_by_wcif_id[activity_code_without_attempt] || {} %> -
-
- <%= t("competitions.schedule.range.from") %> - - <%= l(a[:start], format: time_format_for_current_locale) %> - -
-
- <%= cubing_icon round_data[:event_id] unless round_data.empty? %> -
-
- <%= t("competitions.schedule.range.to") %> - - <%= l(a[:end], format: time_format_for_current_locale) %> - -
-
- <%= a[:title] %> -
-
- <%= activities.map { |a| "#{a[:roomName]}" }.join(", ") %> -
-
- <% round_format = round_data[:format_name] %> - <%= render "schedule_table_responsive_cell", label: t("competitions.events.format"), empty: round_format.blank?, md_size: "2" do %> - <%= round_format %> - <% end %> - <% round_time_limit = round_data[:time_limit] %> - <%= render "schedule_table_responsive_cell", label: t("competitions.events.time_limit"), empty: round_time_limit.blank? do %> - <%= round_time_limit %> - <% cumulative_round_ids_length = round_data[:cumulative_round_ids]&.size || 0 %> - <% if cumulative_round_ids_length == 1 %> - <%= link_to "*", "#cumulative-time-limit" %> - <% elsif cumulative_round_ids_length > 1 %> - <%= link_to "**", "#cumulative-across-rounds-time-limit" %> - <% end %> - <% end %> - <% unless round_time_limit.blank? %> - <%# In "sm" viewport, we have two rows with two cells of information %> - <%# The first cell is "format" and is always present if there is a time limit %> - <%# To avoid misalignment we must clearfix here, but only in "sm" %> -
- <% end %> - <% round_cutoff = round_data[:cutoff] %> - <%= render "schedule_table_responsive_cell", label: t("competitions.events.cutoff"), empty: round_cutoff.blank? do %> - <%= round_cutoff %> - <% end %> - <% round_advancement = round_data[:advancement] %> - <%= render "schedule_table_responsive_cell", label: t("competitions.events.proceed"), empty: round_advancement.blank? do %> - <%= round_advancement %> - <% end %> -
- <% end %> -
- <% end %> -<% end %> diff --git a/app/views/competitions/_competition_schedule_tab.html.erb b/app/views/competitions/_competition_schedule_tab.html.erb index 14699288ac..e2a5fe25cf 100644 --- a/app/views/competitions/_competition_schedule_tab.html.erb +++ b/app/views/competitions/_competition_schedule_tab.html.erb @@ -1,133 +1,6 @@ -<%# Generate round informations for the competition %> -<% rounds_by_wcif_id = Hash[competition.rounds.map { |r| [r.wcif_id, r.to_string_map] }] %> -<% add_to_packs("show_schedule") %> -<% add_fullcalendar_to_packs %> - -
- - <% competition.competition_venues.each_with_index do |venue, index| %> - <% - activities = venue.top_level_activities.sort_by(&:start_time) - min_time, max_time = first_and_last_time_from_activities(activities, venue.timezone_id) - activities.map!{ |a| a.to_event(rounds_by_wcif_id) } - %> - -
" id="schedule-venue-<%= venue.id %>" data-venue="<%= venue.id %>"> -

- <%= t("competitions.schedule.venue_information_html", venue_name: link_to_google_maps_place(venue.name, venue.latitude_degrees, venue.longitude_degrees)) %> -
- <%= t("competitions.schedule.timezone_message", timezone: venue.timezone_id) %> -
- <%= t("competitions.competition_info.add_to_calendar") %> - <%= link_to(ui_icon("calendar plus"), competition_path(competition, format: :ics), - title: t("competitions.competition_info.add_to_calendar"), - data: { - toggle: "tooltip", - placement: "top", - container: "body", - }) %> - <% if competition.competition_venues.size > 1 %> -
- <%= t("competitions.schedule.multiple_venues_available") %> - <% end %> -

- <% venue_rooms = venue.venue_rooms %> -
- - <%# We want to keep these elements in the html for the event filter. %> -
"> - <%= t("competitions.schedule.display_for_room") %> -
- <% venue_rooms.each do |r| %> - -
-
-
-
- <%= r.name %> -
-
- <% end %> -
-
-
-
-
-
-
ALL
- <% competition.events.each do |e| %> - <%= cubing_icon(e.id, class: "selected", data: { event: e.id }) %> - <% end %> -
-
-
-
-
-
- <%= render "competition_schedule_for_venue_table", competition: competition, rounds_by_wcif_id: rounds_by_wcif_id, activities: activities %> -
- -
- <% end %> -
- - +<%= react_component("Schedule", { + wcifSchedule: @competition.schedule_wcif, + wcifEvents: @competition.events_wcif, + competitionName: @competition.name, + calendarLocale: I18n.locale, +}) %> diff --git a/app/views/competitions/_events_tab.html.erb b/app/views/competitions/_events_tab.html.erb index 1d1925a96f..33e130803e 100644 --- a/app/views/competitions/_events_tab.html.erb +++ b/app/views/competitions/_events_tab.html.erb @@ -1,54 +1,4 @@ -<%= wca_table table_class: "show-events-table" do %> - - - <%= t("competitions.results_table.event") %> - <%= t("competitions.results_table.round") %> - <%= link_to t("competitions.events.format"), "#format" %> - <%= link_to t("competitions.events.time_limit"), "#time-limit" %> - <% if @competition.uses_cutoff? %> - <%= link_to t("competitions.events.cutoff"), "#cutoff" %> - <% end %> - <%= t("competitions.events.proceed") %> - <% if @competition.uses_qualification? %> - <%= link_to t("competitions.events.qualification"), "#qualification" %> - <% end %> - - - - - <% @competition.competition_events.sort_by { |ce| ce.event.rank }.each do |competition_event| %> - <% competition_event.rounds.each do |round| %> - "> - - <%= competition_event.event.name if round.number == 1 %> - - <%= round.round_type.name %> - - <%= round.full_format_name(with_short_names: true, with_tooltips: true) %> - - - <%= round.time_limit_to_s %> - <% if competition_event.event.can_change_time_limit? %> - <% if round.time_limit.cumulative_round_ids.length == 1 %> - <%= link_to "*", "#cumulative-time-limit" %> - <% elsif round.time_limit.cumulative_round_ids.length > 1 %> - <%= link_to "**", "#cumulative-across-rounds-time-limit" %> - <% end %> - <% end %> - - <% if @competition.uses_cutoff? %> - <%= round.cutoff_to_s %> - <% end %> - <%= round.advancement_condition_to_s %> - <% if @competition.uses_qualification? %> - <% if round.number == 1 %> - <%= competition_event.qualification_to_s %> - <% else %> - - <% end %> - <% end %> - - <% end %> - <% end %> - -<% end %> +<%= react_component("EventsTable", { + wcifEvents: @competition.events_wcif, + competitionInfo: @competition.to_competition_info +}) %> diff --git a/app/views/competitions/_schedule_table_responsive_cell.html.erb b/app/views/competitions/_schedule_table_responsive_cell.html.erb deleted file mode 100644 index 2f9c780344..0000000000 --- a/app/views/competitions/_schedule_table_responsive_cell.html.erb +++ /dev/null @@ -1,11 +0,0 @@ -<% label ||= "undefined" %> -<% md_size ||= "1" %> -
"> - <%= label %> -
-
"> - <%= yield %> -
-<% unless empty %> -
-<% end %> diff --git a/app/webpacker/components/EventsTable/index.jsx b/app/webpacker/components/EventsTable/index.jsx new file mode 100644 index 0000000000..7cc7b22c2b --- /dev/null +++ b/app/webpacker/components/EventsTable/index.jsx @@ -0,0 +1,97 @@ +import React from 'react'; +import { + Table, + TableBody, + TableCell, + TableHeader, + TableHeaderCell, + TableRow, +} from 'semantic-ui-react'; +import i18n from '../../lib/i18n'; +import { attemptResultToString } from '../../lib/utils/edit-events'; +import { attemptTypeById, centisecondsToClockFormat } from '../../lib/wca-live/attempts'; +import { events, formats } from '../../lib/wca-data.js.erb'; +import { eventQualificationToString, getRoundTypeId } from '../../lib/utils/wcif'; + +export default function EventsTable({ competitionInfo, wcifEvents }) { + return ( + + + + + {i18n.t('competitions.results_table.event')} + + + {i18n.t('competitions.results_table.round')} + + + {i18n.t('competitions.events.format')} + + + {i18n.t('competitions.events.time_limit')} + + {competitionInfo['uses_cutoff?'] && ( + + {i18n.t('competitions.events.cutoff')} + + )} + + {i18n.t('competitions.events.proceed')} + + {competitionInfo['uses_qualification?'] && ( + + {i18n.t('competitions.events.qualification')} + + )} + + + + + {wcifEvents.map((event) => event.rounds.map((round, i) => ( + + {i === 0 && ( + + {events.byId[event.id].name} + + )} + {i18n.t(`rounds.${getRoundTypeId(i + 1, event.rounds.length, Boolean(round.cutoff))}.cellName`)} + + {round.cutoff && `${formats.byId[round.cutoff.numberOfAttempts].shortName} / `} + {formats.byId[round.format].shortName} + + + {round.timeLimit + && centisecondsToClockFormat( + round.timeLimit.centiseconds, + )} + + {competitionInfo['uses_cutoff?'] && ( + + {round.cutoff + && i18n.t( + `cutoff.${attemptTypeById(event.id)}`, + { + time: attemptResultToString(round.cutoff.attemptResult, event.id), + moves: attemptResultToString(round.cutoff.attemptResult, event.id), + points: attemptResultToString(round.cutoff.attemptResult, event.id), + count: round.cutoff.numberOfAttempts, + }, + )} + + )} + + {round.advancementCondition + && i18n.t(`advancement_condition.${round.advancementCondition.type}`, { ranking: round.advancementCondition.level, percent: round.advancementCondition.level })} + + {competitionInfo['uses_qualification?'] && ( + + { i === 0 + && eventQualificationToString(event, event.qualification, { short: true })} + + )} + + )))} + +
+ ); +} diff --git a/app/webpacker/components/RegistrationsV2/Register/CompetingStep.jsx b/app/webpacker/components/RegistrationsV2/Register/CompetingStep.jsx index 09e4ccbf1b..735d55a5dd 100644 --- a/app/webpacker/components/RegistrationsV2/Register/CompetingStep.jsx +++ b/app/webpacker/components/RegistrationsV2/Register/CompetingStep.jsx @@ -18,7 +18,7 @@ import { } from 'semantic-ui-react'; import updateRegistration from '../api/registration/patch/update_registration'; import submitEventRegistration from '../api/registration/post/submit_registration'; -import { getMediumDateString, hasPassed } from '../lib/dates'; +import { getMediumDateString, hasPassed } from '../../../lib/utils/dates'; import Processing from './Processing'; import { userPreferencesRoute } from '../../../lib/requests/routes.js.erb'; import { EventSelector } from '../../CompetitionsOverview/CompetitionsFilters'; diff --git a/app/webpacker/components/RegistrationsV2/RegistrationAdministration/RegistrationAdministrationList.jsx b/app/webpacker/components/RegistrationsV2/RegistrationAdministration/RegistrationAdministrationList.jsx index b7ed0af0dd..9e6758f4ce 100644 --- a/app/webpacker/components/RegistrationsV2/RegistrationAdministration/RegistrationAdministrationList.jsx +++ b/app/webpacker/components/RegistrationsV2/RegistrationAdministration/RegistrationAdministrationList.jsx @@ -4,7 +4,7 @@ import { Checkbox, Flag, Form, Header, Icon, Popup, Sticky, Table, } from 'semantic-ui-react'; import { getAllRegistrations } from '../api/registration/get/get_registrations'; -import { getShortDateString, getShortTimeString } from '../lib/dates'; +import { getShortDateString, getShortTimeString } from '../../../lib/utils/dates'; import createSortReducer from '../reducers/sortReducer'; import RegistrationActions from './RegistrationActions'; import { setMessage } from '../Register/RegistrationMessage'; diff --git a/app/webpacker/components/RegistrationsV2/RegistrationEdit/RegistrationEditor.jsx b/app/webpacker/components/RegistrationsV2/RegistrationEdit/RegistrationEditor.jsx index 2e744ba97c..a33214936b 100644 --- a/app/webpacker/components/RegistrationsV2/RegistrationEdit/RegistrationEditor.jsx +++ b/app/webpacker/components/RegistrationsV2/RegistrationEdit/RegistrationEditor.jsx @@ -24,7 +24,7 @@ import { getShortDateString, getShortTimeString, hasPassed, -} from '../lib/dates'; +} from '../../../lib/utils/dates'; import { useDispatch } from '../../../lib/providers/StoreProvider'; import { setMessage } from '../Register/RegistrationMessage'; import Loading from '../../Requests/Loading'; diff --git a/app/webpacker/components/Schedule/AddToCalendar.jsx b/app/webpacker/components/Schedule/AddToCalendar.jsx new file mode 100644 index 0000000000..f7a514908d --- /dev/null +++ b/app/webpacker/components/Schedule/AddToCalendar.jsx @@ -0,0 +1,31 @@ +import { DateTime } from 'luxon'; +import React from 'react'; +import { List } from 'semantic-ui-react'; + +export default function AddToCalendar({ + startDate, + endDate, + timeZone, + name, + address, + allDay, +}) { + // note: date corresponds to midnight for all-day events, so need to use the day after + const endDateOffset = allDay ? { days: 1 } : {}; + const format = allDay ? 'yyyyMMdd' : "yyyyMMdd'T'HHmmssZ"; + + const formattedStartDate = DateTime.fromISO(startDate, { zone: timeZone }).toFormat(format); + const formattedEndDate = DateTime.fromISO(endDate, { zone: timeZone }) + .plus(endDateOffset) + .toFormat(format); + + const googleCalendarLink = `https://calendar.google.com/calendar/render?action=TEMPLATE&text=${name}&dates=${formattedStartDate}/${formattedEndDate}${ + address ? `&location=${address}` : '' + }`; + + return ( + + + + ); +} diff --git a/app/webpacker/components/Schedule/CalendarView.jsx b/app/webpacker/components/Schedule/CalendarView.jsx new file mode 100644 index 0000000000..4463f13366 --- /dev/null +++ b/app/webpacker/components/Schedule/CalendarView.jsx @@ -0,0 +1,99 @@ +import luxonPlugin from '@fullcalendar/luxon3'; +import FullCalendar from '@fullcalendar/react'; +import timeGridPlugin from '@fullcalendar/timegrid'; +import { DateTime } from 'luxon'; +import React from 'react'; +import { + earliestTimeOfDayWithBuffer, + getActivityEventId, + latestTimeOfDayWithBuffer, + localizeActivityName, +} from '../../lib/utils/activities'; +import { ACTIVITY_OTHER_GREY, getTextColor } from '../../lib/utils/calendar'; +import I18n from '../../lib/i18n'; + +// We can render custom content for the individual fullcalendar events, by +// passing in a render function as the `eventContent` param to the `FullCalendar` +// component. +// This can be used to add a tooltip (to display round information, as the old +// calendar used to do), better indicate that an event continues after midnight, +// etc. +// However, then we have to recreate the default line-break and sizing logic of +// the name/time ourselves, which is annoying... +// Once https://github.com/fullcalendar/fullcalendar/issues/5927 is available, +// we can take the default content and just wrap a tooltip around it, which is +// exactly what we want. +// (Alternatively, implement the `eventClick` param to `FullCalendar` and have it +// open a modal. But that's a bit clunky so we're not doing it, for now at least.) + +export default function CalendarView({ + dates, + timeZone, + activeVenues, + activeRooms, + activeEvents, + calendarLocale, +}) { + const activeEventIds = activeEvents.map(({ id }) => id); + const fcActivities = activeRooms.flatMap((room) => room.activities + .filter((activity) => ['other', ...activeEventIds].includes(getActivityEventId(activity))) + .map((activity) => { + const eventName = activity.activityCode.startsWith('other') ? activity.name : localizeActivityName(activity, activeEvents); + const eventColor = activity.activityCode.startsWith('other') ? ACTIVITY_OTHER_GREY : room.color; + + return ({ + title: eventName, + start: activity.startTime, + end: activity.endTime, + backgroundColor: eventColor, + textColor: getTextColor(eventColor), + }); + })); + + // independent of which activities are visible, + // to prevent calendar height jumping around + const activeVenuesActivities = activeVenues.flatMap( + (venue) => venue.rooms.flatMap((room) => room.activities), + ); + const calendarStart = earliestTimeOfDayWithBuffer(activeVenuesActivities, timeZone) ?? '00:00:00'; + const calendarEnd = latestTimeOfDayWithBuffer(activeVenuesActivities, timeZone) ?? '00:00:00'; + + return ( + <> + + {fcActivities.length === 0 && ( + {I18n.t('competitions.schedule.no_activities')} + )} + + ); +} diff --git a/app/webpacker/components/Schedule/TableView.jsx b/app/webpacker/components/Schedule/TableView.jsx new file mode 100644 index 0000000000..cba02fb3ba --- /dev/null +++ b/app/webpacker/components/Schedule/TableView.jsx @@ -0,0 +1,240 @@ +import { DateTime } from 'luxon'; +import React, { useState } from 'react'; +import { + Checkbox, Header, Segment, Table, TableCell, +} from 'semantic-ui-react'; +import { + activitiesOnDate, + earliestWithLongestTieBreaker, + getActivityEventId, + getActivityRoundId, + groupActivities, localizeActivityName, +} from '../../lib/utils/activities'; +import { getSimpleTimeString } from '../../lib/utils/dates'; +import { toDegrees } from '../../lib/utils/edit-schedule'; +import AddToCalendar from './AddToCalendar'; +import i18n from '../../lib/i18n'; +import { formats } from '../../lib/wca-data.js.erb'; +import { attemptTypeById, centisecondsToClockFormat } from '../../lib/wca-live/attempts'; +import { attemptResultToString } from '../../lib/utils/edit-events'; +import { parseActivityCode } from '../../lib/utils/wcif'; + +export default function TableView({ + dates, + timeZone, + activeRooms, + activeEvents, + activeVenueOrNull, + competitionName, +}) { + const activeRounds = activeEvents.flatMap((event) => event.rounds); + + const [isExpanded, setIsExpanded] = useState(false); + + const sortedActivities = activeRooms + .flatMap((room) => room.activities) + .toSorted(earliestWithLongestTieBreaker); + + const eventIds = activeEvents.map(({ id }) => id); + const visibleActivities = sortedActivities.filter((activity) => ['other', ...eventIds].includes(getActivityEventId(activity))); + + return ( + <> + setIsExpanded(data.checked)} + /> + + {dates.map((date) => { + const activitiesForDay = activitiesOnDate( + visibleActivities, + date, + timeZone, + ); + const groupedActivitiesForDay = groupActivities(activitiesForDay); + + return ( + + ); + })} + + ); +} + +function SingleDayTable({ + date, + timeZone, + groupedActivities, + events, + rounds, + rooms, + isExpanded, + activeVenueOrNull, + competitionName, +}) { + const title = i18n.t('competitions.schedule.schedule_for_full_date', { date: date.toLocaleString(DateTime.DATE_HUGE) }); + + const hasActivities = groupedActivities.length > 0; + const startTime = hasActivities && groupedActivities[0][0].startTime; + const endTime = hasActivities && groupedActivities[groupedActivities.length - 1][0].endTime; + const activeVenueAddress = activeVenueOrNull + && `${toDegrees(activeVenueOrNull.latitudeMicrodegrees)},${toDegrees( + activeVenueOrNull.longitudeMicrodegrees, + )}`; + + return ( + +
+ {hasActivities && ( + + )} + {hasActivities && ' '} + {title} +
+ + + + + + + + {hasActivities ? ( + groupedActivities.map((activityGroup) => { + const activityRound = rounds.find( + (round) => round.id === getActivityRoundId(activityGroup[0]), + ); + + return ( + + ); + }) + ) : ( + + + {i18n.t('competitions.schedule.no_activities')} + + + )} + +
+
+ ); +} + +function HeaderRow({ isExpanded }) { + return ( + + {i18n.t('competitions.schedule.start')} + {i18n.t('competitions.schedule.end')} + {i18n.t('competitions.schedule.activity')} + {i18n.t('competitions.schedule.room_or_stage')} + {isExpanded && ( + <> + {i18n.t('competitions.events.format')} + {i18n.t('competitions.events.time_limit')} + {i18n.t('competitions.events.cutoff')} + {i18n.t('competitions.events.proceed')} + + )} + + ); +} + +function ActivityRow({ + isExpanded, + activityGroup, + events, + round, + rooms, + timeZone, +}) { + const representativeActivity = activityGroup[0]; + + const name = representativeActivity.activityCode.startsWith('other') ? representativeActivity.name : localizeActivityName(representativeActivity, events); + const { startTime, endTime } = representativeActivity; + + const activityIds = activityGroup.map((activity) => activity.id); + + // note: round may be undefined for custom activities like lunch + const { + format, timeLimit, cutoff, advancementCondition, + } = round || {}; + const roomsUsed = rooms.filter( + (room) => room.activities.some((activity) => activityIds.includes(activity.id)), + ); + const { eventId } = cutoff ? parseActivityCode(round.id) : {}; + + return ( + + {getSimpleTimeString(startTime, timeZone)} + + {getSimpleTimeString(endTime, timeZone)} + + {name} + + {roomsUsed.map((room) => room.name).join(', ')} + + {isExpanded && ( + <> + + {cutoff && format && `${formats.byId[cutoff.numberOfAttempts].shortName} / `} + {format && formats.byId[format].shortName} + + + + {timeLimit + && centisecondsToClockFormat( + timeLimit.centiseconds, + )} + + + + {cutoff + && i18n.t( + `cutoff.${attemptTypeById(eventId)}`, + { + time: attemptResultToString(cutoff.attemptResult, eventId), + moves: attemptResultToString(cutoff.attemptResult, eventId), + points: attemptResultToString(cutoff.attemptResult, eventId), + count: cutoff.numberOfAttempts, + }, + )} + + + + {advancementCondition + && i18n.t(`advancement_condition.${advancementCondition.type}`, { ranking: advancementCondition.level, percent: advancementCondition.level })} + + + )} + + ); +} diff --git a/app/webpacker/components/Schedule/TimeZone.jsx b/app/webpacker/components/Schedule/TimeZone.jsx new file mode 100644 index 0000000000..b6f92fc0a4 --- /dev/null +++ b/app/webpacker/components/Schedule/TimeZone.jsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { + Button, + Checkbox, + Dropdown, + Header, + Segment, +} from 'semantic-ui-react'; +import i18n from '../../lib/i18n'; +import { timezoneData } from '../../lib/wca-data.js.erb'; + +const timeZoneOptions = Object.entries(timezoneData).map(([tzName, tzId]) => ({ + key: tzId, + text: tzName, + value: tzId, +})); + +const { timeZone: userTimeZone } = Intl.DateTimeFormat().resolvedOptions(); + +export default function TimeZoneSelector({ + activeVenueOrNull, + hasMultipleVenues, + activeTimeZone, + setActiveTimeZone, + followVenueSelection, + setFollowVenueSelection, +}) { + return ( + +
{i18n.t('competitions.schedule.time_zone')}
+ {i18n.t('competitions.schedule.timezone_setting')} + {' '} + setActiveTimeZone(data.value)} + options={timeZoneOptions} + /> +
+ + + ").click(function(t){w.hasClass(o.getClass("stateDisabled"))||(g(t),(w.hasClass(o.getClass("stateActive"))||w.hasClass(o.getClass("stateDisabled")))&&w.removeClass(o.getClass("stateHover")))}).mousedown(function(){w.not("."+o.getClass("stateActive")).not("."+o.getClass("stateDisabled")).addClass(o.getClass("stateDown"))}).mouseup(function(){w.removeClass(o.getClass("stateDown"))}).hover(function(){w.not("."+o.getClass("stateActive")).not("."+o.getClass("stateDisabled")).addClass(o.getClass("stateHover"))},function(){w.removeClass(o.getClass("stateHover")).removeClass(o.getClass("stateDown"))}),h=h.add(w)))}),f&&h.first().addClass(o.getClass("cornerLeft")).end().last().addClass(o.getClass("cornerRight")).end(),h.length>1?(u=r("
"),f&&u.addClass(o.getClass("buttonGroup")),u.append(h),l.append(u)):l.append(h)}),l},t.prototype.updateTitle=function(t){this.el&&this.el.find("h2").text(t)},t.prototype.activateButton=function(t){this.el&&this.el.find(".fc-"+t+"-button").addClass(this.calendar.theme.getClass("stateActive"))},t.prototype.deactivateButton=function(t){this.el&&this.el.find(".fc-"+t+"-button").removeClass(this.calendar.theme.getClass("stateActive"))},t.prototype.disableButton=function(t){this.el&&this.el.find(".fc-"+t+"-button").prop("disabled",!0).addClass(this.calendar.theme.getClass("stateDisabled"))},t.prototype.enableButton=function(t){this.el&&this.el.find(".fc-"+t+"-button").prop("disabled",!1).removeClass(this.calendar.theme.getClass("stateDisabled"))},t.prototype.getViewsWithButtons=function(){return this.viewsWithButtons},t}();e.default=o},function(t,e,n){Object.defineProperty(e,"__esModule",{value:!0});var r=n(2),i=n(3),o=n(4),s=n(33),a=n(32),l=n(51),u=function(t){function e(e,n){var r=t.call(this)||this;return r._calendar=e,r.overrides=i.extend({},n),r.dynamicOverrides={},r.compute(),r}return r.__extends(e,t),e.prototype.add=function(t){var e,n=0;this.recordOverrides(t);for(e in t)n++;if(1===n){if("height"===e||"contentHeight"===e||"aspectRatio"===e)return void this._calendar.updateViewSize(!0);if("defaultDate"===e)return;if("businessHours"===e)return;if(/^(event|select)(Overlap|Constraint|Allow)$/.test(e))return;if("timezone"===e)return void this._calendar.view.flash("initialEvents")}this._calendar.renderHeader(),this._calendar.renderFooter(),this._calendar.viewsByType={},this._calendar.reinitView()},e.prototype.compute=function(){var t,e,n,r,i;t=o.firstDefined(this.dynamicOverrides.locale,this.overrides.locale),e=a.localeOptionHash[t],e||(t=s.globalDefaults.locale,e=a.localeOptionHash[t]||{}),n=o.firstDefined(this.dynamicOverrides.isRTL,this.overrides.isRTL,e.isRTL,s.globalDefaults.isRTL),r=n?s.rtlDefaults:{},this.dirDefaults=r,this.localeDefaults=e,i=s.mergeOptions([s.globalDefaults,r,e,this.overrides,this.dynamicOverrides]),a.populateInstanceComputableOptions(i),this.reset(i)},e.prototype.recordOverrides=function(t){var e;for(e in t)this.dynamicOverrides[e]=t[e];this._calendar.viewSpecManager.clearCache(),this.compute()},e}(l.default);e.default=u},function(t,e,n){Object.defineProperty(e,"__esModule",{value:!0});var r=n(0),i=n(3),o=n(24),s=n(4),a=n(33),l=n(32),u=function(){function t(t,e){this.optionsManager=t,this._calendar=e,this.clearCache()}return t.prototype.clearCache=function(){this.viewSpecCache={}},t.prototype.getViewSpec=function(t){var e=this.viewSpecCache;return e[t]||(e[t]=this.buildViewSpec(t))},t.prototype.getUnitViewSpec=function(t){var e,n,r;if(-1!==i.inArray(t,s.unitsDesc))for(e=this._calendar.header.getViewsWithButtons(),i.each(o.viewHash,function(t){e.push(t)}),n=0;n",options:{classes:{},disabled:!1,create:null},_createWidget:function(t,e){e=P(e||this.defaultElement||this)[0],this.element=P(e),this.uuid=i++,this.eventNamespace="."+this.widgetName+this.uuid,this.bindings=P(),this.hoverable=P(),this.focusable=P(),this.classesElementLookup={},e!==this&&(P.data(e,this.widgetFullName,this),this._on(!0,this.element,{remove:function(t){t.target===e&&this.destroy()}}),this.document=P(e.style?e.ownerDocument:e.document||e),this.window=P(this.document[0].defaultView||this.document[0].parentWindow)),this.options=P.widget.extend({},this.options,this._getCreateOptions(),t),this._create(),this.options.disabled&&this._setOptionDisabled(this.options.disabled),this._trigger("create",null,this._getCreateEventData()),this._init()},_getCreateOptions:function(){return{}},_getCreateEventData:P.noop,_create:P.noop,_init:P.noop,destroy:function(){var i=this;this._destroy(),P.each(this.classesElementLookup,function(t,e){i._removeClass(e,t)}),this.element.off(this.eventNamespace).removeData(this.widgetFullName),this.widget().off(this.eventNamespace).removeAttr("aria-disabled"),this.bindings.off(this.eventNamespace)},_destroy:P.noop,widget:function(){return this.element},option:function(t,e){var i,s,o,n=t;if(0===arguments.length)return P.widget.extend({},this.options);if("string"==typeof t)if(n={},t=(i=t.split(".")).shift(),i.length){for(s=n[t]=P.widget.extend({},this.options[t]),o=0;o
"),i=e.children()[0];return P("body").append(e),t=i.offsetWidth,e.css("overflow","scroll"),t===(i=i.offsetWidth)&&(i=e[0].clientWidth),e.remove(),s=t-i},getScrollInfo:function(t){var e=t.isWindow||t.isDocument?"":t.element.css("overflow-x"),i=t.isWindow||t.isDocument?"":t.element.css("overflow-y"),e="scroll"===e||"auto"===e&&t.widthx(T(s),T(o))?n.important="horizontal":n.important="vertical",c.using.call(this,t,n)}),r.offset(P.extend(h,{using:t}))})},P.ui.position={fit:{left:function(t,e){var i=e.within,s=i.isWindow?i.scrollLeft:i.offset.left,o=i.width,n=t.left-e.collisionPosition.marginLeft,r=s-n,l=n+e.collisionWidth-o-s;e.collisionWidth>o?0o?0=this.options.distance},_mouseDelayMet:function(){return this.mouseDelayMet},_mouseStart:function(){},_mouseDrag:function(){},_mouseStop:function(){},_mouseCapture:function(){return!0}}),P.ui.plugin={add:function(t,e,i){var s,o=P.ui[t].prototype;for(s in i)o.plugins[s]=o.plugins[s]||[],o.plugins[s].push([e,i[s]])},call:function(t,e,i,s){var o,n=t.plugins[e];if(n&&(s||t.element[0].parentNode&&11!==t.element[0].parentNode.nodeType))for(o=0;o").css("position","absolute").appendTo(t.parent()).outerWidth(t.outerWidth()).outerHeight(t.outerHeight()).offset(t.offset())[0]})},_unblockFrames:function(){this.iframeBlocks&&(this.iframeBlocks.remove(),delete this.iframeBlocks)},_blurActiveElement:function(t){var e=P.ui.safeActiveElement(this.document[0]);P(t.target).closest(e).length||P.ui.safeBlur(e)},_mouseStart:function(t){var e=this.options;return this.helper=this._createHelper(t),this._addClass(this.helper,"ui-draggable-dragging"),this._cacheHelperProportions(),P.ui.ddmanager&&(P.ui.ddmanager.current=this),this._cacheMargins(),this.cssPosition=this.helper.css("position"),this.scrollParent=this.helper.scrollParent(!0),this.offsetParent=this.helper.offsetParent(),this.hasFixedAncestor=0i[2]&&(n=i[2]+this.offset.click.left),t.pageY-this.offset.click.top>i[3]&&(r=i[3]+this.offset.click.top)),s.grid&&(t=s.grid[1]?this.originalPageY+Math.round((r-this.originalPageY)/s.grid[1])*s.grid[1]:this.originalPageY,r=!i||t-this.offset.click.top>=i[1]||t-this.offset.click.top>i[3]?t:t-this.offset.click.top>=i[1]?t-s.grid[1]:t+s.grid[1],t=s.grid[0]?this.originalPageX+Math.round((n-this.originalPageX)/s.grid[0])*s.grid[0]:this.originalPageX,n=!i||t-this.offset.click.left>=i[0]||t-this.offset.click.left>i[2]?t:t-this.offset.click.left>=i[0]?t-s.grid[0]:t+s.grid[0]),"y"===s.axis&&(n=this.originalPageX),"x"===s.axis&&(r=this.originalPageY)),{top:r-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+("fixed"===this.cssPosition?-this.offset.scroll.top:o?0:this.offset.scroll.top),left:n-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+("fixed"===this.cssPosition?-this.offset.scroll.left:o?0:this.offset.scroll.left)}},_clear:function(){this._removeClass(this.helper,"ui-draggable-dragging"),this.helper[0]===this.element[0]||this.cancelHelperRemoval||this.helper.remove(),this.helper=null,this.cancelHelperRemoval=!1,this.destroyOnClear&&this.destroy()},_trigger:function(t,e,i){return i=i||this._uiHash(),P.ui.plugin.call(this,t,[e,i,this],!0),/^(drag|start|stop)/.test(t)&&(this.positionAbs=this._convertPositionTo("absolute"),i.offset=this.positionAbs),P.Widget.prototype._trigger.call(this,t,e,i)},plugins:{},_uiHash:function(){return{helper:this.helper,position:this.position,originalPosition:this.originalPosition,offset:this.positionAbs}}}),P.ui.plugin.add("draggable","connectToSortable",{start:function(e,t,i){var s=P.extend({},t,{item:i.element});i.sortables=[],P(i.options.connectToSortable).each(function(){var t=P(this).sortable("instance");t&&!t.options.disabled&&(i.sortables.push(t),t.refreshPositions(),t._trigger("activate",e,s))})},stop:function(e,t,i){var s=P.extend({},t,{item:i.element});i.cancelHelperRemoval=!1,P.each(i.sortables,function(){var t=this;t.isOver?(t.isOver=0,i.cancelHelperRemoval=!0,t.cancelHelperRemoval=!1,t._storedCSS={position:t.placeholder.css("position"),top:t.placeholder.css("top"),left:t.placeholder.css("left")},t._mouseStop(e),t.options.helper=t.options._helper):(t.cancelHelperRemoval=!0,t._trigger("deactivate",e,s))})},drag:function(i,s,o){P.each(o.sortables,function(){var t=!1,e=this;e.positionAbs=o.positionAbs,e.helperProportions=o.helperProportions,e.offset.click=o.offset.click,e._intersectsWith(e.containerCache)&&(t=!0,P.each(o.sortables,function(){return this.positionAbs=o.positionAbs,this.helperProportions=o.helperProportions,this.offset.click=o.offset.click,t=this!==e&&this._intersectsWith(this.containerCache)&&P.contains(e.element[0],this.element[0])?!1:t})),t?(e.isOver||(e.isOver=1,o._parent=s.helper.parent(),e.currentItem=s.helper.appendTo(e.element).data("ui-sortable-item",!0),e.options._helper=e.options.helper,e.options.helper=function(){return s.helper[0]},i.target=e.currentItem[0],e._mouseCapture(i,!0),e._mouseStart(i,!0,!0),e.offset.click.top=o.offset.click.top,e.offset.click.left=o.offset.click.left,e.offset.parent.left-=o.offset.parent.left-e.offset.parent.left,e.offset.parent.top-=o.offset.parent.top-e.offset.parent.top,o._trigger("toSortable",i),o.dropped=e.element,P.each(o.sortables,function(){this.refreshPositions()}),o.currentItem=o.element,e.fromOutside=o),e.currentItem&&(e._mouseDrag(i),s.position=e.position)):e.isOver&&(e.isOver=0,e.cancelHelperRemoval=!0,e.options._revert=e.options.revert,e.options.revert=!1,e._trigger("out",i,e._uiHash(e)),e._mouseStop(i,!0),e.options.revert=e.options._revert,e.options.helper=e.options._helper,e.placeholder&&e.placeholder.remove(),s.helper.appendTo(o._parent),o._refreshOffsets(i),s.position=o._generatePosition(i,!0),o._trigger("fromSortable",i),o.dropped=!1,P.each(o.sortables,function(){this.refreshPositions()}))})}}),P.ui.plugin.add("draggable","cursor",{start:function(t,e,i){var s=P("body"),i=i.options;s.css("cursor")&&(i._cursor=s.css("cursor")),s.css("cursor",i.cursor)},stop:function(t,e,i){i=i.options;i._cursor&&P("body").css("cursor",i._cursor)}}),P.ui.plugin.add("draggable","opacity",{start:function(t,e,i){e=P(e.helper),i=i.options;e.css("opacity")&&(i._opacity=e.css("opacity")),e.css("opacity",i.opacity)},stop:function(t,e,i){i=i.options;i._opacity&&P(e.helper).css("opacity",i._opacity)}}),P.ui.plugin.add("draggable","scroll",{start:function(t,e,i){i.scrollParentNotHidden||(i.scrollParentNotHidden=i.helper.scrollParent(!1)),i.scrollParentNotHidden[0]!==i.document[0]&&"HTML"!==i.scrollParentNotHidden[0].tagName&&(i.overflowOffset=i.scrollParentNotHidden.offset())},drag:function(t,e,i){var s=i.options,o=!1,n=i.scrollParentNotHidden[0],r=i.document[0];n!==r&&"HTML"!==n.tagName?(s.axis&&"x"===s.axis||(i.overflowOffset.top+n.offsetHeight-t.pageY