CMU Coding Bootcamp

Compare changes

Choose any two refs to compare.

+85
html/css/form.css
··· 1 + .container { 2 + display: flex; 3 + flex-direction: column; 4 + align-items: center; 5 + justify-content: center; 6 + } 7 + 8 + form { 9 + display: flex; 10 + flex-direction: column; 11 + align-items: center; 12 + justify-content: center; 13 + gap: 0.5rem; 14 + width: 80%; 15 + } 16 + 17 + form > * { 18 + width: 100%; 19 + } 20 + 21 + form :nth-child(even) { 22 + margin-bottom: 1rem; 23 + } 24 + 25 + form > button { 26 + width: 100%; 27 + padding: 0.25rem; 28 + margin: 0.5rem; 29 + } 30 + 31 + @media (min-width: 769px) { 32 + form { 33 + gap: 1rem; 34 + width: 100%; 35 + max-width: 48rem; 36 + display: grid; 37 + column-gap: 1rem; 38 + grid-template-columns: repeat(2, minmax(0, 1fr)); 39 + } 40 + 41 + form > button { 42 + width: 100%; 43 + grid-column: span 2; 44 + padding: 0.25rem; 45 + margin: 0.5rem; 46 + } 47 + } 48 + 49 + nav { 50 + display: flex; 51 + padding: 0.5rem 0rem; 52 + background-color: #8aacdf; 53 + position: sticky; 54 + top: 0; 55 + left: 0; 56 + right: 0; 57 + margin-left: -0.5rem; 58 + margin-top: -0.5rem; 59 + justify-content: space-around; 60 + align-items: center; 61 + width: 100vw; 62 + margin-bottom: 4rem; 63 + } 64 + 65 + .navitem { 66 + padding: 0.5rem 4rem; 67 + border: 1px solid #000; 68 + border-radius: 0.5rem; 69 + } 70 + 71 + #footer { 72 + width: 100vw; 73 + height: 6rem; 74 + position: absolute; 75 + bottom: 0; 76 + left: 0; 77 + justify-content: space-around; 78 + align-items: center; 79 + background-color: #000; 80 + color: #fff; 81 + text-align: center; 82 + display: flex; 83 + vertical-align: center; 84 + font-size: 2rem; 85 + }
+28
html/form.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8" /> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 + <title>Form</title> 7 + <link rel="stylesheet" href="css/form.css" /> 8 + <script src="js/form.js"></script> 9 + </head> 10 + <body> 11 + <nav> 12 + <a class="navitem" href="/">Home</a> 13 + <a class="navitem" href="/dino.html">Dino</a> 14 + </nav> 15 + <div class="container"> 16 + <form> 17 + <label for="name">Name:</label> 18 + <input type="text" id="name" name="name" required /> 19 + <label for="email">Email:</label> 20 + <input type="email" id="email" name="email" required /> 21 + <label for="message">Message:</label> 22 + <textarea id="message" name="message" required></textarea> 23 + <button type="submit">Submit</button> 24 + </form> 25 + </div> 26 + <div id="footer">Totally good footer text</div> 27 + </body> 28 + </html>
-5
nilla.nix
··· 78 78 mkShell { 79 79 shellHook = '' 80 80 [ "$(hostname)" = "shorthair" ] && export ZED_PREDICT_EDITS_URL=http://localhost:9000/predict_edits 81 - unset TMPDIR 82 81 ''; 83 82 packages = [ 84 83 (python3.withPackages (ppkgs: [ ··· 108 107 mkShell, 109 108 }: 110 109 mkShell { 111 - shellHook = '' 112 - unset TMPDIR 113 - ''; 114 110 packages = [ 115 111 pkgs.bun 116 112 pkgs.eslint_d ··· 133 129 }: 134 130 mkShell { 135 131 shellHook = '' 136 - unset TMPDIR 137 132 serve() { 138 133 live-server /home/coded/Programming/CMU/html --port 5000 139 134 }
+102
react/bun.lock
··· 5 5 "name": "react", 6 6 "dependencies": { 7 7 "@tailwindcss/vite": "^4.1.17", 8 + "draft-js": "^0.11.7", 8 9 "react": "^19.2.0", 9 10 "react-dom": "^19.2.0", 10 11 "react-router": "^7.9.6", ··· 12 13 }, 13 14 "devDependencies": { 14 15 "@eslint/js": "^9.39.1", 16 + "@happy-dom/global-registrator": "^20.0.10", 17 + "@testing-library/jest-dom": "^6.9.1", 18 + "@testing-library/react": "^16.3.0", 19 + "@types/bun": "latest", 20 + "@types/draft-js": "^0.11.20", 15 21 "@types/node": "^24.10.0", 16 22 "@types/react": "^19.2.2", 17 23 "@types/react-dom": "^19.2.2", ··· 28 34 }, 29 35 }, 30 36 "packages": { 37 + "@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="], 38 + 31 39 "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], 32 40 33 41 "@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="], ··· 59 67 "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], 60 68 61 69 "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], 70 + 71 + "@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="], 62 72 63 73 "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], 64 74 ··· 136 146 137 147 "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], 138 148 149 + "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.0.10", "", { "dependencies": { "@types/node": "^20.0.0", "happy-dom": "^20.0.10" } }, "sha512-GU0UBt9lJKhZlY/U0Bivj9ZVepDIQoAUupAAl/90THG4/urkzXNglkVYETsnt2pGBDgQ+4vBjMAbLu6XzcKcQA=="], 150 + 139 151 "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], 140 152 141 153 "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], ··· 236 248 237 249 "@tailwindcss/vite": ["@tailwindcss/vite@4.1.17", "", { "dependencies": { "@tailwindcss/node": "4.1.17", "@tailwindcss/oxide": "4.1.17", "tailwindcss": "4.1.17" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-4+9w8ZHOiGnpcGI6z1TVVfWaX/koK7fKeSYF3qlYg2xpBtbteP2ddBxiarL+HVgfSJGeK5RIxRQmKm4rTJJAwA=="], 238 250 251 + "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="], 252 + 253 + "@testing-library/jest-dom": ["@testing-library/jest-dom@6.9.1", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA=="], 254 + 255 + "@testing-library/react": ["@testing-library/react@16.3.0", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw=="], 256 + 257 + "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], 258 + 239 259 "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], 240 260 241 261 "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], ··· 244 264 245 265 "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], 246 266 267 + "@types/bun": ["@types/bun@1.3.2", "", { "dependencies": { "bun-types": "1.3.2" } }, "sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg=="], 268 + 269 + "@types/draft-js": ["@types/draft-js@0.11.20", "", { "dependencies": { "@types/react": "*", "immutable": "~3.7.4" } }, "sha512-bZHtHxXnCu4wlUXlDWrIlJSG2LJ6wcycSWoxcTCcGd0cVOm35p0vh87qpIPzGK2NALMMvJhQXdS330iYB3iGlw=="], 270 + 247 271 "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], 248 272 249 273 "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], ··· 253 277 "@types/react": ["@types/react@19.2.6", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w=="], 254 278 255 279 "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], 280 + 281 + "@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="], 256 282 257 283 "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.47.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.47.0", "@typescript-eslint/type-utils": "8.47.0", "@typescript-eslint/utils": "8.47.0", "@typescript-eslint/visitor-keys": "8.47.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.47.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-fe0rz9WJQ5t2iaLfdbDc9T80GJy0AeO453q8C3YCilnGozvOyCG5t+EZtg7j7D88+c3FipfP/x+wzGnh1xp8ZA=="], 258 284 ··· 282 308 283 309 "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], 284 310 311 + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], 312 + 285 313 "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], 286 314 287 315 "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], 316 + 317 + "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], 318 + 319 + "asap": ["asap@2.0.6", "", {}, "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="], 288 320 289 321 "babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="], 290 322 ··· 298 330 299 331 "browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": { "browserslist": "cli.js" } }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], 300 332 333 + "bun-types": ["bun-types@1.3.2", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-i/Gln4tbzKNuxP70OWhJRZz1MRfvqExowP7U6JKoI8cntFrtxg7RJK3jvz7wQW54UuvNC8tbKHHri5fy74FVqg=="], 334 + 301 335 "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], 302 336 303 337 "caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], ··· 314 348 315 349 "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], 316 350 351 + "core-js": ["core-js@3.47.0", "", {}, "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg=="], 352 + 353 + "cross-fetch": ["cross-fetch@3.2.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q=="], 354 + 317 355 "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], 318 356 357 + "css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="], 358 + 319 359 "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], 320 360 321 361 "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], 322 362 323 363 "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], 364 + 365 + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], 324 366 325 367 "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], 326 368 369 + "dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="], 370 + 371 + "draft-js": ["draft-js@0.11.7", "", { "dependencies": { "fbjs": "^2.0.0", "immutable": "~3.7.4", "object-assign": "^4.1.1" }, "peerDependencies": { "react": ">=0.14.0", "react-dom": ">=0.14.0" } }, "sha512-ne7yFfN4sEL82QPQEn80xnADR8/Q6ALVworbC5UOSzOvjffmYfFsr3xSZtxbIirti14R7Y33EZC5rivpLgIbsg=="], 372 + 327 373 "electron-to-chromium": ["electron-to-chromium@1.5.255", "", {}, "sha512-Z9oIp4HrFF/cZkDPMpz2XSuVpc1THDpT4dlmATFlJUIBVCy9Vap5/rIXsASP1CscBacBqhabwh8vLctqBwEerQ=="], 328 374 329 375 "enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="], ··· 364 410 365 411 "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], 366 412 413 + "fbjs": ["fbjs@2.0.0", "", { "dependencies": { "core-js": "^3.6.4", "cross-fetch": "^3.0.4", "fbjs-css-vars": "^1.0.0", "loose-envify": "^1.0.0", "object-assign": "^4.1.0", "promise": "^7.1.1", "setimmediate": "^1.0.5", "ua-parser-js": "^0.7.18" } }, "sha512-8XA8ny9ifxrAWlyhAbexXcs3rRMtxWcs3M0lctLfB49jRDHiaxj+Mo0XxbwE7nKZYzgCFoq64FS+WFd4IycPPQ=="], 414 + 415 + "fbjs-css-vars": ["fbjs-css-vars@1.0.2", "", {}, "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ=="], 416 + 367 417 "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], 368 418 369 419 "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], ··· 388 438 389 439 "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], 390 440 441 + "happy-dom": ["happy-dom@20.0.10", "", { "dependencies": { "@types/node": "^20.0.0", "@types/whatwg-mimetype": "^3.0.2", "whatwg-mimetype": "^3.0.0" } }, "sha512-6umCCHcjQrhP5oXhrHQQvLB0bwb1UzHAHdsXy+FjtKoYjUhmNZsQL8NivwM1vDvNEChJabVrUYxUnp/ZdYmy2g=="], 442 + 391 443 "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], 392 444 393 445 "hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="], ··· 396 448 397 449 "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], 398 450 451 + "immutable": ["immutable@3.7.6", "", {}, "sha512-AizQPcaofEtO11RZhPPHBOJRdo/20MKQF9mBLnVkBoyHi1/zXK8fzVdnEpSV9gxqtnh6Qomfp3F0xT5qP/vThw=="], 452 + 399 453 "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], 400 454 401 455 "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], 456 + 457 + "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], 402 458 403 459 "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], 404 460 ··· 456 512 457 513 "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], 458 514 515 + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], 516 + 459 517 "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], 518 + 519 + "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], 460 520 461 521 "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], 462 522 ··· 464 524 465 525 "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], 466 526 527 + "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], 528 + 467 529 "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], 468 530 469 531 "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], ··· 471 533 "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], 472 534 473 535 "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], 536 + 537 + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], 474 538 475 539 "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], 476 540 541 + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], 542 + 477 543 "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], 478 544 479 545 "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], ··· 494 560 495 561 "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], 496 562 563 + "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="], 564 + 565 + "promise": ["promise@7.3.1", "", { "dependencies": { "asap": "~2.0.3" } }, "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg=="], 566 + 497 567 "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], 498 568 499 569 "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], ··· 502 572 503 573 "react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="], 504 574 575 + "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], 576 + 505 577 "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], 506 578 507 579 "react-router": ["react-router@7.9.6", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-Y1tUp8clYRXpfPITyuifmSoE2vncSME18uVLgaqyxh9H35JWpIfzHo+9y3Fzh5odk/jxPW29IgLgzcdwxGqyNA=="], 580 + 581 + "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="], 508 582 509 583 "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], 510 584 ··· 520 594 521 595 "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], 522 596 597 + "setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="], 598 + 523 599 "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], 524 600 525 601 "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], 526 602 527 603 "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], 528 604 605 + "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="], 606 + 529 607 "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], 530 608 531 609 "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], ··· 538 616 539 617 "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], 540 618 619 + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], 620 + 541 621 "ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="], 542 622 543 623 "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], ··· 546 626 547 627 "typescript-eslint": ["typescript-eslint@8.47.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.47.0", "@typescript-eslint/parser": "8.47.0", "@typescript-eslint/typescript-estree": "8.47.0", "@typescript-eslint/utils": "8.47.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Lwe8i2XQ3WoMjua/r1PHrCTpkubPYJCAfOurtn+mtTzqB6jNd+14n9UN1bJ4s3F49x9ixAm0FLflB/JzQ57M8Q=="], 548 628 629 + "ua-parser-js": ["ua-parser-js@0.7.41", "", { "bin": { "ua-parser-js": "script/cli.js" } }, "sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg=="], 630 + 549 631 "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], 550 632 551 633 "update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], ··· 554 636 555 637 "vite": ["vite@7.2.2", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ=="], 556 638 639 + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], 640 + 641 + "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], 642 + 643 + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], 644 + 557 645 "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], 558 646 559 647 "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], ··· 569 657 "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], 570 658 571 659 "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], 660 + 661 + "@happy-dom/global-registrator/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], 572 662 573 663 "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="], 574 664 ··· 582 672 583 673 "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], 584 674 675 + "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], 676 + 677 + "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], 678 + 585 679 "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], 586 680 587 681 "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], ··· 589 683 "@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], 590 684 591 685 "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], 686 + 687 + "happy-dom/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], 592 688 593 689 "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], 594 690 691 + "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], 692 + 693 + "@happy-dom/global-registrator/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], 694 + 595 695 "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], 696 + 697 + "happy-dom/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], 596 698 } 597 699 }
+2
react/bunfig.toml
··· 1 + [test] 2 + preload=["./preload.ts"]
+7 -1
react/package.json
··· 1 1 { 2 - "name": "react", 2 + "name": "react-blog", 3 3 "private": true, 4 4 "version": "0.0.0", 5 5 "type": "module", ··· 11 11 }, 12 12 "dependencies": { 13 13 "@tailwindcss/vite": "^4.1.17", 14 + "draft-js": "^0.11.7", 14 15 "react": "^19.2.0", 15 16 "react-dom": "^19.2.0", 16 17 "react-router": "^7.9.6", ··· 18 19 }, 19 20 "devDependencies": { 20 21 "@eslint/js": "^9.39.1", 22 + "@happy-dom/global-registrator": "^20.0.10", 23 + "@testing-library/jest-dom": "^6.9.1", 24 + "@testing-library/react": "^16.3.0", 25 + "@types/bun": "latest", 26 + "@types/draft-js": "^0.11.20", 21 27 "@types/node": "^24.10.0", 22 28 "@types/react": "^19.2.2", 23 29 "@types/react-dom": "^19.2.2",
+3
react/preload.ts
··· 1 + import { GlobalRegistrator } from "@happy-dom/global-registrator"; 2 + 3 + GlobalRegistrator.register();
+51 -20
react/src/App.tsx
··· 1 1 import { posts } from "./lib/post"; 2 - import { Entry } from "./components/Entry"; 2 + import { BlogPostList } from "./components/BlogPostList"; 3 + import { Link } from "react-router"; 4 + import { useState } from "react"; 3 5 4 - function App() { 6 + export function App() { 7 + const [searchBarDisplay, displaySearchBar] = useState(false); 5 8 return ( 6 9 <> 7 - <div className="w-lvw h-lvh p-5 flex flex-col items-center gap-10"> 8 - <h1 className="text-5xl font-bold">Posts</h1> 9 - <div className="flex flex-col items-center justify-center gap-4"> 10 - {posts 11 - .sort( 12 - (a, b) => 13 - new Date(a.datePosted).getTime() - 14 - new Date(b.datePosted).getTime(), 15 - ) 16 - .map((post, idx) => ( 17 - <Entry 18 - key={idx} 19 - idx={idx} 20 - title={post.title} 21 - datePosted={post.datePosted} 10 + <title>Posts</title> 11 + <div className="w-screen p-5 flex flex-col items-center gap-10"> 12 + <nav className="flex justify-between items-center w-full sticky top-0"> 13 + <h1 className="text-3xl font-bold text-left">Blog App</h1> 14 + {searchBarDisplay ? ( 15 + <> 16 + <input 17 + type="text" 18 + placeholder="Search..." 19 + className="border border-gray-300 rounded px-2 py-1" 20 + onChange={(e) => { 21 + // Implement search functionality here 22 + }} 22 23 /> 23 - ))} 24 + <button 25 + className="bg-blue-500 hover:bg-blue-700 text-white w-full font-bold py-2 px-4 rounded cursor-pointer text-center" 26 + onClick={() => displaySearchBar(false)} 27 + > 28 + Close 29 + </button> 30 + </> 31 + ) : ( 32 + <button 33 + className="bg-blue-500 hover:bg-blue-700 text-white w-full font-bold py-2 px-4 rounded cursor-pointer text-center" 34 + onClick={() => displaySearchBar(true)} 35 + > 36 + Search 37 + </button> 38 + )} 39 + <div className="flex w-full justify-end items-center"> 40 + <Link to="/post" className="w-1/3"> 41 + <div className="bg-blue-500 hover:bg-blue-700 text-white w-full font-bold py-2 px-4 rounded cursor-pointer text-center"> 42 + New Post 43 + </div> 44 + </Link> 45 + </div> 46 + </nav> 47 + <div className="flex flex-col gap-4 md:grid md:grid-cols-3 items-center justify-between w-full"> 48 + <div className="flex w-full justify-end items-center"> 49 + <h1 className="text-5xl font-bold text-center">Posts</h1> 50 + <Link to="/post" className="w-1/3"> 51 + <div className="bg-blue-500 hover:bg-blue-700 text-white w-full font-bold py-2 px-4 rounded cursor-pointer text-center"> 52 + New Post 53 + </div> 54 + </Link> 55 + </div> 24 56 </div> 57 + <BlogPostList posts={posts} /> 25 58 </div> 26 59 </> 27 60 ); 28 61 } 29 - 30 - export default App;
+17
react/src/component.test.tsx
··· 1 + /// <reference lib="dom" /> 2 + 3 + import { test, expect } from "bun:test"; 4 + import { render, screen } from "@testing-library/react"; 5 + import "@testing-library/jest-dom"; 6 + import { App } from "./App"; 7 + import { BrowserRouter } from "react-router"; 8 + import { posts } from "./lib/post"; 9 + 10 + test("renders app page", () => { 11 + render( 12 + <BrowserRouter> 13 + <App /> 14 + </BrowserRouter>, 15 + ); 16 + expect(screen.getAllByRole("heading")).toHaveLength(posts.length + 1); 17 + });
+95
react/src/components/BlogPostDetail.tsx
··· 1 + import { useParams, Outlet } from "react-router"; 2 + import { posts } from "../lib/post"; 3 + import { Link } from "react-router"; 4 + import { ContentState, convertFromRaw, Editor, EditorState } from "draft-js"; 5 + import { useState } from "react"; 6 + import { useNavigate } from "react-router"; 7 + 8 + export function BlogPostDetail({ 9 + deletePost, 10 + }: { 11 + deletePost: (id: number) => void; 12 + }) { 13 + const { postId } = useParams(); 14 + const post = posts.find((post) => post.id === parseInt(postId!)); 15 + const [editorState, setEditorState] = useState(() => { 16 + try { 17 + const data = JSON.parse(`"${post?.content ?? ""}"`); 18 + return EditorState.createWithContent(convertFromRaw(data)); 19 + } catch { 20 + console.log("fallback"); 21 + return EditorState.createWithContent( 22 + ContentState.createFromText(post?.content ?? ""), 23 + ); 24 + } 25 + }); 26 + const navigate = useNavigate(); 27 + 28 + if (!post) { 29 + return <div>Post not found</div>; 30 + } 31 + 32 + const formattedDate = new Date(post.datePosted).toLocaleDateString("en-US", { 33 + month: "long", 34 + day: "numeric", 35 + year: "numeric", 36 + }); 37 + 38 + return ( 39 + <> 40 + <title>{post.title}</title> 41 + <div className="md:grid md:grid-cols-3 flex flex-col w-full"> 42 + <Link 43 + to="/" 44 + className="text-gray-700 dark:text-gray-200 hover:text-gray-400 flex justify-center items-center w-16 mb-4 md:mb-0" 45 + > 46 + Home 47 + </Link> 48 + <h1 className="text-3xl md:text-4xl font-bold text-center mb-4 md:mb-0"> 49 + {post.title} 50 + </h1> 51 + <div className="flex md:justify-end items-center"> 52 + <Link to={`/post?postId=${post.id}`} className="w-1/3"> 53 + <div className="bg-blue-500 hover:bg-blue-700 text-white w-full font-bold py-2 px-4 rounded cursor-pointer text-center"> 54 + Edit Post 55 + </div> 56 + </Link> 57 + </div> 58 + </div> 59 + <div className="flex flex-col gap-1 md:gap-2.5 justify-center items-center mb-1.5 md:mb-2.5"> 60 + <p className="text-gray-700 dark:text-gray-400 text-sm md:text-lg"> 61 + By: {post.author} 62 + </p> 63 + <p className="text-gray-600 dark:text-gray-500 text-xs md:text-base"> 64 + Published on {formattedDate} 65 + </p> 66 + </div> 67 + <div className="text-sm md:text-lg md:w-3xl w-full md:mb-10"> 68 + <Editor 69 + editorState={editorState} 70 + onChange={setEditorState} 71 + readOnly={true} 72 + /> 73 + </div> 74 + <button 75 + className="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded cursor-pointer w-full md:w-3xl" 76 + onClick={() => { 77 + if (window.confirm("Are you sure you want to delete this post?")) { 78 + deletePost(post.id); 79 + navigate("/"); 80 + } 81 + }} 82 + > 83 + Delete 84 + </button> 85 + </> 86 + ); 87 + } 88 + 89 + export function PostLayout() { 90 + return ( 91 + <div className="flex flex-col justify-center gap-3.5 md:gap-5 items-center p-5 w-screen"> 92 + <Outlet /> 93 + </div> 94 + ); 95 + }
+255
react/src/components/BlogPostForm.tsx
··· 1 + import { posts, type BlogPost } from "../lib/post"; 2 + import { useRef, useState } from "react"; 3 + import { useNavigate } from "react-router"; 4 + import { useSearchParams } from "react-router"; 5 + import { 6 + ContentState, 7 + convertFromRaw, 8 + convertToRaw, 9 + Editor, 10 + EditorState, 11 + RichUtils, 12 + } from "draft-js"; 13 + import "draft-js/dist/Draft.css"; 14 + 15 + export function BlogPostForm({ 16 + post, 17 + onSubmit, 18 + }: { 19 + post: BlogPost | null; 20 + onSubmit: (post: BlogPost) => void; 21 + }) { 22 + const [postState, setPostState] = useState({ 23 + id: post?.id ?? posts.length, 24 + title: post?.title ?? "", 25 + summary: post?.summary ?? "", 26 + content: post?.content ?? "", 27 + author: post?.author ?? "", 28 + datePosted: post?.datePosted ?? new Date().toISOString().split("T")[0], 29 + }); 30 + const [missing, setMissing] = useState<string[]>([]); 31 + const [contentState, setContentState] = useState<EditorState>(() => { 32 + if (post?.content) { 33 + try { 34 + const rawContent = JSON.parse(post.content); 35 + return EditorState.createWithContent(convertFromRaw(rawContent)); 36 + } catch { 37 + // Fallback to plain text if JSON parsing fails 38 + return EditorState.createWithContent( 39 + ContentState.createFromText(post.content), 40 + ); 41 + } 42 + } 43 + return EditorState.createEmpty(); 44 + }); 45 + 46 + const editorRef = useRef<Editor>(null); 47 + 48 + const navigate = useNavigate(); 49 + 50 + const handleChange = ( 51 + event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>, 52 + ) => { 53 + const { name, value } = event.target; 54 + setPostState((prevState) => ({ ...prevState, [name]: value })); 55 + }; 56 + 57 + const handleMissing = () => { 58 + const missingFields = Object.entries(postState) 59 + .map(([key, value]) => (value === "" ? key : null)) 60 + .filter((key) => key !== null); 61 + setMissing(missingFields); 62 + }; 63 + 64 + const handleContentChange = (content: EditorState) => { 65 + setContentState(content); 66 + const rawContent = convertToRaw(content.getCurrentContent()); 67 + setPostState((prevState) => ({ 68 + ...prevState, 69 + content: JSON.stringify(rawContent), 70 + })); 71 + }; 72 + 73 + const handleInlineStyle = (style: string) => { 74 + setContentState((prevState) => 75 + RichUtils.toggleInlineStyle(prevState, style), 76 + ); 77 + }; 78 + 79 + const handleSubmit = (event: React.MouseEvent<HTMLButtonElement>) => { 80 + event.preventDefault(); 81 + 82 + if ( 83 + !postState.title || 84 + !postState.summary || 85 + !postState.content || 86 + !postState.author 87 + ) { 88 + handleMissing(); 89 + return; 90 + } 91 + 92 + onSubmit(postState); 93 + navigate("/"); 94 + }; 95 + 96 + return ( 97 + <form className="flex flex-col gap-4 dark:bg-slate-600 p-10 rounded-lg md:w-4xl w-md"> 98 + <label className="md:grid md:grid-cols-6 flex flex-col w-full gap-2"> 99 + Title: 100 + <input 101 + type="text" 102 + name="title" 103 + className="border-gray-400 md:col-start-2 md:col-span-5 border rounded h-8 py-1 px-2 w-full" 104 + value={postState.title} 105 + onChange={handleChange} 106 + required 107 + /> 108 + </label> 109 + {missing.includes("title") && ( 110 + <p className="text-red-500">Title is required</p> 111 + )} 112 + <label className="md:grid md:grid-cols-6 flex flex-col w-full gap-2"> 113 + Summary: 114 + <textarea 115 + name="summary" 116 + className="border-gray-400 md:col-start-2 md:col-span-5 border rounded min-h-16 h-auto py-1 px-2 w-full" 117 + value={postState.summary} 118 + onChange={handleChange} 119 + required 120 + /> 121 + </label> 122 + {missing.includes("summary") && ( 123 + <p className="text-red-500">Summary is required</p> 124 + )} 125 + <label className="md:grid md:grid-cols-6 flex flex-col w-full gap-2"> 126 + Content: 127 + </label> 128 + <div className="md:grid md:grid-cols-6"> 129 + <div className="md:col-start-2 md:col-span-5"> 130 + <div className="flex gap-2 mb-2 border-b pb-2"> 131 + <button 132 + type="button" 133 + onMouseDown={(e) => { 134 + e.preventDefault(); 135 + handleInlineStyle("BOLD"); 136 + }} 137 + className={`px-3 py-1 border rounded ${ 138 + contentState.getCurrentInlineStyle().has("BOLD") 139 + ? "bg-blue-500 text-white" 140 + : "bg-gray-500" 141 + }`} 142 + > 143 + <strong>B</strong> 144 + </button> 145 + <button 146 + type="button" 147 + onMouseDown={(e) => { 148 + e.preventDefault(); 149 + handleInlineStyle("ITALIC"); 150 + }} 151 + className={`px-3 py-1 border rounded ${ 152 + contentState.getCurrentInlineStyle().has("ITALIC") 153 + ? "bg-blue-500 text-white" 154 + : "bg-gray-500" 155 + }`} 156 + > 157 + <em>I</em> 158 + </button> 159 + <button 160 + type="button" 161 + onMouseDown={(e) => { 162 + e.preventDefault(); 163 + handleInlineStyle("UNDERLINE"); 164 + }} 165 + className={`px-3 py-1 border rounded ${ 166 + contentState.getCurrentInlineStyle().has("UNDERLINE") 167 + ? "bg-blue-500 text-white" 168 + : "bg-gray-500" 169 + }`} 170 + > 171 + <u>U</u> 172 + </button> 173 + </div> 174 + 175 + {/* Editor */} 176 + <div 177 + className="border-gray-400 border rounded p-2 cursor-text min-h-48 pointer-events-auto select-text" 178 + onMouseDown={(e) => { 179 + if (e.target === e.currentTarget) { 180 + e.preventDefault(); 181 + editorRef.current?.focus(); 182 + } 183 + }} 184 + > 185 + <Editor 186 + ref={editorRef} 187 + editorState={contentState} 188 + onChange={handleContentChange} 189 + placeholder="Write your content here..." 190 + /> 191 + </div> 192 + </div> 193 + </div> 194 + {missing.includes("content") && ( 195 + <p className="text-red-500">Content is required</p> 196 + )} 197 + <label className="md:grid md:grid-cols-6 flex flex-col w-full gap-2"> 198 + Author: 199 + <input 200 + type="text" 201 + name="author" 202 + className="border-gray-400 md:col-start-2 md:col-span-5 border rounded h-8 py-1 px-2 w-full" 203 + value={postState.author} 204 + onChange={handleChange} 205 + required 206 + /> 207 + </label> 208 + {missing.includes("author") && ( 209 + <p className="text-red-500">Author is required</p> 210 + )} 211 + <label className="md:grid md:grid-cols-6 flex flex-col w-full gap-2"> 212 + Date Posted: 213 + <input 214 + type="date" 215 + name="datePosted" 216 + value={postState.datePosted} 217 + onChange={handleChange} 218 + required 219 + /> 220 + </label> 221 + <button 222 + type="button" 223 + className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded cursor-pointer" 224 + onClick={handleSubmit} 225 + > 226 + {post ? "Save Post" : "Create Post"} 227 + </button> 228 + </form> 229 + ); 230 + } 231 + 232 + export function NewPostLayout() { 233 + const searchParams = useSearchParams()[0]; 234 + const postId = parseInt(searchParams.get("postId") ?? "-1"); 235 + const post = 236 + postId < 0 || postId >= posts.length 237 + ? null 238 + : posts.find((p) => p.id === postId)!; 239 + 240 + return ( 241 + <div className="flex flex-col gap-4 items-center justify-center dark:bg-slate-700 p-10 h-screen"> 242 + <h1 className="text-2xl font-bold">New Post</h1> 243 + <BlogPostForm 244 + post={post} 245 + onSubmit={(post) => { 246 + if (post.id < posts.length) { 247 + posts[post.id] = post; 248 + } else { 249 + posts.push(post); 250 + } 251 + }} 252 + /> 253 + </div> 254 + ); 255 + }
+33
react/src/components/BlogPostItem.tsx
··· 1 + import { Link } from "react-router"; 2 + 3 + export function BlogPostItem({ 4 + title, 5 + idx, 6 + datePosted, 7 + summary, 8 + }: { 9 + title: string; 10 + idx: number; 11 + datePosted: string; 12 + summary: string; 13 + }) { 14 + return ( 15 + <> 16 + <Link to={`/entries/${idx}`}> 17 + <div className="border border-gray-300 p-3.5 md:p-5 rounded-md flex flex-col gap justify-center items-center max-w-lg"> 18 + <div className="flex flex-row gap-4 justify-center items-center"> 19 + <p className="text-gray-500">#{idx + 1}</p> 20 + <h2 className="text-xl md:text-2xl dark:text-gray-300 text-gray-900 font-bold "> 21 + {title} 22 + </h2> 23 + </div> 24 + <p className="text-gray-500 text-sm">Posted on {datePosted}</p> 25 + <div className="h-3" /> 26 + <p className="text-left w-full"> 27 + {summary.length > 100 ? summary.substring(0, 100) + "..." : summary} 28 + </p> 29 + </div> 30 + </Link> 31 + </> 32 + ); 33 + }
+26
react/src/components/BlogPostList.tsx
··· 1 + import type { BlogPost } from "../lib/post"; 2 + import { BlogPostItem } from "./BlogPostItem"; 3 + 4 + export function BlogPostList({ posts }: { posts: BlogPost[] }) { 5 + if (!posts.length) { 6 + return <p className="text-lg text-center">No blog posts available</p>; 7 + } 8 + return ( 9 + <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 items-center justify-center gap-5"> 10 + {posts 11 + .sort( 12 + (a, b) => 13 + new Date(b.datePosted).getTime() - new Date(a.datePosted).getTime(), 14 + ) 15 + .map((post, idx) => ( 16 + <BlogPostItem 17 + key={idx} 18 + idx={post.id} 19 + title={post.title} 20 + summary={post.summary} 21 + datePosted={post.datePosted} 22 + /> 23 + ))} 24 + </div> 25 + ); 26 + }
-23
react/src/components/Entry.tsx
··· 1 - import { Link } from "react-router"; 2 - 3 - export function Entry({ 4 - title, 5 - idx, 6 - datePosted, 7 - }: { 8 - title: string; 9 - idx: number; 10 - datePosted: string; 11 - }) { 12 - return ( 13 - <> 14 - <Link to={`/entries/${idx}`}> 15 - <div className="border border-gray-300 p-4 rounded-md flex flex-row gap-4 justify-center items-center"> 16 - <p className="text-gray-500">#{idx + 1}</p> 17 - <h2 className="text-lg text-gray-300 font-bold">{title}</h2> 18 - <p className="text-gray-500 text-sm">Posted on {datePosted}</p> 19 - </div> 20 - </Link> 21 - </> 22 - ); 23 - }
-30
react/src/components/Post.tsx
··· 1 - import { useParams, Outlet } from "react-router"; 2 - import { posts } from "../lib/post"; 3 - import { Link } from "react-router"; 4 - 5 - export function Post() { 6 - const { postId } = useParams(); 7 - const post = posts[parseInt(postId!)]; 8 - return ( 9 - <> 10 - <div className="grid grid-cols-3 w-full"> 11 - <Link 12 - to="/" 13 - className="text-gray-200 hover:text-gray-400 justify-self-start" 14 - > 15 - Home 16 - </Link> 17 - <h2 className="text-3xl text-center">{post.title}</h2> 18 - </div> 19 - <p className="text-lg w-[30%]">{post.content}</p> 20 - </> 21 - ); 22 - } 23 - 24 - export function PostLayout() { 25 - return ( 26 - <div className="flex flex-col justify-center gap-3 items-center p-4 w-lvw"> 27 - <Outlet /> 28 - </div> 29 - ); 30 - }
-6
react/src/index.css
··· 19 19 color: #213547; 20 20 background-color: #ffffff; 21 21 } 22 - a:hover { 23 - color: #747bff; 24 - } 25 - button { 26 - background-color: #f9f9f9; 27 - } 28 22 }
+34 -21
react/src/lib/post.ts
··· 1 1 export interface BlogPost { 2 - datePosted: string; 3 - title: string; 4 - content: string; 2 + id: number; 3 + datePosted: string; 4 + title: string; 5 + author: string; 6 + summary: string; 7 + content: string; 5 8 } 6 9 7 10 export const posts: BlogPost[] = [ 8 - { 9 - datePosted: "2025-11-15", 10 - title: "My First Blog Post", 11 - content: 12 - "This is my first blog post. I am excited to share my thoughts with the world! lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", 13 - }, 14 - { 15 - datePosted: "2025-11-17", 16 - title: "My Second Blog Post", 17 - content: 18 - "This is my second blog post. I am excited to share my thoughts with the world!", 19 - }, 20 - { 21 - datePosted: "2025-11-18", 22 - title: "My Third Blog Post", 23 - content: 24 - "This is my third blog post. I am excited to share my thoughts with the world!", 25 - }, 11 + { 12 + id: 0, 13 + datePosted: "2025-11-15", 14 + title: "My First Blog Post", 15 + author: "Samuel Shuert", 16 + summary: "First blog post", 17 + content: 18 + "This is my first blog post. I am excited to share my thoughts with the world! lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", 19 + }, 20 + { 21 + id: 1, 22 + datePosted: "2025-11-17", 23 + title: "My Second Blog Post", 24 + author: "Samuel Shuert", 25 + summary: "Another post", 26 + content: 27 + "This is my second blog post. I am excited to share my thoughts with the world!", 28 + }, 29 + { 30 + id: 2, 31 + datePosted: "2025-11-18", 32 + title: "My Third Blog Post", 33 + author: "Samuel Shuert", 34 + summary: 35 + "The third blog post lorem ipsum dolor sit amet consectetur adipiscing elit The third blog post lorem ipsum dolor sit amet consectetur adipiscing elit", 36 + content: 37 + "This is my third blog post. I am excited to share my thoughts with the world!", 38 + }, 26 39 ];
+16 -3
react/src/main.tsx
··· 2 2 import { createRoot } from "react-dom/client"; 3 3 import { BrowserRouter, Routes, Route } from "react-router"; 4 4 import "./index.css"; 5 - import App from "./App.tsx"; 6 - import { Post, PostLayout } from "./components/Post.tsx"; 5 + import { App } from "./App.tsx"; 6 + import { BlogPostDetail, PostLayout } from "./components/BlogPostDetail.tsx"; 7 + import { NewPostLayout } from "./components/BlogPostForm.tsx"; 8 + import { posts } from "./lib/post.ts"; 9 + 10 + const deletePost = (postId: number) => { 11 + const index = posts.findIndex((post) => post.id === postId); 12 + if (index !== -1) { 13 + posts.splice(index, 1); 14 + } 15 + }; 7 16 8 17 createRoot(document.getElementById("root")!).render( 9 18 <StrictMode> ··· 11 20 <Routes> 12 21 <Route index element={<App />} /> 13 22 <Route path="entries" element={<PostLayout />}> 14 - <Route path=":postId" element={<Post />} /> 23 + <Route 24 + path=":postId" 25 + element={<BlogPostDetail deletePost={deletePost} />} 26 + /> 15 27 </Route> 28 + <Route path="post" element={<NewPostLayout />} /> 16 29 </Routes> 17 30 </BrowserRouter> 18 31 </StrictMode>,
+1 -1
react/tsconfig.app.json
··· 5 5 "useDefineForClassFields": true, 6 6 "lib": ["ES2022", "DOM", "DOM.Iterable"], 7 7 "module": "ESNext", 8 - "types": ["vite/client"], 8 + "types": ["vite/client", "@types/bun"], 9 9 "skipLibCheck": true, 10 10 11 11 /* Bundler mode */
+1 -1
react/tsconfig.node.json
··· 4 4 "target": "ES2023", 5 5 "lib": ["ES2023"], 6 6 "module": "ESNext", 7 - "types": ["node"], 7 + "types": ["@types/bun"], 8 8 "skipLibCheck": true, 9 9 10 10 /* Bundler mode */
+14 -8
react/vite.config.ts
··· 4 4 5 5 // https://vite.dev/config/ 6 6 export default defineConfig({ 7 - plugins: [ 8 - react({ 9 - babel: { 10 - plugins: [["babel-plugin-react-compiler"]], 11 - }, 12 - }), 13 - tailwindcss(), 14 - ], 7 + define: { 8 + global: "globalThis", 9 + }, 10 + server: { 11 + allowedHosts: ["project.coded.codes"], 12 + }, 13 + plugins: [ 14 + react({ 15 + babel: { 16 + plugins: [["babel-plugin-react-compiler"]], 17 + }, 18 + }), 19 + tailwindcss(), 20 + ], 15 21 });
+34
server/.gitignore
··· 1 + # dependencies (bun install) 2 + node_modules 3 + 4 + # output 5 + out 6 + dist 7 + *.tgz 8 + 9 + # code coverage 10 + coverage 11 + *.lcov 12 + 13 + # logs 14 + logs 15 + _.log 16 + report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 17 + 18 + # dotenv environment variable files 19 + .env 20 + .env.development.local 21 + .env.test.local 22 + .env.production.local 23 + .env.local 24 + 25 + # caches 26 + .eslintcache 27 + .cache 28 + *.tsbuildinfo 29 + 30 + # IntelliJ based IDEs 31 + .idea 32 + 33 + # Finder (MacOS) folder config 34 + .DS_Store
+15
server/README.md
··· 1 + # server 2 + 3 + To install dependencies: 4 + 5 + ```bash 6 + bun install 7 + ``` 8 + 9 + To run: 10 + 11 + ```bash 12 + bun run index.ts 13 + ``` 14 + 15 + This project was created using `bun init` in bun v1.2.22. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
+1
server/books.json
··· 1 + [{"id":"9780553212471","title":"Frankenstein","author":"Mary Shelley"},{"id":"9780060935467","title":"To Kill a Mockingbird","author":"Harper Lee"},{"id":"9780141439518","title":"Pride and Prejudice","author":"Jane Austen"}]
+288
server/books.ts
··· 1 + import express, { 2 + type NextFunction, 3 + type Request, 4 + type Response, 5 + } from "express"; 6 + import { writeFile, readFile, exists } from "fs/promises"; 7 + 8 + const ISBN13 = 9 + /^(?:ISBN(?:-13)?:? )?(?=[0-9]{13}$|(?=(?:[0-9]+[- ]){4})[- 0-9]{17}$)[\d-]+$/; 10 + 11 + interface Book { 12 + id: string; 13 + title: string; 14 + author: string; 15 + } 16 + 17 + const initBooks: () => Promise<void> = async () => { 18 + await writeFile( 19 + "books.json", 20 + JSON.stringify([ 21 + { 22 + id: "9780553212471", 23 + title: "Frankenstein", 24 + author: "Mary Shelley", 25 + }, 26 + { 27 + id: "9780060935467", 28 + title: "To Kill a Mockingbird", 29 + author: "Harper Lee", 30 + }, 31 + { 32 + id: "9780141439518", 33 + title: "Pride and Prejudice", 34 + author: "Jane Austen", 35 + }, 36 + ]), 37 + ); 38 + }; 39 + 40 + enum ErrorType { 41 + NotFound, 42 + InvalidId, 43 + BadData, 44 + AlreadyExists, 45 + } 46 + 47 + class BookError extends Error { 48 + public readonly status: number; 49 + constructor(err: ErrorType) { 50 + let msg: string; 51 + let st: number; 52 + switch (err) { 53 + case ErrorType.NotFound: 54 + msg = "Book {{id}} not found"; 55 + st = 404; 56 + break; 57 + case ErrorType.InvalidId: 58 + msg = "Invalid book id ({{id}}) [must be ISBN-13 formatted]"; 59 + st = 400; 60 + break; 61 + case ErrorType.BadData: 62 + msg = "Invalid book data"; 63 + st = 400; 64 + break; 65 + case ErrorType.AlreadyExists: 66 + msg = "Book with id {{id}} already exists"; 67 + st = 409; 68 + break; 69 + } 70 + super(msg); 71 + this.name = "BookError"; 72 + this.status = st; 73 + } 74 + } 75 + 76 + const getBooks: () => Promise<Book[]> = async () => { 77 + if (!(await exists("books.json"))) { 78 + await initBooks(); 79 + } 80 + const file = await readFile("books.json", "utf-8"); 81 + if (file.length < 4) { 82 + await initBooks(); 83 + return await getBooks(); 84 + } 85 + return JSON.parse(file); 86 + }; 87 + 88 + const updateBook = async (task: Book): Promise<void> => { 89 + const books = await getBooks(); 90 + const index = books.findIndex((b) => b.id === task.id); 91 + if (index !== -1) { 92 + books[index] = task; 93 + } else { 94 + books.push(task); 95 + } 96 + await writeFile("books.json", JSON.stringify(books)); 97 + }; 98 + 99 + const removeBook = async (id: string): Promise<void> => { 100 + const books = await getBooks(); 101 + const index = books.findIndex((b) => b.id === id); 102 + if (index !== -1) { 103 + books.splice(index, 1); 104 + await writeFile("books.json", JSON.stringify(books)); 105 + } 106 + }; 107 + 108 + class BadDataIssues extends Error { 109 + missingKeys: string[]; 110 + extraKeys: string[]; 111 + badValues: [string, string][]; 112 + 113 + constructor( 114 + missingKeys: string[], 115 + extraKeys: string[], 116 + badValues: [string, string][], 117 + ) { 118 + super("Bad data issues"); 119 + this.missingKeys = missingKeys; 120 + this.extraKeys = extraKeys; 121 + this.badValues = badValues; 122 + } 123 + } 124 + 125 + const keyTypes = { 126 + id: "ISBN13 code", 127 + title: "string", 128 + author: "string", 129 + }; 130 + 131 + const validateBook = (task: { [key: string]: any }): Book => { 132 + let missingKeys = ["id", "title", "author"].filter( 133 + (key) => !Object.keys(task).includes(key), 134 + ); 135 + let extraKeys = Object.keys(task).filter( 136 + (key) => !["id", "title", "author"].includes(key), 137 + ); 138 + let badValues = Object.entries(task) 139 + .filter(([key, value]) => { 140 + if (key === "id") return typeof value !== "string" || !ISBN13.test(value); 141 + if (key === "title") return typeof value !== "string"; 142 + if (key === "author") return typeof value !== "string"; 143 + return false; 144 + }) 145 + .map( 146 + ([key, _value]) => 147 + [key, keyTypes[key as keyof typeof keyTypes]] as [string, string], 148 + ); 149 + if (missingKeys.length > 0 || extraKeys.length > 0 || badValues.length > 0) { 150 + throw new BadDataIssues(missingKeys, extraKeys, badValues); 151 + } 152 + return task as Book; 153 + }; 154 + 155 + const auth = async (req: Request, res: Response, next: NextFunction) => { 156 + if (req.method === "GET") { 157 + next(); 158 + return; 159 + } 160 + if (!req.headers.authorization) { 161 + res.status(401).json({ error: "Unauthorized" }); 162 + return; 163 + } 164 + let token = req.headers.authorization.split(" ")[1]; 165 + if (token !== "password1!") { 166 + res.status(401).json({ error: "Unauthorized" }); 167 + return; 168 + } 169 + next(); 170 + }; 171 + 172 + const errorHandler = ( 173 + err: Error, 174 + _req: Request, 175 + res: Response, 176 + _next: NextFunction, 177 + ) => { 178 + if (err instanceof BookError) { 179 + let msg = err.message.replace("{{id}}", res.locals.id ?? ""); 180 + 181 + let obj: Map<string, any> = new Map<string, any>([ 182 + ["error", `${err.name}: ${msg}`], 183 + ]); 184 + 185 + if (res.locals.bdi) { 186 + if (res.locals.bdi.missingKeys.length > 0) { 187 + obj.set("missingKeys", res.locals.bdi.missingKeys); 188 + } 189 + if (res.locals.bdi.extraKeys.length > 0) { 190 + obj.set("extraKeys", res.locals.bdi.extraKeys); 191 + } 192 + if (res.locals.bdi.badValues.length > 0) { 193 + obj.set("badValues", res.locals.bdi.badValues); 194 + } 195 + } 196 + 197 + res.status(err.status).json(Object.fromEntries(obj.entries())); 198 + } else { 199 + console.error(err.stack); 200 + res.status(500).json({ error: "Internal Server Error" }); 201 + } 202 + }; 203 + 204 + const router = express.Router(); 205 + 206 + router.use(express.json()); 207 + router.use((req, res, next) => { 208 + console.log(`Recieved a ${req.method} request to ${req.url}`); 209 + next(); 210 + }); 211 + router.use(auth); 212 + 213 + router.get("/", async (_req, res) => { 214 + res.json(await getBooks()); 215 + }); 216 + 217 + router.post("/", async (req, res) => { 218 + const books = await getBooks(); 219 + try { 220 + const bookData = validateBook(req.body); 221 + res.locals.id = bookData.id; 222 + if (books.filter((b) => b.id === bookData.id).length > 0) { 223 + throw new BookError(ErrorType.AlreadyExists); 224 + } 225 + await updateBook(bookData); 226 + res.status(201).json(bookData); 227 + } catch (err) { 228 + if (err instanceof BookError) { 229 + throw err; 230 + } else if (err instanceof BadDataIssues) { 231 + res.locals.bdi = err; 232 + throw new BookError(ErrorType.BadData); 233 + } else { 234 + res.status(500).json({ error: "Internal Server Error" }); 235 + } 236 + } 237 + }); 238 + 239 + router.get("/:id", async (req, res) => { 240 + res.locals.id = req.params.id; 241 + if (!ISBN13.test(req.params.id)) { 242 + throw new BookError(ErrorType.InvalidId); 243 + } 244 + const books = await getBooks(); 245 + const book = books.find((b) => b.id == req.params.id); 246 + if (!book) throw new BookError(ErrorType.NotFound); 247 + res.json(book); 248 + }); 249 + 250 + router.put("/:id", async (req, res) => { 251 + res.locals.id = req.params.id; 252 + if (!ISBN13.test(req.params.id)) { 253 + throw new BookError(ErrorType.InvalidId); 254 + } 255 + const books = await getBooks(); 256 + const book = books.find((b) => b.id == req.params.id); 257 + if (!book) throw new BookError(ErrorType.NotFound); 258 + const bookData = validateBook(req.body); 259 + await updateBook(bookData); 260 + res.sendStatus(204); 261 + }); 262 + 263 + router.delete("/reset", async (_req, res) => { 264 + await initBooks(); 265 + res.sendStatus(204); 266 + }); 267 + 268 + router.delete("/:id", async (req, res) => { 269 + res.locals.id = req.params.id; 270 + if (!ISBN13.test(req.params.id)) { 271 + throw new BookError(ErrorType.InvalidId); 272 + } 273 + const books = await getBooks(); 274 + const book = books.find((b) => b.id == req.params.id); 275 + if (!book) throw new BookError(ErrorType.NotFound); 276 + await removeBook(book.id); 277 + res.sendStatus(204); 278 + }); 279 + 280 + router.all("/{*splat}", async (req, res) => { 281 + res 282 + .status(404) 283 + .json({ error: `path: ${req.method} at /${req.params.splat} Not Found` }); 284 + }); 285 + 286 + router.use(errorHandler); 287 + 288 + export default router;
+177
server/bun.lock
··· 1 + { 2 + "lockfileVersion": 1, 3 + "workspaces": { 4 + "": { 5 + "name": "server", 6 + "dependencies": { 7 + "@types/express": "^5.0.6", 8 + "express": "^5.2.1", 9 + }, 10 + "devDependencies": { 11 + "@types/bun": "latest", 12 + }, 13 + "peerDependencies": { 14 + "typescript": "^5", 15 + }, 16 + }, 17 + }, 18 + "packages": { 19 + "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], 20 + 21 + "@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="], 22 + 23 + "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], 24 + 25 + "@types/express": ["@types/express@5.0.6", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^2" } }, "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA=="], 26 + 27 + "@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.0", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA=="], 28 + 29 + "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], 30 + 31 + "@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], 32 + 33 + "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], 34 + 35 + "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], 36 + 37 + "@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="], 38 + 39 + "@types/serve-static": ["@types/serve-static@2.2.0", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*" } }, "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ=="], 40 + 41 + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], 42 + 43 + "body-parser": ["body-parser@2.2.1", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw=="], 44 + 45 + "bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="], 46 + 47 + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], 48 + 49 + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], 50 + 51 + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], 52 + 53 + "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], 54 + 55 + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], 56 + 57 + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], 58 + 59 + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], 60 + 61 + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], 62 + 63 + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], 64 + 65 + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], 66 + 67 + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], 68 + 69 + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], 70 + 71 + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], 72 + 73 + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], 74 + 75 + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], 76 + 77 + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], 78 + 79 + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], 80 + 81 + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], 82 + 83 + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], 84 + 85 + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], 86 + 87 + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], 88 + 89 + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], 90 + 91 + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], 92 + 93 + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], 94 + 95 + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], 96 + 97 + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], 98 + 99 + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], 100 + 101 + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], 102 + 103 + "iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="], 104 + 105 + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], 106 + 107 + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], 108 + 109 + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], 110 + 111 + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], 112 + 113 + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], 114 + 115 + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], 116 + 117 + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], 118 + 119 + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], 120 + 121 + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 122 + 123 + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], 124 + 125 + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], 126 + 127 + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], 128 + 129 + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], 130 + 131 + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], 132 + 133 + "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], 134 + 135 + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], 136 + 137 + "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], 138 + 139 + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], 140 + 141 + "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], 142 + 143 + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], 144 + 145 + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], 146 + 147 + "send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="], 148 + 149 + "serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="], 150 + 151 + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], 152 + 153 + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], 154 + 155 + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], 156 + 157 + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], 158 + 159 + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], 160 + 161 + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], 162 + 163 + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], 164 + 165 + "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], 166 + 167 + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], 168 + 169 + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], 170 + 171 + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], 172 + 173 + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], 174 + 175 + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], 176 + } 177 + }
+13
server/index.ts
··· 1 + import express from "express"; 2 + import tasks from "./tasks.ts"; 3 + import books from "./books.ts"; 4 + 5 + const app = express(); 6 + const port = process.env["NODE_PORT"] ?? 5173; 7 + 8 + app.use("/tasks", tasks); 9 + app.use("/books", books); 10 + 11 + app.listen(port, () => { 12 + console.log(`Server listening on port ${port}`); 13 + });
+16
server/package.json
··· 1 + { 2 + "name": "server", 3 + "module": "index.ts", 4 + "type": "module", 5 + "private": true, 6 + "devDependencies": { 7 + "@types/bun": "latest" 8 + }, 9 + "peerDependencies": { 10 + "typescript": "^5" 11 + }, 12 + "dependencies": { 13 + "@types/express": "^5.0.6", 14 + "express": "^5.2.1" 15 + } 16 + }
server/tasks.json

This is a binary file and will not be displayed.

+146
server/tasks.ts
··· 1 + import express from "express"; 2 + import { readFile, writeFile, exists } from "fs/promises"; 3 + 4 + const router = express.Router(); 5 + 6 + interface Task { 7 + id: string; 8 + title: string; 9 + completed: boolean; 10 + } 11 + 12 + const getTasks: () => Promise<Task[]> = async () => { 13 + if (!(await exists("tasks.json"))) { 14 + await writeFile("tasks.json", JSON.stringify([])); 15 + } 16 + const file = await readFile("tasks.json", "utf-8"); 17 + if (file.length === 0) { 18 + return []; 19 + } 20 + return JSON.parse(file); 21 + }; 22 + 23 + const updateTask = async (task: Task): Promise<void> => { 24 + const tasks = await getTasks(); 25 + const index = tasks.findIndex((t) => t.id === task.id); 26 + if (index !== -1) { 27 + tasks[index] = task; 28 + } else { 29 + tasks.push(task); 30 + } 31 + await writeFile("tasks.json", JSON.stringify(tasks)); 32 + }; 33 + 34 + const removeTask = async (id: string): Promise<void> => { 35 + const tasks = await getTasks(); 36 + const index = tasks.findIndex((t) => t.id === id); 37 + if (index !== -1) { 38 + tasks.splice(index, 1); 39 + await writeFile("tasks.json", JSON.stringify(tasks)); 40 + } 41 + }; 42 + 43 + router.use(express.json()); 44 + 45 + router.get("/", async (_req, res) => { 46 + res.json(await getTasks()); 47 + }); 48 + 49 + router.post("/", async (req, res) => { 50 + if (!(typeof req.body.title === "string")) { 51 + res.status(400).send("Invalid title"); 52 + return; 53 + } 54 + const newTask = { 55 + id: Math.random().toString(16).substring(2, 8), 56 + title: req.body.title, 57 + completed: false, 58 + }; 59 + await updateTask(newTask); 60 + res.json(newTask).status(201); 61 + }); 62 + 63 + router.get("/:id", async (req, res) => { 64 + const task = (await getTasks()).find((t) => t.id === req.params.id); 65 + if (!task) { 66 + res.status(404).send("Task not found"); 67 + } else { 68 + res.json(task); 69 + } 70 + }); 71 + 72 + router.put("/:id", async (req, res) => { 73 + const task = (await getTasks()).find((t) => t.id === req.params.id); 74 + if (!task) { 75 + res.status(404).send("Task not found"); 76 + } else { 77 + const missing = []; 78 + if (req.body.title === undefined) missing.push("title"); 79 + if (req.body.completed === undefined) missing.push("completed"); 80 + if (missing.length > 0) { 81 + res 82 + .status(400) 83 + .send( 84 + `Missing field${missing.length > 1 ? "s" : ""}: ${missing.join(", ")}`, 85 + ); 86 + } 87 + const badTypes = []; 88 + if (!(typeof req.body.title === "string")) badTypes.push("title"); 89 + if (!(typeof req.body.completed === "boolean")) badTypes.push("completed"); 90 + if (badTypes.length > 0) { 91 + res 92 + .status(400) 93 + .send( 94 + `Invalid type${badTypes.length > 1 ? "s" : ""}: ${badTypes.join(", ")}`, 95 + ); 96 + return; 97 + } 98 + task.title = req.body.title ?? task.title; 99 + task.completed = req.body.completed ?? task.completed; 100 + await updateTask(task); 101 + res.json(task); 102 + } 103 + }); 104 + 105 + router.patch("/:id", async (req, res) => { 106 + const task = (await getTasks()).find((t) => t.id === req.params.id); 107 + if (!task) { 108 + res.status(404).send("Task not found"); 109 + } else { 110 + const badTypes = []; 111 + if ( 112 + Object.keys(req.body).includes("title") && 113 + !(typeof req.body.title === "string") 114 + ) 115 + badTypes.push("title"); 116 + if ( 117 + Object.keys(req.body).includes("completed") && 118 + !(typeof req.body.completed === "boolean") 119 + ) 120 + badTypes.push("completed"); 121 + if (badTypes.length > 0) { 122 + res 123 + .status(400) 124 + .send( 125 + `Invalid type${badTypes.length > 1 ? "s" : ""}: ${badTypes.join(", ")}`, 126 + ); 127 + return; 128 + } 129 + task.title = req.body.title ?? task.title; 130 + task.completed = req.body.completed ?? task.completed; 131 + await updateTask(task); 132 + res.json(task); 133 + } 134 + }); 135 + 136 + router.delete("/:id", async (req, res) => { 137 + const task = (await getTasks()).find((t) => t.id === req.params.id); 138 + if (!task) { 139 + res.status(404).send("Task not found"); 140 + } else { 141 + await removeTask(task.id); 142 + res.status(204).send(); 143 + } 144 + }); 145 + 146 + export default router;
+29
server/tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + // Environment setup & latest features 4 + "lib": ["ESNext"], 5 + "target": "ESNext", 6 + "module": "Preserve", 7 + "moduleDetection": "force", 8 + "jsx": "react-jsx", 9 + "allowJs": true, 10 + 11 + // Bundler mode 12 + "moduleResolution": "bundler", 13 + "allowImportingTsExtensions": true, 14 + "verbatimModuleSyntax": true, 15 + "noEmit": true, 16 + 17 + // Best practices 18 + "strict": true, 19 + "skipLibCheck": true, 20 + "noFallthroughCasesInSwitch": true, 21 + "noUncheckedIndexedAccess": true, 22 + "noImplicitOverride": true, 23 + 24 + // Some stricter flags (disabled by default) 25 + "noUnusedLocals": false, 26 + "noUnusedParameters": false, 27 + "noPropertyAccessFromIndexSignature": false 28 + } 29 + }