tangled
alpha
login
or
join now
thecoded.prof
/
CMU
0
fork
atom
CMU Coding Bootcamp
0
fork
atom
overview
issues
pulls
pipelines
feat/fix: delete posts / get RTE working
thecoded.prof
3 months ago
5d89fae7
20fa6743
verified
This commit was signed with the committer's
known signature
.
thecoded.prof
SSH Key Fingerprint:
SHA256:ePn0u8NlJyz3J4Zl9MHOYW3f4XKoi5K1I4j53bwpG0U=
+221
-36
6 changed files
expand all
collapse all
unified
split
react
bun.lock
package.json
src
components
BlogPostDetail.tsx
BlogPostForm.tsx
main.tsx
vite.config.ts
+36
-1
react/bun.lock
···
5
5
"name": "react",
6
6
"dependencies": {
7
7
"@tailwindcss/vite": "^4.1.17",
8
8
-
"bun-types": "^1.3.2",
8
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
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
269
+
"@types/draft-js": ["@types/draft-js@0.11.20", "", { "dependencies": { "@types/react": "*", "immutable": "~3.7.4" } }, "sha512-bZHtHxXnCu4wlUXlDWrIlJSG2LJ6wcycSWoxcTCcGd0cVOm35p0vh87qpIPzGK2NALMMvJhQXdS330iYB3iGlw=="],
270
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
319
+
"asap": ["asap@2.0.6", "", {}, "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="],
320
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
350
+
351
351
+
"core-js": ["core-js@3.47.0", "", {}, "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg=="],
352
352
+
353
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
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
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
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
414
+
415
415
+
"fbjs-css-vars": ["fbjs-css-vars@1.0.2", "", {}, "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ=="],
416
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
450
+
451
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
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
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
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
538
+
518
539
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
519
540
541
541
+
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
542
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
564
+
565
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
597
+
"setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="],
598
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
619
+
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
620
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
628
+
629
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
639
+
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
640
640
+
608
641
"whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="],
642
642
+
643
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
···
11
11
},
12
12
"dependencies": {
13
13
"@tailwindcss/vite": "^4.1.17",
14
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
25
+
"@types/bun": "latest",
26
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
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",
+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
4
+
import { ContentState, convertFromRaw, Editor, EditorState } from "draft-js";
5
5
+
import { useState } from "react";
6
6
+
import { useNavigate } from "react-router";
4
7
5
5
-
export function BlogPostDetail() {
8
8
+
export function BlogPostDetail({
9
9
+
deletePost,
10
10
+
}: {
11
11
+
deletePost: (id: number) => void;
12
12
+
}) {
6
13
const { postId } = useParams();
7
14
const post = posts.find((post) => post.id === parseInt(postId!));
15
15
+
const [editorState, setEditorState] = useState(() => {
16
16
+
try {
17
17
+
const data = JSON.parse(`"${post?.content ?? ""}"`);
18
18
+
return EditorState.createWithContent(convertFromRaw(data));
19
19
+
} catch {
20
20
+
console.log("fallback");
21
21
+
return EditorState.createWithContent(
22
22
+
ContentState.createFromText(post?.content ?? ""),
23
23
+
);
24
24
+
}
25
25
+
});
26
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
37
+
18
38
return (
19
39
<>
20
40
<title>{post.title}</title>
···
25
45
>
26
46
Home
27
47
</Link>
28
28
-
<h1 className="text-3xl md:text-4xl font-bold text-center mb-4">
48
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
47
-
<div className="text-sm md:text-lg md:w-3xl w-full">{post.content}</div>
67
67
+
<div className="text-sm md:text-lg md:w-3xl w-full md:mb-10">
68
68
+
<Editor
69
69
+
editorState={editorState}
70
70
+
onChange={setEditorState}
71
71
+
readOnly={true}
72
72
+
/>
73
73
+
</div>
74
74
+
<button
75
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
76
+
onClick={() => {
77
77
+
if (window.confirm("Are you sure you want to delete this post?")) {
78
78
+
deletePost(post.id);
79
79
+
navigate("/");
80
80
+
}
81
81
+
}}
82
82
+
>
83
83
+
Delete
84
84
+
</button>
48
85
</>
49
86
);
50
87
}
+116
-19
react/src/components/BlogPostForm.tsx
···
1
1
import { posts, type BlogPost } from "../lib/post";
2
2
-
import { useState } from "react";
2
2
+
import { useRef, useState } from "react";
3
3
import { useNavigate } from "react-router";
4
4
import { useSearchParams } from "react-router";
5
5
+
import {
6
6
+
ContentState,
7
7
+
convertFromRaw,
8
8
+
convertToRaw,
9
9
+
Editor,
10
10
+
EditorState,
11
11
+
RichUtils,
12
12
+
} from "draft-js";
13
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
13
-
const [postState, setPostState] = useState(
14
14
-
post ?? {
15
15
-
id: posts.length,
16
16
-
title: "",
17
17
-
summary: "",
18
18
-
content: "",
19
19
-
author: "",
20
20
-
datePosted: new Date().toISOString().split("T")[0],
21
21
-
},
22
22
-
);
22
22
+
const [postState, setPostState] = useState({
23
23
+
id: post?.id ?? posts.length,
24
24
+
title: post?.title ?? "",
25
25
+
summary: post?.summary ?? "",
26
26
+
content: post?.content ?? "",
27
27
+
author: post?.author ?? "",
28
28
+
datePosted: post?.datePosted ?? new Date().toISOString().split("T")[0],
29
29
+
});
23
30
const [missing, setMissing] = useState<string[]>([]);
31
31
+
const [contentState, setContentState] = useState<EditorState>(() => {
32
32
+
if (post?.content) {
33
33
+
try {
34
34
+
const rawContent = JSON.parse(post.content);
35
35
+
return EditorState.createWithContent(convertFromRaw(rawContent));
36
36
+
} catch {
37
37
+
// Fallback to plain text if JSON parsing fails
38
38
+
return EditorState.createWithContent(
39
39
+
ContentState.createFromText(post.content),
40
40
+
);
41
41
+
}
42
42
+
}
43
43
+
return EditorState.createEmpty();
44
44
+
});
45
45
+
46
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
62
+
};
63
63
+
64
64
+
const handleContentChange = (content: EditorState) => {
65
65
+
setContentState(content);
66
66
+
const rawContent = convertToRaw(content.getCurrentContent());
67
67
+
setPostState((prevState) => ({
68
68
+
...prevState,
69
69
+
content: JSON.stringify(rawContent),
70
70
+
}));
71
71
+
};
72
72
+
73
73
+
const handleInlineStyle = (style: string) => {
74
74
+
setContentState((prevState) =>
75
75
+
RichUtils.toggleInlineStyle(prevState, style),
76
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
89
-
<textarea
90
90
-
name="content"
91
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
92
-
value={postState.content}
93
93
-
onChange={handleChange}
94
94
-
required
95
95
-
/>
96
127
</label>
128
128
+
<div className="md:grid md:grid-cols-6">
129
129
+
<div className="md:col-start-2 md:col-span-5">
130
130
+
<div className="flex gap-2 mb-2 border-b pb-2">
131
131
+
<button
132
132
+
type="button"
133
133
+
onMouseDown={(e) => {
134
134
+
e.preventDefault();
135
135
+
handleInlineStyle("BOLD");
136
136
+
}}
137
137
+
className={`px-3 py-1 border rounded ${
138
138
+
contentState.getCurrentInlineStyle().has("BOLD")
139
139
+
? "bg-blue-500 text-white"
140
140
+
: "bg-gray-500"
141
141
+
}`}
142
142
+
>
143
143
+
<strong>B</strong>
144
144
+
</button>
145
145
+
<button
146
146
+
type="button"
147
147
+
onMouseDown={(e) => {
148
148
+
e.preventDefault();
149
149
+
handleInlineStyle("ITALIC");
150
150
+
}}
151
151
+
className={`px-3 py-1 border rounded ${
152
152
+
contentState.getCurrentInlineStyle().has("ITALIC")
153
153
+
? "bg-blue-500 text-white"
154
154
+
: "bg-gray-500"
155
155
+
}`}
156
156
+
>
157
157
+
<em>I</em>
158
158
+
</button>
159
159
+
<button
160
160
+
type="button"
161
161
+
onMouseDown={(e) => {
162
162
+
e.preventDefault();
163
163
+
handleInlineStyle("UNDERLINE");
164
164
+
}}
165
165
+
className={`px-3 py-1 border rounded ${
166
166
+
contentState.getCurrentInlineStyle().has("UNDERLINE")
167
167
+
? "bg-blue-500 text-white"
168
168
+
: "bg-gray-500"
169
169
+
}`}
170
170
+
>
171
171
+
<u>U</u>
172
172
+
</button>
173
173
+
</div>
174
174
+
175
175
+
{/* Editor */}
176
176
+
<div
177
177
+
className="border-gray-400 border rounded p-2 cursor-text min-h-48 pointer-events-auto select-text"
178
178
+
onMouseDown={(e) => {
179
179
+
if (e.target === e.currentTarget) {
180
180
+
e.preventDefault();
181
181
+
editorRef.current?.focus();
182
182
+
}
183
183
+
}}
184
184
+
>
185
185
+
<Editor
186
186
+
ref={editorRef}
187
187
+
editorState={contentState}
188
188
+
onChange={handleContentChange}
189
189
+
placeholder="Write your content here..."
190
190
+
/>
191
191
+
</div>
192
192
+
</div>
193
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
141
-
: posts.find((p) => p.id === postId);
238
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
···
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
8
+
import { posts } from "./lib/post.ts";
9
9
+
10
10
+
const deletePost = (postId: number) => {
11
11
+
const index = posts.findIndex((post) => post.id === postId);
12
12
+
if (index !== -1) {
13
13
+
posts.splice(index, 1);
14
14
+
}
15
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
15
-
<Route path=":postId" element={<BlogPostDetail />} />
23
23
+
<Route
24
24
+
path=":postId"
25
25
+
element={<BlogPostDetail deletePost={deletePost} />}
26
26
+
/>
16
27
</Route>
17
28
<Route path="post" element={<NewPostLayout />} />
18
29
</Routes>
+14
-11
react/vite.config.ts
···
4
4
5
5
// https://vite.dev/config/
6
6
export default defineConfig({
7
7
-
server: {
8
8
-
allowedHosts: ["project.coded.codes"]
9
9
-
},
10
10
-
plugins: [
11
11
-
react({
12
12
-
babel: {
13
13
-
plugins: [["babel-plugin-react-compiler"]],
14
14
-
},
15
15
-
}),
16
16
-
tailwindcss(),
17
17
-
],
7
7
+
define: {
8
8
+
global: "globalThis",
9
9
+
},
10
10
+
server: {
11
11
+
allowedHosts: ["project.coded.codes"],
12
12
+
},
13
13
+
plugins: [
14
14
+
react({
15
15
+
babel: {
16
16
+
plugins: [["babel-plugin-react-compiler"]],
17
17
+
},
18
18
+
}),
19
19
+
tailwindcss(),
20
20
+
],
18
21
});