From fed3fdc6f7d2c86a928248807982cc6aaa1f4342 Mon Sep 17 00:00:00 2001 From: Jon Eugster Date: Sat, 25 Apr 2026 00:09:18 +0200 Subject: [PATCH 1/3] feat: add local games on landing page --- client/src/components/landing_page.tsx | 26 ++++++----- client/src/config.json | 11 ----- client/src/store/api.ts | 6 +++ client/src/store/tiles-atoms.ts | 17 ++++++++ client/vite.config.ts | 3 ++ relay/src/index.ts | 60 ++++++++++++++++++++++++++ relay/tsconfig.json | 2 +- 7 files changed, 99 insertions(+), 26 deletions(-) create mode 100644 client/src/store/tiles-atoms.ts diff --git a/client/src/components/landing_page.tsx b/client/src/components/landing_page.tsx index a152b611..1f1e7d7a 100644 --- a/client/src/components/landing_page.tsx +++ b/client/src/components/landing_page.tsx @@ -19,18 +19,16 @@ import { navOpenAtom } from '../store/navigation-atoms'; import { gameIdAtom } from '../store/location-atoms'; import { gameInfoAtomFamily } from '../store/query-atoms'; import { preferencesAtom } from '../store/preferences-atoms'; +import { gameTilesAtom } from '../store/tiles-atoms'; +import { GameTileWithName } from '../store/api'; -function Tile({gameId}: {gameId: string}) { +function Tile({tileWithName}: {tileWithName: GameTileWithName}) { const { t, i18n } = useTranslation() - const [{ data: gameInfo }] = useAtom(gameInfoAtomFamily(gameId)) const [, navigateToGame] = useAtom(gameIdAtom) const [preferences] = useAtom(preferencesAtom) - const gameTile = gameInfo?.tile - - if (!gameTile) { - return <> - } + const gameTile = tileWithName.tile + const gameId = `g/${tileWithName.owner}/${tileWithName.game}` return
navigateToGame(gameId)}>
@@ -77,6 +75,7 @@ function Tile({gameId}: {gameId: string}) { function LandingPage() { const [, setPopup] = useAtom(popupAtom) const [navOpen] = useAtom(navOpenAtom) + const [tiles] = useAtom(gameTilesAtom) const [usageCPU, setUsageCPU] = React.useState() const [usageMem, setUsageMem] = React.useState() @@ -86,8 +85,7 @@ function LandingPage() { const { t, i18n } = useTranslation() // Load the namespaces of all games - // TODO: should `allGames` contain game-ids starting with `g/`? - i18n.loadNamespaces(lean4gameConfig.allGames.map(id => `g/${id}`)) + i18n.loadNamespaces(tiles.map(tileWithName => `g/${tileWithName.owner}/${tileWithName.game}`)) /** Parse `games/stats.csv` if present and display server capacity. */ React.useEffect(() => { @@ -126,12 +124,12 @@ function LandingPage() {
{ - lean4gameConfig.allGames.map((id, i) => ( - { + return - )) + }) } {/* {allTiles.filter(x => x != null).length == 0 ?

diff --git a/client/src/config.json b/client/src/config.json index 6abc6db4..3f36e539 100644 --- a/client/src/config.json +++ b/client/src/config.json @@ -1,15 +1,4 @@ { - "allGames": [ - "leanprover-community/nng4", - "hhu-adam/robo", - "alexkontorovich/realanalysisgame", - "djvelleman/stg4", - "trequetrum/lean4game-logic", - "emilyriehl/reintroductiontoproofs", - "jadabouhawili/knightsandknaves-lean4game", - "zrtmrh/linearalgebragame" - ], - "languages": [ { "iso": "en", diff --git a/client/src/store/api.ts b/client/src/store/api.ts index ed685efa..de996211 100644 --- a/client/src/store/api.ts +++ b/client/src/store/api.ts @@ -16,6 +16,12 @@ export interface GameTile { image: string } +export interface GameTileWithName { + owner: string + game: string + tile: GameTile +} + export interface GameInfo { title?: string, introduction: string, diff --git a/client/src/store/tiles-atoms.ts b/client/src/store/tiles-atoms.ts new file mode 100644 index 00000000..ad464c32 --- /dev/null +++ b/client/src/store/tiles-atoms.ts @@ -0,0 +1,17 @@ +import { atomWithQuery } from "jotai-tanstack-query" +import { GameTileWithName } from "./api" +import { atom } from "jotai" + + +const gameTilesQueryAtom = atomWithQuery((get) => { + return { + queryKey: ['gameTiles'], + queryFn: async () => { + const res = await fetch(`${window.location.origin}/api/games`) + return res.json() + }, + } +}) + +/** Tiles aller verfügbaren Spiele */ +export const gameTilesAtom = atom(get => get(gameTilesQueryAtom).data ?? []) diff --git a/client/vite.config.ts b/client/vite.config.ts index c42a4079..6cc7555d 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -69,6 +69,9 @@ export default defineConfig({ '/data': { target: `http://localhost:${backendPort}`, }, + '/api': { + target: `http://localhost:${backendPort}`, + }, '/i18n': { target: `http://localhost:${backendPort}`, }, diff --git a/relay/src/index.ts b/relay/src/index.ts index 1bc06e82..3f27cc0f 100644 --- a/relay/src/index.ts +++ b/relay/src/index.ts @@ -19,6 +19,9 @@ const gameManager = new GameManager(__dirname) const PORT = process.env.PORT || 8080 const API = process.env.API_PORT +const environment = process.env.NODE_ENV; +const isDevelopment = environment === 'development'; + let router = express.Router(); router.get('/import/status/:owner/:repo', importStatus) router.get('/import/trigger/:owner/:repo', importTrigger) @@ -101,6 +104,63 @@ const server = app } }) }) + // endpoint `games`: list of available games for landing page + .use('/api/games', async (req: any, res: any) => { + try { + const games = []; + + // TODO: should be a config file + const featuredGameNames = [ + {owner: "leanprover-community", game: "nng4"}, + {owner: "hhu-adam", game: "robo"}, + {owner: "alexkontorovich", game: "realanalysisgame"}, + {owner: "djvelleman", game: "stg4"}, + {owner: "trequetrum", game: "lean4game-logic"}, + {owner: "emilyriehl", game: "reintroductiontoproofs"}, + {owner: "jadabouhawili", game: "knightsandknaves-lean4game"}, + {owner: "zrtmrh", game: "linearalgebragame"} + ] + + // Load featured games + for (const entry of featuredGameNames) { + const gameJsonPath = path.join(__dirname, "..", "..", "..", "games", entry.owner, entry.game, ".lake", "gamedata", "game.json") + let gameJson : any; + try { + const raw = await fs.promises.readFile(gameJsonPath, "utf-8"); + gameJson = JSON.parse(raw); + } catch (err) { + continue + } + games.push({owner: entry.owner, game: entry.game, tile: gameJson.tile}) + } + + // Load local games + if (isDevelopment){ + const BASE_DIR = path.join(__dirname, "..", "..", "..", "..") + const entries = await fs.promises.readdir(BASE_DIR, { + withFileTypes: true, + }); + + for (const entry of entries) { + if (!entry.isDirectory()) continue; + const gameJsonPath = path.join(BASE_DIR, entry.name, ".lake", "gamedata", "game.json"); + let gameJson : any; + try { + const raw = await fs.promises.readFile(gameJsonPath, "utf-8"); + gameJson = JSON.parse(raw); + } catch (err) { + continue + } + games.push({owner: "local", game: entry.name, tile: gameJson.tile}) + } + } + + res.json(games); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Failed to load game tiles" }); + } + }) .use('/', router) .listen(PORT, () => console.log(`Server listening on ${PORT}`)); diff --git a/relay/tsconfig.json b/relay/tsconfig.json index 15829bc9..d54ec554 100644 --- a/relay/tsconfig.json +++ b/relay/tsconfig.json @@ -19,5 +19,5 @@ "skipLibCheck": true, "sourceMap": true }, - "include": ["./src/"] + "include": ["./src/", "src/config.json"] } From f9f2d5bd7746e8206812fdfa1afa15eccff0c287 Mon Sep 17 00:00:00 2001 From: Jon Eugster Date: Sat, 25 Apr 2026 00:15:37 +0200 Subject: [PATCH 2/3] Apply suggestion from @joneugster --- client/src/store/tiles-atoms.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/store/tiles-atoms.ts b/client/src/store/tiles-atoms.ts index ad464c32..c158fe96 100644 --- a/client/src/store/tiles-atoms.ts +++ b/client/src/store/tiles-atoms.ts @@ -13,5 +13,5 @@ const gameTilesQueryAtom = atomWithQuery((get) => { } }) -/** Tiles aller verfügbaren Spiele */ +/** Tiles of all available games */ export const gameTilesAtom = atom(get => get(gameTilesQueryAtom).data ?? []) From 6e3e4402df46388e5a38670a47698451a2815924 Mon Sep 17 00:00:00 2001 From: Jon Eugster Date: Sat, 25 Apr 2026 00:18:59 +0200 Subject: [PATCH 3/3] npm audit --- package-lock.json | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8f4bdc36..c1641174 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5164,11 +5164,12 @@ } }, "node_modules/cross-fetch": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", - "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz", + "integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==", + "license": "MIT", "dependencies": { - "node-fetch": "^2.6.12" + "node-fetch": "^2.7.0" } }, "node_modules/cross-spawn": { @@ -5219,9 +5220,9 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/cypress": { - "version": "15.12.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-15.12.0.tgz", - "integrity": "sha512-B2BRcudLfA4NZZP5QpA45J70bu1heCH59V1yKRLHAtiC49r7RV03X5ifUh7Nfbk8QNg93RAsc6oAmodm/+j0pA==", + "version": "15.14.1", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-15.14.1.tgz", + "integrity": "sha512-AkuiHNSnmm0a+h/horcvbjmY6dWpCe1Ebp1R0LjMP5I6pjMaNA50Mw1YP/d07pLHJ/sV8FZoGecUWFCJ/Nifpw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -7025,12 +7026,12 @@ } }, "node_modules/i18next-http-backend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-3.0.2.tgz", - "integrity": "sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-3.0.6.tgz", + "integrity": "sha512-mBOqy8993jtqAoj6XaI1XeC/8/9v6EPS+681ziegrPvTB0DoaCY7PpTS0SpY56qLMoS4OI1TZEM2Zf59zNh05w==", "license": "MIT", "dependencies": { - "cross-fetch": "4.0.0" + "cross-fetch": "4.1.0" } }, "node_modules/i18next-scanner": { @@ -9344,6 +9345,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", "dependencies": { "whatwg-url": "^5.0.0" }, @@ -9950,9 +9952,9 @@ } }, "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", "dev": true, "funding": [ { @@ -11664,7 +11666,8 @@ "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" }, "node_modules/tree-dump": { "version": "1.0.3", @@ -12616,12 +12619,14 @@ "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0"