diff --git a/client/public/fonts/JuliaMono-Regular.ttf b/client/public/fonts/JuliaMono-Regular.ttf new file mode 100644 index 0000000..30b84f3 Binary files /dev/null and b/client/public/fonts/JuliaMono-Regular.ttf differ diff --git a/client/public/fonts/LICENSE-JuliaMono b/client/public/fonts/LICENSE-JuliaMono new file mode 100644 index 0000000..cc69da8 --- /dev/null +++ b/client/public/fonts/LICENSE-JuliaMono @@ -0,0 +1,93 @@ +Copyright (c) 2020 - 2023, cormullion +with Reserved Font Name JuliaMono. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/client/public/fonts/LICENSE-NotoColorEmoji b/client/public/fonts/LICENSE-NotoColorEmoji new file mode 100644 index 0000000..979c943 --- /dev/null +++ b/client/public/fonts/LICENSE-NotoColorEmoji @@ -0,0 +1,93 @@ +Copyright 2021 Google Inc. All Rights Reserved. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/client/public/fonts/LICENSE-Roboto.txt b/client/public/fonts/LICENSE-Roboto.txt new file mode 100644 index 0000000..75b5248 --- /dev/null +++ b/client/public/fonts/LICENSE-Roboto.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/client/public/fonts/NotoColorEmoji-Regular.ttf b/client/public/fonts/NotoColorEmoji-Regular.ttf new file mode 100644 index 0000000..05b42fd Binary files /dev/null and b/client/public/fonts/NotoColorEmoji-Regular.ttf differ diff --git a/client/public/fonts/Roboto-Regular.ttf b/client/public/fonts/Roboto-Regular.ttf new file mode 100644 index 0000000..ddf4bfa Binary files /dev/null and b/client/public/fonts/Roboto-Regular.ttf differ diff --git a/client/src/app.tsx b/client/src/app.tsx index 42c5531..dfa7def 100644 --- a/client/src/app.tsx +++ b/client/src/app.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import { Outlet, useParams } from "react-router-dom"; +import { useEffect, useState } from 'react'; import '@fontsource/roboto/300.css'; import '@fontsource/roboto/400.css'; @@ -8,31 +9,55 @@ import '@fontsource/roboto/700.css'; import './css/reset.css'; import './css/app.css'; -import { PreferencesContext} from './components/infoview/context'; +import { PageContext, PopupContext, PreferencesContext} from './components/infoview/context'; import UsePreferences from "./state/hooks/use_preferences" import i18n from './i18n'; +import { Navigation } from './components/navigation'; +import { useSelector } from 'react-redux'; +import { changeTypewriterMode, selectTypewriterMode } from './state/progress'; +import { useAppDispatch } from './hooks'; +import { Popup } from './components/popup/popup'; -export const GameIdContext = React.createContext(undefined); +export const GameIdContext = React.createContext<{ + gameId: string, + worldId: string|null, + levelId: number|null}>({gameId: null, worldId: null, levelId: null}); function App() { const params = useParams() - const gameId = "g/" + params.owner + "/" + params.repo + const gameId = (params.owner && params.repo) ? "g/" + params.owner + "/" + params.repo : null + const worldId = params.worldId + const levelId = parseInt(params.levelId) const {mobile, layout, isSavePreferences, language, setLayout, setIsSavePreferences, setLanguage} = UsePreferences() - React.useEffect(() => { + const dispatch = useAppDispatch() + const typewriterMode = useSelector(selectTypewriterMode(gameId)) + const setTypewriterMode = (newTypewriterMode: boolean) => dispatch(changeTypewriterMode({game: gameId, typewriterMode: newTypewriterMode})) + const [lockEditorMode, setLockEditorMode] = useState(false) + const [typewriterInput, setTypewriterInput] = useState("") + const [page, setPage] = useState(0) + const [popupContent, setPopupContent] = useState(null) + + useEffect(() => { i18n.changeLanguage(language) }, [language]) return (
- - - - - - + + + + + + + + + + { popupContent && } + +
) diff --git a/client/src/components/app_bar.tsx b/client/src/components/app_bar.tsx deleted file mode 100644 index 026400b..0000000 --- a/client/src/components/app_bar.tsx +++ /dev/null @@ -1,321 +0,0 @@ -/** - * @file contains the navigation bars of the app. - */ -import * as React from 'react' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faDownload, faUpload, faEraser, faBook, faBookOpen, faGlobe, faHome, - faArrowRight, faArrowLeft, faXmark, faBars, faCode, - faCircleInfo, faTerminal, faGear } from '@fortawesome/free-solid-svg-icons' -import { GameIdContext } from "../app" -import { InputModeContext, PreferencesContext, WorldLevelIdContext } from "./infoview/context" -import { GameInfo, useGetGameInfoQuery } from '../state/api' -import { changedOpenedIntro, selectCompleted, selectDifficulty, selectProgress } from '../state/progress' -import { useAppDispatch, useAppSelector } from '../hooks' -import { Button } from './button' -import { downloadProgress } from './popup/erase' -import { useTranslation } from 'react-i18next' - -/** navigation buttons for mobile welcome page to switch between intro/tree/inventory. */ -function MobileNavButtons({pageNumber, setPageNumber}: - { pageNumber: number, - setPageNumber: any}) { - const gameId = React.useContext(GameIdContext) - const { t } = useTranslation() - const dispatch = useAppDispatch() - - // if `prevText` or `prevIcon` is set, show a button to go back - let prevText = {0: null, 1: t("Intro"), 2: null}[pageNumber] - let prevIcon = {0: null, 1: null, 2: faBookOpen}[pageNumber] - let prevTitle = {0: null, 1: t("Game Introduction"), 2: t("World selection")}[pageNumber] - // if `nextText` or `nextIcon` is set, show a button to go forward - let nextText = {0: t("Start"), 1: null, 2: null}[pageNumber] - let nextIcon = {0: null, 1: faBook, 2: null}[pageNumber] - let nextTitle = {0: t("World selection"), 1: t("Inventory"), 2: null}[pageNumber] - - return <> - {(prevText || prevIcon) && - - } - {(nextText || nextIcon) && - - } - -} - -/** button to toggle dropdown menu. */ -export function MenuButton({navOpen, setNavOpen}) { - return -} - -/** button to go one level futher. - * for the last level, this button turns into a button going back to the welcome page. - */ -function NextButton({worldSize, difficulty, completed, setNavOpen}) { - const { t } = useTranslation() - const gameId = React.useContext(GameIdContext) - const {worldId, levelId} = React.useContext(WorldLevelIdContext) - return (levelId < worldSize ? - - : - - ) -} - -/** button to go one level back. - * only renders if the current level id is > 0. - */ -function PreviousButton({setNavOpen}) { - const { t } = useTranslation() - const gameId = React.useContext(GameIdContext) - const {worldId, levelId} = React.useContext(WorldLevelIdContext) - return (levelId > 0 && <> - - ) -} - -/** button to toggle between editor and typewriter */ -function InputModeButton({setNavOpen, isDropdown}) { - const { t } = useTranslation() - const {levelId} = React.useContext(WorldLevelIdContext) - const {typewriterMode, setTypewriterMode, lockEditorMode} = React.useContext(InputModeContext) - - /** toggle input mode if allowed */ - function toggleInputMode(ev: React.MouseEvent) { - if (!lockEditorMode){ - setTypewriterMode(!typewriterMode) - setNavOpen(false) - } - } - - return -} - -export function ImpressumButton({setNavOpen, toggleImpressum, isDropdown}) { - const { t } = useTranslation() - return -} - -export function PrivacyButton({setNavOpen, togglePrivacy, isDropdown}) { - const { t } = useTranslation() - return -} - -export function PreferencesButton({setNavOpen, togglePreferencesPopup}) { - const { t } = useTranslation() - return -} - -function GameInfoButton({setNavOpen, toggleInfo}) { - const { t } = useTranslation() - return -} - -function EraseButton ({setNavOpen, toggleEraseMenu}) { - const { t } = useTranslation() - return -} - -function DownloadButton ({setNavOpen, gameId, gameProgress}) { - const { t } = useTranslation() - return -} - -function UploadButton ({setNavOpen, toggleUploadMenu}) { - const { t } = useTranslation() - return -} - -/** button to go back to welcome page */ -function HomeButton({isDropdown}) { - const { t } = useTranslation() - const gameId = React.useContext(GameIdContext) - return -} - -function LandingPageButton() { - const { t } = useTranslation() - return -} - -/** button in mobile level to toggle inventory. - * only displays a button if `setPageNumber` is set. - */ -function InventoryButton({pageNumber, setPageNumber}) { - const { t } = useTranslation() - return (setPageNumber && - - ) -} - -/** the navigation bar on the welcome page */ -export function WelcomeAppBar({pageNumber, setPageNumber, gameInfo, toggleImpressum, togglePrivacy, toggleEraseMenu, toggleUploadMenu, toggleInfo, togglePreferencesPopup} : { - pageNumber: number, - setPageNumber: any, - gameInfo: GameInfo, - toggleImpressum: any, - togglePrivacy: any, - toggleEraseMenu: any, - toggleUploadMenu: any, - toggleInfo: any, - togglePreferencesPopup: () => void; -}) { - const { t } = useTranslation() - const gameId = React.useContext(GameIdContext) - const gameProgress = useAppSelector(selectProgress(gameId)) - const {mobile} = React.useContext(PreferencesContext) - const [navOpen, setNavOpen] = React.useState(false) - - return
-
- - -
-
- {!mobile && {t(gameInfo?.title, {ns: gameId})}} -
-
- {mobile && } - -
-
- - - - - - - -
-
-} - -/** the navigation bar in a level */ -export function LevelAppBar({isLoading, levelTitle, toggleImpressum, togglePrivacy, toggleInfo, togglePreferencesPopup, pageNumber=undefined, setPageNumber=undefined} : { - isLoading: boolean, - levelTitle: string, - toggleImpressum: any, - togglePrivacy: any, - toggleInfo: any, - togglePreferencesPopup: any, - pageNumber?: number, - setPageNumber?: any, -}) { - const { t } = useTranslation() - const gameId = React.useContext(GameIdContext) - const {worldId, levelId} = React.useContext(WorldLevelIdContext) - const {mobile} = React.useContext(PreferencesContext) - const [navOpen, setNavOpen] = React.useState(false) - const gameInfo = useGetGameInfoQuery({game: gameId}) - const completed = useAppSelector(selectCompleted(gameId, worldId, levelId)) - const difficulty = useAppSelector(selectDifficulty(gameId)) - - let worldTitle = gameInfo.data?.worlds.nodes[worldId].title - - return
- {mobile ? - <> - {/* MOBILE VERSION */} -
- {levelTitle} -
-
- - -
-
- - - - - - - - -
- : - <> - {/* DESKTOP VERSION */} -
- - {worldTitle && `${t("World")}: ${t(worldTitle, {ns: gameId})}`} -
-
- {levelTitle} -
-
- - - - -
-
- - - - -
- - } -
-} diff --git a/client/src/components/error_page.tsx b/client/src/components/error_page.tsx index 6ac7768..c3e8964 100644 --- a/client/src/components/error_page.tsx +++ b/client/src/components/error_page.tsx @@ -11,8 +11,8 @@ export default function ErrorPage() {

Oops!

Something unexpected happened:

-

{error.statusText || error.message}

-

Please create an issue on the lean4game repo.

+

({error.status}) {error.statusText || error.message}
{error.data}

+

Please create an issue at the lean4game repo.

diff --git a/client/src/components/game.tsx b/client/src/components/game.tsx new file mode 100644 index 0000000..32366f0 --- /dev/null +++ b/client/src/components/game.tsx @@ -0,0 +1,16 @@ +import i18next from "i18next" +import React from "react" +import { useParams } from "react-router-dom" +import { GameIdContext } from "../app" +import { useGetGameInfoQuery } from "../state/api" + +function Game() { + const params = useParams() + const levelId = parseInt(params.levelId) + const worldId = params.worldId + + return
+ + +
+} diff --git a/client/src/components/hints.tsx b/client/src/components/hints.tsx index 5fa5a47..257e521 100644 --- a/client/src/components/hints.tsx +++ b/client/src/components/hints.tsx @@ -12,7 +12,7 @@ import { GameIdContext } from "../app"; * and have the variables substituted just before displaying. */ function getHintText(hint: GameHint): string { - const gameId = React.useContext(GameIdContext) + const {gameId} = React.useContext(GameIdContext) let { t } = useTranslation() if (hint.rawText) { // Replace the variable names used in the hint with the ones used by the player diff --git a/client/src/components/infoview/context.ts b/client/src/components/infoview/context.ts index e6dd6ee..363d5c9 100644 --- a/client/src/components/infoview/context.ts +++ b/client/src/components/infoview/context.ts @@ -123,6 +123,26 @@ export const DeletedChatContext = React.createContext<{ setShowHelp: () => {} }) +export const PageContext = React.createContext<{ + typewriterMode: boolean, + setTypewriterMode: React.Dispatch>, + typewriterInput: string, + setTypewriterInput: React.Dispatch>, + lockEditorMode: boolean, + setLockEditorMode: React.Dispatch>, + page: number, /* only for mobile */ + setPage: React.Dispatch>, +}>({ + typewriterMode: true, + setTypewriterMode: () => {}, + typewriterInput: "", + setTypewriterInput: () => {}, + lockEditorMode: false, + setLockEditorMode: () => {}, + page: 0, + setPage: () => {} +}); + export const InputModeContext = React.createContext<{ typewriterMode: boolean, setTypewriterMode: React.Dispatch>, diff --git a/client/src/components/infoview/goals.tsx b/client/src/components/infoview/goals.tsx index 41e79bf..f340144 100644 --- a/client/src/components/infoview/goals.tsx +++ b/client/src/components/infoview/goals.tsx @@ -9,7 +9,7 @@ import { EditorContext } from '../../../../node_modules/lean4-infoview/src/infov import { Locations, LocationsContext, SelectableLocation } from '../../../../node_modules/lean4-infoview/src/infoview/goalLocation'; import { InteractiveCode } from '../../../../node_modules/lean4-infoview/src/infoview/interactiveCode' import { WithTooltipOnHover } from '../../../../node_modules/lean4-infoview/src/infoview/tooltips'; -import { InputModeContext } from './context'; +import { PageContext } from './context'; import { InteractiveGoal, InteractiveGoals, InteractiveGoalsWithHints, InteractiveHypothesisBundle, ProofState } from './rpc_api'; import { RpcSessionAtPos } from '@leanprover/infoview/*'; import { DocumentPosition } from '../../../../node_modules/lean4-infoview/src/infoview/util'; diff --git a/client/src/components/infoview/main.tsx b/client/src/components/infoview/main.tsx index fc089fd..ab072d9 100644 --- a/client/src/components/infoview/main.tsx +++ b/client/src/components/infoview/main.tsx @@ -27,7 +27,7 @@ import Markdown from '../markdown'; import { Infos } from './infos'; import { AllMessages, Errors, WithLspDiagnosticsContext } from './messages'; import { Goal, isLastStepWithErrors, lastStepHasErrors, loadGoals } from './goals'; -import { DeletedChatContext, InputModeContext, PreferencesContext, MonacoEditorContext, ProofContext, SelectionContext, WorldLevelIdContext } from './context'; +import { DeletedChatContext, PageContext, PreferencesContext, MonacoEditorContext, ProofContext, SelectionContext, WorldLevelIdContext } from './context'; import { Typewriter, getInteractiveDiagsAt, hasErrors, hasInteractiveErrors } from './typewriter'; import { InteractiveDiagnostic } from '@leanprover/infoview/*'; import { Button } from '../button'; @@ -46,7 +46,7 @@ import path from 'path'; */ export function DualEditor({ level, codeviewRef, levelId, worldId, worldSize }) { const ec = React.useContext(EditorContext) - const { typewriterMode, lockEditorMode } = React.useContext(InputModeContext) + const { typewriterMode, lockEditorMode } = React.useContext(PageContext) return <>
@@ -63,8 +63,8 @@ export function DualEditor({ level, codeviewRef, levelId, worldId, worldSize }) /** The part of the two editors that needs the editor connection first */ function DualEditorMain({ worldId, levelId, level, worldSize }: { worldId: string, levelId: number, level: LevelInfo, worldSize: number }) { const ec = React.useContext(EditorContext) - const gameId = React.useContext(GameIdContext) - const { typewriterMode, lockEditorMode } = React.useContext(InputModeContext) + const {gameId} = React.useContext(GameIdContext) + const { typewriterMode, lockEditorMode } = React.useContext(PageContext) const {proof, setProof} = React.useContext(ProofContext) @@ -137,7 +137,7 @@ function DualEditorMain({ worldId, levelId, level, worldSize }: { worldId: strin */ function ExerciseStatement({ data, showLeanStatement = false }) { let { t } = useTranslation() - const gameId = React.useContext(GameIdContext) + const {gameId} = React.useContext(GameIdContext) if (!(data?.descrText || data?.descrFormat)) { return <> } return <> @@ -162,7 +162,7 @@ function ExerciseStatement({ data, showLeanStatement = false }) { export function Main(props: { world: string, level: number, data: LevelInfo}) { let { t } = useTranslation() const ec = React.useContext(EditorContext); - const gameId = React.useContext(GameIdContext) + const {gameId} = React.useContext(GameIdContext) const {worldId, levelId} = React.useContext(WorldLevelIdContext) const { proof, setProof } = React.useContext(ProofContext) @@ -314,7 +314,7 @@ function Command({ proof, i, deleteProof }: { proof: ProofState, i: number, dele // message = diag.message // } -// const { typewriterMode } = React.useContext(InputModeContext) +// const { typewriterMode } = React.useContext(PageContext) // return ( // //
@@ -369,7 +369,7 @@ function GoalsTabs({ proofStep, last, onClick, onGoalChange=(n)=>{}}: { proofSte // Splitting up Typewriter into two parts is a HACK export function TypewriterInterfaceWrapper(props: { world: string, level: number, data: LevelInfo, worldSize: number }) { const ec = React.useContext(EditorContext) - const gameId = React.useContext(GameIdContext) + const {gameId} = React.useContext(GameIdContext) useClientNotificationEffect( 'textDocument/didClose', @@ -403,7 +403,7 @@ export function TypewriterInterfaceWrapper(props: { world: string, level: number export function TypewriterInterface({props}) { let { t } = useTranslation() const ec = React.useContext(EditorContext) - const gameId = React.useContext(GameIdContext) + const {gameId} = React.useContext(GameIdContext) const editor = React.useContext(MonacoEditorContext) const model = editor.getModel() const uri = model.uri.toString() @@ -418,7 +418,7 @@ export function TypewriterInterface({props}) { const { setDeletedChat, showHelp, setShowHelp } = React.useContext(DeletedChatContext) const {mobile} = React.useContext(PreferencesContext) const { proof, setProof, crashed, setCrashed, interimDiags } = React.useContext(ProofContext) - const { setTypewriterInput } = React.useContext(InputModeContext) + const { setTypewriterInput } = React.useContext(PageContext) const { selectedStep, setSelectedStep } = React.useContext(SelectionContext) const proofPanelRef = React.useRef(null) diff --git a/client/src/components/infoview/messages.tsx b/client/src/components/infoview/messages.tsx index a6daab8..4dc0123 100644 --- a/client/src/components/infoview/messages.tsx +++ b/client/src/components/infoview/messages.tsx @@ -10,7 +10,7 @@ import { Details } from '../../../../node_modules/lean4-infoview/src/infoview/co import { InteractiveMessage } from '../../../../node_modules/lean4-infoview/src/infoview/traceExplorer' import { RpcContext, useRpcSessionAtPos } from '../../../../node_modules/lean4-infoview/src/infoview/rpcSessions' -import { InputModeContext } from './context' +import { PageContext } from './context' import { useTranslation } from 'react-i18next' interface MessageViewProps { @@ -80,7 +80,7 @@ const MessageView = React.memo(({uri, diag}: MessageViewProps) => { message = diag.message } - const { typewriterMode, lockEditorMode } = React.useContext(InputModeContext) + const { typewriterMode, lockEditorMode } = React.useContext(PageContext) return ( //
diff --git a/client/src/components/infoview/typewriter.tsx b/client/src/components/infoview/typewriter.tsx index bdf3553..3488aa6 100644 --- a/client/src/components/infoview/typewriter.tsx +++ b/client/src/components/infoview/typewriter.tsx @@ -17,7 +17,7 @@ import { InteractiveDiagnostic, RpcSessionAtPos, getInteractiveDiagnostics } fro import { Diagnostic } from 'vscode-languageserver-types'; import { DocumentPosition } from '../../../../node_modules/lean4-infoview/src/infoview/util'; import { RpcContext } from '../../../../node_modules/lean4-infoview/src/infoview/rpcSessions'; -import { DeletedChatContext, InputModeContext, MonacoEditorContext, ProofContext } from './context' +import { DeletedChatContext, PageContext, MonacoEditorContext, ProofContext } from './context' import { goalsToString, lastStepHasErrors, loadGoals } from './goals' import { GameHint, ProofState } from './rpc_api' import { useTranslation } from 'react-i18next' @@ -87,7 +87,7 @@ export function Typewriter({disabled}: {disabled?: boolean}) { const [oneLineEditor, setOneLineEditor] = useState(null) const [processing, setProcessing] = useState(false) - const {typewriterInput, setTypewriterInput} = React.useContext(InputModeContext) + const {typewriterInput, setTypewriterInput} = React.useContext(PageContext) const inputRef = useRef() diff --git a/client/src/components/inventory.tsx b/client/src/components/inventory.tsx index 363f3db..964752d 100644 --- a/client/src/components/inventory.tsx +++ b/client/src/components/inventory.tsx @@ -64,7 +64,7 @@ function InventoryList({items, docType, openDoc, tab=null, setTab=undefined, lev // TODO: `level` is only used in the `useEffect` below to check if a new level has // been loaded. Is there a better way to observe this? - const gameId = React.useContext(GameIdContext) + const {gameId} = React.useContext(GameIdContext) const {worldId, levelId} = React.useContext(WorldLevelIdContext) const difficulty = useSelector(selectDifficulty(gameId)) @@ -147,7 +147,7 @@ return
@@ -161,7 +161,7 @@ export function Documentation({name, type, handleClose}) { /** The panel (on the welcome page) showing the user's inventory with tactics, definitions, and lemmas */ export function InventoryPanel({levelInfo, visible = true}) { - const gameId = React.useContext(GameIdContext) + const {gameId} = React.useContext(GameIdContext) const [lemmaTab, setLemmaTab] = useState(levelInfo?.lemmaTab) diff --git a/client/src/components/landing_page.tsx b/client/src/components/landing_page.tsx index 955950c..26a08bd 100644 --- a/client/src/components/landing_page.tsx +++ b/client/src/components/landing_page.tsx @@ -11,27 +11,14 @@ import '../css/landing_page.css' import bgImage from '../assets/bg.jpg' import Markdown from './markdown'; -import {PrivacyPolicyPopup, ImpressumPopup} from './popup/privacy_policy' import { GameTile, useGetGameInfoQuery } from '../state/api' import path from 'path'; -import { PreferencesPopup } from './popup/preferences'; -import { ImpressumButton, MenuButton, PreferencesButton, PrivacyButton } from './app_bar'; import ReactCountryFlag from 'react-country-flag'; import lean4gameConfig from '../config.json' import i18next from 'i18next'; - -function GithubIcon({url='https://github.com'}) { - let { t } = useTranslation() - - return -} +import { useContext } from 'react'; +import { PopupContext } from './popup/popup'; function Tile({gameId, data}: {gameId: string, data: GameTile|undefined}) { let { t } = useTranslation() @@ -75,7 +62,7 @@ function Tile({gameId, data}: {gameId: string, data: GameTile|undefined}) { if (lean4gameConfig.useFlags) { return } else { - return {lang} + return {lang} } })} @@ -89,19 +76,7 @@ function Tile({gameId, data}: {gameId: string, data: GameTile|undefined}) { function LandingPage() { const navigate = useNavigate(); - - const [impressumPopup, setImpressumPopup] = React.useState(false); - const [privacyPopup, setPrivacyPopup] = React.useState(false); - const [preferencesPopup, setPreferencesPopup] = React.useState(false); - const [navOpen, setNavOpen] = React.useState(false); - const openImpressum = () => setImpressumPopup(true); - const closeImpressum = () => setImpressumPopup(false); - const toggleImpressum = () => setImpressumPopup(!impressumPopup); - const openPrivacy = () => setPrivacyPopup(true); - const closePrivacy = () => setPrivacyPopup(false); - const togglePrivacy = () => setPrivacyPopup(!privacyPopup); - const closePreferencesPopup = () => setPreferencesPopup(false); - const togglePreferencesPopup = () => setPreferencesPopup(!preferencesPopup); + const { setPopupContent } = useContext(PopupContext) const { t, i18n } = useTranslation() @@ -127,15 +102,6 @@ function LandingPage() { return
-

{t("Lean Game Server")}

@@ -218,11 +184,8 @@ function LandingPage() {

diff --git a/client/src/components/level.tsx b/client/src/components/level.tsx index 37710e3..92eddc3 100644 --- a/client/src/components/level.tsx +++ b/client/src/components/level.tsx @@ -29,11 +29,10 @@ import Markdown from './markdown' import {InventoryPanel} from './inventory' import { hasInteractiveErrors } from './infoview/typewriter' import { DeletedChatContext, InputModeContext, PreferencesContext, MonacoEditorContext, - ProofContext, SelectionContext, WorldLevelIdContext } from './infoview/context' + ProofContext, SelectionContext, WorldLevelIdContext, PageContext } from './infoview/context' import { DualEditor } from './infoview/main' import { GameHint, InteractiveGoalsWithHints, ProofState } from './infoview/rpc_api' import { DeletedHints, Hint, Hints, MoreHelpButton, filterHints } from './hints' -import { ImpressumPopup, PrivacyPolicyPopup } from './popup/privacy_policy' import path from 'path'; import '@fontsource/roboto/300.css' @@ -43,7 +42,6 @@ import '@fontsource/roboto/700.css' import 'lean4web/client/src/editor/infoview.css' import 'lean4web/client/src/editor/vscode.css' import '../css/level.css' -import { LevelAppBar } from './app_bar' import { LeanClient } from 'lean4web/client/src/editor/leanclient' import { DisposingWebSocketMessageReader } from 'lean4web/client/src/reader' import { WebSocketMessageWriter, toSocket } from 'vscode-ws-jsonrpc' @@ -61,10 +59,10 @@ monacoSetup() function Level() { const params = useParams() - const levelId = parseInt(params.levelId) - const worldId = params.worldId + // const levelId = parseInt(params.levelId) + // const worldId = params.worldId - const gameId = React.useContext(GameIdContext) + const {gameId, worldId, levelId} = React.useContext(GameIdContext) // Load the namespace of the game i18next.loadNamespaces(gameId).catch(err => { @@ -87,14 +85,12 @@ function Level() { function toggleInfo() {setInfo(!info)} function togglePreferencesPopup() {setPreferencesPopup(!preferencesPopup)} + useEffect(() => {}, []) + return {levelId == 0 ? : } - {impressum ? : null} - {privacy ? : null} - {info ? : null} - {preferencesPopup ? : null} } @@ -102,7 +98,7 @@ function ChatPanel({lastLevel, visible = true}) { let { t } = useTranslation() const chatRef = useRef(null) const {mobile} = useContext(PreferencesContext) - const gameId = useContext(GameIdContext) + const {gameId} = useContext(GameIdContext) const {worldId, levelId} = useContext(WorldLevelIdContext) const level = useLoadLevelQuery({game: gameId, world: worldId, level: levelId}) const {proof, setProof} = useContext(ProofContext) @@ -204,7 +200,7 @@ function ChatPanel({lastLevel, visible = true}) { function ExercisePanel({codeviewRef, visible=true}: {codeviewRef: React.MutableRefObject, visible?: boolean}) { - const gameId = React.useContext(GameIdContext) + const {gameId} = React.useContext(GameIdContext) const {worldId, levelId} = useContext(WorldLevelIdContext) const level = useLoadLevelQuery({game: gameId, world: worldId, level: levelId}) const gameInfo = useGetGameInfoQuery({game: gameId}) @@ -218,7 +214,7 @@ function ExercisePanel({codeviewRef, visible=true}: {codeviewRef: React.MutableR function PlayableLevel({impressum, setImpressum, privacy, setPrivacy, toggleInfo, togglePreferencesPopup}) { let { t } = useTranslation() const codeviewRef = useRef(null) - const gameId = React.useContext(GameIdContext) + const {gameId} = React.useContext(GameIdContext) const {worldId, levelId} = useContext(WorldLevelIdContext) const {mobile} = React.useContext(PreferencesContext) @@ -245,7 +241,7 @@ function PlayableLevel({impressum, setImpressum, privacy, setPrivacy, toggleInfo // A set of row numbers where help is displayed const [showHelp, setShowHelp] = useState>(new Set()) // Only for mobile layout - const [pageNumber, setPageNumber] = useState(0) + const {page, setPage} = useContext(PageContext) // set to true to prevent switching between typewriter and editor const [lockEditorMode, setLockEditorMode] = useState(false) @@ -418,8 +414,8 @@ function PlayableLevel({impressum, setImpressum, privacy, setPrivacy, toggleInfo - + /> */} {mobile? // TODO: This is copied from the `Split` component below... <>
- + visible={page == 0} /> +
: @@ -457,7 +453,7 @@ function PlayableLevel({impressum, setImpressum, privacy, setPrivacy, toggleInfo function IntroductionPanel({gameInfo}) { let { t } = useTranslation() - const gameId = React.useContext(GameIdContext) + const {gameId} = React.useContext(GameIdContext) const {worldId} = useContext(WorldLevelIdContext) const {mobile} = React.useContext(PreferencesContext) @@ -487,7 +483,7 @@ export default Level function Introduction({impressum, setImpressum, privacy, setPrivacy, toggleInfo, togglePreferencesPopup}) { let { t } = useTranslation() - const gameId = React.useContext(GameIdContext) + const {gameId} = React.useContext(GameIdContext) const {mobile} = useContext(PreferencesContext) const inventory = useLoadInventoryOverviewQuery({game: gameId}) @@ -506,7 +502,7 @@ function Introduction({impressum, setImpressum, privacy, setPrivacy, toggleInfo, setPrivacy(!privacy) } return <> - + {/* */} {gameInfo.isLoading ?
: mobile ? @@ -552,7 +548,7 @@ function Introduction({impressum, setImpressum, privacy, setPrivacy, toggleInfo, function useLevelEditor(codeviewRef, initialCode, initialSelections, onDidChangeContent, onDidChangeSelection) { - const gameId = React.useContext(GameIdContext) + const {gameId} = React.useContext(GameIdContext) const {worldId, levelId} = useContext(WorldLevelIdContext) const [editor, setEditor] = useState(null) diff --git a/client/src/components/navigation.tsx b/client/src/components/navigation.tsx new file mode 100644 index 0000000..2b9e4f5 --- /dev/null +++ b/client/src/components/navigation.tsx @@ -0,0 +1,276 @@ +import * as React from 'react' +import { createContext, useContext, useState } from 'react' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faDownload, faUpload, faEraser, faBook, faBookOpen, faGlobe, faHome, + faArrowRight, faArrowLeft, faXmark, faBars, faCode, + faCircleInfo, faTerminal, faGear, IconDefinition, faShield } from '@fortawesome/free-solid-svg-icons' +import { GameIdContext } from "../app" +import { PageContext, PreferencesContext } from "./infoview/context" +import { useGetGameInfoQuery, useLoadLevelQuery } from '../state/api' +import { downloadProgress } from './popup/erase' +import { useTranslation } from 'react-i18next' +import '../css/navigation.css' +import { PopupContext } from './popup/popup' + +/** SVG github icon */ +function GithubIcon () { + return +} + +/** A button to appear in the navigation (both, top bar or dropdown). */ +export const NavButton: React.FC<{ + icon?: IconDefinition + iconElement?: JSX.Element + text?: string + onClick?: React.MouseEventHandler + title?: string + href?: string + inverted?: boolean + disabled?: boolean +}> = ({icon, iconElement, text, onClick=()=>{}, title, href=null, inverted=false, disabled=false}) => { + return + {iconElement ?? (icon && )}{text && <> {text}} + +} + +/** Context which manages the dropdown navigation */ +const NavigationContext = createContext<{ + navOpen: boolean, + setNavOpen: React.Dispatch> +}>({navOpen: false, setNavOpen: () => {}}) + + +/** Content of the navigation on Desktop during world selection. */ +function DesktopNavigationOverview () { + const { t } = useTranslation() + const {gameId} = useContext(GameIdContext) + const gameInfo = useGetGameInfoQuery({game: gameId}) + + return
+
+
+ {t(gameInfo.data?.title, {ns: gameId})} +
+
+
+} + +/** Content of the navigation on Mobile during world selection. */ +function MobileNavigationOverview () { + const { t } = useTranslation() + const {page, setPage} = useContext(PageContext) + + return
+
+
+ + +
+
+ {page > 0 && + setPage(page - 1)} + inverted={true} /> + } + { page < 2 && + setPage(page+1)} + inverted={true} /> + } +
+ +
+} + +/** Content of the navigation during game selection. */ +function NavigationLandingPage () { + return
+
+
+
+
+} + +/** Content of the navigation on Desktop in a level. */ +function DesktopNavigationLevel () { + const { t } = useTranslation() + const { gameId, worldId, levelId } = useContext(GameIdContext) + const { typewriterMode, setTypewriterMode, lockEditorMode } = useContext(PageContext) + const gameInfo = useGetGameInfoQuery({game: gameId}) + const levelInfo = useLoadLevelQuery({game: gameId, world: worldId, level: levelId}) + + /** toggle input mode if allowed */ + function toggleInputMode(ev: React.MouseEvent) { + if (!lockEditorMode) { + setTypewriterMode(!typewriterMode) + console.log('test') + } + } + + const worldTitle = gameInfo.data?.worlds.nodes[worldId]?.title + const levelTitle = ((levelId == 0) ? + t("Introduction") : + ( + t("Level") + + ` ${levelId}` + + (gameInfo.data?.worldSize[worldId] ? ` / ${gameInfo.data?.worldSize[worldId]}` : '') + + (levelInfo.data?.title ? ` : ${t(levelInfo?.data?.title, {ns: gameId})}` : '') + ) + ) + + return
+
+ {worldTitle ? `${t("World")}: ${t(worldTitle, {ns: gameId})}` : ''} + +
+
+ + { levelTitle + } + +
+
+ { levelId > 0 && + + } + { levelId == gameInfo.data?.worldSize[worldId] ? + : + + } + { levelId > 0 && + toggleInputMode(ev)} + title={lockEditorMode ? t("Editor mode is enforced!") : typewriterMode ? t("Editor mode") : t("Typewriter mode")} /> + } +
+
+} + +/** Content of the navigation on Mobile in a level. */ +function MobileNavigationLevel () { + const { t } = useTranslation() + const {gameId, worldId, levelId} = useContext(GameIdContext) + const {page, setPage} = useContext(PageContext) + const gameInfo = useGetGameInfoQuery({game: gameId}) + const levelInfo = useLoadLevelQuery({game: gameId, world: worldId, level: levelId}) + + let title = worldId ? + ` ${levelId} / ${gameInfo.data?.worldSize[worldId]}`+ (levelInfo?.data?.title && ` : ${t(levelInfo?.data?.title, {ns: gameId})}`) + : + '' + + return
+
+
+ + {title} + +
+
+ setPage(page?0:1)} + inverted={true}/> +
+
+} + +/** The skeleton of the navigation which is the same across all layouts. */ +export function Navigation () { + const { t } = useTranslation() + const { gameId, worldId } = useContext(GameIdContext) + const { mobile } = useContext(PreferencesContext) + const { setPopupContent } = useContext(PopupContext) + + const [navOpen, setNavOpen] = useState(false) + function toggleNav () {setNavOpen(!navOpen)} + + return +} diff --git a/client/src/components/popup/erase.tsx b/client/src/components/popup/erase.tsx index 41f8005..6440425 100644 --- a/client/src/components/popup/erase.tsx +++ b/client/src/components/popup/erase.tsx @@ -1,6 +1,3 @@ -/** - * @fileOverview -*/ import * as React from 'react' import { useSelector } from 'react-redux' import { GameIdContext } from '../../app' @@ -9,10 +6,14 @@ import { deleteProgress, selectProgress } from '../../state/progress' import { downloadFile } from '../world_tree' import { Button } from '../button' import { Trans, useTranslation } from 'react-i18next' +import { useContext } from 'react' +import { PopupContext } from './popup' /** download the current progress (i.e. what's saved in the browser store) */ -export function downloadProgress(gameId: string, gameProgress: any, ev: React.MouseEvent) { - ev.preventDefault() +export function downloadProgress(gameId: string) { + const gameProgress = useSelector(selectProgress(gameId)) + + // ev.preventDefault() downloadFile({ data: JSON.stringify(gameProgress, null, 2), fileName: `lean4game-${gameId}-${new Date().toLocaleDateString()}.json`, @@ -25,26 +26,24 @@ export function downloadProgress(gameId: string, gameProgress: any, ev: React.Mo * `handleClose` is the function to close it again because it's open/closed state is * controlled by the containing element. */ -export function ErasePopup ({handleClose}) { +export function ErasePopup () { let { t } = useTranslation() - const gameId = React.useContext(GameIdContext) + const {gameId} = React.useContext(GameIdContext) const gameProgress = useSelector(selectProgress(gameId)) const dispatch = useAppDispatch() + const { setPopupContent } = useContext(PopupContext) const eraseProgress = () => { dispatch(deleteProgress({game: gameId})) - handleClose() + setPopupContent(null) } const downloadAndErase = (ev) => { - downloadProgress(gameId, gameProgress, ev) + downloadProgress(gameId) eraseProgress() } - return
-
-
-
+ return <>

{t("Delete Progress?")}

Do you want to delete your saved progress irreversibly?

@@ -55,7 +54,6 @@ export function ErasePopup ({handleClose}) {
- -
-
+ + } diff --git a/client/src/components/popup/game_info.tsx b/client/src/components/popup/game_info.tsx index cf0e118..200b564 100644 --- a/client/src/components/popup/game_info.tsx +++ b/client/src/components/popup/game_info.tsx @@ -6,22 +6,21 @@ import { Typography } from '@mui/material' import Markdown from '../markdown' import { Trans, useTranslation } from 'react-i18next' import { GameIdContext } from '../../app' +import { useGetGameInfoQuery } from '../../state/api' /** Pop-up that is displaying the Game Info. * * `handleClose` is the function to close it again because it's open/closed state is * controlled by the containing element. */ -export function InfoPopup ({info, handleClose}: {info: string, handleClose: () => void}) { +export function InfoPopup () { let { t } = useTranslation() - const gameId = React.useContext(GameIdContext) + const {gameId} = React.useContext(GameIdContext) + const gameInfo = useGetGameInfoQuery({game: gameId}) - return
-
-
-
+ return <> - {t(info, {ns: gameId})} + {t(gameInfo.data?.info, {ns: gameId})}

Progress saving

@@ -51,6 +50,5 @@ export function InfoPopup ({info, handleClose}: {info: string, handleClose: () =

-
-
+ } diff --git a/client/src/components/popup/impressum.tsx b/client/src/components/popup/impressum.tsx new file mode 100644 index 0000000..436624d --- /dev/null +++ b/client/src/components/popup/impressum.tsx @@ -0,0 +1,43 @@ +import * as React from 'react' +import { Trans, useTranslation } from 'react-i18next'; + +/** Pop-up that is displayed when opening the privacy policy. */ +export function ImpressumPopup () { + let {t, i18n} = useTranslation() + + function content (lng = i18n.language) { + const tt = i18n.getFixedT(lng); + return +

Impressum

+

+ Contact:
+ Marcus Zibrowius, Jon Eugster
+ Mathematisches Institut der Heinrich-Heine-Universität Düsseldorf
+ Universitätsstr. 1
+ 40225 Düsseldorf
+ Germany
+ +49 211 81-14690
+ Contact Details +

+

+ Legal form:
+ The Heinrich Heine University Düsseldorf is a corporation under public law. It is legally represented by the Rector Prof. Dr. Anja Steinbeck. The responsible supervisory authority is the Ministry of Culture and Science of North Rhine-Westphalia, Völklinger Straße 49, 40221 Düsseldorf. +

+

+ VAT identification number:
+ according to §27a Sales Tax Act
+ DE 811222416 +

+

Impressum HHU

+
+ } + + return <> + {i18n.language != 'en' && <> +

(English version below)

+ {content()} +
+ } + {content('en')} + +} diff --git a/client/src/components/popup/popup.tsx b/client/src/components/popup/popup.tsx new file mode 100644 index 0000000..c75bcdd --- /dev/null +++ b/client/src/components/popup/popup.tsx @@ -0,0 +1,50 @@ +import * as React from 'react' +import { useContext } from 'react' +import { PrivacyPolicyPopup } from './privacy_policy' +import { ImpressumPopup } from './impressum' +import { InfoPopup } from './game_info' +import { ErasePopup } from './erase' +import { PreferencesPopup } from './preferences' +import { UploadPopup } from './upload' + +/** The context which manages if a popup is shown. + * If `popupContent` is `null`, the popup is closed. + */ +export const PopupContext = React.createContext<{ + popupContent: string, + setPopupContent: React.Dispatch> +}>({ + popupContent: null, + setPopupContent: () => {} +}) + +/** To create a new Popup, one needs to add its content as `React.JSX.Element` here + * and then call `setPopupConent(key)` at the place where to popup should be opened. + * + * TODO: The drawback of this design is that there is no check for key missmatches. + * How could that be achieved? + */ +export const Popups = { + "erase": , + "impressum": , + "info": , + "preferences": , + "privacy": , + "upload": , +} + +/** The skeleton for the popups. */ +export function Popup () { + const {popupContent, setPopupContent} = useContext(PopupContext) + function closePopup() { + setPopupContent(null) + } + + return
+
+
+
+ {Popups[popupContent]} +
+
+} diff --git a/client/src/components/popup/preferences.tsx b/client/src/components/popup/preferences.tsx index 303ec30..39401b0 100644 --- a/client/src/components/popup/preferences.tsx +++ b/client/src/components/popup/preferences.tsx @@ -12,12 +12,11 @@ import { IPreferencesContext, PreferencesContext } from "../infoview/context" import ReactCountryFlag from 'react-country-flag'; import { useTranslation } from 'react-i18next'; -export function PreferencesPopup({ handleClose }: { handleClose: () => void }) { +export function PreferencesPopup () { let { t } = useTranslation() const {layout, isSavePreferences, language, setLayout, setIsSavePreferences, setLanguage} = React.useContext(PreferencesContext) - const marks = [ { value: 0, @@ -44,10 +43,7 @@ export function PreferencesPopup({ handleClose }: { handleClose: () => void }) { setLanguage(ev.target.value as IPreferencesContext["language"]) } - return
-
-
-
+ return <>
@@ -118,6 +114,5 @@ export function PreferencesPopup({ handleClose }: { handleClose: () => void }) {
-
-
+ } diff --git a/client/src/components/popup/privacy_policy.tsx b/client/src/components/popup/privacy_policy.tsx index 50a3665..ce102c3 100644 --- a/client/src/components/popup/privacy_policy.tsx +++ b/client/src/components/popup/privacy_policy.tsx @@ -1,19 +1,11 @@ -/** - * @fileOverview The impressum/privacy policy -*/ -import { faShield } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import * as React from 'react' import { Trans, useTranslation } from 'react-i18next'; /** Pop-up that is displayed when opening the privacy policy. - * - * `handleClose` is the function to close it again because it's open/closed state is - * controlled by the containing element. * * Note: Do not translate the Impressum! */ -export function PrivacyPolicyPopup ({handleClose}: {handleClose: () => void}) { +export function PrivacyPolicyPopup () { let {t, i18n} = useTranslation() function content (lng = i18n.language) { const tt = i18n.getFixedT(lng); @@ -45,68 +37,12 @@ export function PrivacyPolicyPopup ({handleClose}: {handleClose: () => void}) { } - - return
-
-
-
- {i18n.language != 'en' && <> -

(English version below)

- {content()} -
- } - {content('en')} -
-
-} - -/** Pop-up that is displayed when opening the privacy policy. - * - * `handleClose` is the function to close it again because it's open/closed state is - * controlled by the containing element. - * - * Note: Do not translate the Impressum! - */ -export function ImpressumPopup ({handleClose}: {handleClose: () => void}) { - let {t, i18n} = useTranslation() - - function content (lng = i18n.language) { - const tt = i18n.getFixedT(lng); - return -

Impressum

-

- Contact:
- Marcus Zibrowius, Jon Eugster
- Mathematisches Institut der Heinrich-Heine-Universität Düsseldorf
- Universitätsstr. 1
- 40225 Düsseldorf
- Germany
- +49 211 81-14690
- Contact Details -

-

- Legal form:
- The Heinrich Heine University Düsseldorf is a corporation under public law. It is legally represented by the Rector Prof. Dr. Anja Steinbeck. The responsible supervisory authority is the Ministry of Culture and Science of North Rhine-Westphalia, Völklinger Straße 49, 40221 Düsseldorf. -

-

- VAT identification number:
- according to §27a Sales Tax Act
- DE 811222416 -

-

Impressum HHU

-
- } - - return
-
-
-
- {i18n.language != 'en' && <> -

(English version below)

- {content()} -
- } - {content('en')} -
-
+ return <> + {i18n.language != 'en' && <> +

(English version below)

+ {content()} +
+ } + {content('en')} + } diff --git a/client/src/components/popup/upload.tsx b/client/src/components/popup/upload.tsx index 0a9d9ec..7519926 100644 --- a/client/src/components/popup/upload.tsx +++ b/client/src/components/popup/upload.tsx @@ -9,20 +9,24 @@ import { GameProgressState, loadProgress, selectProgress } from '../../state/pro import { downloadFile } from '../world_tree' import { Button } from '../button' import { Trans, useTranslation } from 'react-i18next' +import { PopupContext } from './popup' +import { useContext } from 'react' /** Pop-up that is displaying the Game Info. * * `handleClose` is the function to close it again because it's open/closed state is * controlled by the containing element. */ -export function UploadPopup ({handleClose}) { +export function UploadPopup () { let { t } = useTranslation() const [file, setFile] = React.useState(); - const gameId = React.useContext(GameIdContext) + const {gameId} = React.useContext(GameIdContext) const gameProgress = useSelector(selectProgress(gameId)) const dispatch = useAppDispatch() + const { setPopupContent } = useContext(PopupContext) + const handleFileChange = (e) => { if (e.target.files) { setFile(e.target.files[0]) @@ -39,7 +43,7 @@ export function UploadPopup ({handleClose}) { console.debug("Json Data", data) dispatch(loadProgress({game: gameId, data: data})) } - handleClose() + setPopupContent(null) // close the popup } /** Download the current progress (i.e. what's saved in the browser store) */ @@ -53,10 +57,7 @@ export function UploadPopup ({handleClose}) { } - return
-
-
-
+ return <>

{t("Upload Saved Progress")}

Select a JSON file with the saved game progress to load your progress.

@@ -70,6 +71,5 @@ export function UploadPopup ({handleClose}) {

-
-
+ } diff --git a/client/src/components/welcome.tsx b/client/src/components/welcome.tsx index 8aaea5e..4b061b4 100644 --- a/client/src/components/welcome.tsx +++ b/client/src/components/welcome.tsx @@ -10,18 +10,17 @@ import { useAppDispatch, useAppSelector } from '../hooks' import { changedOpenedIntro, selectOpenedIntro } from '../state/progress' import { useGetGameInfoQuery, useLoadInventoryOverviewQuery } from '../state/api' import { Button } from './button' -import { PreferencesContext } from './infoview/context' +import { PageContext, PreferencesContext } from './infoview/context' import { InventoryPanel } from './inventory' import { ErasePopup } from './popup/erase' import { InfoPopup } from './popup/game_info' -import { ImpressumPopup, PrivacyPolicyPopup } from './popup/privacy_policy' +import { PrivacyPolicyPopup } from './popup/privacy_policy' import { RulesHelpPopup } from './popup/rules_help' import { UploadPopup } from './popup/upload' import { PreferencesPopup} from "./popup/preferences" import { WorldTreePanel } from './world_tree' import '../css/welcome.css' -import { WelcomeAppBar } from './app_bar' import { Hint } from './hints' import i18next from 'i18next' import { useTranslation } from 'react-i18next' @@ -30,7 +29,7 @@ import { useTranslation } from 'react-i18next' /** the panel showing the game's introduction text */ function IntroductionPanel({introduction, setPageNumber}: {introduction: string, setPageNumber}) { const {mobile} = React.useContext(PreferencesContext) - const gameId = React.useContext(GameIdContext) + const {gameId} = React.useContext(GameIdContext) let { t } = useTranslation() @@ -68,7 +67,7 @@ function IntroductionPanel({introduction, setPageNumber}: {introduction: string, /** main page of the game showing among others the tree of worlds/levels */ function Welcome() { - const gameId = React.useContext(GameIdContext) + const {gameId} = React.useContext(GameIdContext) // Load the namespace of the game i18next.loadNamespaces(gameId) @@ -81,7 +80,12 @@ function Welcome() { // For mobile only const openedIntro = useAppSelector(selectOpenedIntro(gameId)) - const [pageNumber, setPageNumber] = React.useState(openedIntro ? 1 : 0) + + const {page, setPage} = React.useContext(PageContext) + + // TODO: recover `openedIntro` functionality + + // const [pageNumber, setPageNumber] = React.useState(openedIntro ? 1 : 0) // pop-ups const [eraseMenu, setEraseMenu] = React.useState(false) @@ -118,15 +122,15 @@ function Welcome() { : <> - + toggleInfo={toggleInfo} togglePreferencesPopup={togglePreferencesPopup}/> */}
{ mobile ?
- {(pageNumber == 0 ? - - : pageNumber == 1 ? + {(page == 0 ? + + : page == 1 ? : @@ -135,20 +139,13 @@ function Welcome() {
: - + }
- {impressum ? : null} - {privacy ? : null} - {rulesHelp ? : null} - {eraseMenu? : null} - {uploadMenu? : null} - {info ? : null} - {preferencesPopup ? : null} } diff --git a/client/src/components/world_tree.tsx b/client/src/components/world_tree.tsx index 6e57a57..3d1bc92 100644 --- a/client/src/components/world_tree.tsx +++ b/client/src/components/world_tree.tsx @@ -65,7 +65,7 @@ export function LevelIcon({ world, level, position, completed, unlocked, worldSi // Sinus-Satz: (1.1*r) / sin(β/2) = R / sin(π/2) let R = 1.1 * r / Math.sin(beta/2) - const gameId = React.useContext(GameIdContext) + const {gameId} = React.useContext(GameIdContext) const difficulty = useSelector(selectDifficulty(gameId)) const levelDisabled = (difficulty >= 2 && !(unlocked || completed)) @@ -137,7 +137,7 @@ export function WorldIcon({world, title, position, completedLevels, difficulty, nextLevel = 0 } let playable = difficulty <= 1 || completed || unlocked - const gameId = React.useContext(GameIdContext) + const {gameId} = React.useContext(GameIdContext) return div { + /* border: 1px solid red; */ + text-align: center; +} + +.nav-title-left, .nav-title-right { + flex-grow: 0; + flex-shrink: 0; + display: flex; + align-items: center; +} + +.nav-title-middle { + flex-grow: 1; + flex-shrink: 1; + margin-left: .5rem; + margin-right: .5rem; +} + +.nav-title { + color: white; + font-weight: 500; + font-size: 1.3rem; + display: inline-block; + margin: 0; + /* margin: 0 1em; */ +} + +/* fix to make toggle buttons work */ +.svg-inline--fa { + width: 1em; +} + +/* TODO */ +.nav-button:not(.btn-inverted) { + font-size: 1.3rem; +} + +.dropdown { + z-index: 10; +} + +.dropdown { + position: absolute; + display: flex; + flex-direction: column; + right: 0; + top: 100%; + background-color: #fff; + z-index: 5; + border-top: 1px solid rgba(0, 0, 0, 0.1); + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + box-shadow: -.1rem .3rem .3rem 0 rgba(0, 0, 0, 0.1); +} + +.dropdown .svg-inline--fa { + width: 1.8rem; +} diff --git a/client/src/index.tsx b/client/src/index.tsx index 5748fb9..f88ce46 100644 --- a/client/src/index.tsx +++ b/client/src/index.tsx @@ -16,23 +16,21 @@ import './i18n'; // If `VITE_LEAN4GAME_SINGLE` is set to true, then `/` should be redirected to // `/g/local/game`. This is used for the devcontainer setup let single_game = (import.meta.env.VITE_LEAN4GAME_SINGLE == "true") -let root_object: RouteObject = single_game ? { - path: "/", - loader: () => redirect("/g/local/game") -} : { - path: "/", - element: , - errorElement: , - children: [ - { - path: "/", - element: , - } - ] -} +// let root_object: RouteObject = single_game ? { +// path: "/", +// loader: () => redirect("/g/local/game") +// } + +let landing_page: RouteObject = single_game ? { + path: "/", + loader: () => redirect("/g/local/game") + } : { + path: "/", + element: , + } const router = createHashRouter([ - root_object, + // root_object, { // For backwards compatibility path: "/game/nng", @@ -44,10 +42,11 @@ const router = createHashRouter([ loader: () => redirect("/g/leanprover-community/NNG4") }, { - path: "/g/:owner/:repo", + path: "/", element: , errorElement: , children: [ + landing_page, { path: "/g/:owner/:repo", element: , diff --git a/client/src/state/progress.ts b/client/src/state/progress.ts index c01eca0..1f5961d 100644 --- a/client/src/state/progress.ts +++ b/client/src/state/progress.ts @@ -205,7 +205,7 @@ export function selectOpenedIntro(game: string) { /** return typewriter mode for the current game if it exists */ export function selectTypewriterMode(game: string) { return (state) => { - return state.progress.games[game.toLowerCase()]?.typewriterMode ?? true + return state.progress.games[game?.toLowerCase()]?.typewriterMode ?? true } } diff --git a/index.html b/index.html index 4d2935b..2fbf556 100644 --- a/index.html +++ b/index.html @@ -5,11 +5,6 @@ Lean Game Server -