Merge branch 'refactor'

pull/139/merge
joneugster 1 year ago
commit c2b5754371

@ -6,33 +6,23 @@ import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css'; import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css'; import '@fontsource/roboto/700.css';
import './reset.css'; import './css/reset.css';
import './app.css'; import './css/app.css';
import { MobileContext } from './components/infoview/context'; import { MobileContext } from './components/infoview/context';
import { useWindowDimensions } from './window_width'; import { useWindowDimensions } from './window_width';
import { selectOpenedIntro } from './state/progress';
import { useSelector } from 'react-redux';
export const GameIdContext = React.createContext<string>(undefined); export const GameIdContext = React.createContext<string>(undefined);
function App() { function App() {
const params = useParams() const params = useParams()
const gameId = "g/" + params.owner + "/" + params.repo const gameId = "g/" + params.owner + "/" + params.repo
// TODO: Make mobileLayout be changeable in settings
// TODO: Handle resize Events
const {width, height} = useWindowDimensions() const {width, height} = useWindowDimensions()
const [mobile, setMobile] = React.useState(width < 800) const [mobile, setMobile] = React.useState(width < 800)
// On mobile, there are multiple pages on the welcome page to switch between
const openedIntro = useSelector(selectOpenedIntro(gameId))
// On mobile, there are multiple pages to switch between
const [pageNumber, setPageNumber] = React.useState(openedIntro ? 1 : 0)
return ( return (
<div className="app"> <div className="app">
<GameIdContext.Provider value={gameId}> <GameIdContext.Provider value={gameId}>
<MobileContext.Provider value={{mobile, setMobile, pageNumber, setPageNumber}}> <MobileContext.Provider value={{mobile, setMobile}}>
<Outlet /> <Outlet />
</MobileContext.Provider> </MobileContext.Provider>
</GameIdContext.Provider> </GameIdContext.Provider>

@ -1,45 +1,45 @@
/**
* @file contains the navigation bars of the app.
*/
import * as React from 'react' 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 } from '@fortawesome/free-solid-svg-icons'
import { GameIdContext } from "../app" import { GameIdContext } from "../app"
import { InputModeContext, MobileContext, WorldLevelIdContext } from "./infoview/context" import { InputModeContext, MobileContext, WorldLevelIdContext } from "./infoview/context"
import { GameInfo, useGetGameInfoQuery } from '../state/api' import { GameInfo, useGetGameInfoQuery } from '../state/api'
import { changedOpenedIntro, selectCompleted, selectDifficulty, selectProgress } from '../state/progress' import { changedOpenedIntro, selectCompleted, selectDifficulty, selectProgress } from '../state/progress'
import { useSelector } from 'react-redux'
import { useAppDispatch, useAppSelector } from '../hooks' import { useAppDispatch, useAppSelector } from '../hooks'
import { Button } from './button' import { Button } from './button'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { downloadProgress } from './popup/erase'
import { faDownload, faUpload, faEraser, faBook, faBookOpen, faGlobe, faHome, faArrowRight, faArrowLeft, faXmark, faBars, faCode, faCircleInfo, faTerminal } from '@fortawesome/free-solid-svg-icons'
import { PrivacyPolicyPopup } from './popup/privacy_policy'
import { WorldSelectionMenu, downloadFile } from './world_tree'
/** navigation to switch between pages on mobile */ /** navigation buttons for mobile welcome page to switch between intro/tree/inventory. */
function MobileNav({pageNumber, setPageNumber}: function MobileNavButtons({pageNumber, setPageNumber}:
{ pageNumber: number, { pageNumber: number,
setPageNumber: any}) { setPageNumber: any}) {
const gameId = React.useContext(GameIdContext) const gameId = React.useContext(GameIdContext)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
let prevText = {0 : null, 1: "Intro", 2: null}[pageNumber] // if `prevText` or `prevIcon` is set, show a button to go back
let prevIcon = {0 : null, 1: null, 2: faBookOpen}[pageNumber] let prevText = {0: null, 1: "Intro", 2: null}[pageNumber]
let prevTitle = { let prevIcon = {0: null, 1: null, 2: faBookOpen}[pageNumber]
0: null, let prevTitle = {0: null, 1: "Game Introduction", 2: "World selection"}[pageNumber]
1: "Game Introduction", // if `nextText` or `nextIcon` is set, show a button to go forward
2: "World selection"}[pageNumber] let nextText = {0: "Start", 1: null, 2: null}[pageNumber]
let nextText = {0 : "Start", 1: null, 2: null}[pageNumber] let nextIcon = {0: null, 1: faBook, 2: null}[pageNumber]
let nextIcon = {0 : null, 1: faBook, 2: null}[pageNumber] let nextTitle = {0: "World selection", 1: "Inventory", 2: null}[pageNumber]
let nextTitle = {
0: "World selection",
1: "Inventory",
2: null}[pageNumber]
return <> return <>
{(prevText || prevTitle || prevIcon) && {(prevText || prevIcon) &&
<Button className="btn btn-inverted toggle-width" to={pageNumber == 0 ? "/" : ""} inverted="true" title={prevTitle} <Button className="btn btn-inverted toggle-width" to={pageNumber == 0 ? "/" : ""}
inverted="true" title={prevTitle}
onClick={() => {pageNumber == 0 ? null : setPageNumber(pageNumber - 1)}}> onClick={() => {pageNumber == 0 ? null : setPageNumber(pageNumber - 1)}}>
{prevIcon && <FontAwesomeIcon icon={prevIcon} />} {prevIcon && <FontAwesomeIcon icon={prevIcon} />}
{prevText && `${prevText}`} {prevText && `${prevText}`}
</Button> </Button>
} }
{(nextText || nextTitle || nextIcon) && {(nextText || nextIcon) &&
<Button className="btn btn-inverted toggle-width" to="" inverted="true" <Button className="btn btn-inverted toggle-width" to="" inverted="true"
title={nextTitle} onClick={() => { title={nextTitle} onClick={() => {
console.log(`page number: ${pageNumber}`) console.log(`page number: ${pageNumber}`)
@ -52,357 +52,203 @@ function MobileNav({pageNumber, setPageNumber}:
</> </>
} }
export function WelcomeAppBar({gameInfo, toggleImpressum, openEraseMenu, openUploadMenu, toggleInfo} : { /** button to toggle dropdown menu. */
gameInfo: GameInfo, function MenuButton({navOpen, setNavOpen}) {
toggleImpressum: any, return <Button to="" className="btn toggle-width" id="menu-btn" onClick={(ev) => {setNavOpen(!navOpen)}}>
openEraseMenu: any, {navOpen ? <FontAwesomeIcon icon={faXmark} /> : <FontAwesomeIcon icon={faBars} />}
openUploadMenu: any, </Button>
toggleInfo: any }
}) {
/** 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 gameId = React.useContext(GameIdContext) const gameId = React.useContext(GameIdContext)
const {mobile, pageNumber, setPageNumber} = React.useContext(MobileContext) const {worldId, levelId} = React.useContext(WorldLevelIdContext)
const [navOpen, setNavOpen] = React.useState(false) return (levelId < worldSize ?
<Button inverted="true"
to={`/${gameId}/world/${worldId}/level/${levelId + 1}`} title="next level"
disabled={difficulty >= 2 && !(completed || levelId == 0)}
onClick={() => setNavOpen(false)}>
<FontAwesomeIcon icon={faArrowRight} />&nbsp;{levelId ? "Next" : "Start"}
</Button>
:
<Button to={`/${gameId}`} inverted="true" title="back to world selection" id="home-btn">
<FontAwesomeIcon icon={faHome} />&nbsp;Leave World
</Button>
)
}
/** button to go one level back.
* only renders if the current level id is > 0.
*/
function PreviousButton({setNavOpen}) {
const gameId = React.useContext(GameIdContext)
const {worldId, levelId} = React.useContext(WorldLevelIdContext)
return (levelId > 0 && <>
<Button disabled={levelId <= 0} inverted="true"
to={`/${gameId}/world/${worldId}/level/${levelId - 1}`}
title="previous level"
onClick={() => setNavOpen(false)}>
<FontAwesomeIcon icon={faArrowLeft} />&nbsp;Previous
</Button>
</>)
}
/** button to toggle between editor and typewriter */
function InputModeButton({setNavOpen, isDropdown}) {
const {levelId} = React.useContext(WorldLevelIdContext)
const {typewriterMode, setTypewriterMode, lockInputMode} = React.useContext(InputModeContext)
/** Download the current progress (i.e. what's saved in the browser store) */ /** toggle input mode if allowed */
const gameProgress = useSelector(selectProgress(gameId)) function toggleInputMode(ev: React.MouseEvent) {
const downloadProgress = (e) => { if (!lockInputMode){
e.preventDefault() setTypewriterMode(!typewriterMode)
downloadFile({ setNavOpen(false)
data: JSON.stringify(gameProgress, null, 2), }
fileName: `lean4game-${gameId}-${new Date().toLocaleDateString()}.json`,
fileType: 'text/json',
})
} }
return <Button
className={`btn btn-inverted ${isDropdown? '' : 'toggle-width'}`} disabled={levelId <= 0 || lockInputMode}
inverted="true" to=""
onClick={(ev) => toggleInputMode(ev)}
title={lockInputMode ? "Editor mode is enforced!" : typewriterMode ? "Editor mode" : "Typewriter mode"}>
<FontAwesomeIcon icon={typewriterMode ? faCode : faTerminal} />
{isDropdown && (typewriterMode ? <>&nbsp;Editor mode</> : <>&nbsp;Typewriter mode</>)}
</Button>
}
return <div className="app-bar" > /** button to toggle iimpressum popup */
<> function ImpressumButton({setNavOpen, toggleImpressum, isDropdown}) {
<div> return <Button className="btn btn-inverted toggle-width"
<Button inverted="false" title="back to games selection" to="/"> title="information, Impressum, privacy policy" inverted="true" to="" onClick={(ev) => {toggleImpressum(ev); setNavOpen(false)}}>
<FontAwesomeIcon icon={faArrowLeft} />&nbsp;<FontAwesomeIcon icon={faGlobe} /> <FontAwesomeIcon icon={faCircleInfo} />
</Button> {isDropdown && <>&nbsp;Info &amp; Impressum</>}
<span className="app-bar-title"></span> </Button>
</div> }
<div>
<span className="app-bar-title">
{mobile ? '' : gameInfo?.title}
</span>
</div>
<div className="nav-btns">
{mobile && <>
{/* BUTTONS for MOBILE */}
<MobileNav pageNumber={pageNumber} setPageNumber={setPageNumber} />
</>} /** button to go back to welcome page */
function HomeButton({isDropdown}) {
const gameId = React.useContext(GameIdContext)
return <Button to={`/${gameId}`} inverted="true" title="back to world selection" id="home-btn">
<FontAwesomeIcon icon={faHome} />
{isDropdown && <>&nbsp;Home</>}
</Button>
}
<Button to="" className="btn toggle-width" id="menu-btn" onClick={(ev) => {setNavOpen(!navOpen)}} > /** button in mobile level to toggle inventory.
{navOpen ? <FontAwesomeIcon icon={faXmark} /> : <FontAwesomeIcon icon={faBars} />} * only displays a button if `setPageNumber` is set.
</Button> */
function InventoryButton({pageNumber, setPageNumber}) {
return (setPageNumber &&
<Button to="" className="btn btn-inverted toggle-width"
title={pageNumber ? "close inventory" : "show inventory"}
inverted="true" onClick={() => {setPageNumber(pageNumber ? 0 : 1)}}>
<FontAwesomeIcon icon={pageNumber ? faBookOpen : faBook} />
</Button>
)
}
</div> /** the navigation bar on the welcome page */
<div className={'menu dropdown' + (navOpen ? '' : ' hidden')}> export function WelcomeAppBar({pageNumber, setPageNumber, gameInfo, toggleImpressum, toggleEraseMenu, toggleUploadMenu, toggleInfo} : {
{/* {levelId < gameInfo.data?.worldSize[worldId] && pageNumber: number,
<Button inverted="true" setPageNumber: any,
to={`/${gameId}/world/${worldId}/level/${levelId + 1}`} title="next level" gameInfo: GameInfo,
disabled={difficulty >= 2 && !(completed || levelId == 0)} toggleImpressum: any,
onClick={() => setNavOpen(false)}> toggleEraseMenu: any,
<FontAwesomeIcon icon={faArrowRight} />&nbsp;{levelId ? "Next" : "Start"} toggleUploadMenu: any,
</Button> toggleInfo: any
} }) {
{levelId > 0 && <> const gameId = React.useContext(GameIdContext)
<Button disabled={levelId <= 0} inverted="true" const gameProgress = useAppSelector(selectProgress(gameId))
to={`/${gameId}/world/${worldId}/level/${levelId - 1}`} const {mobile} = React.useContext(MobileContext)
title="previous level" const [navOpen, setNavOpen] = React.useState(false)
onClick={() => setNavOpen(false)}>
<FontAwesomeIcon icon={faArrowLeft} />&nbsp;Previous
</Button>
</>}
<Button to={`/${gameId}`} inverted="true" title="back to world selection">
<FontAwesomeIcon icon={faHome} />&nbsp;Home
</Button>
<Button disabled={levelId <= 0} inverted="true" to=""
onClick={(ev) => { setTypewriterMode(!typewriterMode); setNavOpen(false) }}
title="toggle Editor mode">
<FontAwesomeIcon icon={faCode} />&nbsp;Toggle Editor
</Button> */}
<Button title="Game Info & Credits" inverted="true" to="" onClick={(ev) => {toggleInfo(); setNavOpen(false)}}> return <div className="app-bar">
<FontAwesomeIcon icon={faCircleInfo} />&nbsp;Game Info <div>
</Button> <Button inverted="false" title="back to games selection" to="/">
<Button title="Clear Progress" inverted="true" to="" onClick={(ev) => {openEraseMenu(); setNavOpen(false)}}> <FontAwesomeIcon icon={faArrowLeft} />&nbsp;<FontAwesomeIcon icon={faGlobe} />
<FontAwesomeIcon icon={faEraser} />&nbsp;Erase </Button>
</Button> <span className="app-bar-title"></span>
<Button title="Download Progress" inverted="true" to="" onClick={(ev) => {downloadProgress(ev); setNavOpen(false)}}> </div>
<FontAwesomeIcon icon={faDownload} />&nbsp;Download <div>
</Button> {!mobile && <span className="app-bar-title">{gameInfo?.title}</span>}
<Button title="Load Progress from JSON" inverted="true" to="" onClick={(ev) => {openUploadMenu(); setNavOpen(false)}}> </div>
<FontAwesomeIcon icon={faUpload} />&nbsp;Upload <div className="nav-btns">
</Button> {mobile && <MobileNavButtons pageNumber={pageNumber} setPageNumber={setPageNumber} />}
<Button title="Impressum, privacy policy" inverted="true" to="" onClick={(ev) => {toggleImpressum(); setNavOpen(false)}}> <MenuButton navOpen={navOpen} setNavOpen={setNavOpen} />
<FontAwesomeIcon icon={faCircleInfo} />&nbsp;Impressum </div>
</Button> <div className={'menu dropdown' + (navOpen ? '' : ' hidden')}>
</div> <Button title="Game Info & Credits" inverted="true" to="" onClick={() => {toggleInfo(); setNavOpen(false)}}>
</> <FontAwesomeIcon icon={faCircleInfo} />&nbsp;Game Info
</Button>
<Button title="Clear Progress" inverted="true" to="" onClick={() => {toggleEraseMenu(); setNavOpen(false)}}>
<FontAwesomeIcon icon={faEraser} />&nbsp;Erase
</Button>
<Button title="Download Progress" inverted="true" to="" onClick={(ev) => {downloadProgress(gameId, gameProgress, ev); setNavOpen(false)}}>
<FontAwesomeIcon icon={faDownload} />&nbsp;Download
</Button>
<Button title="Load Progress from JSON" inverted="true" to="" onClick={() => {toggleUploadMenu(); setNavOpen(false)}}>
<FontAwesomeIcon icon={faUpload} />&nbsp;Upload
</Button>
<Button title="Impressum, privacy policy" inverted="true" to="" onClick={() => {toggleImpressum(); setNavOpen(false)}}>
<FontAwesomeIcon icon={faCircleInfo} />&nbsp;Impressum
</Button>
</div>
</div> </div>
} }
// /** The menu that is shown next to the world selection graph */ /** the navigation bar in a level */
// function WorldSelectionMenu() { export function LevelAppBar({isLoading, levelTitle, toggleImpressum, pageNumber=undefined, setPageNumber=undefined} : {
// const [file, setFile] = React.useState<File>(); isLoading: boolean,
levelTitle: string,
// const gameId = React.useContext(GameIdContext) toggleImpressum: any,
// const store = useStore() pageNumber?: number,
// const difficulty = useSelector(selectDifficulty(gameId)) setPageNumber?: any,
}) {
// /* state variables to toggle the pop-up menus */
// const [eraseMenu, setEraseMenu] = React.useState(false);
// const openEraseMenu = () => setEraseMenu(true);
// const closeEraseMenu = () => setEraseMenu(false);
// const [uploadMenu, setUploadMenu] = React.useState(false);
// const openUploadMenu = () => setUploadMenu(true);
// const closeUploadMenu = () => setUploadMenu(false);
// const gameProgress = useSelector(selectProgress(gameId))
// const dispatch = useAppDispatch()
// /** Download the current progress (i.e. what's saved in the browser store) */
// const downloadProgress = (e) => {
// e.preventDefault()
// downloadFile({
// data: JSON.stringify(gameProgress, null, 2),
// fileName: `lean4game-${gameId}-${new Date().toLocaleDateString()}.json`,
// fileType: 'text/json',
// })
// }
// const handleFileChange = (e) => {
// if (e.target.files) {
// setFile(e.target.files[0])
// }
// }
// /** Upload progress from a */
// const uploadProgress = (e) => {
// if (!file) {return}
// const fileReader = new FileReader()
// fileReader.readAsText(file, "UTF-8")
// fileReader.onload = (e) => {
// const data = JSON.parse(e.target.result.toString()) as GameProgressState
// console.debug("Json Data", data)
// dispatch(loadProgress({game: gameId, data: data}))
// }
// closeUploadMenu()
// }
// const eraseProgress = () => {
// dispatch(deleteProgress({game: gameId}))
// closeEraseMenu()
// }
// const downloadAndErase = (e) => {
// downloadProgress(e)
// eraseProgress()
// }
// function label(x : number) {
// return x == 0 ? 'none' : x == 1 ? 'lax' : 'regular'
// }
// return <nav className="world-selection-menu">
// <Button onClick={downloadProgress} title="Download game progress" to=""><FontAwesomeIcon icon={faDownload} /></Button>
// <Button title="Load game progress from JSON" onClick={openUploadMenu} to=""><FontAwesomeIcon icon={faUpload} /></Button>
// <Button title="Clear game progress" to="" onClick={openEraseMenu}><FontAwesomeIcon icon={faEraser} /></Button>
// <div className="slider-wrap">
// <span className="difficulty-label">Game Rules:</span>
// <Slider
// title="Game Rules:&#10;- regular: 🔐 levels, 🔐 tactics&#10;- lax: 🔓 levels, 🔐 tactics&#10;- none: 🔓 levels, 🔓 tactics"
// min={0} max={2}
// aria-label="Game Rules"
// defaultValue={difficulty}
// marks={[
// {value: 0, label: label(0)},
// {value: 1, label: label(1)},
// {value: 2, label: label(2)}
// ]}
// valueLabelFormat={label}
// getAriaValueText={label}
// valueLabelDisplay="auto"
// onChange={(ev, val: number) => {
// dispatch(changedDifficulty({game: gameId, difficulty: val}))
// }}
// ></Slider>
// </div>
// {eraseMenu?
// <div className="modal-wrapper">
// <div className="modal-backdrop" onClick={closeEraseMenu} />
// <div className="modal">
// <div className="codicon codicon-close modal-close" onClick={closeEraseMenu}></div>
// <h2>Delete Progress?</h2>
// <p>Do you want to delete your saved progress irreversibly?</p>
// <p>
// (This deletes your proofs and your collected inventory.
// Saves from other games are not deleted.)
// </p>
// <Button onClick={eraseProgress} to="">Delete</Button>
// <Button onClick={downloadAndErase} to="">Download & Delete</Button>
// <Button onClick={closeEraseMenu} to="">Cancel</Button>
// </div>
// </div> : null}
// {uploadMenu ?
// <div className="modal-wrapper">
// <div className="modal-backdrop" onClick={closeUploadMenu} />
// <div className="modal">
// <div className="codicon codicon-close modal-close" onClick={closeUploadMenu}></div>
// <h2>Upload Saved Progress</h2>
// <p>Select a JSON file with the saved game progress to load your progress.</p>
// <p><b>Warning:</b> This will delete your current game progress!
// Consider <a className="download-link" onClick={downloadProgress} >downloading your current progress</a> first!</p>
// <input type="file" onChange={handleFileChange}/>
// <Button to="" onClick={uploadProgress}>Load selected file</Button>
// </div>
// </div> : null}
// </nav>
// }
/** The top-navigation bar */
export function LevelAppBar({
isLoading, levelTitle, impressum, toggleImpressum,
pageNumber = undefined, setPageNumber = undefined, lockEditorMode=false}) {
const gameId = React.useContext(GameIdContext) const gameId = React.useContext(GameIdContext)
const {worldId, levelId} = React.useContext(WorldLevelIdContext) const {worldId, levelId} = React.useContext(WorldLevelIdContext)
const gameInfo = useGetGameInfoQuery({game: gameId})
const {mobile} = React.useContext(MobileContext) const {mobile} = React.useContext(MobileContext)
const difficulty = useSelector(selectDifficulty(gameId))
const completed = useAppSelector(selectCompleted(gameId, worldId, levelId))
const { typewriterMode, setTypewriterMode } = React.useContext(InputModeContext)
const [navOpen, setNavOpen] = React.useState(false) const [navOpen, setNavOpen] = React.useState(false)
const gameInfo = useGetGameInfoQuery({game: gameId})
const completed = useAppSelector(selectCompleted(gameId, worldId, levelId))
const difficulty = useAppSelector(selectDifficulty(gameId))
function toggleEditor(ev) { let worldTitle = gameInfo.data?.worlds.nodes[worldId].title
if (!lockEditorMode){
setTypewriterMode(!typewriterMode)
setNavOpen(false)
}
}
return <div className="app-bar" style={isLoading ? {display: "none"} : null} > return <div className="app-bar" style={isLoading ? {display: "none"} : null} >
{mobile ? {mobile ?
<> <>
{/* MOBILE VERSION */} {/* MOBILE VERSION */}
<div> <div>
<span className="app-bar-title"> <span className="app-bar-title">{levelTitle}</span>
{levelTitle}
</span>
</div> </div>
<div className="nav-btns"> <div className="nav-btns">
{mobile && pageNumber == 0 ? <InventoryButton pageNumber={pageNumber} setPageNumber={setPageNumber}/>
<Button to="" className="btn btn-inverted toggle-width" <MenuButton navOpen={navOpen} setNavOpen={setNavOpen}/>
title="show inventory" inverted="true" onClick={() => {setPageNumber(1)}}>
<FontAwesomeIcon icon={faBook}/>
</Button>
: pageNumber == 1 &&
<Button className="btn btn-inverted toggle-width" to=""
title="close inventory" inverted="true" onClick={() => {setPageNumber(0)}}>
<FontAwesomeIcon icon={faBookOpen}/>
</Button>
}
<Button to="" className="btn toggle-width" id="menu-btn" onClick={(ev) => {setNavOpen(!navOpen)}} >
{navOpen ? <FontAwesomeIcon icon={faXmark} /> : <FontAwesomeIcon icon={faBars} />}
</Button>
</div> </div>
<div className={'menu dropdown' + (navOpen ? '' : ' hidden')}> <div className={'menu dropdown' + (navOpen ? '' : ' hidden')}>
{levelId < gameInfo.data?.worldSize[worldId] && <NextButton worldSize={gameInfo.data?.worldSize[worldId]} difficulty={difficulty} completed={completed} setNavOpen={setNavOpen} />
<Button inverted="true" <PreviousButton setNavOpen={setNavOpen} />
to={`/${gameId}/world/${worldId}/level/${levelId + 1}`} title="next level" <HomeButton isDropdown={true} />
disabled={difficulty >= 2 && !(completed || levelId == 0)} <InputModeButton setNavOpen={setNavOpen} isDropdown={true}/>
onClick={() => setNavOpen(false)}> <ImpressumButton setNavOpen={setNavOpen} toggleImpressum={toggleImpressum} isDropdown={true} />
<FontAwesomeIcon icon={faArrowRight} />&nbsp;{levelId ? "Next" : "Start"}
</Button>
}
{levelId > 0 && <>
<Button disabled={levelId <= 0} inverted="true"
to={`/${gameId}/world/${worldId}/level/${levelId - 1}`}
title="previous level"
onClick={() => setNavOpen(false)}>
<FontAwesomeIcon icon={faArrowLeft} />&nbsp;Previous
</Button>
</>}
<Button to={`/${gameId}`} inverted="true" title="back to world selection">
<FontAwesomeIcon icon={faHome} />&nbsp;Home
</Button>
<Button disabled={levelId <= 0} inverted="true" to=""
onClick={(ev) => { setTypewriterMode(!typewriterMode); setNavOpen(false) }}
title={typewriterMode ? "Editor mode" : "Typewriter mode"}>
<FontAwesomeIcon icon={typewriterMode ? faCode : faTerminal} />
&nbsp;{typewriterMode ? "Editor mode" : "Typewriter mode"}
</Button>
<Button title="information, Impressum, privacy policy" inverted="true" to="" onClick={(ev) => {toggleImpressum(ev); setNavOpen(false)}}>
<FontAwesomeIcon icon={faCircleInfo} />&nbsp;Info &amp; Impressum
</Button>
</div> </div>
</> </> :
:
<> <>
{/* DESKTOP VERSION */} {/* DESKTOP VERSION */}
<div> <div>
<Button to={`/${gameId}`} inverted="true" title="back to world selection" id="home-btn"> <HomeButton isDropdown={false} />
<FontAwesomeIcon icon={faHome} /> <span className="app-bar-title">{worldTitle && `World: ${worldTitle}`}</span>
</Button>
<span className="app-bar-title">
{gameInfo.data?.worlds.nodes[worldId].title && `World: ${gameInfo.data?.worlds.nodes[worldId].title}`}
</span>
</div> </div>
<div> <div>
<span className="app-bar-title"> <span className="app-bar-title">{levelTitle}</span>
{levelTitle}
</span>
</div> </div>
<div className="nav-btns"> <div className="nav-btns">
{levelId > 0 && <> <PreviousButton setNavOpen={setNavOpen} />
<Button disabled={levelId <= 0} inverted="true" <NextButton worldSize={gameInfo.data?.worldSize[worldId]} difficulty={difficulty} completed={completed} setNavOpen={setNavOpen} />
to={`/${gameId}/world/${worldId}/level/${levelId - 1}`} <InputModeButton setNavOpen={setNavOpen} isDropdown={false}/>
title="previous level" <ImpressumButton setNavOpen={setNavOpen} toggleImpressum={toggleImpressum} isDropdown={false} />
onClick={() => setNavOpen(false)}>
<FontAwesomeIcon icon={faArrowLeft} />&nbsp;Previous
</Button>
</>}
{levelId < gameInfo.data?.worldSize[worldId] ?
<Button inverted="true"
to={`/${gameId}/world/${worldId}/level/${levelId + 1}`} title="next level"
disabled={difficulty >= 2 && !(completed || levelId == 0)}
onClick={() => setNavOpen(false)}>
<FontAwesomeIcon icon={faArrowRight} />&nbsp;{levelId ? "Next" : "Start"}
</Button>
:
<Button to={`/${gameId}`} inverted="true" title="back to world selection" id="home-btn">
<FontAwesomeIcon icon={faHome} />&nbsp;Leave World
</Button>
}
<Button className="btn btn-inverted toggle-width" disabled={levelId <= 0 || lockEditorMode} inverted="true" to=""
onClick={(ev) => toggleEditor(ev)}
title={lockEditorMode ? "Editor mode is enforced!" : typewriterMode ? "Editor mode" : "Typewriter mode"}>
<FontAwesomeIcon icon={typewriterMode ? faCode : faTerminal} />
</Button>
<Button className="btn btn-inverted toggle-width" title="information, Impressum, privacy policy" inverted="true" to="" onClick={(ev) => {toggleImpressum(ev); setNavOpen(false)}}>
<FontAwesomeIcon icon={impressum ? faXmark : faCircleInfo} />
</Button>
</div> </div>
</> </>
} }

@ -65,13 +65,9 @@ export const ProofStateContext = React.createContext<{
export const MobileContext = React.createContext<{ export const MobileContext = React.createContext<{
mobile : boolean, mobile : boolean,
setMobile: React.Dispatch<React.SetStateAction<Boolean>>, setMobile: React.Dispatch<React.SetStateAction<Boolean>>,
pageNumber: number,
setPageNumber: React.Dispatch<React.SetStateAction<Number>>
}>({ }>({
mobile : false, mobile : false,
setMobile: () => {}, setMobile: () => {},
pageNumber: 0,
setPageNumber: () => {}
}) })
export const WorldLevelIdContext = React.createContext<{ export const WorldLevelIdContext = React.createContext<{
@ -108,10 +104,14 @@ export const InputModeContext = React.createContext<{
typewriterMode: boolean, typewriterMode: boolean,
setTypewriterMode: React.Dispatch<React.SetStateAction<boolean>>, setTypewriterMode: React.Dispatch<React.SetStateAction<boolean>>,
typewriterInput: string, typewriterInput: string,
setTypewriterInput: React.Dispatch<React.SetStateAction<string>> setTypewriterInput: React.Dispatch<React.SetStateAction<string>>,
lockInputMode: boolean,
setLockInputMode: React.Dispatch<React.SetStateAction<boolean>>,
}>({ }>({
typewriterMode: true, typewriterMode: true,
setTypewriterMode: () => {}, setTypewriterMode: () => {},
typewriterInput: "", typewriterInput: "",
setTypewriterInput: () => {}, setTypewriterInput: () => {},
lockInputMode: false,
setLockInputMode: () => {},
}); });

@ -6,7 +6,7 @@ import type { DidCloseTextDocumentParams, DidChangeTextDocumentParams, Location,
import 'tachyons/css/tachyons.css'; import 'tachyons/css/tachyons.css';
import '@vscode/codicons/dist/codicon.css'; import '@vscode/codicons/dist/codicon.css';
import '../../../../node_modules/lean4-infoview/src/infoview/index.css'; import '../../../../node_modules/lean4-infoview/src/infoview/index.css';
import './infoview.css' import '../../css/infoview.css'
import { LeanFileProgressParams, LeanFileProgressProcessingInfo, defaultInfoviewConfig, EditorApi, InfoviewApi } from '@leanprover/infoview-api'; import { LeanFileProgressParams, LeanFileProgressProcessingInfo, defaultInfoviewConfig, EditorApi, InfoviewApi } from '@leanprover/infoview-api';
import { useClientNotificationEffect, useServerNotificationEffect, useEventResult, useServerNotificationState } from '../../../../node_modules/lean4-infoview/src/infoview/util'; import { useClientNotificationEffect, useServerNotificationEffect, useEventResult, useServerNotificationState } from '../../../../node_modules/lean4-infoview/src/infoview/util';

@ -1,6 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import './inventory.css' import '../css/inventory.css'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faLock, faBan } from '@fortawesome/free-solid-svg-icons' import { faLock, faBan } from '@fortawesome/free-solid-svg-icons'
import { GameIdContext } from '../app'; import { GameIdContext } from '../app';

@ -6,7 +6,7 @@ import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css'; import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css'; import '@fontsource/roboto/700.css';
import './landing_page.css' import '../css/landing_page.css'
import coverRobo from '../assets/covers/formaloversum.png' import coverRobo from '../assets/covers/formaloversum.png'
import bgImage from '../assets/bg.jpg' import bgImage from '../assets/bg.jpg'

@ -4,7 +4,7 @@ import { useSelector, useStore } from 'react-redux'
import Split from 'react-split' import Split from 'react-split'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faBars, faCode, faXmark, faHome, faCircleInfo, faArrowRight, faArrowLeft, faTerminal } from '@fortawesome/free-solid-svg-icons' import { faHome, faArrowRight } from '@fortawesome/free-solid-svg-icons'
import { CircularProgress } from '@mui/material' import { CircularProgress } from '@mui/material'
import type { Location } from 'vscode-languageserver-protocol' import type { Location } from 'vscode-languageserver-protocol'
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js' import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js'
@ -41,7 +41,7 @@ import '@fontsource/roboto/500.css'
import '@fontsource/roboto/700.css' import '@fontsource/roboto/700.css'
import 'lean4web/client/src/editor/infoview.css' import 'lean4web/client/src/editor/infoview.css'
import 'lean4web/client/src/editor/vscode.css' import 'lean4web/client/src/editor/vscode.css'
import './level.css' import '../css/level.css'
import { LevelAppBar } from './app_bar' import { LevelAppBar } from './app_bar'
function Level() { function Level() {
@ -222,6 +222,8 @@ function PlayableLevel({impressum, setImpressum}) {
// Only for mobile layout // Only for mobile layout
const [pageNumber, setPageNumber] = useState(0) const [pageNumber, setPageNumber] = useState(0)
const [typewriterMode, setTypewriterMode] = useState(true) const [typewriterMode, setTypewriterMode] = useState(true)
// set to true to prevent switching between typewriter and editor
const [lockInputMode, setLockInputMode] = useState(false)
const [typewriterInput, setTypewriterInput] = useState("") const [typewriterInput, setTypewriterInput] = useState("")
const lastLevel = levelId >= gameInfo.data?.worldSize[worldId] const lastLevel = levelId >= gameInfo.data?.worldSize[worldId]
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
@ -392,18 +394,16 @@ function PlayableLevel({impressum, setImpressum}) {
<div style={level.isLoading ? null : {display: "none"}} className="app-content loading"><CircularProgress /></div> <div style={level.isLoading ? null : {display: "none"}} className="app-content loading"><CircularProgress /></div>
<DeletedChatContext.Provider value={{deletedChat, setDeletedChat, showHelp, setShowHelp}}> <DeletedChatContext.Provider value={{deletedChat, setDeletedChat, showHelp, setShowHelp}}>
<SelectionContext.Provider value={{selectedStep, setSelectedStep}}> <SelectionContext.Provider value={{selectedStep, setSelectedStep}}>
<InputModeContext.Provider value={{typewriterMode, setTypewriterMode, typewriterInput, setTypewriterInput}}> <InputModeContext.Provider value={{typewriterMode, setTypewriterMode, typewriterInput, setTypewriterInput, lockInputMode, setLockInputMode}}>
<ProofContext.Provider value={{proof, setProof}}> <ProofContext.Provider value={{proof, setProof}}>
<EditorContext.Provider value={editorConnection}> <EditorContext.Provider value={editorConnection}>
<MonacoEditorContext.Provider value={editor}> <MonacoEditorContext.Provider value={editor}>
<LevelAppBar <LevelAppBar
pageNumber={pageNumber} setPageNumber={setPageNumber}
isLoading={level.isLoading} isLoading={level.isLoading}
lockEditorMode={level.data?.template !== null}
levelTitle={`${mobile ? '' : 'Level '}${levelId} / ${gameInfo.data?.worldSize[worldId]}` + levelTitle={`${mobile ? '' : 'Level '}${levelId} / ${gameInfo.data?.worldSize[worldId]}` +
(level?.data?.title && ` : ${level?.data?.title}`)} (level?.data?.title && ` : ${level?.data?.title}`)}
impressum={impressum} toggleImpressum={toggleImpressum} />
toggleImpressum={toggleImpressum}
pageNumber={pageNumber} setPageNumber={setPageNumber} />
{mobile? {mobile?
// TODO: This is copied from the `Split` component below... // TODO: This is copied from the `Split` component below...
<> <>
@ -471,7 +471,7 @@ function Introduction({impressum, setImpressum}) {
} }
return <> return <>
<LevelAppBar isLoading={gameInfo.isLoading} levelTitle="Introduction" impressum={impressum} toggleImpressum={toggleImpressum}/> <LevelAppBar isLoading={gameInfo.isLoading} levelTitle="Introduction" toggleImpressum={toggleImpressum}/>
{gameInfo.isLoading ? {gameInfo.isLoading ?
<div className="app-content loading"><CircularProgress /></div> <div className="app-content loading"><CircularProgress /></div>
: mobile ? : mobile ?

@ -9,6 +9,15 @@ import { deleteProgress, selectProgress } from '../../state/progress'
import { downloadFile } from '../world_tree' import { downloadFile } from '../world_tree'
import { Button } from '../button' import { Button } from '../button'
/** 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()
downloadFile({
data: JSON.stringify(gameProgress, null, 2),
fileName: `lean4game-${gameId}-${new Date().toLocaleDateString()}.json`,
fileType: 'text/json',
})
}
/** Pop-up to delete game progress. /** Pop-up to delete game progress.
* *
@ -20,23 +29,13 @@ export function ErasePopup ({handleClose}) {
const gameProgress = useSelector(selectProgress(gameId)) const gameProgress = useSelector(selectProgress(gameId))
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
/** Download the current progress (i.e. what's saved in the browser store) */
const downloadProgress = (e) => {
e.preventDefault()
downloadFile({
data: JSON.stringify(gameProgress, null, 2),
fileName: `lean4game-${gameId}-${new Date().toLocaleDateString()}.json`,
fileType: 'text/json',
})
}
const eraseProgress = () => { const eraseProgress = () => {
dispatch(deleteProgress({game: gameId})) dispatch(deleteProgress({game: gameId}))
handleClose() handleClose()
} }
const downloadAndErase = (e) => { const downloadAndErase = (ev) => {
downloadProgress(e) downloadProgress(gameId, gameProgress, ev)
eraseProgress() eraseProgress()
} }

@ -1,13 +1,13 @@
import * as React from 'react' import * as React from 'react'
import { useState, useEffect } from 'react' import { useEffect } from 'react'
import Split from 'react-split' import Split from 'react-split'
import { Box, CircularProgress } from '@mui/material' import { Box, CircularProgress } from '@mui/material'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faGlobe, faArrowRight, faArrowLeft } from '@fortawesome/free-solid-svg-icons' import { faArrowRight } from '@fortawesome/free-solid-svg-icons'
import { GameIdContext } from '../app' import { GameIdContext } from '../app'
import { useAppDispatch } from '../hooks' import { useAppDispatch, useAppSelector } from '../hooks'
import { changedOpenedIntro } from '../state/progress' import { changedOpenedIntro, selectOpenedIntro } from '../state/progress'
import { useGetGameInfoQuery, useLoadInventoryOverviewQuery } from '../state/api' import { useGetGameInfoQuery, useLoadInventoryOverviewQuery } from '../state/api'
import { Button } from './button' import { Button } from './button'
import { MobileContext } from './infoview/context' import { MobileContext } from './infoview/context'
@ -19,14 +19,14 @@ import { RulesHelpPopup } from './popup/rules_help'
import { UploadPopup } from './popup/upload' import { UploadPopup } from './popup/upload'
import { WorldTreePanel } from './world_tree' import { WorldTreePanel } from './world_tree'
import './welcome.css' import '../css/welcome.css'
import { WelcomeAppBar } from './app_bar' import { WelcomeAppBar } from './app_bar'
import { Hint } from './hints' import { Hint } from './hints'
/** The panel showing the game's introduction text */ /** the panel showing the game's introduction text */
function IntroductionPanel({introduction}: {introduction: string}) { function IntroductionPanel({introduction, setPageNumber}: {introduction: string, setPageNumber}) {
const {mobile, setPageNumber} = React.useContext(MobileContext) const {mobile} = React.useContext(MobileContext)
const gameId = React.useContext(GameIdContext) const gameId = React.useContext(GameIdContext)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
@ -46,11 +46,6 @@ function IntroductionPanel({introduction}: {introduction: string}) {
: <></> : <></>
))} ))}
</div> </div>
{/* <Typography variant="body1" component="div" className="welcome-text">
<h1>{title}</h1>
<Markdown>{introduction}</Markdown>
</Typography>
*/}
{mobile && {mobile &&
<div className="button-row"> <div className="button-row">
<Button className="btn" to="" <Button className="btn" to=""
@ -65,34 +60,32 @@ function IntroductionPanel({introduction}: {introduction: string}) {
</div> </div>
} }
/** main page of the game showing amoung others the tree of worlds/levels */ /** main page of the game showing among others the tree of worlds/levels */
function Welcome() { function Welcome() {
const gameId = React.useContext(GameIdContext) const gameId = React.useContext(GameIdContext)
const {mobile, pageNumber, setPageNumber} = React.useContext(MobileContext) const {mobile} = React.useContext(MobileContext)
const gameInfo = useGetGameInfoQuery({game: gameId}) const gameInfo = useGetGameInfoQuery({game: gameId})
const inventory = useLoadInventoryOverviewQuery({game: gameId}) const inventory = useLoadInventoryOverviewQuery({game: gameId})
// impressum pop-up // For mobile only
const [impressum, setImpressum] = React.useState(false) const openedIntro = useAppSelector(selectOpenedIntro(gameId))
const [rulesHelp, setRulesHelp] = React.useState(false) const [pageNumber, setPageNumber] = React.useState(openedIntro ? 1 : 0)
function closeImpressum() {setImpressum(false)}
function toggleImpressum() {setImpressum(!impressum)}
function closeRulesHelp() {setRulesHelp(false)}
// pop-ups
const [eraseMenu, setEraseMenu] = React.useState(false)
const [impressum, setImpressum] = React.useState(false)
const [info, setInfo] = React.useState(false) const [info, setInfo] = React.useState(false)
function closeInfo() {setInfo(false)} const [rulesHelp, setRulesHelp] = React.useState(false)
function toggleInfo() {setInfo(!impressum)} const [uploadMenu, setUploadMenu] = React.useState(false)
function closeEraseMenu() {setEraseMenu(false)}
function closeImpressum() {setImpressum(false)}
/* state variables to toggle the pop-up menus */ function closeInfo() {setInfo(false)}
const [eraseMenu, setEraseMenu] = React.useState(false); function closeRulesHelp() {setRulesHelp(false)}
const openEraseMenu = () => setEraseMenu(true); function closeUploadMenu() {setUploadMenu(false)}
const closeEraseMenu = () => setEraseMenu(false); function toggleEraseMenu() {setEraseMenu(!eraseMenu)}
const [uploadMenu, setUploadMenu] = React.useState(false); function toggleImpressum() {setImpressum(!impressum)}
const openUploadMenu = () => setUploadMenu(true); function toggleInfo() {setInfo(!info)}
const closeUploadMenu = () => setUploadMenu(false); function toggleUploadMenu() {setUploadMenu(!uploadMenu)}
// set the window title // set the window title
useEffect(() => { useEffect(() => {
@ -106,23 +99,26 @@ function Welcome() {
<CircularProgress /> <CircularProgress />
</Box> </Box>
: <> : <>
<WelcomeAppBar gameInfo={gameInfo.data} toggleImpressum={toggleImpressum} openEraseMenu={openEraseMenu} <WelcomeAppBar pageNumber={pageNumber} setPageNumber={setPageNumber} gameInfo={gameInfo.data} toggleImpressum={toggleImpressum}
openUploadMenu={openUploadMenu} toggleInfo={toggleInfo} /> toggleEraseMenu={toggleEraseMenu} toggleUploadMenu={toggleUploadMenu}
toggleInfo={toggleInfo} />
<div className="app-content"> <div className="app-content">
{ mobile ? { mobile ?
<div className="welcome mobile"> <div className="welcome mobile">
{(pageNumber == 0 ? {(pageNumber == 0 ?
<IntroductionPanel introduction={gameInfo.data?.introduction} /> <IntroductionPanel introduction={gameInfo.data?.introduction} setPageNumber={setPageNumber} />
: pageNumber == 1 ? : pageNumber == 1 ?
<WorldTreePanel worlds={gameInfo.data?.worlds} worldSize={gameInfo.data?.worldSize} rulesHelp={rulesHelp} setRulesHelp={setRulesHelp} /> <WorldTreePanel worlds={gameInfo.data?.worlds} worldSize={gameInfo.data?.worldSize}
rulesHelp={rulesHelp} setRulesHelp={setRulesHelp} />
: :
<InventoryPanel levelInfo={inventory?.data} /> <InventoryPanel levelInfo={inventory?.data} />
)} )}
</div> </div>
: :
<Split className="welcome" minSize={0} snapOffset={200} sizes={[25, 50, 25]}> <Split className="welcome" minSize={0} snapOffset={200} sizes={[25, 50, 25]}>
<IntroductionPanel introduction={gameInfo.data?.introduction} /> <IntroductionPanel introduction={gameInfo.data?.introduction} setPageNumber={setPageNumber} />
<WorldTreePanel worlds={gameInfo.data?.worlds} worldSize={gameInfo.data?.worldSize} rulesHelp={rulesHelp} setRulesHelp={setRulesHelp} /> <WorldTreePanel worlds={gameInfo.data?.worlds} worldSize={gameInfo.data?.worldSize}
rulesHelp={rulesHelp} setRulesHelp={setRulesHelp} />
<InventoryPanel levelInfo={inventory?.data} /> <InventoryPanel levelInfo={inventory?.data} />
</Split> </Split>
} }

@ -15,7 +15,7 @@ import { useAppDispatch } from '../hooks'
import { selectDifficulty, changedDifficulty, selectCompleted } from '../state/progress' import { selectDifficulty, changedDifficulty, selectCompleted } from '../state/progress'
import { store } from '../state/store' import { store } from '../state/store'
import './world_tree.css' import '../css/world_tree.css'
// Settings for the world tree // Settings for the world tree
cytoscape.use( klay ) cytoscape.use( klay )

@ -108,6 +108,7 @@ em {
flex: 0; flex: 0;
background: var(--clr-primary); background: var(--clr-primary);
display: flex; display: flex;
position: relative;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
padding: 1.1em; padding: 1.1em;
Loading…
Cancel
Save