+85
html/css/form.css
+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
+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
-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
}
+36
-1
react/bun.lock
+36
-1
react/bun.lock
···
5
5
"name": "react",
6
6
"dependencies": {
7
7
"@tailwindcss/vite": "^4.1.17",
8
-
"bun-types": "^1.3.2",
8
+
"draft-js": "^0.11.7",
9
9
"react": "^19.2.0",
10
10
"react-dom": "^19.2.0",
11
11
"react-router": "^7.9.6",
···
17
17
"@testing-library/jest-dom": "^6.9.1",
18
18
"@testing-library/react": "^16.3.0",
19
19
"@types/bun": "latest",
20
+
"@types/draft-js": "^0.11.20",
20
21
"@types/node": "^24.10.0",
21
22
"@types/react": "^19.2.2",
22
23
"@types/react-dom": "^19.2.2",
···
265
266
266
267
"@types/bun": ["@types/bun@1.3.2", "", { "dependencies": { "bun-types": "1.3.2" } }, "sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg=="],
267
268
269
+
"@types/draft-js": ["@types/draft-js@0.11.20", "", { "dependencies": { "@types/react": "*", "immutable": "~3.7.4" } }, "sha512-bZHtHxXnCu4wlUXlDWrIlJSG2LJ6wcycSWoxcTCcGd0cVOm35p0vh87qpIPzGK2NALMMvJhQXdS330iYB3iGlw=="],
270
+
268
271
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
269
272
270
273
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
···
313
316
314
317
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
315
318
319
+
"asap": ["asap@2.0.6", "", {}, "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="],
320
+
316
321
"babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="],
317
322
318
323
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
···
342
347
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
343
348
344
349
"cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
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=="],
345
354
346
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=="],
347
356
···
359
368
360
369
"dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="],
361
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
+
362
373
"electron-to-chromium": ["electron-to-chromium@1.5.255", "", {}, "sha512-Z9oIp4HrFF/cZkDPMpz2XSuVpc1THDpT4dlmATFlJUIBVCy9Vap5/rIXsASP1CscBacBqhabwh8vLctqBwEerQ=="],
363
374
364
375
"enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="],
···
399
410
400
411
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
401
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
+
402
417
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
403
418
404
419
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
···
432
447
"hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
433
448
434
449
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
450
+
451
+
"immutable": ["immutable@3.7.6", "", {}, "sha512-AizQPcaofEtO11RZhPPHBOJRdo/20MKQF9mBLnVkBoyHi1/zXK8fzVdnEpSV9gxqtnh6Qomfp3F0xT5qP/vThw=="],
435
452
436
453
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
437
454
···
495
512
496
513
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
497
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
+
498
517
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
499
518
500
519
"lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
···
515
534
516
535
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
517
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=="],
538
+
518
539
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
519
540
541
+
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
542
+
520
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=="],
521
544
522
545
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
···
538
561
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
539
562
540
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=="],
541
566
542
567
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
543
568
···
569
594
570
595
"set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
571
596
597
+
"setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="],
598
+
572
599
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
573
600
574
601
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
···
589
616
590
617
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
591
618
619
+
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
620
+
592
621
"ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="],
593
622
594
623
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
···
596
625
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
597
626
598
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=="],
628
+
629
+
"ua-parser-js": ["ua-parser-js@0.7.41", "", { "bin": { "ua-parser-js": "script/cli.js" } }, "sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg=="],
599
630
600
631
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
601
632
···
605
636
606
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=="],
607
638
639
+
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
640
+
608
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=="],
609
644
610
645
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
611
646
+3
-1
react/package.json
+3
-1
react/package.json
···
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",
···
21
22
"@happy-dom/global-registrator": "^20.0.10",
22
23
"@testing-library/jest-dom": "^6.9.1",
23
24
"@testing-library/react": "^16.3.0",
25
+
"@types/bun": "latest",
26
+
"@types/draft-js": "^0.11.20",
24
27
"@types/node": "^24.10.0",
25
28
"@types/react": "^19.2.2",
26
29
"@types/react-dom": "^19.2.2",
27
-
"@types/bun": "latest",
28
30
"@vitejs/plugin-react": "^5.1.0",
29
31
"babel-plugin-react-compiler": "^1.0.0",
30
32
"eslint": "^9.39.1",
+38
-3
react/src/App.tsx
+38
-3
react/src/App.tsx
···
1
1
import { posts } from "./lib/post";
2
2
import { BlogPostList } from "./components/BlogPostList";
3
3
import { Link } from "react-router";
4
+
import { useState } from "react";
4
5
5
6
export function App() {
7
+
const [searchBarDisplay, displaySearchBar] = useState(false);
6
8
return (
7
9
<>
8
10
<title>Posts</title>
9
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
+
}}
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>
10
47
<div className="flex flex-col gap-4 md:grid md:grid-cols-3 items-center justify-between w-full">
11
-
<h1 className="text-5xl font-bold md:col-start-2 text-center w-full">
12
-
Posts
13
-
</h1>
14
48
<div className="flex w-full justify-end items-center">
49
+
<h1 className="text-5xl font-bold text-center">Posts</h1>
15
50
<Link to="/post" className="w-1/3">
16
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">
17
52
New Post
+40
-3
react/src/components/BlogPostDetail.tsx
+40
-3
react/src/components/BlogPostDetail.tsx
···
1
1
import { useParams, Outlet } from "react-router";
2
2
import { posts } from "../lib/post";
3
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";
4
7
5
-
export function BlogPostDetail() {
8
+
export function BlogPostDetail({
9
+
deletePost,
10
+
}: {
11
+
deletePost: (id: number) => void;
12
+
}) {
6
13
const { postId } = useParams();
7
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();
8
27
9
28
if (!post) {
10
29
return <div>Post not found</div>;
···
15
34
day: "numeric",
16
35
year: "numeric",
17
36
});
37
+
18
38
return (
19
39
<>
20
40
<title>{post.title}</title>
···
25
45
>
26
46
Home
27
47
</Link>
28
-
<h1 className="text-3xl md:text-4xl font-bold text-center mb-4">
48
+
<h1 className="text-3xl md:text-4xl font-bold text-center mb-4 md:mb-0">
29
49
{post.title}
30
50
</h1>
31
51
<div className="flex md:justify-end items-center">
···
44
64
Published on {formattedDate}
45
65
</p>
46
66
</div>
47
-
<div className="text-sm md:text-lg md:w-3xl w-full">{post.content}</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>
48
85
</>
49
86
);
50
87
}
+116
-19
react/src/components/BlogPostForm.tsx
+116
-19
react/src/components/BlogPostForm.tsx
···
1
1
import { posts, type BlogPost } from "../lib/post";
2
-
import { useState } from "react";
2
+
import { useRef, useState } from "react";
3
3
import { useNavigate } from "react-router";
4
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";
5
14
6
15
export function BlogPostForm({
7
16
post,
···
10
19
post: BlogPost | null;
11
20
onSubmit: (post: BlogPost) => void;
12
21
}) {
13
-
const [postState, setPostState] = useState(
14
-
post ?? {
15
-
id: posts.length,
16
-
title: "",
17
-
summary: "",
18
-
content: "",
19
-
author: "",
20
-
datePosted: new Date().toISOString().split("T")[0],
21
-
},
22
-
);
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
+
});
23
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);
24
47
25
48
const navigate = useNavigate();
26
49
···
36
59
.map(([key, value]) => (value === "" ? key : null))
37
60
.filter((key) => key !== null);
38
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
+
);
39
77
};
40
78
41
79
const handleSubmit = (event: React.MouseEvent<HTMLButtonElement>) => {
···
86
124
)}
87
125
<label className="md:grid md:grid-cols-6 flex flex-col w-full gap-2">
88
126
Content:
89
-
<textarea
90
-
name="content"
91
-
className="border-gray-400 md:col-start-2 md:col-span-5 border rounded min-h-24 h-auto py-1 px-2 w-full"
92
-
value={postState.content}
93
-
onChange={handleChange}
94
-
required
95
-
/>
96
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>
97
194
{missing.includes("content") && (
98
195
<p className="text-red-500">Content is required</p>
99
196
)}
···
138
235
const post =
139
236
postId < 0 || postId >= posts.length
140
237
? null
141
-
: posts.find((p) => p.id === postId);
238
+
: posts.find((p) => p.id === postId)!;
142
239
143
240
return (
144
241
<div className="flex flex-col gap-4 items-center justify-center dark:bg-slate-700 p-10 h-screen">
+12
-1
react/src/main.tsx
+12
-1
react/src/main.tsx
···
5
5
import { App } from "./App.tsx";
6
6
import { BlogPostDetail, PostLayout } from "./components/BlogPostDetail.tsx";
7
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
+
};
8
16
9
17
createRoot(document.getElementById("root")!).render(
10
18
<StrictMode>
···
12
20
<Routes>
13
21
<Route index element={<App />} />
14
22
<Route path="entries" element={<PostLayout />}>
15
-
<Route path=":postId" element={<BlogPostDetail />} />
23
+
<Route
24
+
path=":postId"
25
+
element={<BlogPostDetail deletePost={deletePost} />}
26
+
/>
16
27
</Route>
17
28
<Route path="post" element={<NewPostLayout />} />
18
29
</Routes>
+14
-11
react/vite.config.ts
+14
-11
react/vite.config.ts
···
4
4
5
5
// https://vite.dev/config/
6
6
export default defineConfig({
7
-
server: {
8
-
allowedHosts: ["project.coded.codes"]
9
-
},
10
-
plugins: [
11
-
react({
12
-
babel: {
13
-
plugins: [["babel-plugin-react-compiler"]],
14
-
},
15
-
}),
16
-
tailwindcss(),
17
-
],
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
+
],
18
21
});
+34
server/.gitignore
+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
+15
server/README.md
+1
server/books.json
+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
+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
+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
+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
+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
server/tasks.json
This is a binary file and will not be displayed.
+146
server/tasks.ts
+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
+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
+
}