use redux

This commit is contained in:
Alexander Bentkamp
2022-11-29 17:06:20 +01:00
parent 50339da74f
commit cc6eb8c3f9
10 changed files with 399 additions and 90 deletions
+5 -17
View File
@@ -14,29 +14,15 @@ import { AppBar, CssBaseline, Toolbar, Typography } from '@mui/material';
import Welcome from './components/Welcome';
import Level from './components/Level';
import GoodBye from './components/GoodBye';
import useWebSocket from 'react-use-websocket';
import { LeanClient } from 'lean4web/client/src/editor/leanclient';
import { monacoSetup } from 'lean4web/client/src/monacoSetup'
const socketUrl = ((window.location.protocol === "https:") ? "wss://" : "ws://") + window.location.host + '/websocket/'
monacoSetup()
import { useAppSelector } from './hooks';
function App() {
const [title, setTitle] = useState("")
const [conclusion, setConclusion] = useState("")
const [levelTitle, setLevelTitle] = useState("")
const [nbLevels, setNbLevels] = useState(0)
const [curLevel, setCurLevel] = useState(0)
const [finished, setFinished] = useState(false)
const [leanClient, setLeanClient] = useState<null|LeanClient>(null)
useEffect(() => {
const uri = monaco.Uri.parse('file:///')
const leanClient = new LeanClient(socketUrl, undefined, uri, () => {})
setLeanClient(leanClient)
}, [])
const mathJaxConfig = {
loader: {
@@ -58,11 +44,13 @@ function App() {
if (finished) {
mainComponent = <GoodBye message={conclusion} />
} else if (curLevel > 0) {
mainComponent = <Level leanClient={leanClient} nbLevels={nbLevels} level={curLevel} setCurLevel={setCurLevel} setLevelTitle={setLevelTitle} setFinished={setFinished}/>
mainComponent = <Level nbLevels={nbLevels} level={curLevel} setCurLevel={setCurLevel} setLevelTitle={setLevelTitle} setFinished={setFinished}/>
} else {
mainComponent = <Welcome leanClient={leanClient} setNbLevels={setNbLevels} setTitle={setTitle} startGame={startGame} setConclusion={setConclusion}/>
mainComponent = <Welcome setNbLevels={setNbLevels} startGame={startGame} setConclusion={setConclusion}/>
}
const title = useAppSelector(state => state.game.title)
return (
<div className="App">
<MathJaxContext config={mathJaxConfig}>
+4 -2
View File
@@ -22,9 +22,9 @@ import 'lean4web/client/src/editor/infoview.css'
import { renderInfoview } from '@leanprover/infoview'
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js'
import './level.css'
import { ConnectionContext } from '../connection';
interface LevelProps {
leanClient: null|LeanClient;
nbLevels: any;
level: any;
setCurLevel: any;
@@ -32,7 +32,7 @@ interface LevelProps {
setFinished: any
}
function Level({ leanClient, nbLevels, level, setCurLevel, setLevelTitle, setFinished }: LevelProps) {
function Level({ nbLevels, level, setCurLevel, setLevelTitle, setFinished }: LevelProps) {
const [index, setIndex] = useState(level) // Level number
const [tacticDocs, setTacticDocs] = useState([])
const [lemmaDocs, setLemmaDocs] = useState([])
@@ -72,6 +72,8 @@ function Level({ leanClient, nbLevels, level, setCurLevel, setLevelTitle, setFin
messagePanelRef.current!.scrollTo(0,0)
}, [level])
const leanClient = React.useContext(ConnectionContext)
useEffect(() => {
const editor = monaco.editor.create(codeviewRef.current!, {
glyphMargin: true,
+28 -41
View File
@@ -9,30 +9,25 @@ import '@fontsource/roboto/700.css';
import * as rpc from 'vscode-ws-jsonrpc';
import cytoscape from 'cytoscape'
import klay from 'cytoscape-klay';
import { useSelector, useDispatch } from 'react-redux'
import { fetchGame } from '../game/gameSlice'
cytoscape.use( klay );
import { Box, Typography, Button, CircularProgress, Grid } from '@mui/material';
import { LeanClient } from 'lean4web/client/src/editor/leanclient';
import { ConnectionContext } from '../connection';
import { useAppDispatch, useAppSelector } from '../hooks';
interface WelcomeProps {
leanClient: null|LeanClient;
setNbLevels: any;
setTitle: any;
startGame: any;
setConclusion: any;
}
type infoResultType = {
title: string,
nb_levels: any[],
conclusion: string
worlds: {edges: string[][], nodes: string[]}
}
function Welcome({ setNbLevels, startGame, setConclusion }: WelcomeProps) {
const dispatch = useAppDispatch()
function Welcome({ leanClient, setNbLevels, setTitle, startGame, setConclusion }: WelcomeProps) {
const [leanData, setLeanData] = useState<null|infoResultType>(null)
const worldsRef = useRef<HTMLDivElement>(null)
const drawWorlds = (worlds) => {
@@ -78,42 +73,34 @@ function Welcome({ leanClient, setNbLevels, setTitle, startGame, setConclusion }
})
}
useEffect(() => {
if (!leanClient) return;
useEffect(() => { dispatch(fetchGame); }, [])
const getInfo = async () => {
await leanClient.start() // TODO: need a way to wait for start without restarting
leanClient.sendRequest("info", {}).then((res: infoResultType) =>{
console.log(res)
setLeanData(res)
setNbLevels(res.nb_levels)
setTitle(res.title)
document.title = res.title
setConclusion(res.conclusion)
drawWorlds(res.worlds)
});
}
getInfo()
}, [leanClient])
const worlds = useAppSelector(state => state.game.worlds)
useEffect(() => { if (worlds) { drawWorlds(worlds); } }, [worlds])
let content
if (leanData) {
content = (<Box sx={{ m: 3 }}>
<Typography variant="body1" component="div">
<MathJax>
<ReactMarkdown>{leanData["introduction"]}</ReactMarkdown>
</MathJax>
</Typography>
const title = useAppSelector(state => state.game.title)
useEffect(() => { window.document.title = title }, [title])
const introduction = useAppSelector(state => state.game.introduction)
return <div>
{ introduction?// TODO: find a better way to mark loading state?
<div>
<Box sx={{ m: 3 }}>
<Typography variant="body1" component="div">
<MathJax>
<ReactMarkdown>{introduction}</ReactMarkdown>
</MathJax>
</Typography>
</Box>
<Box textAlign='center' sx={{ m: 5 }}>
<Button onClick={startGame} variant="contained">Start rescue mission</Button>
</Box>
</Box>)
} else {
content = <Box display="flex" alignItems="center" justifyContent="center" sx={{ height: "calc(100vh - 64px)" }}><CircularProgress /></Box>
<div ref={worldsRef} style={{"width": "100%","height": "50em"}} />
</div>
: <Box display="flex" alignItems="center" justifyContent="center" sx={{ height: "calc(100vh - 64px)" }}><CircularProgress /></Box>
}
return <div>
<div>{content}</div>
<div ref={worldsRef} style={{"width": "100%","height": "50em"}} />
</div>
}
+15
View File
@@ -0,0 +1,15 @@
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js'
import { LeanClient } from 'lean4web/client/src/editor/leanclient';
import * as React from 'react';
import { monacoSetup } from 'lean4web/client/src/monacoSetup';
monacoSetup()
const socketUrl = ((window.location.protocol === "https:") ? "wss://" : "ws://") + window.location.host + '/websocket/'
const uri = monaco.Uri.parse('file:///')
export const leanClient = new LeanClient(socketUrl, undefined, uri, () => {})
export const ConnectionContext = React.createContext(null);
+59
View File
@@ -0,0 +1,59 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { LeanClient } from 'lean4web/client/src/editor/leanclient'
import type { RootState } from '../store'
interface GameState {
title: null|string,
introduction: null|string,
worlds: null|{nodes: string[], edges: string[][2]},
authors: null|string[],
conclusion: null|string,
}
const initialState : GameState = {
title: null,
introduction: null,
worlds: null,
authors: null,
conclusion: null,
}
export const gameSlice = createSlice({
name: 'game',
initialState,
reducers: {
loadedGame: (state, action: PayloadAction<any>) => {
state.title = action.payload.title
state.introduction = action.payload.introduction
state.worlds = action.payload.worlds
state.authors = action.payload.authors
state.conclusion = action.payload.conclusion
},
},
})
export const { loadedGame } = gameSlice.actions
// TODO: Move this into LeanClient?
/** Call `callback` when the leanClient has started. If not already started, start it. */
const whenLeanClientStarted = (leanClient, callback) => {
if (leanClient.isRunning()) {
callback()
} else {
if (!leanClient.isStarted()) {
leanClient.start()
}
leanClient.restarted(callback)
}
}
export const fetchGame = (dispatch, getState, extraArgument) => {
const leanClient : LeanClient = extraArgument.leanClient
whenLeanClientStarted(leanClient, () => {
leanClient.sendRequest("info", {}).then((res) =>{
dispatch(loadedGame(res))
})
})
}
export default gameSlice.reducer
+6
View File
@@ -0,0 +1,6 @@
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from './store'
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
+13 -1
View File
@@ -2,7 +2,19 @@ import * as React from 'react';
import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App';
import { store } from './store';
import { Provider } from 'react-redux';
import { ConnectionContext, leanClient } from './connection'
const container = document.getElementById('root');
const root = createRoot(container!);
root.render(<App />);
root.render(
<React.StrictMode>
<Provider store={store}>
<ConnectionContext.Provider value={leanClient}>
<App />
</ConnectionContext.Provider>
</Provider>
</React.StrictMode>
);
+24
View File
@@ -0,0 +1,24 @@
import { configureStore } from '@reduxjs/toolkit';
import gameReducer from './game/gameSlice';
import { leanClient } from './connection'
import thunkMiddleware from 'redux-thunk'
const thunkMiddlewareWithArg = thunkMiddleware.withExtraArgument({ leanClient })
export const store = configureStore({
reducer: {
game: gameReducer,
},
middleware: getDefaultMiddleware =>
getDefaultMiddleware({
thunk: {
extraArgument: { leanClient }
}
})
});
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch