diff --git a/tnoodle-ui/src/App.css b/tnoodle-ui/src/App.css index 62a24dfd8..81f34f793 100644 --- a/tnoodle-ui/src/App.css +++ b/tnoodle-ui/src/App.css @@ -59,6 +59,18 @@ nav li { padding-top: 10px; } -#side-bar { - height: 100vh; +@media (min-width: 992px) { + #side-bar { + height: 100vh; + position: sticky; + top: 0; + z-index: 1020; + } +} + +@media (max-width: 991.98px) { + #title { + margin-bottom: 0; + margin-left: 12px; + } } diff --git a/tnoodle-ui/src/App.js b/tnoodle-ui/src/App.js index c9e98d8e6..73238580d 100644 --- a/tnoodle-ui/src/App.js +++ b/tnoodle-ui/src/App.js @@ -10,12 +10,12 @@ class App extends Component {
-
+
diff --git a/tnoodle-ui/src/main/api/wca.api.js b/tnoodle-ui/src/main/api/wca.api.js index 1003d208d..0815bf839 100644 --- a/tnoodle-ui/src/main/api/wca.api.js +++ b/tnoodle-ui/src/main/api/wca.api.js @@ -151,7 +151,7 @@ async function wcaApiFetch(path, fetchOptions) { const response = await fetch(`${baseApiUrl}${path}`, fetchOptions); if (!response.ok) { - throw new Error(`${response.status}: ${response.statusText}`); + return Promise.reject(); } return response; } diff --git a/tnoodle-ui/src/main/components/EntryInterface.jsx b/tnoodle-ui/src/main/components/EntryInterface.jsx index 451a71d69..101b19c8f 100644 --- a/tnoodle-ui/src/main/components/EntryInterface.jsx +++ b/tnoodle-ui/src/main/components/EntryInterface.jsx @@ -1,123 +1,81 @@ -import React, { Component } from "react"; +import React, { useState } from "react"; import { updatePassword, updateCompetitionName, updateFileZipBlob, } from "../redux/ActionCreators"; -import { connect } from "react-redux"; -import { getDefaultCompetitionName } from "../util/competition.name.util"; import { FaEye, FaEyeSlash } from "react-icons/fa"; +import { useSelector, useDispatch } from "react-redux"; -const mapStateToProps = (store) => ({ - editingDisabled: store.editingDisabled, - competitionName: store.wcif.name, - generatingScrambles: store.generatingScrambles, -}); +const EntryInterface = () => { + const [showPassword, setShowPassword] = useState(false); -const mapDispatchToProps = { - updatePassword, - updateCompetitionName, - updateFileZipBlob, -}; - -const EntryInterface = connect( - mapStateToProps, - mapDispatchToProps -)( - class EntryInterface extends Component { - constructor(props) { - super(props); + const editingDisabled = useSelector((state) => state.editingDisabled); + const password = useSelector((state) => state.password); + const competitionName = useSelector((state) => state.wcif.name); + const generatingScrambles = useSelector( + (state) => state.generatingScrambles + ); - this.state = { - editingDisabled: props.editingDisabled, - password: "", - showPassword: false, - }; - } + const dispatch = useDispatch(); - componentDidMount() { - this.props.updateCompetitionName(getDefaultCompetitionName()); - } + const handleCompetitionNameChange = (event) => { + dispatch(updateCompetitionName(event.target.value)); - handleCompetitionNameChange = (event) => { - this.props.updateCompetitionName(event.target.value); + // Require another zip with the new name. + dispatch(updateFileZipBlob(null)); + }; - // Require another zip with the new name. - this.props.updateFileZipBlob(null); - }; + const handlePasswordChange = (evt) => { + dispatch(updatePassword(evt.target.value)); - handlePasswordChange = (event) => { - let state = this.state; - state.password = event.target.value; - this.setState(state); + // Require another zip with the new password, in case there was a zip generated. + dispatch(updateFileZipBlob(null)); + }; - this.props.updatePassword(this.state.password); + return ( + <> +
+ + +
- // Require another zip with the new password, in case there was a zip generated. - this.props.updateFileZipBlob(null); - }; - - toogleShowPassword = () => { - let state = this.state; - state.showPassword = !state.showPassword; - this.setState(state); - }; - - render() { - let competitionName = this.props.competitionName; - let disabled = this.props.editingDisabled; - return ( - -
- - +
+ +
+ +
setShowPassword(!showPassword)} + > + + {showPassword ? : } +
- -
- -
- -
- - {this.state.showPassword ? ( - - ) : ( - - )} - -
-
-
- - ); - } - } -); +
+
+ + ); +}; export default EntryInterface; diff --git a/tnoodle-ui/src/main/components/EventPicker.jsx b/tnoodle-ui/src/main/components/EventPicker.jsx index 9a80ec807..572bf2a6b 100644 --- a/tnoodle-ui/src/main/components/EventPicker.jsx +++ b/tnoodle-ui/src/main/components/EventPicker.jsx @@ -1,5 +1,4 @@ -import React, { Component } from "react"; -import { connect } from "react-redux"; +import React from "react"; import { MAX_WCA_ROUNDS } from "../constants/wca.constants"; import { updateWcaEvent, updateFileZipBlob } from "../redux/ActionCreators"; import { @@ -10,298 +9,249 @@ import MbldDetail from "./MbldDetail"; import FmcTranslationsDetail from "./FmcTranslationsDetail"; import "./EventPicker.css"; import { ProgressBar } from "react-bootstrap"; +import { useDispatch, useSelector } from "react-redux"; -const mapStateToProps = (store) => ({ - editingDisabled: store.editingDisabled, - wcif: store.wcif, - wcaFormats: store.wcaFormats, - generatingScrambles: store.generatingScrambles, - scramblingProgressTarget: store.scramblingProgressTarget, - scramblingProgressCurrent: store.scramblingProgressCurrent, - fileZipBlob: store.fileZipBlob, -}); +const EventPicker = ({ event, wcifEvent }) => { + const wcaFormats = useSelector((state) => state.wcaFormats); + const editingDisabled = useSelector((state) => state.editingDisabled); + const generatingScrambles = useSelector( + (state) => state.generatingScrambles + ); + const scramblingProgressCurrent = useSelector( + (state) => state.scramblingProgressCurrent + ); + const scramblingProgressTarget = useSelector( + (state) => state.scramblingProgressTarget + ); -const mapDispatchToProps = { - updateWcaEvent, - updateFileZipBlob, -}; - -const EventPicker = connect( - mapStateToProps, - mapDispatchToProps -)( - class extends Component { - getWcaEvent = (rounds) => { - return { id: this.props.event.id, rounds }; - }; - - handleNumberOfRoundsChange = (numberOfRounds, rounds) => { - // Ajust the number of rounds in case we have to remove - while (rounds.length > numberOfRounds) { - rounds.pop(); - } - - // case we have to add - let eventId = this.props.event.id; - while (rounds.length < numberOfRounds) { - rounds.push({ - id: eventId + "-r" + (rounds.length + 1), - format: this.props.event.format_ids[0], - scrambleSetCount: 1, - extensions: [getDefaultCopiesExtension()], - }); - } - let wcaEvent = this.getWcaEvent(rounds); - this.updateEvent(wcaEvent); - }; - - handleNumberOfScrambleSetsChange = (round, value, rounds) => { - rounds[round].scrambleSetCount = value; - let wcaEvent = this.getWcaEvent(rounds); - this.updateEvent(wcaEvent); - }; - - handleRoundFormatChanged = (round, value, rounds) => { - rounds[round].format = value; - let wcaEvent = this.getWcaEvent(rounds); - this.updateEvent(wcaEvent); - }; - - handleNumberOfCopiesChange = (round, value, rounds) => { - rounds[round].extensions.find( - (extension) => extension.id === copiesExtensionId - ).data.numCopies = value; - let wcaEvent = this.getWcaEvent(rounds); - this.updateEvent(wcaEvent); - }; + const dispatch = useDispatch(); - abbreviate = (str) => { - if (this.props.wcaFormats != null) { - return this.props.wcaFormats[str].shortName; - } - return "-"; - }; + const updateEvent = (rounds) => { + let wcaEvent = { id: event.id, rounds }; + dispatch(updateFileZipBlob(null)); + dispatch(updateWcaEvent(wcaEvent)); + }; - updateEvent = (wcaEvent) => { - this.props.updateFileZipBlob(null); - this.props.updateWcaEvent(wcaEvent); - }; - - maybeShowTableTitles = (rounds) => { - if (rounds.length === 0) { - return null; - } - return ( - - # - Format - Scramble Sets - Copies - - ); - }; - - maybeShowTableBody = (rounds) => { - if (rounds.length === 0) { - return; - } - - return ( - - {Array.from({ length: rounds.length }, (_, i) => { - let copies = rounds[i].extensions.find( - (extension) => extension.id === copiesExtensionId - ).data.numCopies; - return ( - - - {i + 1} - - - - - - - this.handleNumberOfScrambleSetsChange( - i, - evt.target.value, - rounds - ) - } - min={1} - required - disabled={ - this.props.editingDisabled - ? "disabled" - : "" - } - /> - - - - this.handleNumberOfCopiesChange( - i, - evt.target.value, - rounds - ) - } - min={1} - required - disabled={ - this.props.generatingScrambles - } - /> - - - ); - })} - - ); - }; - - maybeShowProgressBar = (rounds) => { - let eventId = this.props.event.id; + const handleNumberOfRoundsChange = (numberOfRounds, rounds) => { + // Ajust the number of rounds in case we have to remove + while (rounds.length > numberOfRounds) { + rounds.pop(); + } - let current = this.props.scramblingProgressCurrent[eventId] || 0; - let target = this.props.scramblingProgressTarget[eventId]; + // case we have to add + while (rounds.length < numberOfRounds) { + rounds.push({ + id: event.id + "-r" + (rounds.length + 1), + format: event.format_ids[0], + scrambleSetCount: 1, + extensions: [getDefaultCopiesExtension()], + }); + } + updateEvent(rounds); + }; - if ( - rounds.length === 0 || - !this.props.generatingScrambles || - target === undefined - ) { - return; - } + const handleGeneralRoundChange = (round, value, rounds, name) => { + rounds[round][name] = value; + updateEvent(rounds); + }; - let progress = (current / target) * 100; - let miniThreshold = 2; + const handleNumberOfCopiesChange = (round, value, rounds) => { + rounds[round].extensions.find( + (extension) => extension.id === copiesExtensionId + ).data.numCopies = value; + updateEvent(rounds); + }; - if (progress === 0) { - progress = miniThreshold; - } + const abbreviate = (str) => + !!wcaFormats ? wcaFormats[str].shortName : "-"; - return ( - - ); - }; + const maybeShowTableTitles = (rounds) => { + if (rounds.length === 0) { + return null; + } + return ( + + # + Format + Scramble Sets + Copies + + ); + }; - render() { - let wcaEvent = this.props.wcifEvent; - let rounds = wcaEvent != null ? wcaEvent.rounds : []; + const maybeShowTableBody = (rounds) => { + if (rounds.length === 0) { + return; + } - return ( - - - - - - + {Array.from({ length: rounds.length }, (_, i) => { + let copies = rounds[i].extensions.find( + (extension) => extension.id === copiesExtensionId + ).data.numCopies; + return ( + + - + + - {this.maybeShowTableTitles(rounds)} - - {this.maybeShowTableBody(rounds)} - {this.props.event.is_multiple_blindfolded && - rounds.length > 0 && } - {this.props.event.is_fewest_moves && rounds.length > 0 && ( - - )} -
- TNoodle logo - -
- {this.props.event.name} -
- {this.maybeShowProgressBar(rounds)} + return ( +
+ {i + 1} - + - + + + handleGeneralRoundChange( + i, + evt.target.value, + rounds, + "scrambleSetCount" + ) + } + min={1} + required + disabled={ + editingDisabled || generatingScrambles + } + /> + + + handleNumberOfCopiesChange( + i, + evt.target.value, + rounds + ) + } + min={1} + required + disabled={generatingScrambles} + /> +
- ); + ); + })} + + ); + }; + + const maybeShowProgressBar = (rounds) => { + let eventId = event.id; + + let current = scramblingProgressCurrent[eventId] || 0; + let target = scramblingProgressTarget[eventId]; + + if (rounds.length === 0 || !generatingScrambles || !target) { + return; } - } -); + + let progress = (current / target) * 100; + let miniThreshold = 2; + + if (progress === 0) { + progress = miniThreshold; + } + + return ( + + ); + }; + + let rounds = !!wcifEvent ? wcifEvent.rounds : []; + + return ( + + + + + + + + + {maybeShowTableTitles(rounds)} + + {maybeShowTableBody(rounds)} + {event.is_multiple_blindfolded && rounds.length > 0 && ( + + )} + {event.is_fewest_moves && rounds.length > 0 && ( + + )} +
+ TNoodle logo + +
{event.name}
+ {maybeShowProgressBar(rounds)} +
+ + +
+ ); +}; export default EventPicker; diff --git a/tnoodle-ui/src/main/components/EventPickerTable.jsx b/tnoodle-ui/src/main/components/EventPickerTable.jsx index 399d904d4..1470cf4b4 100644 --- a/tnoodle-ui/src/main/components/EventPickerTable.jsx +++ b/tnoodle-ui/src/main/components/EventPickerTable.jsx @@ -1,6 +1,5 @@ -import React, { Component } from "react"; +import React, { useEffect } from "react"; import _ from "lodash"; -import { connect } from "react-redux"; import { fetchAvailableFmcTranslations, fetchFormats, @@ -13,147 +12,109 @@ import { setWcaEvents, } from "../redux/ActionCreators"; import EventPicker from "./EventPicker"; +import { useDispatch, useSelector } from "react-redux"; -const mapStateToProps = (store) => ({ - wcif: store.wcif, - editingDisabled: store.editingDisabled, - competitionId: store.competitionId, - translations: store.translations, - wcaEvents: store.wcaEvents, -}); - -const mapDispatchToProps = { - updateTranslations, - setWcaFormats, - setWcaEvents, -}; - -const BOOTSTRAP_GRID = 12; const EVENTS_PER_LINE = 2; -const EventPickerTable = connect( - mapStateToProps, - mapDispatchToProps -)( - class extends Component { - componentDidMount() { - this.getFormats(); - this.getWcaEvents(); - this.getFmcTranslations(); - } - - getFormats = () => { - fetchFormats().then((response) => { - this.props.setWcaFormats(response); - }); - }; - - getWcaEvents = () => { - fetchWcaEvents().then((response) => { - this.props.setWcaEvents(response); - }); - }; +const EventPickerTable = () => { + const competitionId = useSelector((state) => state.competitionId); + const wcif = useSelector((state) => state.wcif); + const wcaEvents = useSelector((state) => state.wcaEvents); + const editingDisabled = useSelector((state) => state.editingDisabled); - getFmcTranslations = () => { - fetchAvailableFmcTranslations().then((availableTranslations) => { - if (!availableTranslations) { - return; - } - let translations = Object.keys(availableTranslations).map( - (translationId) => ({ - id: translationId, - display: availableTranslations[translationId], - status: true, - }) - ); - this.props.updateTranslations(translations); - }); - }; + const dispatch = useDispatch(); - maybeShowEditWarning = () => { - if (this.props.competitionId == null) { + const getFmcTranslations = () => { + fetchAvailableFmcTranslations().then((availableTranslations) => { + if (!availableTranslations) { return; } - return ( -
-
-

- Found {this.props.wcif.events.length} event - {this.props.wcif.events.length > 1 - ? "s" - : ""} for {this.props.wcif.name}. -

-

- You can view and change the rounds over on{" "} - - the WCA. - - - {" "} - Refresh this page after making any changes on - the WCA website. - -

-
-
+ let translations = Object.keys(availableTranslations).map( + (translationId) => ({ + id: translationId, + display: availableTranslations[translationId], + status: true, + }) ); - }; + dispatch(updateTranslations(translations)); + }); + }; - render() { - // Prevent from remembering previous order - let wcaEvents = this.props.wcaEvents; - if (wcaEvents == null) { - return null; - } + const fetchInformation = () => { + fetchFormats().then((response) => dispatch(setWcaFormats(response))); + fetchWcaEvents().then((response) => dispatch(setWcaEvents(response))); + getFmcTranslations(); + }; - let events = this.props.wcif.events; - let editingDisabled = this.props.editingDisabled; + useEffect(fetchInformation, []); - // This filters events to show only those in the competition. - if (editingDisabled) { - wcaEvents = wcaEvents.filter((wcaEvent) => - events.find((item) => item.id === wcaEvent.id) - ); - } - - let eventChunks = _.chunk(wcaEvents, EVENTS_PER_LINE); - - let classColPerEvent = ` col-md-${ - BOOTSTRAP_GRID / EVENTS_PER_LINE - }`; - return ( -
- {this.maybeShowEditWarning()} - {eventChunks.map((chunk, i) => { - return ( -
- {chunk.map((event) => { - return ( -
- - item.id === event.id - )} - /> -
- ); - })} -
- ); - })} -
- ); + const maybeShowEditWarning = () => { + if (!competitionId) { + return; } + return ( +
+
+

+ Found {wcif.events.length} event + {wcif.events.length > 1 ? "s" : ""} for {wcif.name}. +

+

+ You can view and change the rounds over on + + the WCA. + + + Refresh this page after making any changes on the + WCA website. + +

+
+
+ ); + }; + + // Prevent from remembering previous order + if (!wcaEvents) { + return null; } -); + + // This filters events to show only those in the competition. + let filteredEvents = wcaEvents.filter( + (wcaEvent) => + !editingDisabled || + wcif.events.find((item) => item.id === wcaEvent.id) + ); + + let eventChunks = _.chunk(filteredEvents, EVENTS_PER_LINE); + + return ( +
+ {maybeShowEditWarning()} + {eventChunks.map((chunk, i) => { + return ( +
+ {chunk.map((event) => { + return ( +
+ item.id === event.id + )} + /> +
+ ); + })} +
+ ); + })} +
+ ); +}; export default EventPickerTable; diff --git a/tnoodle-ui/src/main/components/FmcTranslationsDetail.jsx b/tnoodle-ui/src/main/components/FmcTranslationsDetail.jsx index 007e1d079..bc627a1d5 100644 --- a/tnoodle-ui/src/main/components/FmcTranslationsDetail.jsx +++ b/tnoodle-ui/src/main/components/FmcTranslationsDetail.jsx @@ -1,5 +1,4 @@ -import React, { Component } from "react"; -import { connect } from "react-redux"; +import React, { useState } from "react"; import _ from "lodash"; import { updateFileZipBlob, @@ -9,203 +8,144 @@ import { setSuggestedFmcTranslations, } from "../redux/ActionCreators"; import "./FmcTranslationsDetail.css"; +import { useDispatch, useSelector } from "react-redux"; const TRANSLATIONS_PER_LINE = 3; -const mapStateToProps = (store) => ({ - translations: store.translations, - suggestedFmcTranslations: store.suggestedFmcTranslations, - generatingScrambles: store.generatingScrambles, -}); +const FmcTranslationsDetail = () => { + const [showTranslations, setShowTranslations] = useState(false); -const mapDispatchToProps = { - updateFileZipBlob, - updateTranslation, - selectAllTranslations, - resetTranslations, - setSuggestedFmcTranslations, -}; + const suggestedFmcTranslations = useSelector( + (state) => state.suggestedFmcTranslations + ); + const translations = useSelector((state) => state.translations); + const generatingScrambles = useSelector( + (state) => state.generatingScrambles + ); -const FmcTranslationsDetail = connect( - mapStateToProps, - mapDispatchToProps -)( - class extends Component { - constructor() { - super(); - this.state = { showTranslations: false }; - } + const dispatch = useDispatch(); - handleTranslation = (id, status) => { - this.props.updateFileZipBlob(null); - this.props.updateTranslation(id, status); - }; + const handleTranslation = (id, status) => { + dispatch(updateFileZipBlob(null)); + dispatch(updateTranslation(id, status)); + }; - selectAllTranslations = () => { - this.props.updateFileZipBlob(null); - this.props.selectAllTranslations(); - }; + const handleSelectAllTranslations = () => { + dispatch(updateFileZipBlob(null)); + dispatch(selectAllTranslations()); + }; - selectNoneTranslation = () => { - this.props.updateFileZipBlob(null); - this.props.resetTranslations(); - }; + const selectNoneTranslation = () => { + dispatch(updateFileZipBlob(null)); + dispatch(resetTranslations()); + }; - selectSuggestedTranslations = () => { - this.props.updateFileZipBlob(null); - this.props.setSuggestedFmcTranslations( - this.props.suggestedFmcTranslations - ); - }; + const selectSuggestedTranslations = () => { + dispatch(updateFileZipBlob(null)); + dispatch(setSuggestedFmcTranslations(suggestedFmcTranslations)); + }; - toggleTranslations = () => { - this.setState({ - ...this.state, - showTranslations: !this.state.showTranslations, - }); - }; - - maybeShowFmcTranslationsDetails = () => { - if ( - !this.state.showTranslations || - this.props.translations == null - ) { - return; - } - let translations = _.chunk( - this.props.translations, - TRANSLATIONS_PER_LINE - ); + const translationsDetail = () => { + let translationsChunks = _.chunk(translations, TRANSLATIONS_PER_LINE); + return translationsChunks.map((translationsChunk, i) => ( + + {translationsChunk.map((translation, j) => { + let checkboxId = `fmc-${translation.id}`; + return ( + + + + + + + handleTranslation( + translation.id, + e.target.checked + ) + } + /> + + {j < TRANSLATIONS_PER_LINE - 1 && } + + ); + })} + + )); + }; - return ( - + if (!translations) { + return null; + } + return ( + + + + + + + {showTranslations && ( + <> - - - {this.props.suggestedFmcTranslations != null && ( +
- )} + + {!!suggestedFmcTranslations && ( + + )} +
- - - {translations.map( - (translationsChunk, i) => ( - - {translationsChunk.map( - (translation, j) => { - let checkboxId = `fmc-${translation.id}`; - return ( - - - - {j < - TRANSLATIONS_PER_LINE - - 1 && ( - - ) - )} - + {translationsDetail()}
- - - - this.handleTranslation( - translation.id, - e - .target - .checked - ) - } - /> - - )} - - ); - } - )} -
-
- ); - }; - - render() { - if (this.props.translations == null) { - return null; - } - return ( - - - - - - - {this.maybeShowFmcTranslationsDetails()} - - ); - } - } -); + + )} + + ); +}; export default FmcTranslationsDetail; diff --git a/tnoodle-ui/src/main/components/MbldDetail.jsx b/tnoodle-ui/src/main/components/MbldDetail.jsx index 5c6c5ddff..d89352c1b 100644 --- a/tnoodle-ui/src/main/components/MbldDetail.jsx +++ b/tnoodle-ui/src/main/components/MbldDetail.jsx @@ -1,76 +1,49 @@ -import React, { Component } from "react"; -import { connect } from "react-redux"; +import React from "react"; +import { useSelector, useDispatch } from "react-redux"; import { MBLD_MIN } from "../constants/wca.constants"; import { updateMbld, updateFileZipBlob } from "../redux/ActionCreators"; -const mapStateToProps = (store) => ({ - mbld: store.mbld, - bestMbldAttempt: store.bestMbldAttempt, - generatingScrambles: store.generatingScrambles, -}); - -const mapDispatchToProps = { - updateMbld, - updateFileZipBlob, +const MbldDetail = () => { + const mbld = useSelector((state) => state.mbld); + const bestMbldAttempt = useSelector((state) => state.bestMbldAttempt); + const generatingScrambles = useSelector( + (state) => state.generatingScrambles + ); + + const dispatch = useDispatch(); + + const handleMbldChange = (evt) => { + dispatch(updateMbld(evt.target.value)); + dispatch(updateFileZipBlob(null)); + }; + + return ( + + + +

Select the number of scrambles

+ + + + + + {mbld < bestMbldAttempt && ( + + + {`You selected ${mbld} cubes for Multi-Blind, but there's a competitor who already tried ${bestMbldAttempt} at a competition. Proceed if you are really certain of it.`} + + + )} + + ); }; -const MbldDetail = connect( - mapStateToProps, - mapDispatchToProps -)( - class extends Component { - handleMbldChange = (mbld) => { - this.props.updateMbld(mbld); - }; - - showMbldWarning = () => { - if (!this.props.mbld) { - return null; - } - - let bestMbldAttempt = this.props.bestMbldAttempt; - let showMbldWarning = - bestMbldAttempt != null && this.props.mbld < bestMbldAttempt; - - if (showMbldWarning) { - return ( - - - {`You selected ${this.props.mbld} cubes for Multi-Blind, but there's a competitor who already tried ${this.props.bestMbldAttempt} at a competition. Proceed if you are really certain of it.`} - - - ); - } - }; - - render() { - return ( - - - -

- Select the number of scrambles -

- - - - this.handleMbldChange(evt.target.value) - } - min={MBLD_MIN} - required - disabled={this.props.generatingScrambles} - /> - - - {this.showMbldWarning()} - - ); - } - } -); - export default MbldDetail; diff --git a/tnoodle-ui/src/main/components/SideBar.jsx b/tnoodle-ui/src/main/components/SideBar.jsx index fb0a37326..c654a4633 100644 --- a/tnoodle-ui/src/main/components/SideBar.jsx +++ b/tnoodle-ui/src/main/components/SideBar.jsx @@ -1,6 +1,6 @@ -import React, { Component } from "react"; -import { connect } from "react-redux"; +import React, { useEffect, useState } from "react"; import Loading from "./Loading"; +import { Collapse } from "react-bootstrap"; import { updateWcif, updateEditingStatus, @@ -11,7 +11,6 @@ import { updateFileZipBlob, addCachedObject, addSuggestedFmcTranslations, - setSuggestedFmcTranslations, setBestMbldAttempt, } from "../redux/ActionCreators"; import { defaultWcif } from "../constants/default.wcif"; @@ -23,377 +22,289 @@ import { getUpcomingManageableCompetitions, getCompetitionJson, } from "../api/wca.api"; -import { getQueryParameter } from "../util/query.param.util"; +import { + getQueryParameter, + removeQueryParam, + updateQueryParam, +} from "../util/query.param.util"; import { fetchSuggestedFmcTranslations, fetchBestMbldAttempt, } from "../api/tnoodle.api"; import { getDefaultCompetitionName } from "../util/competition.name.util"; import "./SideBar.css"; +import { useSelector, useDispatch } from "react-redux"; -const mapStateToProps = (store) => ({ - me: store.me, - competitions: store.competitions, - cachedObjects: store.cachedObjects, - generatingScrambles: store.generatingScrambles, -}); - -const mapDispatchToProps = { - updateWcif, - updateEditingStatus, - updateCompetitionName, - updateCompetitions, - updateMe, - updateCompetitionId, - updateFileZipBlob, - addCachedObject, - addSuggestedFmcTranslations, - setSuggestedFmcTranslations, - setBestMbldAttempt, -}; +const SideBar = () => { + const [loadingUser, setLoadingUser] = useState(false); + const [loadingCompetitions, setLoadingCompetitions] = useState(false); + const [loadingCompetitionInfo, setLoadingCompetitionInfo] = useState(false); -const SideBar = connect( - mapStateToProps, - mapDispatchToProps -)( - class extends Component { - constructor(props) { - super(props); + const me = useSelector((state) => state.me); + const competitions = useSelector((state) => state.competitions); + const cachedObjects = useSelector((state) => state.cachedObjects); + const generatingScrambles = useSelector( + (state) => state.generatingScrambles + ); + const [isOpen, setIsOpen] = useState(true); - this.state = { - me: props.me, - competitions: props.competitions, - loadingUser: false, - loadingCompetitions: false, - loadingCompetitionInformation: false, - competitionId: null, - }; - } - margin = 1; // Margin for login button and "Manual Selection" + const dispatch = useDispatch(); - componentDidMount() { - if (this.state.me == null && isLogged()) { - this.setLoadingUser(true); - fetchMe() - .then((me) => { - this.setState({ ...this.state, me }); - this.props.updateMe(me); - this.setLoadingUser(false); - }) - .catch((e) => { - console.error( - "Could not get information about the logged user", - e - ); - this.setLoadingUser(false); - }); - } + const handleIsOpen = () => setIsOpen(window.innerWidth > 992); - if (this.state.competitions == null && isLogged()) { - this.setLoadingCompetitions(true); - getUpcomingManageableCompetitions() - .then((competitions) => { - this.setState({ ...this.state, competitions }); - this.props.updateCompetitions(competitions); - this.setLoadingCompetitions(false); - }) - .catch((e) => { - console.error("Could not get upcoming competitions", e); - this.setLoadingCompetitions(false); - }); - } + const init = () => { + window.addEventListener("resize", handleIsOpen); - let competitionId = getQueryParameter("competitionId"); - if (competitionId != null) { - this.handleCompetitionSelection(competitionId); - } + if (!isLogged()) { + return; + } + if (!me) { + setLoadingUser(true); + fetchMe() + .then((me) => { + dispatch(updateMe(me)); + }) + .finally(() => setLoadingUser(false)); } - setLoadingUser = (flag) => { - this.setState({ ...this.state, loadingUser: flag }); - }; - - setLoadingCompetitions = (flag) => { - this.setState({ ...this.state, loadingCompetitions: flag }); - }; - - setLoadingCompetitionInformation = (flag) => { - this.setState({ - ...this.state, - loadingCompetitionInformation: flag, - }); - }; - - pluralize = (string, number) => { - return string + (number > 1 ? "s" : ""); - }; - - currentLocationWithoutQuery = () => { - return window.location.origin + window.location.pathname; - }; - - setPageWithoutRedirect = (url) => { - window.history.pushState(null, "", url); - }; - - handleManualSelection = () => { - this.props.updateEditingStatus(false); - this.props.updateCompetitionId(null); - this.props.updateWcif({ ...defaultWcif }); - this.props.setBestMbldAttempt(null); - this.props.updateCompetitionName(getDefaultCompetitionName()); - this.props.updateFileZipBlob(null); - this.removeCompetitionIdQueryParam(); - }; - - handleCompetitionSelection = (competitionId) => { - this.updateCompetitionIdQueryParam(competitionId); - - // For quick switching between competitions. - let cachedObject = this.props.cachedObjects[competitionId]; - if (cachedObject != null) { - let cachedWcif = cachedObject.wcif; - this.setWcif(cachedWcif); - this.maybeAddCompetition(cachedWcif.id, cachedWcif.name); - - let cachedSuggestedFmcTranslations = - cachedObject.suggestedFmcTranslations; - this.props.addSuggestedFmcTranslations( - cachedSuggestedFmcTranslations - ); + if (!competitions) { + setLoadingCompetitions(true); + getUpcomingManageableCompetitions() + .then((competitions) => + dispatch(updateCompetitions(competitions)) + ) + .finally(() => setLoadingCompetitions(false)); + } - let cachedBestMbldAttempt = cachedObject.bestMbldAttempt; - this.props.setBestMbldAttempt(cachedBestMbldAttempt); - return; - } + let competitionId = getQueryParameter("competitionId"); + if (!!competitionId) { + handleCompetitionSelection(competitionId); + } + }; + useEffect(init, []); + + const pluralize = (string, number) => string + (number > 1 ? "s" : ""); + + const handleManualSelection = () => { + dispatch(updateEditingStatus(false)); + dispatch(updateCompetitionId(null)); + dispatch(updateWcif({ ...defaultWcif })); + dispatch(setBestMbldAttempt(null)); + dispatch(updateCompetitionName(getDefaultCompetitionName())); + dispatch(updateFileZipBlob(null)); + dispatch(addSuggestedFmcTranslations(null)); + + removeQueryParam("competitionId"); + }; + + const handleCompetitionSelection = (competitionId) => { + updateQueryParam("competitionId", competitionId); + + // For quick switching between competitions. + let cachedObject = cachedObjects[competitionId]; + if (!!cachedObject) { + let cachedWcif = cachedObject.wcif; + setWcif(cachedWcif); + maybeAddCompetition(cachedWcif.id, cachedWcif.name); + + let cachedSuggestedFmcTranslations = + cachedObject.suggestedFmcTranslations; + dispatch( + addSuggestedFmcTranslations(cachedSuggestedFmcTranslations) + ); - this.setState({ - ...this.state, - loadingCompetitionInformation: true, - competitionId, - }); + let cachedBestMbldAttempt = cachedObject.bestMbldAttempt; + dispatch(setBestMbldAttempt(cachedBestMbldAttempt)); + } else { + setLoadingCompetitionInfo(true); getCompetitionJson(competitionId) .then((wcif) => { - this.setWcif(wcif); - this.props.addCachedObject(competitionId, "wcif", wcif); - this.maybeAddCompetition(wcif.id, wcif.name); - - this.getAndCacheSuggestedFmcTranslations(wcif); - - this.getAndCacheBestMbldAttempt(wcif); + setWcif(wcif); + dispatch(addCachedObject(competitionId, "wcif", wcif)); + maybeAddCompetition(wcif.id, wcif.name); + getAndCacheSuggestedFmcTranslations(wcif); + getAndCacheBestMbldAttempt(wcif); }) - .catch((e) => { - console.error( - "Could not get information for " + competitionId, - e - ); - this.setLoadingCompetitionInformation(false); - }); - }; + .finally(() => setLoadingCompetitionInfo(false)); + } + }; - getAndCacheSuggestedFmcTranslations = (wcif) => { - fetchSuggestedFmcTranslations(wcif).then((translations) => { - this.props.addCachedObject( + const getAndCacheSuggestedFmcTranslations = (wcif) => { + fetchSuggestedFmcTranslations(wcif).then((translations) => { + dispatch( + addCachedObject( wcif.id, "suggestedFmcTranslations", translations - ); - this.props.addSuggestedFmcTranslations(translations); - }); - }; - - getAndCacheBestMbldAttempt = (wcif) => { - fetchBestMbldAttempt(wcif).then((bestAttempt) => { - if (!bestAttempt) { - return; - } - let attempted = bestAttempt.attempted; - this.props.addCachedObject( - wcif.id, - "bestMbldAttempt", - attempted - ); - this.props.setBestMbldAttempt(attempted); - }); - }; - - // In case we use competitionId from query params, it's not fetched. - // We add it to the list. - maybeAddCompetition = (competitionId, competitionName) => { - if (!this.state.competitions) { - return; - } - if ( - !this.state.competitions.find( - (competition) => competition.name === competitionName ) - ) { - this.setState({ - ...this.state, - competitions: [ - ...this.state.competitions, - { id: competitionId, name: competitionName }, - ], - }); - } - }; - - updateCompetitionIdQueryParam = (competitionId) => { - var searchParams = new URLSearchParams(window.location.search); - searchParams.set("competitionId", competitionId); - this.setPageWithoutRedirect( - this.currentLocationWithoutQuery() + - "?" + - searchParams.toString() ); - }; + dispatch(addSuggestedFmcTranslations(translations)); + }); + }; - removeCompetitionIdQueryParam = () => { - var searchParams = new URLSearchParams(window.location.search); - searchParams.delete("competitionId"); - this.setPageWithoutRedirect( - this.currentLocationWithoutQuery() + - "?" + - searchParams.toString() - ); - }; - - setWcif = (wcif) => { - this.setLoadingCompetitionInformation(false); - this.props.updateEditingStatus(true); - this.props.updateWcif(wcif); - this.props.updateCompetitionId(wcif.id); - this.props.updateCompetitionName(wcif.name); - this.props.updateFileZipBlob(null); - }; - - setSuggestedFmcTranslations = (suggestedFmcTranslations) => { - if (suggestedFmcTranslations != null) { - this.props.setSuggestedFmcTranslations( - suggestedFmcTranslations - ); + const getAndCacheBestMbldAttempt = (wcif) => { + fetchBestMbldAttempt(wcif).then((bestAttempt) => { + if (!bestAttempt) { + return; } - }; - - logInButton = () => { - return ( -
- - {this.state.me != null && ( -

- Welcome, {this.state.me.name}.{" "} - {this.state.competitions != null && - `You have ${ - this.state.competitions.length - } manageable ${this.pluralize( - " competition", - this.state.competitions.length - )} upcoming.`} -

- )} -
+ let attempted = bestAttempt.attempted; + dispatch(addCachedObject(wcif.id, "bestMbldAttempt", attempted)); + dispatch(setBestMbldAttempt(attempted)); + }); + }; + + // In case we use competitionId from query params, it's not fetched. + // We add it to the list. + const maybeAddCompetition = (competitionId, competitionName) => { + if (!competitions) { + return; + } + if ( + !competitions.find( + (competition) => competition.name === competitionName + ) + ) { + dispatch( + updateCompetitions([ + ...competitions, + { id: competitionId, name: competitionName }, + ]) ); - }; - - loadingArea = () => { - if (this.state.loadingUser) { - return ( -
- -

Loading user...

-
- ); - } - - if (this.state.loadingCompetitions) { - return ( -
- -

Loading competitions...

-
- ); - } + } + }; + + const setWcif = (wcif) => { + dispatch(updateEditingStatus(true)); + dispatch(updateWcif(wcif)); + dispatch(updateCompetitionId(wcif.id)); + dispatch(updateCompetitionName(wcif.name)); + dispatch(updateFileZipBlob(null)); + }; + + const logInButton = () => { + return ( +
+ + {!!me && ( +

+ Welcome, {me.name}. + {!!competitions && + ` You have ${ + competitions.length + } manageable ${pluralize( + " competition", + competitions.length + )} upcoming.`} +

+ )} +
+ ); + }; + + const loadingElement = (text) => ( +
+ +

{text}...

+
+ ); + + const loadingArea = () => { + if (loadingUser) { + return loadingElement("Loading user"); + } - if (this.state.loadingCompetitionInformation) { - return ( -
- -

- Loading information for {this.state.competitionId} - ... -

-
- ); - } - }; + if (loadingCompetitions) { + return loadingElement("Loading competitions"); + } - render() { - return ( -
- TNoodle logo -

- TNoodle -

+ if (loadingCompetitionInfo) { + return loadingElement("Loading competition information"); + } + }; + return ( +
+
+ TNoodle logo +

+ TNoodle +

+ +
+ +
  • - {(this.state.competitions != null && - this.state.competitions.length) > 0 && ( + {!!competitions && competitions.length > 0 && ( )}
  • - {this.state.competitions != null && - this.state.competitions.map( - (competition, i) => ( -
  • - -
  • - ) - )} + {!!competitions && + competitions.map((competition) => ( +
  • + +
  • + ))}
- - {this.loadingArea()} + {loadingArea()}
- {this.logInButton()} + {logInButton()}
- ); - } - } -); +
+
+ ); +}; export default SideBar; diff --git a/tnoodle-ui/src/main/components/VersionInfo.jsx b/tnoodle-ui/src/main/components/VersionInfo.jsx index 104319be8..5600e6188 100644 --- a/tnoodle-ui/src/main/components/VersionInfo.jsx +++ b/tnoodle-ui/src/main/components/VersionInfo.jsx @@ -1,148 +1,112 @@ -import React, { Component } from "react"; -import { connect } from "react-redux"; +import React, { useEffect, useState } from "react"; +import { useDispatch } from "react-redux"; import { fetchRunningVersion } from "../api/tnoodle.api"; import { fetchVersionInfo } from "../api/wca.api"; import { updateOfficialZipStatus } from "../redux/ActionCreators"; -const mapDispatchToProps = { - updateOfficialZipStatus, -}; - -const VersionInfo = connect( - null, - mapDispatchToProps -)( - class extends Component { - constructor(props) { - super(props); - this.state = { - currentTnoodle: null, - allowedTnoodleVersions: null, - runningVersion: null, - signedBuild: null, - signatureKeyBytes: null, - wcaPublicKeyBytes: null, - wcaResponse: false, - tnoodleResponse: false, - }; - } +const VersionInfo = () => { + const [currentTnoodle, setCurrentTnoodle] = useState(null); + const [allowedTnoodleVersions, setAllowedTnoodleVersions] = useState(null); + const [runningVersion, setRunningVersion] = useState(null); + const [signedBuild, setSignedBuild] = useState(null); + const [signatureKeyBytes, setSignatureKeyBytes] = useState(null); + const [wcaPublicKeyBytes, setWcaPublicKeyBytes] = useState(null); + const [signatureValid, setSignatureValid] = useState(true); - componentDidMount() { - // Fetch from WCA API. - fetchVersionInfo().then((response) => { - if (!response) { - return; - } - let { current, allowed, publicKeyBytes } = response; - this.setState({ - ...this.state, - currentTnoodle: current, - allowedTnoodleVersions: allowed, - wcaPublicKeyBytes: publicKeyBytes, - wcaResponse: true, - }); - this.analyzeVersion(); - }); + useEffect( + () => + setSignatureValid( + signedBuild && signatureKeyBytes === wcaPublicKeyBytes + ), + [signedBuild, signatureKeyBytes, wcaPublicKeyBytes] + ); - fetchRunningVersion().then((response) => { - if (!response) { - return; - } - let { - projectName, - projectVersion, - signedBuild, - signatureKeyBytes, - } = response; - this.setState({ - ...this.state, - // Running version is based on projectName and projectVersion - runningVersion: - projectName != null && projectVersion != null - ? `${projectName}-${projectVersion}` - : "", - signedBuild: signedBuild, - signatureKeyBytes: signatureKeyBytes, - tnoodleResponse: true, - }); - this.analyzeVersion(); - }); - } + const dispatch = useDispatch(); - signatureValid() { - return ( - this.state.signedBuild && - this.state.signatureKeyBytes === this.state.wcaPublicKeyBytes - ); - } + useEffect(() => { + // Fetch from WCA API. + fetchVersionInfo().then((response) => { + if (!response) { + return; + } + setCurrentTnoodle(response.current); + setAllowedTnoodleVersions(response.allowed); + setWcaPublicKeyBytes(response.publicKeyBytes); + }); - // This method avoids global state update while rendering - analyzeVersion() { - // We wait until both wca and tnoodle answers - if (!this.state.tnoodleResponse || !this.state.wcaResponse) { + fetchRunningVersion().then((response) => { + if (!response) { return; } - let runningVersion = this.state.runningVersion; - let allowedVersions = this.state.allowedTnoodleVersions; - let signedBuild = this.signatureValid(); + setRunningVersion( + !!response.projectName && !!response.projectVersion + ? `${response.projectName}-${response.projectVersion}` + : "" + ); + setSignedBuild(response.signedBuild); + setSignatureKeyBytes(response.signatureKeyBytes); + }); + }, []); - if (!signedBuild || !allowedVersions.includes(runningVersion)) { - this.props.updateOfficialZipStatus(false); - } + // This avoids global state update while rendering + const analyzeVerion = () => { + // We wait until both wca and tnoodle answers + if (!allowedTnoodleVersions || !runningVersion) { + return; } - render() { - let runningVersion = this.state.runningVersion; - let allowedVersions = this.state.allowedTnoodleVersions; - let currentTnoodle = this.state.currentTnoodle; - let signedBuild = this.signatureValid(); + dispatch( + updateOfficialZipStatus( + signatureValid && + allowedTnoodleVersions.includes(runningVersion) + ) + ); + }; + useEffect(analyzeVerion, [allowedTnoodleVersions, runningVersion]); - // We cannot analyze TNoodle version here. We do not bother the user. - if (!runningVersion || !allowedVersions) { - return null; - } - - // Running version is not the most recent release - if (runningVersion !== currentTnoodle.name) { - // Running version is allowed, but not the latest. - if (allowedVersions.includes(runningVersion)) { - return ( -
- You are running {runningVersion}, which is still - allowed, but you should upgrade to{" "} - {currentTnoodle.name} available{" "} - here as soon - as possible. -
- ); - } + // We cannot analyze TNoodle version here. We do not bother the user. + if (!runningVersion || !allowedTnoodleVersions) { + return null; + } - return ( -
- You are running {runningVersion}, which is not allowed. - Do not use scrambles generated in any official - competition and consider downloading the latest version{" "} - here. -
- ); - } + // Running version is not the most recent release + if (runningVersion !== currentTnoodle.name) { + // Running version is allowed, but not the latest. + if (allowedTnoodleVersions.includes(runningVersion)) { + return ( +
+ You are running {runningVersion}, which is still allowed, + but you should upgrade to {currentTnoodle.name} available{" "} + here as soon as + possible. +
+ ); + } - // Generated version is not an officially signed jar - if (!signedBuild) { - return ( -
- You are running an unsigned TNoodle release. Do not use - scrambles generated in any official competition and - consider downloading the official program{" "} - here -
- ); - } + return ( +
+ You are running {runningVersion}, which is not allowed. Do not + use scrambles generated in any official competition and consider + downloading the latest version{" "} + here. +
+ ); + } - return null; - } + // Generated version is not an officially signed jar + if (!signatureValid) { + return ( +
+ You are running an unsigned TNoodle release. Do not use + scrambles generated in any official competition and consider + downloading the official program{" "} + here +
+ ); } -); + + return null; +}; export default VersionInfo; diff --git a/tnoodle-ui/src/main/constants/default.wcif.js b/tnoodle-ui/src/main/constants/default.wcif.js index 2a3ae9d5e..51b6020f8 100644 --- a/tnoodle-ui/src/main/constants/default.wcif.js +++ b/tnoodle-ui/src/main/constants/default.wcif.js @@ -1,4 +1,5 @@ import { getDefaultCopiesExtension } from "../helper/wcif.helper"; +import { getDefaultCompetitionName } from "../util/competition.name.util"; // Add 1 round of 3x3x3 let default333 = { @@ -13,10 +14,11 @@ let default333 = { ], }; +let name = getDefaultCompetitionName(); export const defaultWcif = { formatVersion: "1.0", - name: "", - shortName: "", + name, + shortName: name, id: "", events: [default333], persons: [], diff --git a/tnoodle-ui/src/main/redux/Reducers.js b/tnoodle-ui/src/main/redux/Reducers.js index b1bc683d2..f13c7e5f5 100644 --- a/tnoodle-ui/src/main/redux/Reducers.js +++ b/tnoodle-ui/src/main/redux/Reducers.js @@ -8,10 +8,10 @@ const defaultStore = { wcif: defaultWcif, mbld: MBLD_DEFAULT, password: "", - editingDisabled: false, + editingDisabled: false, // If we fetch competition info, some fields can't be changed officialZip: true, fileZipBlob: null, - generatingScrambles: null, + generatingScrambles: false, scramblingProgressTarget: {}, scramblingProgressCurrent: {}, cachedObjects: {}, diff --git a/tnoodle-ui/src/main/util/query.param.util.js b/tnoodle-ui/src/main/util/query.param.util.js index 10bc05b64..901d6cddd 100644 --- a/tnoodle-ui/src/main/util/query.param.util.js +++ b/tnoodle-ui/src/main/util/query.param.util.js @@ -24,3 +24,24 @@ function parseQueryString(query) { return params; }, {}); } + +const setPageWithoutRedirect = (url) => window.history.pushState(null, "", url); + +const currentLocationWithoutQuery = () => + window.location.origin + window.location.pathname; + +export const updateQueryParam = (name, value) => { + var searchParams = new URLSearchParams(window.location.search); + searchParams.set(name, value); + setPageWithoutRedirect( + currentLocationWithoutQuery() + "?" + searchParams.toString() + ); +}; + +export const removeQueryParam = (name) => { + var searchParams = new URLSearchParams(window.location.search); + searchParams.delete(name); + setPageWithoutRedirect( + currentLocationWithoutQuery() + "?" + searchParams.toString() + ); +}; diff --git a/tnoodle-ui/src/test/EventPicker.test.js b/tnoodle-ui/src/test/EventPicker.test.js index 793ad5542..f0190c870 100644 --- a/tnoodle-ui/src/test/EventPicker.test.js +++ b/tnoodle-ui/src/test/EventPicker.test.js @@ -62,14 +62,22 @@ it("Changing values from event", () => { numberOfRounds ); + // There's a harmless change of type here. 1 -> "1" + // Initial value should be 1 + expect(store.getState().wcif.events[0].rounds[0].scrambleSetCount).toBe(1); + + // This should be numberOfRounds * 2, since each round has 2 inputs. + // It's not, probably because not updating DOM after dispatching const inputs = Array.from(container.querySelectorAll("input")); + let roundToChange = 0; + let value = "10"; + // Change last scramble sets to 10 - fireEvent.change(inputs[inputs.length - 2], { target: { value: 10 } }); + fireEvent.change(inputs[roundToChange * 2], { target: { value } }); expect( - store.getState().wcif.events[0].rounds[numberOfRounds - 1] - .scrambleSetCount - ).toEqual("10"); + store.getState().wcif.events[0].rounds[roundToChange].scrambleSetCount + ).toEqual(value); // Remove 1 round numberOfRounds--; @@ -77,10 +85,6 @@ it("Changing values from event", () => { expect(store.getState().wcif.events[0].rounds.length).toEqual( numberOfRounds ); - expect( - store.getState().wcif.events[0].rounds[numberOfRounds - 1] - .scrambleSetCount - ).not.toEqual("10"); const scrambleSets = inputs[0]; const copies = inputs[1]; @@ -89,11 +93,6 @@ it("Changing values from event", () => { expect(scrambleSets.disabled).toBe(false); expect(copies.disabled).toBe(false); - // There's a harmless change of type here. 1 -> "1" - - // Initial value should be 1 - expect(store.getState().wcif.events[0].rounds[0].scrambleSetCount).toBe(1); - // Changes to scrambleSet should go to the store const newScrambleSets = "3"; fireEvent.change(scrambleSets, { target: { value: newScrambleSets } }); diff --git a/tnoodle-ui/src/test/SideBar.test.js b/tnoodle-ui/src/test/SideBar.test.js index 92b3433d6..611c9ce77 100644 --- a/tnoodle-ui/src/test/SideBar.test.js +++ b/tnoodle-ui/src/test/SideBar.test.js @@ -49,15 +49,20 @@ it("Each competition fetched from the website must become a button", async () => const buttons = Array.from(container.querySelectorAll("button")); - // First button should be manual selection - expect(buttons[0].innerHTML).toBe("Manual Selection"); + // First button should be the collapse button + expect(buttons[0].innerHTML).toBe( + `` + ); + + // Second button should be manual selection + expect(buttons[1].innerHTML).toBe("Manual Selection"); // Last button should be Log Out expect(buttons[buttons.length - 1].innerHTML).toBe("Log Out"); // Each competition must have a button for (let i = 0; i < competitions.length; i++) { - expect(competitions[i].name).toBe(buttons[i + 1].innerHTML); + expect(competitions[i].name).toBe(buttons[i + 2].innerHTML); } // We should welcome the user