diff --git a/client/src/components/Level.tsx b/client/src/components/Level.tsx index fc4723f..9bbe71c 100644 --- a/client/src/components/Level.tsx +++ b/client/src/components/Level.tsx @@ -21,7 +21,10 @@ import './level.css' import { ConnectionContext } from '../connection'; import Infoview from './Infoview'; import { useParams } from 'react-router-dom'; -import { useLoadLevelQuery } from '../game/api'; +import { useLoadLevelQuery } from '../state/api'; +import { codeEdited, selectCode } from '../state/progress'; +import { useAppDispatch } from '../hooks'; +import { useSelector } from 'react-redux'; @@ -45,7 +48,17 @@ function Level() { const connection = React.useContext(ConnectionContext) const level = useLoadLevelQuery({world: worldId, level: levelId}) - const {editor, infoProvider} = useLevelEditor(worldId, levelId, codeviewRef, infoviewRef) + + const dispatch = useAppDispatch() + + const onDidChangeContent = (code) => { + dispatch(codeEdited({world: worldId, level: levelId, code})) + } + + const initialCode = useSelector(selectCode(worldId, levelId)) + + const {editor, infoProvider} = + useLevelEditor(worldId, levelId, codeviewRef, infoviewRef, initialCode, onDidChangeContent) return <> @@ -84,7 +97,7 @@ function Level() { export default Level -function useLevelEditor(worldId: string, levelId: number, codeviewRef, infoviewRef) { +function useLevelEditor(worldId: string, levelId: number, codeviewRef, infoviewRef, initialCode, onDidChangeContent) { const connection = React.useContext(ConnectionContext) @@ -128,8 +141,11 @@ function useLevelEditor(worldId: string, levelId: number, codeviewRef, infoviewR if (editor) { const uri = monaco.Uri.parse(`file:///${worldId}/${levelId}`) - const model = monaco.editor.getModel(uri) ?? - monaco.editor.createModel('', 'lean4', uri) + let model = monaco.editor.getModel(uri) + if (!model) { + model = monaco.editor.createModel(initialCode, 'lean4', uri) + model.onDidChangeContent(() => onDidChangeContent(model.getValue())) + } editor.setModel(model) infoviewApi.serverRestarted(leanClient.initializeResult) @@ -139,7 +155,7 @@ function useLevelEditor(worldId: string, levelId: number, codeviewRef, infoviewR new AbbreviationRewriter(new AbbreviationProvider(), model, editor) } }) - + // TODO: Properly close the file to stop send "keepAlive" calls to the server }, [editor, levelId, connection]) return {editor, infoProvider} diff --git a/client/src/components/Welcome.tsx b/client/src/components/Welcome.tsx index 330d520..45b4063 100644 --- a/client/src/components/Welcome.tsx +++ b/client/src/components/Welcome.tsx @@ -14,7 +14,7 @@ import { Link as RouterLink, useNavigate } from 'react-router-dom'; cytoscape.use( klay ); import { Box, Typography, Button, CircularProgress, Grid } from '@mui/material'; -import { useGetGameInfoQuery } from '../game/api'; +import { useGetGameInfoQuery } from '../state/api'; import { Link } from 'react-router-dom'; diff --git a/client/src/hooks.ts b/client/src/hooks.ts index 60a4163..afdce0a 100644 --- a/client/src/hooks.ts +++ b/client/src/hooks.ts @@ -1,5 +1,5 @@ import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux' -import type { RootState, AppDispatch } from './store' +import type { RootState, AppDispatch } from './state/store' // Use throughout your app instead of plain `useDispatch` and `useSelector` export const useAppDispatch: () => AppDispatch = useDispatch diff --git a/client/src/index.tsx b/client/src/index.tsx index 9c9790b..f936b83 100644 --- a/client/src/index.tsx +++ b/client/src/index.tsx @@ -3,7 +3,7 @@ import { createRoot } from 'react-dom/client'; import './index.css'; import App from './App'; import { ConnectionContext, connection } from './connection' -import { store } from './store'; +import { store } from './state/store'; import { Provider } from 'react-redux'; import { createHashRouter, diff --git a/client/src/game/api.ts b/client/src/state/api.ts similarity index 93% rename from client/src/game/api.ts rename to client/src/state/api.ts index d4f1684..5ee564a 100644 --- a/client/src/game/api.ts +++ b/client/src/state/api.ts @@ -37,7 +37,7 @@ const customBaseQuery = async ( } // Define a service using a base URL and expected endpoints -export const gameApi = createApi({ +export const apiSlice = createApi({ reducerPath: 'gameApi', baseQuery: customBaseQuery, endpoints: (builder) => ({ @@ -52,4 +52,4 @@ export const gameApi = createApi({ // Export hooks for usage in functional components, which are // auto-generated based on the defined endpoints -export const { useGetGameInfoQuery, useLoadLevelQuery } = gameApi +export const { useGetGameInfoQuery, useLoadLevelQuery } = apiSlice diff --git a/client/src/state/progress.ts b/client/src/state/progress.ts new file mode 100644 index 0000000..6e65467 --- /dev/null +++ b/client/src/state/progress.ts @@ -0,0 +1,30 @@ +import { createSlice } from '@reduxjs/toolkit' +import type { PayloadAction } from '@reduxjs/toolkit' + +interface ProgressState { + code: {[world: string]: {[level: number]: string}} +} + +const initialState = { code: {} } as ProgressState + +export const progressSlice = createSlice({ + name: 'progress', + initialState, + reducers: { + codeEdited(state, action: PayloadAction<{world: string, level: number, code: string}>) { + if (!state.code[action.payload.world]) { + state.code[action.payload.world] = {} + } + state.code[action.payload.world][action.payload.level] = action.payload.code + }, + } +}) + +export function selectCode(world: string, level: number) { + return (state) => { + if (!state.progress.code[world]) { return undefined } + state.progress.code[world][level]; + } +} + +export const { codeEdited } = progressSlice.actions diff --git a/client/src/store.ts b/client/src/state/store.ts similarity index 68% rename from client/src/store.ts rename to client/src/state/store.ts index 1e6b216..927460e 100644 --- a/client/src/store.ts +++ b/client/src/state/store.ts @@ -1,19 +1,21 @@ import { configureStore } from '@reduxjs/toolkit'; -import { connection } from './connection' +import { connection } from '../connection' import thunkMiddleware from 'redux-thunk' -import { gameApi } from './game/api' +import { apiSlice } from './api' +import { progressSlice } from './progress' export const store = configureStore({ reducer: { - [gameApi.reducerPath]: gameApi.reducer, + [apiSlice.reducerPath]: apiSlice.reducer, + [progressSlice.name]: progressSlice.reducer, }, middleware: getDefaultMiddleware => getDefaultMiddleware({ thunk: { extraArgument: { connection } } - }).concat(gameApi.middleware), + }).concat(apiSlice.middleware), }); // Infer the `RootState` and `AppDispatch` types from the store itself