diff --git a/truthseeker/static/css/game_ui_lobby.css b/truthseeker/static/css/game_ui_lobby.css new file mode 100644 index 0000000..6a9c59d --- /dev/null +++ b/truthseeker/static/css/game_ui_lobby.css @@ -0,0 +1,216 @@ +/* Global properties */ +html { + background-color: black; + color: white; +} + +:root { + --button-background-color: #FF0000; +} + +.multi_player_mode_choice_title, .multi_player_mode_waiting_for_host, .player_name, .players_title, .rounds_count_title, .room_code_text_title, .room_title { + font-family: "Titan One", sans-serif; + margin-bottom: 0.5em; + margin-top: 0.5em; +} + +.join_room_view, .room_view { + height: calc(100vh - var(--body-margin) * 2); +} + +/* Action buttons */ +.action_button { + border-color: black; + border-style: solid; + border-width: 0.125em; + background-color: var(--button-background-color); + border-radius: var(--button-and-dialog-border-radius); + color: white; + cursor: pointer; + font-family: "Titan One", sans-serif; + margin-left: auto; + margin-right: auto; + padding-top: 0.5em; + padding-bottom: 0.5em; + padding-left: 1em; + padding-right: 1em; + text-transform: uppercase; + overflow: hidden; + transition: box-shadow 0.5s, transform 0.5s; +} + +.action_button:hover { + transform: translate(0.1em, 0.1em); + box-shadow: 10px 10px 0px 0px black; +} + +.multi_player_mode_choice .action_button, .room_code_text .action_button { + font-size: 1.5em; + min-width: 10em; +} + +/* Room view major elements */ +.room_title { + color: var(--button-background-color); + font-family: "Spicy Rice", sans-serif; + font-weight: bold; + font-size: 4em; + margin: 0.25em; +} + +.room_view_container { + align-items: center; + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: space-around; +} + +/* Room code */ +.room_code { + color: var(--button-background-color); + text-decoration: none; +} + +.room_code_text { + align-items: center; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + justify-content: center; +} + +.room_code_text_title { + font-size: 2em; + margin: 0.25em; +} + +#invite_friends_button { + font-size: 1em; + text-transform: none; + background-color: #c2c0c0; + border-radius: 0.5em; +} + +#invite_friends_button:hover { + transform: translate(0.1em, 0.1em); + box-shadow: 10px 10px 0px 0px black; +} + +/* Waiting for host */ +.multi_player_mode_waiting_for_host { + font-size: 2.5em; + max-width: 20em; + text-align: center; +} + +/* Multi-player mode choice */ +.multi_player_mode_choices { + padding: 1em; +} + +.multi_player_mode_choice { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + justify-content: center; + align-items: center; + text-align: center; +} + +.multi_player_mode_choice_title { + font-size: 2em; + margin: 0.5em; +} + +.multi_player_mode_choice_number { + align-items: center; + display: flex; + flex-direction: row; + margin: 0.75em; +} + +.multi_player_challenge_mode_invalid_input { + color: var(--button-background-color); + font-family: "Roboto Mono", sans-serif; + font-size: 1em; + font-weight: bold; + margin: 0.5em; +} + +/* Rounds count */ +#rounds_count { + background-color: white; + border: none; + border-radius: 0.5em; + color: black; + font-family: "Titan One", sans-serif; + font-size: 1em; + padding: 0.5em; + width: 2.5em; +} + +.rounds_count_title { + font-size: 1.375em; + margin: 0.5em; +} + +/* Players list */ +.players_title { + align-content: center; + align-items: center; + display: flex; + font-size: 3em; + flex-direction: column; + flex-wrap: wrap; + justify-content: center; + margin: 1em; +} + +.player_names { + border: 0.25em white solid; + border-radius: 0.75em; + max-height: 12em; + overflow-y: scroll; +} + +.player_name { + font-size: 1.5em; + margin-bottom: 0.5em; + margin-top: 0.5em; + text-align: center; + color: white; +} + +/* Game join view */ +.join_room_view { + align-items: center; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + justify-content: center; +} + +#game_username { + background-color: white; + border: none; + border-radius: 0.375em; + color: black; + font-family: "Titan One", sans-serif; + font-size: 1.25em; + margin: 0.5em; + padding: 0.5em; +} + +#join_game_button { + font-size: 1.25em; + margin: 1em; +} + +/* Game start failure */ +.game_start_failed { + color: var(--button-background-color); + font-family: "Roboto Mono", sans-serif; + font-weight: bold; + font-size: 1em; +} diff --git a/truthseeker/static/css/game_ui_start.css b/truthseeker/static/css/game_ui_start.css index a160f98..e50c89a 100644 --- a/truthseeker/static/css/game_ui_start.css +++ b/truthseeker/static/css/game_ui_start.css @@ -175,8 +175,7 @@ input::placeholder { } #game_username { - margin: 0.5em; - width: calc(100% - 1.5em); + width: calc(100% - 1.25em); } #play_button { diff --git a/truthseeker/static/js/game_common.js b/truthseeker/static/js/game_common.js index 51498bf..a0655e7 100644 --- a/truthseeker/static/js/game_common.js +++ b/truthseeker/static/js/game_common.js @@ -31,76 +31,6 @@ function checkWebSocketAvailability() { } } -/** - * Set the current theme for the game. - * - *
- * The theme preference is read from the local storage. - *
- * - *- * If accessing to the local storage is not allow, an error message which prevents playing the game - * and requesting user to enable localStorage is shown, and the error is logged in the console. - *
- */ -function setCurrentTheme() { - const htmlElement = document.getElementsByTagName("html")[0]; - - try { - const currentTheme = localStorage.getItem("pref_theme"); - - if (currentTheme == "light") { - htmlElement.classList.remove("dark"); - htmlElement.classList.add("light"); - } else { - // Use dark theme by default - htmlElement.classList.remove("light"); - htmlElement.classList.add("dark"); - } - - const btn = document.getElementsByClassName("theme_switcher")[0]; - btn.addEventListener("pointerup", changeTheme); - } catch (e) { - console.error("Unable to set theme from localStorage", e); - htmlElement.classList.add("dark"); - showUnsupportedBrowserMessage("Votre navigateur ne semble pas supporter le localStorage. Certains navigateurs nécessitant l'autorisation d'utiliser des cookies pour utiliser le localStorage, vérifiez que les cookies sont autorisés pour le site du jeu dans le vôtre."); - } -} - -/** - * Change the theme from the current theme to its opposite. - * - *- * If the current theme is "dark", it will become "light" and vice versa. - *
- * - *- * The new theme is saved in the localStorage, if the browser allows this action; otherwise, an - * error message is shown in the console. - *
- */ -function changeTheme() { - const currentTheme = localStorage.getItem("pref_theme"); - - const htmlElement = document.getElementsByTagName("html")[0]; - let newTheme; - if (currentTheme == "light") { - htmlElement.classList.remove("light"); - htmlElement.classList.add("dark"); - newTheme = "dark"; - } else { - htmlElement.classList.remove("dark"); - htmlElement.classList.add("light"); - newTheme = "light"; - } - - try { - localStorage.setItem("pref_theme", newTheme); - } catch (e) { - console.error("Unable to save theme change to localStorage", e); - } -} - /** * Show the unsupported browser dialog, which disables ability to play the game, using the given * unsupported browser message text. @@ -136,6 +66,5 @@ function showAlertDialog(element) { // Execution of main functions -setCurrentTheme(); detectIEBrowsers(); checkWebSocketAvailability(); \ No newline at end of file diff --git a/truthseeker/static/js/game_lobby.js b/truthseeker/static/js/game_lobby.js new file mode 100644 index 0000000..c47c880 --- /dev/null +++ b/truthseeker/static/js/game_lobby.js @@ -0,0 +1,336 @@ +// Display functions + +/** + * Display the invalid rounds count message element, by removing the hidden CSS class. + * + * @param {Element} invalidRoundsCountMessageElement the invalid rounds counts message + */ +function displayInvalidRoundsCountErrorMessage(invalidRoundsCountMessageElement) { + invalidRoundsCountMessageElement.classList.remove("hidden"); +} + +/** + * Get the room code and display the room code element. + */ +function displayRoomCode() { + let roomCode = getRoomCode(); + let roomCodeElement = document.getElementsByClassName("room_code")[0]; + roomCodeElement.textContent = roomCode; + roomCodeElement.setAttribute("href", "/lobby/" + roomCode); + document.getElementsByClassName("room_code_text")[0].classList.remove("hidden"); +} + +/** + * Display the players list element. + */ +function displayPlayerList() { + document.getElementsByClassName("players_list")[0].classList.remove("hidden"); +} + +/** + * Display the multi player mode choices, by removing the hidden CSS class on the first + * multi_player_mode_choices element. + */ +function displayMultiPlayerModeChoices() { + document.getElementsByClassName("multi_player_mode_choices")[0].classList.remove("hidden"); +} + +/** + * Display the room view, by removing the hidden CSS class on the first room_view element. + */ +function displayRoomView() { + document.getElementsByClassName("room_view")[0].classList.remove("hidden"); +} + +/** + * Display the join room view, by removing the hidden CSS class on the first join_room_view + * element. + */ +function displayJoinRoomView() { + document.getElementsByClassName("join_room_view")[0].classList.remove("hidden"); +} + +/** + * Show an error message on the first game_start_failed CSS element. + * + *+ * The current error message text will be replaced by the given message and the element will be + * shown, by removing the hidden CSS class on the element. + *
+ * + * @param {boolean} errorMessage the error message to show + */ +function displayInvalidNickNameErrorMessage(errorMessage) { + let gameStartFailedElement = document.getElementsByClassName("game_start_failed")[0]; + gameStartFailedElement.textContent = errorMessage; + gameStartFailedElement.classList.remove("hidden"); +} + +/** + * Hide an error message on the first game_start_failed CSS element. + * + *+ * The element will be hidden by removing the hidden CSS class on the element. + *
+ */ +function hideInvalidNickNameErrorMessage() { + document.getElementsByClassName("game_start_failed")[0].classList.add("hidden"); +} + +/** + * Hide the invalid rounds count message element, by adding the hidden CSS class. + * + * @param {Element} invalidRoundsCountMessageElement the invalid rounds counts message + */ +function hideInvalidRoundsCountErrorMessage(invalidRoundsCountMessageElement) { + invalidRoundsCountMessageElement.classList.add("hidden"); +} + +// Start game functions + +function startHistoryGame() { + //TODO: start the history game and handle server errors + connection errors +} + +function startChallengeGame() { + let roundsCount = getChallengeModeRoundsCount(); + if (roundsCount == -1) { + return; + } + + alert("Ce mode de jeu n'est malheureusement pas disponible."); +} + +// Join room functions + +function joinRoom() { + unsetListenerToJoinRoomButton(); + if (isNickNameInvalid()) { + displayInvalidNickNameErrorMessage("Le nom saisi n'est pas valide."); + setListenerToJoinRoomButton(); + return; + } + + hideInvalidNickNameErrorMessage(); + //TODO: join the game room and handle server errors + connection errors +} + +// Room code functions + +/** + * Copy the room code to the clipboard. + * + *+ * In order to not make an additional API call to get the room code, we use the value from the + * room code HTML element and generate a HTTP link from this value, copied to the clipboard using + * {@link copyTextToClipboard}. + *
+ */ +function copyCode() { + // Get the room code from the displayed text to avoid an extra API call + let roomCode = document.getElementsByClassName("room_code")[0].textContent; + if (roomCode == "") { + alert("Veuillez patientez, le code d'équipe est en cours de génération."); + } + copyTextToClipboard(window.location.protocol + "//" + window.location.hostname + ":" + + window.location.port + "/lobby/" + roomCode); +} + +// Listeners functions + +/** + * Set listeners to game buttons. + * + *+ * This function adds a click event listener on start game buttons. + *
+ */ +function setListenersToGameButtons() { + document.getElementById("multi_player_history_start_button").addEventListener("click", startHistoryGame); + document.getElementById("multi_player_challenge_start_button").addEventListener("click", startChallengeGame); +} + +/** + * Set listeners to the join room button. + * + *+ * This function adds a click event listener on the join room button. + *
+ */ +function setListenerToJoinRoomButton() { + document.getElementById("join_game_button").addEventListener("click", joinRoom); +} + +/** + * Set listeners to the copy room code button. + * + *+ * This function adds a click event listener on the copy room code button. + *
+ */ +function setListenerToCopyCodeButton() { + document.getElementById("invite_friends_button").addEventListener("click", copyCode); +} + +/** + * Unset listeners to game buttons. + * + *+ * This function removes the click event listener set with {@link setListenersToGameButtons} on + * start game buttons. + *
+ */ +function unsetListenersToButtons() { + document.getElementById("multi_player_history_start_button").removeEventListener("click", startHistoryGame); + document.getElementById("multi_player_challenge_start_button").removeEventListener("click", startChallengeGame); +} + +/** + * Unset listeners to the join room button. + * + *+ * This function removes the click event listener set with {@link setListenerToJoinRoomButton} on + * the join room button. + *
+ */ +function unsetListenerToJoinRoomButton() { + document.getElementById("join_game_button").removeEventListener("click", joinRoom); +} + +/** + * Unset listeners to the copy room code button. + * + *+ * This function removes the click event listener set with {@link setListenerToCopyCodeButton} on + * the copy room code button. + *
+ */ +function unsetListenerToCopyCodeButton() { + document.getElementById("invite_friends_button").removeEventListener("click", copyCode); +} + +// Utility functions + +function isRoomOwner() { + //FIXME: check if player is room owner + return true; +} + +function hasJoinedRoom() { + //FIXME: check if player has joined the room + return true; +} + +/** + * Copy the given text in the clipboard, if the browser allows it. + * + *+ * A JavaScript alert is created witn an appropriate message, regardless of whether the copy succeeded. + *
+ * + *+ * This function uses the Clipboard API. In the case it is not supported by the browser used, a JavaScript alert is shown.. + *
+ * + * @param {string}} textToCopy the text to copy to the clipboard + */ +function copyTextToClipboard(textToCopy) { + if (!navigator.clipboard) { + alert("Votre navigateur ne supporte pas l'API Clipboard. Veuillez copier le texte en ouvrant le menu contextuel de votre navigateur sur le lien et sélectionner l'option pour copier le lien."); + } + navigator.clipboard.writeText(textToCopy).then(() => { + alert("Code copié avec succès dans le presse-papiers."); + }, () => { + alert("Impossible de copier le texte. Vérifiez si vous avez donné la permission d'accès au presse-papiers pour le site de Thruth Inquiry dans les paramètres de votre navigateur."); + }); +} + +/** + * Determine whether a nickname is invalid. + * + *+ * A nickname is invalid when it only contains spaces characters or is empty. + *
+ * + * @returns whether a nickname is invalid + */ +function isNickNameInvalid() { + return document.getElementById("game_username").value.trim() == ""; +} + +/** + * Get the rounds count for the challenge mode from the user input. + * + *+ * As browsers allow to enter any character on a number imput, we need to validate the user value. + * A regular expression which checks that every character is a number digit is used. + *
+ * + *+ * If the user input isn't matched by the regular expression, an error message is shown to the user. + *
+ * + * @returns the rounds count or -1 if it is invalid + */ +function getChallengeModeRoundsCount() { + let roundsCountText = document.getElementById("rounds_count").value; + let errorElement = document.getElementsByClassName("multi_player_challenge_mode_invalid_input")[0]; + if (!/^\d+$/.test(roundsCountText)) { + displayInvalidRoundsCountErrorMessage(errorElement); + return -1; + } + + let roundsCountNumber = parseInt(roundsCountText); + if (roundsCountNumber < 5 || roundsCountNumber > 15) { + displayInvalidRoundsCountErrorMessage(errorElement); + return -1; + } + + hideInvalidRoundsCountErrorMessage(errorElement); + return roundsCountNumber; +} + +/** + * Get the code of the room. + * + * @returns the code of the room + */ +function getRoomCode() { + //FIXME get the real room code + return "ABCDEF"; +} + +// Lobby initialization + +/** + * Initialize the lobby page. + * + *+ * If the player has joined the room, the room view will be shown. In the case the player is the + * owner of the room, the room code and the multi player mode choice will be shown and the + * listeners to the game buttons will be done. + *
+ * + *+ * If the player has not joined the room, the join room view will be shown and a listener to the + * join room button will be set. + *
+ */ +function initLobby() { + if (hasJoinedRoom()) { + displayRoomView(); + if (isRoomOwner()) { + displayRoomCode(); + displayMultiPlayerModeChoices(); + setListenersToGameButtons(); + setListenerToCopyCodeButton(); + } + + displayPlayerList(); + } else { + displayJoinRoomView(); + setListenerToJoinRoomButton(); + } +} + +initLobby(); \ No newline at end of file diff --git a/truthseeker/static/js/game_start_page.js b/truthseeker/static/js/game_start_page.js index 4df0062..52b68c0 100644 --- a/truthseeker/static/js/game_start_page.js +++ b/truthseeker/static/js/game_start_page.js @@ -138,8 +138,84 @@ function joinMultiPlayerRoom() { //TODO: code to join multi player game } +/** + * Set the current theme for the game. + * + *+ * The theme preference is read from the local storage. + *
+ * + *+ * If accessing to the local storage is not allow, an error message which prevents playing the game + * and requesting user to enable localStorage is shown, and the error is logged in the console. + *
+ */ +function setCurrentTheme() { + const htmlElement = document.getElementsByTagName("html")[0]; + + try { + const currentTheme = localStorage.getItem("pref_theme"); + + if (currentTheme == "light") { + htmlElement.classList.remove("dark"); + htmlElement.classList.add("light"); + } else { + // Use dark theme by default + htmlElement.classList.remove("light"); + htmlElement.classList.add("dark"); + } + + const btn = document.getElementsByClassName("theme_switcher")[0]; + btn.addEventListener("pointerup", changeTheme); + } catch (e) { + console.error("Unable to set theme from localStorage", e); + htmlElement.classList.add("dark"); + showUnsupportedBrowserMessage("Votre navigateur ne semble pas supporter le localStorage. Certains navigateurs nécessitant l'autorisation d'utiliser des cookies pour utiliser le localStorage, vérifiez que les cookies sont autorisés pour le site du jeu dans le vôtre."); + } +} + +/** + * Change the theme from the current theme to its opposite. + * + *+ * If the current theme is "dark", it will become "light" and vice versa. + *
+ * + *+ * The new theme is saved in the localStorage, if the browser allows this action; otherwise, an + * error message is shown in the console. + *
+ */ +function changeTheme() { + const currentTheme = localStorage.getItem("pref_theme"); + + const htmlElement = document.getElementsByTagName("html")[0]; + let newTheme; + if (currentTheme == "light") { + htmlElement.classList.remove("light"); + htmlElement.classList.add("dark"); + newTheme = "dark"; + } else { + htmlElement.classList.remove("dark"); + htmlElement.classList.add("light"); + newTheme = "light"; + } + + try { + localStorage.setItem("pref_theme", newTheme); + } catch (e) { + console.error("Unable to save theme change to localStorage", e); + } +} + +// Set event listeners + document.getElementById("play_button").addEventListener("click", showGameModeSelection); document.getElementById("back_button").addEventListener("click", hideGameModeSelection); document.getElementById("start_solo_game_button").addEventListener("click", startSoloGame); document.getElementById("create_room_button").addEventListener("click", createMultiPlayerRoom); document.getElementById("join_room_button").addEventListener("click", joinMultiPlayerRoom); + +// Execution of functions + +setCurrentTheme(); diff --git a/truthseeker/templates/lobby.html b/truthseeker/templates/lobby.html index 6e411f2..e3bdfdc 100644 --- a/truthseeker/templates/lobby.html +++ b/truthseeker/templates/lobby.html @@ -1,3 +1,67 @@ -lobby.html template -