cleanup: popups

pull/251/merge
Jon Eugster 2 years ago
parent d82ef8af8f
commit c1642cf09b

@ -9,14 +9,14 @@ import '@fontsource/roboto/700.css';
import './css/reset.css'; import './css/reset.css';
import './css/app.css'; import './css/app.css';
import { PageContext, PopupContext, PreferencesContext} from './components/infoview/context'; import { PageContext, PreferencesContext} from './components/infoview/context';
import UsePreferences from "./state/hooks/use_preferences" import UsePreferences from "./state/hooks/use_preferences"
import i18n from './i18n'; import i18n from './i18n';
import { Navigation } from './components/navigation'; import { Navigation } from './components/navigation';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { changeTypewriterMode, selectTypewriterMode } from './state/progress'; import { changeTypewriterMode, selectOpenedIntro, selectTypewriterMode } from './state/progress';
import { useAppDispatch } from './hooks'; import { useAppDispatch } from './hooks';
import { Popup } from './components/popup/popup'; import { Popup, PopupContext } from './components/popup/popup';
export const GameIdContext = React.createContext<{ export const GameIdContext = React.createContext<{
gameId: string, gameId: string,
@ -40,6 +40,16 @@ function App() {
const [page, setPage] = useState(0) const [page, setPage] = useState(0)
const [popupContent, setPopupContent] = useState(null) const [popupContent, setPopupContent] = useState(null)
const openedIntro = useSelector(selectOpenedIntro(gameId))
useEffect(() => {
if (openedIntro && !worldId && page == 0) {
setPage(1)
}
}, [openedIntro])
useEffect(() => { useEffect(() => {
i18n.changeLanguage(language) i18n.changeLanguage(language)
}, [language]) }, [language])

@ -49,7 +49,7 @@ import { IConnectionProvider } from 'monaco-languageclient'
import { monacoSetup } from 'lean4web/client/src/monacoSetup' import { monacoSetup } from 'lean4web/client/src/monacoSetup'
import { onigasmH } from 'onigasm/lib/onigasmH' import { onigasmH } from 'onigasm/lib/onigasmH'
import { isLastStepWithErrors, lastStepHasErrors } from './infoview/goals' import { isLastStepWithErrors, lastStepHasErrors } from './infoview/goals'
import { InfoPopup } from './popup/game_info' import { InfoPopup } from './popup/info'
import { PreferencesPopup } from './popup/preferences' import { PreferencesPopup } from './popup/preferences'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import i18next from 'i18next' import i18next from 'i18next'

@ -11,6 +11,8 @@ import { downloadProgress } from './popup/erase'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import '../css/navigation.css' import '../css/navigation.css'
import { PopupContext } from './popup/popup' import { PopupContext } from './popup/popup'
import { useSelector } from 'react-redux'
import { selectProgress } from '../state/progress'
/** SVG github icon */ /** SVG github icon */
function GithubIcon () { function GithubIcon () {
@ -41,15 +43,29 @@ const NavigationContext = createContext<{
setNavOpen: React.Dispatch<React.SetStateAction<boolean>> setNavOpen: React.Dispatch<React.SetStateAction<boolean>>
}>({navOpen: false, setNavOpen: () => {}}) }>({navOpen: false, setNavOpen: () => {}})
/** Content of the navigation during game selection. */
function NavigationLandingPage () {
return <div className="nav-content">
<div className="nav-title-left"></div>
<div className="nav-title-middle"></div>
<div className="nav-title-right"></div>
</div>
}
/** Content of the navigation on Desktop during world selection. */ /** Content of the navigation on Desktop during world selection. */
function DesktopNavigationOverview () { function DesktopNavigationOverview () {
const { t } = useTranslation() const { t } = useTranslation()
const {gameId} = useContext(GameIdContext) const { gameId } = useContext(GameIdContext)
const { setPopupContent } = useContext(PopupContext)
const gameInfo = useGetGameInfoQuery({game: gameId}) const gameInfo = useGetGameInfoQuery({game: gameId})
return <div className="nav-content"> return <div className="nav-content">
<div className="nav-title-left"></div> <div className="nav-title-left">
<NavButton
text={t("Rules")}
onClick={() => {setPopupContent("rules")}}
inverted={true} />
</div>
<div className="nav-title-middle"> <div className="nav-title-middle">
<span className="nav-title">{t(gameInfo.data?.title, {ns: gameId})}</span> <span className="nav-title">{t(gameInfo.data?.title, {ns: gameId})}</span>
</div> </div>
@ -61,9 +77,15 @@ function DesktopNavigationOverview () {
function MobileNavigationOverview () { function MobileNavigationOverview () {
const { t } = useTranslation() const { t } = useTranslation()
const {page, setPage} = useContext(PageContext) const {page, setPage} = useContext(PageContext)
const { setPopupContent } = useContext(PopupContext)
return <div className="nav-content"> return <div className="nav-content">
<div className="nav-title-left"></div> <div className="nav-title-left">
<NavButton
text={t("Rules")}
onClick={() => {setPopupContent("rules")}}
inverted={true} />
</div>
<div className="nav-title-middle"> <div className="nav-title-middle">
<span className="nav-title"> <span className="nav-title">
</span> </span>
@ -88,15 +110,6 @@ function MobileNavigationOverview () {
</div> </div>
} }
/** Content of the navigation during game selection. */
function NavigationLandingPage () {
return <div className="nav-content">
<div className="nav-title-left"></div>
<div className="nav-title-middle"></div>
<div className="nav-title-right"></div>
</div>
}
/** Content of the navigation on Desktop in a level. */ /** Content of the navigation on Desktop in a level. */
function DesktopNavigationLevel () { function DesktopNavigationLevel () {
const { t } = useTranslation() const { t } = useTranslation()
@ -201,6 +214,8 @@ export function Navigation () {
const { gameId, worldId } = useContext(GameIdContext) const { gameId, worldId } = useContext(GameIdContext)
const { mobile } = useContext(PreferencesContext) const { mobile } = useContext(PreferencesContext)
const { setPopupContent } = useContext(PopupContext) const { setPopupContent } = useContext(PopupContext)
const gameProgress = useSelector(selectProgress(gameId))
const [navOpen, setNavOpen] = useState(false) const [navOpen, setNavOpen] = useState(false)
function toggleNav () {setNavOpen(!navOpen)} function toggleNav () {setNavOpen(!navOpen)}
@ -246,7 +261,7 @@ export function Navigation () {
<NavButton <NavButton
icon={faDownload} icon={faDownload}
text={t("Download")} text={t("Download")}
onClick={() => {downloadProgress(gameId)}} onClick={() => {downloadProgress(gameId, gameProgress)}}
inverted={true} /> inverted={true} />
<NavButton <NavButton
icon={faUpload} icon={faUpload}

@ -10,8 +10,7 @@ import { useContext } from 'react'
import { PopupContext } from './popup' import { PopupContext } from './popup'
/** download the current progress (i.e. what's saved in the browser store) */ /** download the current progress (i.e. what's saved in the browser store) */
export function downloadProgress(gameId: string) { export function downloadProgress(gameId: string, gameProgress) {
const gameProgress = useSelector(selectProgress(gameId))
// ev.preventDefault() // ev.preventDefault()
downloadFile({ downloadFile({
@ -33,14 +32,15 @@ export function ErasePopup () {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { setPopupContent } = useContext(PopupContext) const { setPopupContent } = useContext(PopupContext)
const eraseProgress = () => { const eraseProgress = (ev) => {
dispatch(deleteProgress({game: gameId})) dispatch(deleteProgress({game: gameId}))
setPopupContent(null) setPopupContent(null)
ev.preventDefault() // TODO: this is a hack to prevent the buttons below from opening a link
} }
const downloadAndErase = (ev) => { const downloadAndErase = (ev) => {
downloadProgress(gameId) downloadProgress(gameId, gameProgress)
eraseProgress() eraseProgress(ev)
} }
return <> return <>
@ -54,6 +54,6 @@ export function ErasePopup () {
</Trans> </Trans>
<Button onClick={eraseProgress} to="">{t("Delete")}</Button> <Button onClick={eraseProgress} to="">{t("Delete")}</Button>
<Button onClick={downloadAndErase} to="">{t("Download & Delete")}</Button> <Button onClick={downloadAndErase} to="">{t("Download & Delete")}</Button>
<Button onClick={() => {setPopupContent(null)}} to="">{t("Cancel")}</Button> <Button onClick={(ev) => {setPopupContent(null); ev.preventDefault()}} to="">{t("Cancel")}</Button>
</> </>
} }

@ -1,6 +1,3 @@
/**
* @fileOverview
*/
import * as React from 'react' import * as React from 'react'
import { Typography } from '@mui/material' import { Typography } from '@mui/material'
import Markdown from '../markdown' import Markdown from '../markdown'

@ -1,11 +1,13 @@
import * as React from 'react' import * as React from 'react'
import { useContext } from 'react' import { useContext } from 'react'
import { PrivacyPolicyPopup } from './privacy_policy' import { PrivacyPolicyPopup } from './privacy'
import { ImpressumPopup } from './impressum' import { ImpressumPopup } from './impressum'
import { InfoPopup } from './game_info' import { InfoPopup } from './info'
import { ErasePopup } from './erase' import { ErasePopup } from './erase'
import { PreferencesPopup } from './preferences' import { PreferencesPopup } from './preferences'
import { UploadPopup } from './upload' import { UploadPopup } from './upload'
import { RulesPopup } from './rules'
import '../../css/popup.css'
/** The context which manages if a popup is shown. /** The context which manages if a popup is shown.
* If `popupContent` is `null`, the popup is closed. * If `popupContent` is `null`, the popup is closed.
@ -30,6 +32,7 @@ export const Popups = {
"info": <InfoPopup />, "info": <InfoPopup />,
"preferences": <PreferencesPopup />, "preferences": <PreferencesPopup />,
"privacy": <PrivacyPolicyPopup />, "privacy": <PrivacyPolicyPopup />,
"rules": <RulesPopup />,
"upload": <UploadPopup />, "upload": <UploadPopup />,
} }

@ -0,0 +1,88 @@
import { Box, Slider } from '@mui/material'
import * as React from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { GameIdContext } from '../../app'
import { changedDifficulty, selectDifficulty } from '../../state/progress'
import { useSelector } from 'react-redux'
import { useContext } from 'react'
import { useAppDispatch } from '../../hooks'
/** Pop-up that is displayed when opening the help explaining the game rules.
*
*/
export function RulesPopup () {
const { t } = useTranslation()
const { gameId } = useContext(GameIdContext)
const difficulty = useSelector(selectDifficulty(gameId))
const dispatch = useAppDispatch()
function label(x : number) {
return x == 0 ? t("none") : x == 1 ? t("relaxed") : t("regular")
}
return <>
<h2>{t("Game Rules")}</h2>
{/* <span className="difficulty-label">{t("Rules")}
<FontAwesomeIcon icon={rulesHelp ? faXmark : faCircleQuestion} className='helpButton' onClick={() => (setRulesHelp(!rulesHelp))}/>
</span> */}
<Box className="slider-wrapper">
<Slider
orientation="horizontal"
title={t("Game Rules")}
min={0} max={2}
aria-label={t("Game Rules")}
value={difficulty}
marks={[
{value: 0, label: label(0)},
{value: 1, label: label(1)},
{value: 2, label: label(2)}
]}
valueLabelFormat={label}
getAriaValueText={label}
valueLabelDisplay="off"
onChange={(ev, val: number) => {
dispatch(changedDifficulty({game: gameId, difficulty: val}))
}}
/>
</Box>
<Trans>
<p>
Game rules determine if it is allowed to skip levels and if the games runs checks to only
allow unlocked tactics and theorems in proofs.
</p>
<p>
Note: "Unlocked" tactics (or theorems) are determined by two things: The set of minimal
tactics needed to solve a level, plus any tactics you unlocked in another level. That means
if you unlock <code>simp</code> in a level, you can use it henceforth in any level.
</p>
<p>The options are:</p>
</Trans>
<table>
<thead>
<tr>
<th scope="col"></th>
<th scope="col">{t("levels")}</th>
<th scope="col">{t("tactics")}</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">{t("regular")}</th>
<td>🔐</td>
<td>🔐</td>
</tr>
<tr>
<th scope="row">{t("relaxed")}</th>
<td>🔓</td>
<td>🔐</td>
</tr>
<tr>
<th scope="row">{t("none")}</th>
<td>🔓</td>
<td>🔓</td>
</tr>
</tbody>
</table>
</>
}

@ -1,60 +0,0 @@
/**
* @fileOverview
*/
import * as React from 'react'
import { Trans, useTranslation } from 'react-i18next'
/** Pop-up that is displayed when opening the help explaining the game rules.
*
* `handleClose` is the function to close it again because it's open/closed state is
* controlled by the containing element.
*/
export function RulesHelpPopup ({handleClose}: {handleClose: () => void}) {
const { t } = useTranslation()
return <div className="privacy-policy modal-wrapper">
<div className="modal-backdrop" onClick={handleClose} />
<div className="modal">
<div className="codicon codicon-close modal-close" onClick={handleClose}></div>
<h2>{t("Game Rules")}</h2>
<Trans>
<p>
Game rules determine if it is allowed to skip levels and if the games runs checks to only
allow unlocked tactics and theorems in proofs.
</p>
<p>
Note: "Unlocked" tactics (or theorems) are determined by two things: The set of minimal
tactics needed to solve a level, plus any tactics you unlocked in another level. That means
if you unlock <code>simp</code> in a level, you can use it henceforth in any level.
</p>
<p>The options are:</p>
</Trans>
<table>
<thead>
<tr>
<th scope="col"></th>
<th scope="col">{t("levels")}</th>
<th scope="col">{t("tactics")}</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">{t("regular")}</th>
<td>🔐</td>
<td>🔐</td>
</tr>
<tr>
<th scope="row">{t("relaxed")}</th>
<td>🔓</td>
<td>🔐</td>
</tr>
<tr>
<th scope="row">{t("none")}</th>
<td>🔓</td>
<td>🔓</td>
</tr>
</tbody>
</table>
</div>
</div>
}

@ -13,9 +13,8 @@ import { Button } from './button'
import { PageContext, PreferencesContext } from './infoview/context' import { PageContext, PreferencesContext } from './infoview/context'
import { InventoryPanel } from './inventory' import { InventoryPanel } from './inventory'
import { ErasePopup } from './popup/erase' import { ErasePopup } from './popup/erase'
import { InfoPopup } from './popup/game_info' import { InfoPopup } from './popup/info'
import { PrivacyPolicyPopup } from './popup/privacy_policy' import { PrivacyPolicyPopup } from './popup/privacy'
import { RulesHelpPopup } from './popup/rules_help'
import { UploadPopup } from './popup/upload' import { UploadPopup } from './popup/upload'
import { PreferencesPopup} from "./popup/preferences" import { PreferencesPopup} from "./popup/preferences"
import { WorldTreePanel } from './world_tree' import { WorldTreePanel } from './world_tree'

@ -360,7 +360,7 @@ export function WorldTreePanel({worlds, worldSize, rulesHelp, setRulesHelp}:
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 rulesHelp={rulesHelp} setRulesHelp={setRulesHelp} /> {/* <WorldSelectionMenu rulesHelp={rulesHelp} setRulesHelp={setRulesHelp} /> */}
<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}` : ''}

@ -0,0 +1,7 @@
/* For the "Rules" popup. */
.slider-wrapper {
padding-left: 2rem;
padding-right: 2rem;
padding-top: 1rem;
}

@ -184,7 +184,7 @@ export function selectCompleted(game: string, world: string, level: number) {
/** return progress for the current game if it exists */ /** return progress for the current game if it exists */
export function selectProgress(game: string) { export function selectProgress(game: string) {
return (state) => { return (state) => {
return state.progress.games[game.toLowerCase()] ?? null return state.progress.games[game?.toLowerCase()] ?? null
} }
} }
@ -198,7 +198,7 @@ export function selectDifficulty(game: string) {
/** return whether the intro has been read */ /** return whether the intro has been read */
export function selectOpenedIntro(game: string) { export function selectOpenedIntro(game: string) {
return (state) => { return (state) => {
return state.progress.games[game.toLowerCase()]?.openedIntro return state.progress.games[game?.toLowerCase()]?.openedIntro
} }
} }

Loading…
Cancel
Save