This repo is a portal aggregator for Zoltan's three classic games. Each game
has its own standalone GitHub repo that runs on its own; the portal pulls a
snapshot of each into apps/ via git subtree --squash and wraps them in a
shared shell.
The three upstream repos are independent and intact — their index.html,
main.tsx, vite.config.ts, package.json, etc. are untouched. Only this
monorepo carries the portal-specific transformations (App.tsx renamed to
<Game>Game.tsx, CSS scoped under .tetris-app / .g2048 / Snake's CSS
Modules, original chrome dropped, workspace-style package.json).
Upstream repos:
- https://github.com/ZoliQua/Tetris-Game
- https://github.com/ZoliQua/Snake-Game
- https://github.com/ZoliQua/2048-Game
Monorepo: https://github.com/ZoliQua/Arcade-Game-Center
arcade-portal/
├── package.json # workspaces root
├── tsconfig.base.json
├── scripts/
│ └── sync-to-upstream.sh # push logic-only changes back to a game's own repo
├── apps/
│ ├── portal/ # @arcade/portal — Vite app, the user-facing portal
│ ├── tetris/ # @arcade/tetris — exports <TetrisGame />
│ ├── snake/ # @arcade/snake — exports <SnakeGame />
│ └── 2048/ # @arcade/2048 — exports <Game2048 />
└── packages/
└── ui/ # @arcade/ui — Sidebar, Header, GAMES, fmtTime, icons, CSS
npm install # one install for the whole repo
npm run dev # starts the portal Vite dev server
npm run build # type-checks + builds the portal into apps/portal/dist
npm run typecheck # tsc --noEmit across every workspace
All day-to-day work happens in this monorepo. The portal is the primary build target; the standalone game repos are kept as-is so they remain shippable on their own.
When you fix a bug in (e.g.) apps/tetris/src/logic/game.ts, that fix should
flow back to Tetris-Game/main so the standalone repo stays current.
It looks like the natural inverse of git subtree pull, but it would push the
current monorepo state of apps/tetris/ to the upstream — which would
delete the upstream's index.html, main.tsx, original package.json, etc.,
because those don't exist here anymore. The standalone repos would stop running.
Use scripts/sync-to-upstream.sh instead.
Use the helper script. It only syncs the parts that are byte-identical between
this monorepo and upstream (src/logic, src/hooks, src/components); it does
not touch the upstream's chrome (App.tsx, main.tsx, styles, configs).
# Dry run — copies, commits in a temp clone, shows diff, asks before exiting:
./scripts/sync-to-upstream.sh tetris
# Same, but also pushes upstream/main automatically when the diff looks right:
./scripts/sync-to-upstream.sh tetris --pushThe script supports tetris, snake, 2048 out of the box. Adding a new
game means extending the case statement near the top of the script with the
new repo URL and its src/ prefix (the Tetris repo has its sources nested under
Tetris-Game/src; most are flat at src/).
If your change touches:
App.tsx,<Game>Game.tsx(the renamed root component)styles/(we scoped these under.tetris-app/.g2048; the upstream's styles use.appand:rootglobals)- the workspace
package.json,tsconfig.json
…then don't sync. Those files live different lives in the two worlds. Apply the change to the upstream repo manually if it's relevant there.
The script only ever copies logic/, hooks/, components/, on the
assumption that those three folders are pure (no portal-specific imports, no
@arcade/ui references, no rename-driven changes). If you ever add a
portal-only import inside one of them, make sure to gate it (e.g., behind a
prop) before next sync.
If somebody (or a future you) commits directly to a standalone game repo, bring those changes into the monorepo with:
git subtree pull --prefix=apps/tetris https://github.com/ZoliQua/Tetris-Game main --squash
git subtree pull --prefix=apps/snake https://github.com/ZoliQua/Snake-Game main --squash
git subtree pull --prefix=apps/2048 https://github.com/ZoliQua/2048-Game main --squashA pull will produce merge conflicts on every file that diverges (i.e.,
everything outside logic/hooks/components). Resolve by keeping the
monorepo's version of the chrome and accepting upstream changes only in the
three pure folders. After resolving, run npm run typecheck && npm run build
to make sure nothing broke.
Tetris specifically: the upstream has the project nested in Tetris-Game/. The
first subtree add produced apps/tetris/Tetris-Game/... and we flattened it
manually. Future subtree pulls will re-introduce the nested folder; flatten
again with mv apps/tetris/Tetris-Game/* apps/tetris/ && rmdir apps/tetris/Tetris-Game.
The new game must already exist as its own GitHub repo, structured the same way
as the three current ones (Vite + React + TS, with src/logic, src/hooks,
src/components).
-
Subtree add:
git subtree add --prefix=apps/<id> https://github.com/<user>/<Repo> main --squash
-
Strip portal-irrelevant chrome (we keep the originals upstream, only delete locally so the portal builds cleanly):
cd apps/<id> rm -f index.html main.tsx vite.config.ts \ tsconfig.app.json tsconfig.node.json eslint.config.js \ package-lock.json README.md LICENSE CLAUDE.md *.md rm -f src/main.tsx src/App.css src/index.css src/vite-env.d.ts
-
Rename and adapt the root component so it can be embedded in the portal:
mv src/App.tsx src/<Game>Game.tsx(e.g.,PongGame.tsx)- Inside, drop any standalone
<header>/<footer>blocks that duplicate the portal shell. Keep only the playfield + game-specific side panels. - Wrap the root JSX in
<div className="<id>-app">(or similar unique name) so its CSS can be scoped without colliding with the other games. export function <Game>Game()and a default export.
-
Scope the CSS so it cannot leak into the portal shell or other games. Every selector should start with
.<id>-app(e.g.,.pong-app). Move any:root { --token: ... }declarations onto.<id>-app { --token: ... }so they apply only inside the game. -
Create
src/index.tsas the library entry:export { <Game>Game } from './<Game>Game'; export { default } from './<Game>Game';
-
Replace
apps/<id>/package.jsonwith a workspace package:{ "name": "@arcade/<id>", "version": "0.0.0", "private": true, "type": "module", "main": "./src/index.ts", "types": "./src/index.ts", "exports": { ".": "./src/index.ts" }, "peerDependencies": { "react": "^18", "react-dom": "^18" }, "scripts": { "typecheck": "tsc --noEmit" }, "devDependencies": { "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "typescript": "^5.6.3" } } -
Replace
apps/<id>/tsconfig.json:{ "extends": "../../tsconfig.base.json", "compilerOptions": { "noEmit": true, "rootDir": "src" }, "include": ["src"] } -
If the game uses CSS Modules, add
apps/<id>/src/css-modules.d.ts:declare module '*.module.css' { const classes: { readonly [key: string]: string }; export default classes; }
-
Register in the portal:
packages/ui/src/Shell.tsx— extend theGameIdtype and add an entry toGAMES({ id, name, Icon, color, best }). Add an icon topackages/ui/src/Icons.tsxif needed.apps/portal/package.json— add"@arcade/<id>": "*"todependencies.apps/portal/src/App.tsx— import<GameComponent>from@arcade/<id>, add aroute.gameId === '<id>'branch in the play route.apps/portal/src/Landing.tsx— add a preview component (<id>Mini) and aMETA[<id>]entry; the game card will render automatically once it's inGAMES.
-
Extend the sync script (
scripts/sync-to-upstream.sh) — add acasearm for the new game with its upstream URL andsrc/prefix. -
Install + verify:
npm install npm run typecheck npm run build npm run dev # smoke-test the new game card on the home page
LocalStorage keys are preserved across the standalone and embedded modes:
tetris.highScoresnake.highScore2048.highScore
If a new game lands a high score in localStorage, use the same convention:
<id>.highScore.
One app, one build. The portal builds a static SPA into apps/portal/dist
with relative asset paths (./assets/...), so it works under any subpath.
Server is cPanel + LFTP (no SSH), same hosting as fogorvosa.hu. Two pieces ship together:
- Frontend (
apps/portal/dist/) →public_html/arcade/ - PHP API (
arcade-api/) →public_html/arcade/api/
The API uses MySQL with the fogorvosa_ prefix the host enforces.
-
Create the MySQL database in cPanel named
fogorvosa_arcade, with a user (e.g.fogorvosa_arcade) that has full privileges on it. -
Generate a shared secret locally:
php -r "echo bin2hex(random_bytes(24));" -
Create
arcade-api/_config.phplocally by copyingarcade-api/_config.example.phpand filling in DB credentials + the secret. Do not commit this file (it's in.gitignore). -
Create
apps/portal/.envby copying.env.exampleand putting the same secret inVITE_ARCADE_API_KEY. SetVITE_ARCADE_API_BASEtohttps://drdul.hu/arcade/api. -
Upload
_config.phponce topublic_html/arcade/api/_config.php. It never needs to be uploaded again unless credentials change.
The first request to score.php or leaderboard.php runs
CREATE TABLE IF NOT EXISTS automatically — no manual SQL needed.
Build, then push everything via lftp (same credentials/pattern as DentalQuoteCreator). Frontend goes first, API second:
npm run build
lftp -e "
set ssl:verify-certificate no
set ftp:ssl-allow yes
open -u USER,PASS ftp.drdul.hu
# Frontend
mirror -R --delete --exclude '\\.well-known/' apps/portal/dist public_html/arcade
# PHP API (skip _config.php so we don't overwrite the live one)
mirror -R --exclude '_config\\.php\$' arcade-api public_html/arcade/api
bye
"Notes:
- The
mirror -R --deleteon the frontend is safe becausedist/is regenerated from scratch every build. - The API mirror does not use
--deleteand excludes_config.php— this is what protects the live secrets file from being wiped. - If you ever need a clean wipe of the API (e.g. removing an old file),
delete it manually via FTP or cPanel File Manager rather than enabling
--delete.
cd apps/portal/dist && python3 -m http.server 8765
# open http://localhost:8765/Leaderboard will say "server not configured" because .env isn't read by a
plain static server — that's expected. To test the API integration locally,
add http://localhost:5173 to allowed_origins in _config.php on the
server, then run npm run dev.