new design for welcome page #96
This commit is contained in:
@@ -125,9 +125,13 @@ em {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.app-content {
|
.app-content {
|
||||||
|
height: 100%;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.markdown li ul, .markdown li ol {
|
.markdown li ul, .markdown li ol {
|
||||||
margin:0 1.5em;
|
margin:0 1.5em;
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-1
@@ -10,6 +10,8 @@ import './reset.css';
|
|||||||
import './app.css';
|
import './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);
|
||||||
|
|
||||||
@@ -22,10 +24,15 @@ function App() {
|
|||||||
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}}>
|
<MobileContext.Provider value={{mobile, setMobile, pageNumber, setPageNumber}}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</MobileContext.Provider>
|
</MobileContext.Provider>
|
||||||
</GameIdContext.Provider>
|
</GameIdContext.Provider>
|
||||||
|
|||||||
@@ -0,0 +1,403 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
import { GameIdContext } from "../app"
|
||||||
|
import { InputModeContext, MobileContext, WorldLevelIdContext } from "./infoview/context"
|
||||||
|
import { GameInfo, useGetGameInfoQuery } from '../state/api'
|
||||||
|
import { changedOpenedIntro, selectCompleted, selectDifficulty, selectProgress } from '../state/progress'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import { useAppDispatch, useAppSelector } from '../hooks'
|
||||||
|
import { Button } from './button'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import { faDownload, faUpload, faEraser, faBook, faGlobe, faHome, faArrowRight, faArrowLeft, faXmark, faBars, faCode, faCircleInfo, faTerminal } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { PrivacyPolicyPopup } from './privacy_policy'
|
||||||
|
import { WorldSelectionMenu, downloadFile } from './world_tree'
|
||||||
|
|
||||||
|
/** navigation to switch between pages on mobile */
|
||||||
|
function MobileNav({pageNumber, setPageNumber}:
|
||||||
|
{ pageNumber: number,
|
||||||
|
setPageNumber: any}) {
|
||||||
|
const gameId = React.useContext(GameIdContext)
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
|
let prevText = {0 : null, 1: "Intro", 2: null}[pageNumber]
|
||||||
|
let prevIcon = {0 : null, 1: null, 2: faXmark}[pageNumber]
|
||||||
|
let prevTitle = {
|
||||||
|
0: null,
|
||||||
|
1: "Game Introduction",
|
||||||
|
2: "World selection"}[pageNumber]
|
||||||
|
let nextText = {0 : "Start", 1: null, 2: null}[pageNumber]
|
||||||
|
let nextIcon = {0 : null, 1: faBook, 2: null}[pageNumber]
|
||||||
|
let nextTitle = {
|
||||||
|
0: "World selection",
|
||||||
|
1: "Inventory",
|
||||||
|
2: null}[pageNumber]
|
||||||
|
|
||||||
|
return <>
|
||||||
|
{(prevText || prevTitle || prevIcon) &&
|
||||||
|
<Button className="btn btn-inverted toggle-width" to={pageNumber == 0 ? "/" : ""} inverted="true" title={prevTitle}
|
||||||
|
onClick={() => {pageNumber == 0 ? null : setPageNumber(pageNumber - 1)}}>
|
||||||
|
{prevIcon && <FontAwesomeIcon icon={prevIcon} />}
|
||||||
|
{prevText && `${prevText}`}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
{(nextText || nextTitle || nextIcon) &&
|
||||||
|
<Button className="btn btn-inverted toggle-width" to="" inverted="true"
|
||||||
|
title={nextTitle} onClick={() => {
|
||||||
|
console.log(`page number: ${pageNumber}`)
|
||||||
|
setPageNumber(pageNumber+1);
|
||||||
|
dispatch(changedOpenedIntro({game: gameId, openedIntro: true}))}}>
|
||||||
|
{nextText && `${nextText}`}
|
||||||
|
{nextIcon && <FontAwesomeIcon icon={nextIcon} />}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WelcomeAppBar({gameInfo, toggleImpressum, openEraseMenu, openUploadMenu} : {
|
||||||
|
gameInfo: GameInfo,
|
||||||
|
toggleImpressum: any,
|
||||||
|
openEraseMenu: any,
|
||||||
|
openUploadMenu: any,
|
||||||
|
}) {
|
||||||
|
const gameId = React.useContext(GameIdContext)
|
||||||
|
const {mobile, pageNumber, setPageNumber} = React.useContext(MobileContext)
|
||||||
|
const [navOpen, setNavOpen] = React.useState(false)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/** Download the current progress (i.e. what's saved in the browser store) */
|
||||||
|
const gameProgress = useSelector(selectProgress(gameId))
|
||||||
|
const downloadProgress = (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
downloadFile({
|
||||||
|
data: JSON.stringify(gameProgress, null, 2),
|
||||||
|
fileName: `lean4game-${gameId}-${new Date().toLocaleDateString()}.json`,
|
||||||
|
fileType: 'text/json',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return <div className="app-bar" >
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<Button inverted="false" title="back to games selection" to="/">
|
||||||
|
<FontAwesomeIcon icon={faArrowLeft} /> <FontAwesomeIcon icon={faGlobe} />
|
||||||
|
</Button>
|
||||||
|
<span className="app-bar-title">
|
||||||
|
{}
|
||||||
|
</span>
|
||||||
|
</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="" className="btn toggle-width" id="menu-btn" onClick={(ev) => {setNavOpen(!navOpen)}} >
|
||||||
|
{navOpen ? <FontAwesomeIcon icon={faXmark} /> : <FontAwesomeIcon icon={faBars} />}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div className={'menu dropdown' + (navOpen ? '' : ' hidden')}>
|
||||||
|
{/* {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} /> {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} /> Previous
|
||||||
|
</Button>
|
||||||
|
</>}
|
||||||
|
<Button to={`/${gameId}`} inverted="true" title="back to world selection">
|
||||||
|
<FontAwesomeIcon icon={faHome} /> Home
|
||||||
|
</Button>
|
||||||
|
<Button disabled={levelId <= 0} inverted="true" to=""
|
||||||
|
onClick={(ev) => { setCommandLineMode(!commandLineMode); setNavOpen(false) }}
|
||||||
|
title="toggle Editor mode">
|
||||||
|
<FontAwesomeIcon icon={faCode} /> Toggle Editor
|
||||||
|
</Button> */}
|
||||||
|
|
||||||
|
<Button title="Clear Progress" inverted="true" to="" onClick={(ev) => {openEraseMenu(); setNavOpen(false)}}>
|
||||||
|
<FontAwesomeIcon icon={faEraser} /> Erase
|
||||||
|
</Button>
|
||||||
|
<Button title="Download Progress" inverted="true" to="" onClick={(ev) => {downloadProgress(ev); setNavOpen(false)}}>
|
||||||
|
<FontAwesomeIcon icon={faDownload} /> Download
|
||||||
|
</Button>
|
||||||
|
<Button title="Load Progress from JSON" inverted="true" to="" onClick={(ev) => {openUploadMenu(); setNavOpen(false)}}>
|
||||||
|
<FontAwesomeIcon icon={faUpload} /> Upload
|
||||||
|
</Button>
|
||||||
|
<Button title="Credits" inverted="true" to="" onClick={(ev) => {setNavOpen(false)}}>
|
||||||
|
<FontAwesomeIcon icon={faCircleInfo} /> Info & Credits
|
||||||
|
</Button>
|
||||||
|
<Button title="Impressum, privacy policy" inverted="true" to="" onClick={(ev) => {toggleImpressum(); setNavOpen(false)}}>
|
||||||
|
<FontAwesomeIcon icon={faCircleInfo} /> Impressum
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// /** The menu that is shown next to the world selection graph */
|
||||||
|
// function WorldSelectionMenu() {
|
||||||
|
// const [file, setFile] = React.useState<File>();
|
||||||
|
|
||||||
|
// const gameId = React.useContext(GameIdContext)
|
||||||
|
// const store = useStore()
|
||||||
|
// const difficulty = useSelector(selectDifficulty(gameId))
|
||||||
|
|
||||||
|
|
||||||
|
// /* 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: - regular: 🔐 levels, 🔐 tactics - lax: 🔓 levels, 🔐 tactics - 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}) {
|
||||||
|
const gameId = React.useContext(GameIdContext)
|
||||||
|
const {worldId, levelId} = React.useContext(WorldLevelIdContext)
|
||||||
|
const gameInfo = useGetGameInfoQuery({game: gameId})
|
||||||
|
|
||||||
|
const {mobile} = React.useContext(MobileContext)
|
||||||
|
|
||||||
|
const difficulty = useSelector(selectDifficulty(gameId))
|
||||||
|
const completed = useAppSelector(selectCompleted(gameId, worldId, levelId))
|
||||||
|
|
||||||
|
const { commandLineMode, setCommandLineMode } = React.useContext(InputModeContext)
|
||||||
|
|
||||||
|
const [navOpen, setNavOpen] = React.useState(false)
|
||||||
|
|
||||||
|
return <div className="app-bar" style={isLoading ? {display: "none"} : null} >
|
||||||
|
{mobile ?
|
||||||
|
<>
|
||||||
|
{/* MOBILE VERSION */}
|
||||||
|
<div>
|
||||||
|
<span className="app-bar-title">
|
||||||
|
{levelTitle}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="nav-btns">
|
||||||
|
{mobile && pageNumber == 0 ?
|
||||||
|
<Button to="" className="btn btn-inverted toggle-width"
|
||||||
|
title="show inventory" inverted="true" onClick={() => {setPageNumber(1)}}>
|
||||||
|
<FontAwesomeIcon icon={faBook}/>
|
||||||
|
</Button>
|
||||||
|
: pageNumber == 1 &&
|
||||||
|
<Button className="btn btn-inverted toggle-width" to=""
|
||||||
|
title="show inventory" inverted="true" onClick={() => {setPageNumber(0)}}>
|
||||||
|
<FontAwesomeIcon icon={faXmark}/>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
<Button to="" className="btn toggle-width" id="menu-btn" onClick={(ev) => {setNavOpen(!navOpen)}} >
|
||||||
|
{navOpen ? <FontAwesomeIcon icon={faXmark} /> : <FontAwesomeIcon icon={faBars} />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className={'menu dropdown' + (navOpen ? '' : ' hidden')}>
|
||||||
|
{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} /> {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} /> Previous
|
||||||
|
</Button>
|
||||||
|
</>}
|
||||||
|
<Button to={`/${gameId}`} inverted="true" title="back to world selection">
|
||||||
|
<FontAwesomeIcon icon={faHome} /> Home
|
||||||
|
</Button>
|
||||||
|
<Button disabled={levelId <= 0} inverted="true" to=""
|
||||||
|
onClick={(ev) => { setCommandLineMode(!commandLineMode); setNavOpen(false) }}
|
||||||
|
title="toggle Editor mode">
|
||||||
|
<FontAwesomeIcon icon={faCode} /> Toggle Editor
|
||||||
|
</Button>
|
||||||
|
<Button title="information, Impressum, privacy policy" inverted="true" to="" onClick={(ev) => {toggleImpressum(ev); setNavOpen(false)}}>
|
||||||
|
<FontAwesomeIcon icon={faCircleInfo} /> Info & Impressum
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
:
|
||||||
|
<>
|
||||||
|
{/* DESKTOP VERSION */}
|
||||||
|
<div>
|
||||||
|
<Button to={`/${gameId}`} inverted="true" title="back to world selection" id="home-btn">
|
||||||
|
<FontAwesomeIcon icon={faHome} />
|
||||||
|
</Button>
|
||||||
|
<span className="app-bar-title">
|
||||||
|
{gameInfo.data?.worlds.nodes[worldId].title && `World: ${gameInfo.data?.worlds.nodes[worldId].title}`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="app-bar-title">
|
||||||
|
{levelTitle}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="nav-btns">
|
||||||
|
{levelId > 0 && <>
|
||||||
|
<Button disabled={levelId <= 0} inverted="true"
|
||||||
|
to={`/${gameId}/world/${worldId}/level/${levelId - 1}`}
|
||||||
|
title="previous level"
|
||||||
|
onClick={() => setNavOpen(false)}>
|
||||||
|
<FontAwesomeIcon icon={faArrowLeft} /> 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} /> {levelId ? "Next" : "Start"}
|
||||||
|
</Button>
|
||||||
|
:
|
||||||
|
<Button to={`/${gameId}`} inverted="true" title="back to world selection" id="home-btn">
|
||||||
|
<FontAwesomeIcon icon={faHome} /> Leave World
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
<Button className="btn btn-inverted toggle-width" disabled={levelId <= 0} inverted="true" to=""
|
||||||
|
onClick={(ev) => { setCommandLineMode(!commandLineMode); setNavOpen(false) }}
|
||||||
|
title="toggle Editor mode">
|
||||||
|
<FontAwesomeIcon icon={commandLineMode ? 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>
|
||||||
|
}
|
||||||
@@ -64,10 +64,14 @@ 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<{
|
||||||
|
|||||||
@@ -418,68 +418,70 @@ export function CommandLineInterface(props: { world: string, level: number, data
|
|||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<ExerciseStatement data={props.data} />
|
<div className='proof'>
|
||||||
{proof.length ?
|
<ExerciseStatement data={props.data} />
|
||||||
<div className='proof'>
|
{proof.length ?
|
||||||
{proof.map((step, i) => {
|
<>
|
||||||
if (i == proof.length - 1 && lastStepErrors) {
|
{proof.map((step, i) => {
|
||||||
// if the last command contains an error, we only display the errors but not the
|
if (i == proof.length - 1 && lastStepErrors) {
|
||||||
// entered command as it is still present in the command line.
|
// if the last command contains an error, we only display the errors but not the
|
||||||
// TODO: Should not use index as key.
|
// entered command as it is still present in the command line.
|
||||||
return <div key={`proof-step-${i}`}>
|
// TODO: Should not use index as key.
|
||||||
<Errors errors={step.errors} commandLineMode={true} />
|
return <div key={`proof-step-${i}`}>
|
||||||
</div>
|
<Errors errors={step.errors} commandLineMode={true} />
|
||||||
} else {
|
</div>
|
||||||
return <div key={`proof-step-${i}`} className={`step step-${i}` + (selectedStep == i ? ' selected' : '')} onClick={toggleSelectStep(i)}>
|
} else {
|
||||||
<Command command={step.command} deleteProof={deleteProof(i)} />
|
return <div key={`proof-step-${i}`} className={`step step-${i}` + (selectedStep == i ? ' selected' : '')} onClick={toggleSelectStep(i)}>
|
||||||
<Errors errors={step.errors} commandLineMode={true} />
|
<Command command={step.command} deleteProof={deleteProof(i)} />
|
||||||
{mobile && i == 0 && props.data?.introduction &&
|
<Errors errors={step.errors} commandLineMode={true} />
|
||||||
<div className={`message information step-0${selectedStep === 0 ? ' selected' : ''}`} onClick={toggleSelectStep(0)}>
|
{mobile && i == 0 && props.data?.introduction &&
|
||||||
<Markdown>{props.data?.introduction}</Markdown>
|
<div className={`message information step-0${selectedStep === 0 ? ' selected' : ''}`} onClick={toggleSelectStep(0)}>
|
||||||
</div>
|
<Markdown>{props.data?.introduction}</Markdown>
|
||||||
}
|
</div>
|
||||||
{mobile && <>
|
|
||||||
<Hints key={`hints-${i}`}
|
|
||||||
hints={step.hints} showHidden={showHelp.has(i)} step={i}
|
|
||||||
selected={selectedStep} toggleSelection={toggleSelectStep(i)}/>
|
|
||||||
{i == proof.length - 1 && hasHiddenHints(proof.length - 1) && !showHelp.has(k - withErr) &&
|
|
||||||
<Button className="btn btn-help" to="" onClick={activateHiddenHints}>
|
|
||||||
Show more help!
|
|
||||||
</Button>
|
|
||||||
}
|
}
|
||||||
</>
|
{mobile && <>
|
||||||
}
|
<Hints key={`hints-${i}`}
|
||||||
<GoalsTab proofStep={step} last={i == proof.length - (lastStepErrors ? 2 : 1)} />
|
hints={step.hints} showHidden={showHelp.has(i)} step={i}
|
||||||
{/* Show a message that there are no goals left */}
|
selected={selectedStep} toggleSelection={toggleSelectStep(i)}/>
|
||||||
{!step.goals.length && (
|
{i == proof.length - 1 && hasHiddenHints(proof.length - 1) && !showHelp.has(k - withErr) &&
|
||||||
<div className="message information">
|
<Button className="btn btn-help" to="" onClick={activateHiddenHints}>
|
||||||
{completed ?
|
Show more help!
|
||||||
<p>Level completed! 🎉</p> :
|
</Button>
|
||||||
<p>
|
|
||||||
<b>no goals left</b><br />
|
|
||||||
<i>This probably means you solved the level with warnings or Lean encountered a parsing error.</i>
|
|
||||||
</p>
|
|
||||||
}
|
}
|
||||||
</div>
|
</>
|
||||||
)}
|
}
|
||||||
|
<GoalsTab proofStep={step} last={i == proof.length - (lastStepErrors ? 2 : 1)} />
|
||||||
|
{/* Show a message that there are no goals left */}
|
||||||
|
{!step.goals.length && (
|
||||||
|
<div className="message information">
|
||||||
|
{completed ?
|
||||||
|
<p>Level completed! 🎉</p> :
|
||||||
|
<p>
|
||||||
|
<b>no goals left</b><br />
|
||||||
|
<i>This probably means you solved the level with warnings or Lean encountered a parsing error.</i>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
{mobile && completed &&
|
||||||
|
<div className="button-row">
|
||||||
|
{props.level >= props.worldSize ?
|
||||||
|
<Button to={`/${gameId}`}>
|
||||||
|
<FontAwesomeIcon icon={faHome} /> Leave World
|
||||||
|
</Button>
|
||||||
|
:
|
||||||
|
<Button to={`/${gameId}/world/${props.world}/level/${props.level + 1}`}>
|
||||||
|
Next <FontAwesomeIcon icon={faArrowRight} />
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
})}
|
</> : <></>
|
||||||
{mobile && completed &&
|
}
|
||||||
<div className="button-row">
|
</div>
|
||||||
{props.level >= props.worldSize ?
|
|
||||||
<Button to={`/${gameId}`}>
|
|
||||||
<FontAwesomeIcon icon={faHome} /> Leave World
|
|
||||||
</Button>
|
|
||||||
:
|
|
||||||
<Button to={`/${gameId}/world/${props.world}/level/${props.level + 1}`}>
|
|
||||||
Next <FontAwesomeIcon icon={faArrowRight} />
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div> : <></>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<CommandLine proofPanelRef={proofPanelRef} hidden={!withErr && proof[proof.length - 1]?.goals.length == 0}/>
|
<CommandLine proofPanelRef={proofPanelRef} hidden={!withErr && proof[proof.length - 1]?.goals.length == 0}/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,3 @@
|
|||||||
.level {
|
|
||||||
height: 100%;
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.level-mobile {
|
.level-mobile {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -333,6 +326,6 @@ td code {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.toggle-width {
|
.toggle-width {
|
||||||
width: 40px;
|
min-width: 40px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ 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 './level.css'
|
||||||
|
import { LevelAppBar } from './app_bar'
|
||||||
|
|
||||||
function Level() {
|
function Level() {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
@@ -422,127 +423,6 @@ function Introduction() {
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The top-navigation bar */
|
|
||||||
function LevelAppBar({isLoading, levelTitle, impressum, toggleImpressum, pageNumber = undefined, setPageNumber = undefined}) {
|
|
||||||
const gameId = React.useContext(GameIdContext)
|
|
||||||
const {worldId, levelId} = useContext(WorldLevelIdContext)
|
|
||||||
const gameInfo = useGetGameInfoQuery({game: gameId})
|
|
||||||
|
|
||||||
const {mobile} = React.useContext(MobileContext)
|
|
||||||
|
|
||||||
const difficulty = useSelector(selectDifficulty(gameId))
|
|
||||||
const completed = useAppSelector(selectCompleted(gameId, worldId, levelId))
|
|
||||||
|
|
||||||
const { commandLineMode, setCommandLineMode } = React.useContext(InputModeContext)
|
|
||||||
|
|
||||||
const [navOpen, setNavOpen] = React.useState(false)
|
|
||||||
|
|
||||||
return <div className="app-bar" style={isLoading ? {display: "none"} : null} >
|
|
||||||
{mobile ?
|
|
||||||
<>
|
|
||||||
{/* MOBILE VERSION */}
|
|
||||||
<div>
|
|
||||||
<span className="app-bar-title">
|
|
||||||
{levelTitle}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="nav-btns">
|
|
||||||
{mobile && pageNumber == 0 ?
|
|
||||||
<Button to="" className="btn btn-inverted toggle-width"
|
|
||||||
title="show inventory" inverted="true" onClick={() => {setPageNumber(1)}}>
|
|
||||||
<FontAwesomeIcon icon={faBook}/>
|
|
||||||
</Button>
|
|
||||||
: pageNumber == 1 &&
|
|
||||||
<Button className="btn btn-inverted toggle-width" to=""
|
|
||||||
title="show inventory" inverted="true" onClick={() => {setPageNumber(0)}}>
|
|
||||||
<FontAwesomeIcon icon={faXmark}/>
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
<Button to="" className="btn toggle-width" id="menu-btn" onClick={(ev) => {setNavOpen(!navOpen)}} >
|
|
||||||
{navOpen ? <FontAwesomeIcon icon={faXmark} /> : <FontAwesomeIcon icon={faBars} />}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className={'menu dropdown' + (navOpen ? '' : ' hidden')}>
|
|
||||||
{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} /> {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} /> Previous
|
|
||||||
</Button>
|
|
||||||
</>}
|
|
||||||
<Button to={`/${gameId}`} inverted="true" title="back to world selection">
|
|
||||||
<FontAwesomeIcon icon={faHome} /> Home
|
|
||||||
</Button>
|
|
||||||
<Button disabled={levelId <= 0} inverted="true" to=""
|
|
||||||
onClick={(ev) => { setCommandLineMode(!commandLineMode); setNavOpen(false) }}
|
|
||||||
title="toggle Editor mode">
|
|
||||||
<FontAwesomeIcon icon={faCode} /> Toggle Editor
|
|
||||||
</Button>
|
|
||||||
<Button title="information, Impressum, privacy policy" inverted="true" to="" onClick={(ev) => {toggleImpressum(ev); setNavOpen(false)}}>
|
|
||||||
<FontAwesomeIcon icon={faCircleInfo} /> Info & Impressum
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
:
|
|
||||||
<>
|
|
||||||
{/* DESKTOP VERSION */}
|
|
||||||
<div>
|
|
||||||
<Button to={`/${gameId}`} inverted="true" title="back to world selection" id="home-btn">
|
|
||||||
<FontAwesomeIcon icon={faHome} />
|
|
||||||
</Button>
|
|
||||||
<span className="app-bar-title">
|
|
||||||
{gameInfo.data?.worlds.nodes[worldId].title && `World: ${gameInfo.data?.worlds.nodes[worldId].title}`}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="app-bar-title">
|
|
||||||
{levelTitle}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="nav-btns">
|
|
||||||
{levelId > 0 && <>
|
|
||||||
<Button disabled={levelId <= 0} inverted="true"
|
|
||||||
to={`/${gameId}/world/${worldId}/level/${levelId - 1}`}
|
|
||||||
title="previous level"
|
|
||||||
onClick={() => setNavOpen(false)}>
|
|
||||||
<FontAwesomeIcon icon={faArrowLeft} /> 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} /> {levelId ? "Next" : "Start"}
|
|
||||||
</Button>
|
|
||||||
:
|
|
||||||
<Button to={`/${gameId}`} inverted="true" title="back to world selection" id="home-btn">
|
|
||||||
<FontAwesomeIcon icon={faHome} /> Leave World
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
<Button className="btn btn-inverted toggle-width" disabled={levelId <= 0} inverted="true" to=""
|
|
||||||
onClick={(ev) => { setCommandLineMode(!commandLineMode); setNavOpen(false) }}
|
|
||||||
title="toggle Editor mode">
|
|
||||||
<FontAwesomeIcon icon={commandLineMode ? 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>
|
|
||||||
}
|
|
||||||
|
|
||||||
function useLevelEditor(codeviewRef, initialCode, initialSelections, onDidChangeContent, onDidChangeSelection) {
|
function useLevelEditor(codeviewRef, initialCode, initialSelections, onDidChangeContent, onDidChangeSelection) {
|
||||||
|
|
||||||
const connection = React.useContext(ConnectionContext)
|
const connection = React.useContext(ConnectionContext)
|
||||||
|
|||||||
@@ -10,6 +10,10 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.welcome.mobile .column {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.app-content {
|
.app-content {
|
||||||
height: 100%
|
height: 100%
|
||||||
}
|
}
|
||||||
@@ -17,6 +21,7 @@
|
|||||||
.welcome .column {
|
.welcome .column {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
position: relative;
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,33 +236,6 @@ svg .disabled {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-nav {
|
|
||||||
padding-top: .5em;
|
|
||||||
padding-bottom: .5em;
|
|
||||||
position:relative;
|
|
||||||
height: 3em;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-nav .btn-next, .mobile-nav .btn-previous {
|
|
||||||
position: absolute;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-nav .btn-previous {
|
|
||||||
left: 0;
|
|
||||||
border-top-left-radius: 0;
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
padding-left: 0.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-nav .btn-next {
|
|
||||||
right: 0;
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
padding-right: .3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-nav .svg-inline--fa {
|
.mobile-nav .svg-inline--fa {
|
||||||
margin-left: 0.3rem;
|
margin-left: 0.3rem;
|
||||||
margin-right: 0.3rem;
|
margin-right: 0.3rem;
|
||||||
|
|||||||
@@ -8,83 +8,162 @@ import { faGlobe, faBook, faArrowRight, faArrowLeft } from '@fortawesome/free-so
|
|||||||
|
|
||||||
import { GameIdContext } from '../app'
|
import { GameIdContext } from '../app'
|
||||||
import { useAppDispatch } from '../hooks'
|
import { useAppDispatch } from '../hooks'
|
||||||
import { changedOpenedIntro, selectOpenedIntro } from '../state/progress'
|
import { GameProgressState, changedOpenedIntro, deleteProgress, loadProgress, selectOpenedIntro, selectProgress } 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'
|
||||||
import { InventoryPanel } from './inventory'
|
import { InventoryPanel } from './inventory'
|
||||||
import Markdown from './markdown'
|
import Markdown from './markdown'
|
||||||
import {PrivacyPolicy} from './privacy_policy'
|
import {PrivacyPolicyPopup} from './privacy_policy'
|
||||||
import { WelcomeMenu, WorldTreePanel } from './world_tree'
|
import { WorldTreePanel, downloadFile } from './world_tree'
|
||||||
|
|
||||||
import './welcome.css'
|
import './welcome.css'
|
||||||
|
import { WelcomeAppBar } from './app_bar'
|
||||||
|
|
||||||
/** navigation to switch between pages on mobile */
|
/** The panel showing the game's introduction text */
|
||||||
function MobileNav({pageNumber, setPageNumber}:
|
function IntroductionPanel({introduction, title}: { introduction: string, title?: string}) {
|
||||||
{ pageNumber: number,
|
const {mobile, setPageNumber} = React.useContext(MobileContext)
|
||||||
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: "Game"}[pageNumber]
|
return <div className="column chat-panel">
|
||||||
let prevIcon = {0 : faGlobe, 1: null, 2: null}[pageNumber]
|
<Typography variant="body1" component="div" className="welcome-text">
|
||||||
let prevTitle = {
|
{mobile && <h1>{title}</h1>}
|
||||||
0: "back to games selection",
|
<Markdown>{introduction}</Markdown>
|
||||||
1: "back to introduction",
|
</Typography>
|
||||||
2: "game tree"}[pageNumber]
|
{mobile &&
|
||||||
let nextText = {0 : "Game", 1: null, 2: null}[pageNumber]
|
<div className="button-row">
|
||||||
let nextIcon = {0 : null, 1: faBook, 2: null}[pageNumber]
|
<Button className="btn" to=""
|
||||||
let nextTitle = {
|
title="" onClick={() => {
|
||||||
0: "game tree",
|
setPageNumber(1);
|
||||||
1: "inventory",
|
dispatch(changedOpenedIntro({game: gameId, openedIntro: true}))
|
||||||
2: null}[pageNumber]
|
}}>
|
||||||
|
Start <FontAwesomeIcon icon={faArrowRight}/>
|
||||||
return <div className="mobile-nav">
|
</Button>
|
||||||
{(prevText || prevTitle || prevIcon) &&
|
</div>
|
||||||
<Button className="btn btn-previous" to={pageNumber == 0 ? "/" : ""} title={prevTitle}
|
|
||||||
onClick={() => {pageNumber == 0 ? null : setPageNumber(pageNumber - 1)}}>
|
|
||||||
|
|
||||||
<FontAwesomeIcon icon={faArrowLeft} />
|
|
||||||
{prevIcon && <FontAwesomeIcon icon={prevIcon} />}
|
|
||||||
{prevText && `${prevText}`}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
{(nextText || nextTitle || nextIcon) &&
|
|
||||||
<Button className="btn btn-next" to=""
|
|
||||||
title={nextTitle} onClick={() => {
|
|
||||||
console.log(`page number: ${pageNumber}`)
|
|
||||||
setPageNumber(pageNumber+1);
|
|
||||||
dispatch(changedOpenedIntro({game: gameId, openedIntro: true}))}}>
|
|
||||||
{nextText && `${nextText}`}
|
|
||||||
{nextIcon && <FontAwesomeIcon icon={nextIcon} />}
|
|
||||||
<FontAwesomeIcon icon={faArrowRight}/>
|
|
||||||
</Button>
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The panel showing the game's introduction text */
|
function ErasePopup ({handleClose}) {
|
||||||
function IntroductionPanel({introduction}: { introduction: string}) {
|
const gameId = React.useContext(GameIdContext)
|
||||||
const {mobile} = React.useContext(MobileContext)
|
const gameProgress = useSelector(selectProgress(gameId))
|
||||||
return <div className="column">
|
const dispatch = useAppDispatch()
|
||||||
<Typography variant="body1" component="div" className="welcome-text">
|
|
||||||
{!mobile && <WelcomeMenu />}
|
/** Download the current progress (i.e. what's saved in the browser store) */
|
||||||
<Markdown>{introduction}</Markdown>
|
const downloadProgress = (e) => {
|
||||||
</Typography>
|
e.preventDefault()
|
||||||
|
downloadFile({
|
||||||
|
data: JSON.stringify(gameProgress, null, 2),
|
||||||
|
fileName: `lean4game-${gameId}-${new Date().toLocaleDateString()}.json`,
|
||||||
|
fileType: 'text/json',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const eraseProgress = () => {
|
||||||
|
dispatch(deleteProgress({game: gameId}))
|
||||||
|
handleClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadAndErase = (e) => {
|
||||||
|
downloadProgress(e)
|
||||||
|
eraseProgress()
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="modal-wrapper">
|
||||||
|
<div className="modal-backdrop" onClick={handleClose} />
|
||||||
|
<div className="modal">
|
||||||
|
<div className="codicon codicon-close modal-close" onClick={handleClose}></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={handleClose} to="">Cancel</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
function UploadPopup ({handleClose}) {
|
||||||
|
const [file, setFile] = React.useState<File>();
|
||||||
|
const gameId = React.useContext(GameIdContext)
|
||||||
|
const gameProgress = useSelector(selectProgress(gameId))
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
|
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}))
|
||||||
|
}
|
||||||
|
handleClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return <div className="modal-wrapper">
|
||||||
|
<div className="modal-backdrop" onClick={handleClose} />
|
||||||
|
<div className="modal">
|
||||||
|
<div className="codicon codicon-close modal-close" onClick={handleClose}></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>
|
||||||
}
|
}
|
||||||
|
|
||||||
/** main page of the game showing amoung others the tree of worlds/levels */
|
/** main page of the game showing amoung others the tree of worlds/levels */
|
||||||
function Welcome() {
|
function Welcome() {
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const {mobile} = React.useContext(MobileContext)
|
const {mobile, pageNumber, setPageNumber} = React.useContext(MobileContext)
|
||||||
const gameInfo = useGetGameInfoQuery({game: gameId})
|
const gameInfo = useGetGameInfoQuery({game: gameId})
|
||||||
// On mobile, the intro page should only be shown the first time
|
|
||||||
const openedIntro = useSelector(selectOpenedIntro(gameId))
|
|
||||||
// On mobile, there are multiple pages to switch between
|
|
||||||
const [pageNumber, setPageNumber] = useState(openedIntro ? 1 : 0)
|
|
||||||
const inventory = useLoadInventoryOverviewQuery({game: gameId})
|
const inventory = useLoadInventoryOverviewQuery({game: gameId})
|
||||||
|
|
||||||
|
// impressum pop-up
|
||||||
|
const [impressum, setImpressum] = React.useState(false)
|
||||||
|
function closeImpressum() {setImpressum(false)}
|
||||||
|
function toggleImpressum() {setImpressum(!impressum)}
|
||||||
|
|
||||||
|
/* 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);
|
||||||
|
|
||||||
|
|
||||||
// set the window title
|
// set the window title
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (gameInfo.data?.title) {
|
if (gameInfo.data?.title) {
|
||||||
@@ -92,32 +171,36 @@ function Welcome() {
|
|||||||
}
|
}
|
||||||
}, [gameInfo.data?.title])
|
}, [gameInfo.data?.title])
|
||||||
|
|
||||||
return <div className="app-content">
|
return gameInfo.isLoading ?
|
||||||
{ gameInfo.isLoading?
|
|
||||||
<Box display="flex" alignItems="center" justifyContent="center" sx={{ height: "calc(100vh - 64px)" }}>
|
<Box display="flex" alignItems="center" justifyContent="center" sx={{ height: "calc(100vh - 64px)" }}>
|
||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
</Box>
|
</Box>
|
||||||
: mobile ?
|
: <>
|
||||||
<>
|
<WelcomeAppBar gameInfo={gameInfo.data} toggleImpressum={toggleImpressum} openEraseMenu={openEraseMenu}
|
||||||
<MobileNav pageNumber={pageNumber} setPageNumber={setPageNumber} />
|
openUploadMenu={openUploadMenu} />
|
||||||
{(pageNumber == 0 ?
|
<div className="app-content">
|
||||||
<IntroductionPanel introduction={gameInfo.data?.introduction} />
|
{ mobile ?
|
||||||
: pageNumber == 1 ?
|
<div className="welcome mobile">
|
||||||
<WorldTreePanel worlds={gameInfo.data?.worlds} worldSize={gameInfo.data?.worldSize} />
|
{(pageNumber == 0 ?
|
||||||
|
<IntroductionPanel introduction={gameInfo.data?.introduction} title={gameInfo.data?.title}/>
|
||||||
|
: pageNumber == 1 ?
|
||||||
|
<WorldTreePanel worlds={gameInfo.data?.worlds} worldSize={gameInfo.data?.worldSize} />
|
||||||
|
:
|
||||||
|
<InventoryPanel levelInfo={inventory?.data} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
:
|
:
|
||||||
<InventoryPanel levelInfo={inventory?.data} />
|
<Split className="welcome" minSize={0} snapOffset={200} sizes={[25, 50, 25]}>
|
||||||
)}
|
<IntroductionPanel introduction={gameInfo.data?.introduction} />
|
||||||
</>
|
<WorldTreePanel worlds={gameInfo.data?.worlds} worldSize={gameInfo.data?.worldSize} />
|
||||||
:
|
<InventoryPanel levelInfo={inventory?.data} />
|
||||||
|
</Split>
|
||||||
<Split className="welcome" minSize={0} snapOffset={200} sizes={[40, 35, 25]}>
|
}
|
||||||
<IntroductionPanel introduction={gameInfo.data?.introduction} />
|
</div>
|
||||||
<WorldTreePanel worlds={gameInfo.data?.worlds} worldSize={gameInfo.data?.worldSize} />
|
{impressum ? <PrivacyPolicyPopup handleClose={closeImpressum} /> : null}
|
||||||
<InventoryPanel levelInfo={inventory?.data} />
|
{eraseMenu? <ErasePopup handleClose={closeEraseMenu}/> : null}
|
||||||
</Split>
|
{uploadMenu? <UploadPopup handleClose={closeUploadMenu}/> : null}
|
||||||
}
|
</>
|
||||||
<PrivacyPolicy />
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Welcome
|
export default Welcome
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
.world-selection-menu {
|
.world-selection-menu {
|
||||||
padding: .5em;
|
padding: .5em 1em;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 1em;
|
||||||
|
/* margin: 1em; */
|
||||||
|
/* border: 1px solid var(--clr-primary); */
|
||||||
|
border-bottom: 1px solid var(--clr-primary);
|
||||||
|
border-left: 1px solid var(--clr-primary);
|
||||||
|
border-top: 1px solid var(--clr-primary);
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: .5em 0 0 .5em;
|
||||||
|
filter: drop-shadow(4px 4px 5px rgba(0,0,0,0.5));
|
||||||
}
|
}
|
||||||
|
|
||||||
.world-selection-menu .btn, .welcome .btn {
|
.world-selection-menu .btn, .welcome .btn {
|
||||||
@@ -12,19 +23,18 @@
|
|||||||
|
|
||||||
.world-selection-menu .slider-wrap {
|
.world-selection-menu .slider-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: column;
|
||||||
width: 100%;
|
/* width: 100%; */
|
||||||
|
height: 7em;
|
||||||
/* min-width: 16em; */
|
/* min-width: 16em; */
|
||||||
padding-left: 0.5em;
|
padding-bottom: .8em;
|
||||||
padding-right: 3em;
|
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.difficulty-label {
|
.difficulty-label {
|
||||||
font-size: 0.875em;
|
/* font-size: 0.875em; */
|
||||||
padding-top: 4px;
|
padding-bottom: 1.2em;
|
||||||
padding-right: 1em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Test for mobile `title`s */
|
/* Test for mobile `title`s */
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ export function WorldPath({source, target, unlocked} : {source: any, target: any
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Download a file containing `data` */
|
/** Download a file containing `data` */
|
||||||
const downloadFile = ({ data, fileName, fileType } :
|
export const downloadFile = ({ data, fileName, fileType } :
|
||||||
{ data: string
|
{ data: string
|
||||||
fileName: string
|
fileName: string
|
||||||
fileType: string}) => {
|
fileType: string}) => {
|
||||||
@@ -186,88 +186,21 @@ const downloadFile = ({ data, fileName, fileType } :
|
|||||||
a.remove()
|
a.remove()
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: I think this should be removed and that single button incorporated differently
|
|
||||||
/** Menu on desktop to go back to game server */
|
|
||||||
export function WelcomeMenu() {
|
|
||||||
|
|
||||||
return <nav className="world-selection-menu">
|
|
||||||
<Button inverted="false" title="back to games selection" to="/">
|
|
||||||
<FontAwesomeIcon icon={faArrowLeft} /> <FontAwesomeIcon icon={faGlobe} />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
</nav>
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The menu that is shown next to the world selection graph */
|
/** The menu that is shown next to the world selection graph */
|
||||||
function WorldSelectionMenu() {
|
export function WorldSelectionMenu() {
|
||||||
const [file, setFile] = React.useState<File>();
|
|
||||||
|
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const store = useStore()
|
|
||||||
const difficulty = useSelector(selectDifficulty(gameId))
|
const difficulty = useSelector(selectDifficulty(gameId))
|
||||||
|
|
||||||
|
|
||||||
/* 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()
|
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) {
|
function label(x : number) {
|
||||||
return x == 0 ? 'none' : x == 1 ? 'lax' : 'regular'
|
return x == 0 ? 'none' : x == 1 ? 'lax' : 'regular'
|
||||||
}
|
}
|
||||||
|
|
||||||
return <nav className="world-selection-menu">
|
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">
|
<div className="slider-wrap">
|
||||||
<span className="difficulty-label">Game Rules:</span>
|
<span className="difficulty-label">Game Rules</span>
|
||||||
<Slider
|
<Slider
|
||||||
|
orientation="vertical"
|
||||||
title="Game Rules: - regular: 🔐 levels, 🔐 tactics - lax: 🔓 levels, 🔐 tactics - none: 🔓 levels, 🔓 tactics"
|
title="Game Rules: - regular: 🔐 levels, 🔐 tactics - lax: 🔓 levels, 🔐 tactics - none: 🔓 levels, 🔓 tactics"
|
||||||
min={0} max={2}
|
min={0} max={2}
|
||||||
aria-label="Game Rules"
|
aria-label="Game Rules"
|
||||||
@@ -279,48 +212,12 @@ function WorldSelectionMenu() {
|
|||||||
]}
|
]}
|
||||||
valueLabelFormat={label}
|
valueLabelFormat={label}
|
||||||
getAriaValueText={label}
|
getAriaValueText={label}
|
||||||
valueLabelDisplay="auto"
|
valueLabelDisplay="off"
|
||||||
onChange={(ev, val: number) => {
|
onChange={(ev, val: number) => {
|
||||||
dispatch(changedDifficulty({game: gameId, difficulty: val}))
|
dispatch(changedDifficulty({game: gameId, difficulty: val}))
|
||||||
}}
|
}}
|
||||||
></Slider>
|
></Slider>
|
||||||
|
|
||||||
</div>
|
</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>
|
</nav>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -444,7 +341,7 @@ export function WorldTreePanel({worlds, worldSize}:
|
|||||||
let dx = bounds ? s*(bounds.x2 - bounds.x1) + 2*padding : null
|
let dx = bounds ? s*(bounds.x2 - bounds.x1) + 2*padding : null
|
||||||
|
|
||||||
return <div className="column">
|
return <div className="column">
|
||||||
<WorldSelectionMenu />
|
<WorldSelectionMenu />
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink"
|
<svg xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||||
width={bounds ? `${ds * dx}` : ''}
|
width={bounds ? `${ds * dx}` : ''}
|
||||||
viewBox={bounds ? `${s*bounds.x1 - padding} ${s*bounds.y1 - padding} ${dx} ${s*(bounds.y2 - bounds.y1) + 2 * padding}` : ''}
|
viewBox={bounds ? `${s*bounds.x1 - padding} ${s*bounds.y1 - padding} ${dx} ${s*(bounds.y2 - bounds.y1) + 2 * padding}` : ''}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
|
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
|
||||||
import { Connection } from '../connection'
|
import { Connection } from '../connection'
|
||||||
|
|
||||||
interface GameInfo {
|
export interface GameInfo {
|
||||||
title: null|string,
|
title: null|string,
|
||||||
introduction: null|string,
|
introduction: null|string,
|
||||||
worlds: null|{nodes: {[id:string]: {id: string, title: string, introduction: string}}, edges: string[][]},
|
worlds: null|{nodes: {[id:string]: {id: string, title: string, introduction: string}}, edges: string[][]},
|
||||||
|
|||||||
Reference in New Issue
Block a user