implemented backend / frontend structure

main
Inga 🏳‍🌈 7 days ago
parent fcc87648be
commit 3c982e845f
  1. 1
      README.md
  2. 1662
      package-lock.json
  3. 18
      package.json
  4. 13
      src/backend/app.ts
  5. 30
      src/backend/main.tsx
  6. 19
      src/backend/utils.ts
  7. 6
      src/frontend/app.ts
  8. 4
      src/frontend/static/style.css
  9. 10
      src/server.ts
  10. 0
      src/shared/board.spec.ts
  11. 0
      src/shared/board.ts
  12. 0
      src/shared/datatypes.spec.ts
  13. 0
      src/shared/datatypes.ts
  14. 0
      src/shared/opponent.spec.ts
  15. 0
      src/shared/opponent.ts
  16. 0
      src/shared/solver.spec.ts
  17. 0
      src/shared/solver.ts
  18. 0
      src/shared/tictactoe-rules.spec.ts
  19. 0
      src/shared/tictactoe-rules.ts
  20. 0
      src/shared/tictactoe.test.ts
  21. 0
      src/shared/utils.ts
  22. 14
      tsconfig.build.json
  23. 8
      tsconfig.json

@ -30,4 +30,5 @@ Source: https://www.programmfabrik.de/en/assignment-frontend-backend-developer-j
* ~0.5 hours to set up the project; * ~0.5 hours to set up the project;
* ~5 hours to implement game board serialization, tic-tac-toe rules and game solver (with tests); * ~5 hours to implement game board serialization, tic-tac-toe rules and game solver (with tests);
* ~1 hour to set up the backend / frontend structure with JSX templating and static resources
* ... * ...

1662
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -2,10 +2,16 @@
"name": "test-assignment-tictactoe", "name": "test-assignment-tictactoe",
"version": "1.0.0", "version": "1.0.0",
"scripts": { "scripts": {
"build": "npm run build:clean && npm run build:only && npm run build:static",
"build:clean": "rimraf dist",
"build:only": "tsc --project tsconfig.build.json",
"build:static": "copyfiles -u 1 \"src/frontend/static/**/*\" dist",
"check-and-start": "npm run prerelease && npm run build && npm run start",
"lint": "eslint src", "lint": "eslint src",
"prerelease": "npm run lint && npm run typecheck && npm run test", "prerelease": "npm run lint && npm run typecheck && npm run test",
"start": "node ./dist/server.js",
"test": "tap", "test": "tap",
"typecheck": "tsc --noEmit --project tsconfig.json" "typecheck": "tsc --project tsconfig.json"
}, },
"author": "Inga", "author": "Inga",
"license": "CNPLv7", "license": "CNPLv7",
@ -14,12 +20,20 @@
"@eslint/js": "^9.15.0", "@eslint/js": "^9.15.0",
"@tsconfig/strictest": "^2.0.5", "@tsconfig/strictest": "^2.0.5",
"@types/eslint__js": "^8.42.3", "@types/eslint__js": "^8.42.3",
"@types/express": "^5.0.0",
"copyfiles": "^2.4.1",
"eslint": "9.14.x", "eslint": "9.14.x",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1", "eslint-plugin-prettier": "^5.2.1",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"rimraf": "^6.0.1",
"tap": "^21.0.1", "tap": "^21.0.1",
"typescript": "^5.6.3", "typescript": "^5.7.1-rc",
"typescript-eslint": "^8.14.0" "typescript-eslint": "^8.14.0"
},
"dependencies": {
"express": "^4.21.1",
"preact": "^10.24.3",
"preact-render-to-string": "^6.5.11"
} }
} }

@ -0,0 +1,13 @@
import path from "node:path";
import express from "express";
import { mainPageHandler } from "./main.tsx";
export const createApp = () => {
const app = express();
app.get("/", mainPageHandler);
app.use("/frontend", express.static(path.join(import.meta.dirname, "..", "frontend")));
app.use("/shared", express.static(path.join(import.meta.dirname, "..", "shared")));
return app;
};

@ -0,0 +1,30 @@
import type { RequestHandler } from "express";
import { safeGetQueryValue, sendHtml } from "./utils.ts";
export const mainPageHandler: RequestHandler = (req, res) => {
const pageNumber = parseInt(safeGetQueryValue(req, "page") ?? "0", 10);
sendHtml(
res,
<html>
<head>
{/*
We need to use ".js" here instead of ".ts" (and "incorrect" path)
because we're loading the compiled file on frontend, from path relative to the root page,
and because TS won't rewrite the extension here.
*/}
<script src="frontend/app.js" type="module"></script>
<link rel="stylesheet" href="frontend/static/style.css" />
<title>Hi</title>
</head>
<body>
<p>Hello world!</p>
<p>
Page number <span class="pageNumber">{pageNumber}</span>
</p>
<p>
<a href={`?page=${pageNumber + 1}`}>Go to the next page</a>
</p>
</body>
</html>,
);
};

@ -0,0 +1,19 @@
import type { Request, Response } from "express";
import type { VNode } from "preact";
import { render } from "preact-render-to-string";
export const sendHtml = (res: Response, document: VNode) => {
const html = render(document);
res.send(`<!DOCTYPE html>${html}`);
};
// Quick and dirty workaround, should not be used in real world
// with real complex query strings and with the need for error handling
export const safeGetQueryValue = (req: Request, name: string) => {
const result = req.query[name];
if (typeof result !== "string") {
return null;
}
return result || null;
};

@ -0,0 +1,6 @@
import { computeAllSolutions } from "../shared/solver.ts";
import { rules } from "../shared/tictactoe-rules.ts";
const allSolutions = computeAllSolutions(3, 3, rules);
console.log(allSolutions.size);
console.log(allSolutions);

@ -0,0 +1,4 @@
.pageNumber {
font-weight: bold;
color: red;
}

@ -0,0 +1,10 @@
import { createApp } from "./backend/app.ts";
const app = createApp();
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- we actually want to coalesce empty strings here, too
const port = process.env["PORT"] || 3000;
app.listen(port, () => {
console.log(`Example app listening on port ${port}`);
});

@ -1,8 +1,10 @@
{ {
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"exclude": ["src/**/*.spec.*"], "exclude": ["src/**/*.spec.*"],
"compilerOptions": { "compilerOptions": {
"module": "Preserve" "module": "Preserve",
} "noEmit": false,
"outDir": "dist",
"rewriteRelativeImportExtensions": true
} }
}

@ -3,9 +3,11 @@
"include": ["src/**/*"], "include": ["src/**/*"],
"compilerOptions": { "compilerOptions": {
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"noEmit": true, "forceConsistentCasingInFileNames": true,
"target": "ES2020", "jsx": "react-jsx",
"jsxImportSource": "preact",
"module": "NodeNext", "module": "NodeNext",
"forceConsistentCasingInFileNames": true "noEmit": true,
"target": "ES2020"
} }
} }

Loading…
Cancel
Save