CMU Coding Bootcamp

feat/fix: delete posts / get RTE working

thecoded.prof 5d89fae7 20fa6743

verified
+221 -36
+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
··· 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",
+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
··· 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
··· 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
··· 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 });