CMU Coding Bootcamp

feat: Add Post Creation

thecoded.prof 20fa6743 dc1fe066

verified
+223 -34
+14 -1
react/src/App.tsx
··· 1 1 import { posts } from "./lib/post"; 2 2 import { BlogPostList } from "./components/BlogPostList"; 3 + import { Link } from "react-router"; 3 4 4 5 export function App() { 5 6 return ( 6 7 <> 8 + <title>Posts</title> 7 9 <div className="w-screen p-5 flex flex-col items-center gap-10"> 8 - <h1 className="text-5xl font-bold">Posts</h1> 10 + <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 + <div className="flex w-full justify-end items-center"> 15 + <Link to="/post" className="w-1/3"> 16 + <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 + New Post 18 + </div> 19 + </Link> 20 + </div> 21 + </div> 9 22 <BlogPostList posts={posts} /> 10 23 </div> 11 24 </>
+14 -2
react/src/components/BlogPostDetail.tsx
··· 4 4 5 5 export function BlogPostDetail() { 6 6 const { postId } = useParams(); 7 - const post = posts[parseInt(postId!)]; 7 + const post = posts.find((post) => post.id === parseInt(postId!)); 8 + 9 + if (!post) { 10 + return <div>Post not found</div>; 11 + } 8 12 9 13 const formattedDate = new Date(post.datePosted).toLocaleDateString("en-US", { 10 14 month: "long", ··· 13 17 }); 14 18 return ( 15 19 <> 20 + <title>{post.title}</title> 16 21 <div className="md:grid md:grid-cols-3 flex flex-col w-full"> 17 22 <Link 18 23 to="/" ··· 20 25 > 21 26 Home 22 27 </Link> 23 - <h1 className="text-3xl md:text-4xl font-bold text-center"> 28 + <h1 className="text-3xl md:text-4xl font-bold text-center mb-4"> 24 29 {post.title} 25 30 </h1> 31 + <div className="flex md:justify-end items-center"> 32 + <Link to={`/post?postId=${post.id}`} className="w-1/3"> 33 + <div className="bg-blue-500 hover:bg-blue-700 text-white w-full font-bold py-2 px-4 rounded cursor-pointer text-center"> 34 + Edit Post 35 + </div> 36 + </Link> 37 + </div> 26 38 </div> 27 39 <div className="flex flex-col gap-1 md:gap-2.5 justify-center items-center mb-1.5 md:mb-2.5"> 28 40 <p className="text-gray-700 dark:text-gray-400 text-sm md:text-lg">
+158
react/src/components/BlogPostForm.tsx
··· 1 + import { posts, type BlogPost } from "../lib/post"; 2 + import { useState } from "react"; 3 + import { useNavigate } from "react-router"; 4 + import { useSearchParams } from "react-router"; 5 + 6 + export function BlogPostForm({ 7 + post, 8 + onSubmit, 9 + }: { 10 + post: BlogPost | null; 11 + onSubmit: (post: BlogPost) => void; 12 + }) { 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 + ); 23 + const [missing, setMissing] = useState<string[]>([]); 24 + 25 + const navigate = useNavigate(); 26 + 27 + const handleChange = ( 28 + event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>, 29 + ) => { 30 + const { name, value } = event.target; 31 + setPostState((prevState) => ({ ...prevState, [name]: value })); 32 + }; 33 + 34 + const handleMissing = () => { 35 + const missingFields = Object.entries(postState) 36 + .map(([key, value]) => (value === "" ? key : null)) 37 + .filter((key) => key !== null); 38 + setMissing(missingFields); 39 + }; 40 + 41 + const handleSubmit = (event: React.MouseEvent<HTMLButtonElement>) => { 42 + event.preventDefault(); 43 + 44 + if ( 45 + !postState.title || 46 + !postState.summary || 47 + !postState.content || 48 + !postState.author 49 + ) { 50 + handleMissing(); 51 + return; 52 + } 53 + 54 + onSubmit(postState); 55 + navigate("/"); 56 + }; 57 + 58 + return ( 59 + <form className="flex flex-col gap-4 dark:bg-slate-600 p-10 rounded-lg md:w-4xl w-md"> 60 + <label className="md:grid md:grid-cols-6 flex flex-col w-full gap-2"> 61 + Title: 62 + <input 63 + type="text" 64 + name="title" 65 + className="border-gray-400 md:col-start-2 md:col-span-5 border rounded h-8 py-1 px-2 w-full" 66 + value={postState.title} 67 + onChange={handleChange} 68 + required 69 + /> 70 + </label> 71 + {missing.includes("title") && ( 72 + <p className="text-red-500">Title is required</p> 73 + )} 74 + <label className="md:grid md:grid-cols-6 flex flex-col w-full gap-2"> 75 + Summary: 76 + <textarea 77 + name="summary" 78 + className="border-gray-400 md:col-start-2 md:col-span-5 border rounded min-h-16 h-auto py-1 px-2 w-full" 79 + value={postState.summary} 80 + onChange={handleChange} 81 + required 82 + /> 83 + </label> 84 + {missing.includes("summary") && ( 85 + <p className="text-red-500">Summary is required</p> 86 + )} 87 + <label className="md:grid md:grid-cols-6 flex flex-col w-full gap-2"> 88 + 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 + </label> 97 + {missing.includes("content") && ( 98 + <p className="text-red-500">Content is required</p> 99 + )} 100 + <label className="md:grid md:grid-cols-6 flex flex-col w-full gap-2"> 101 + Author: 102 + <input 103 + type="text" 104 + name="author" 105 + className="border-gray-400 md:col-start-2 md:col-span-5 border rounded h-8 py-1 px-2 w-full" 106 + value={postState.author} 107 + onChange={handleChange} 108 + required 109 + /> 110 + </label> 111 + {missing.includes("author") && ( 112 + <p className="text-red-500">Author is required</p> 113 + )} 114 + <label className="md:grid md:grid-cols-6 flex flex-col w-full gap-2"> 115 + Date Posted: 116 + <input 117 + type="date" 118 + name="datePosted" 119 + value={postState.datePosted} 120 + onChange={handleChange} 121 + required 122 + /> 123 + </label> 124 + <button 125 + type="button" 126 + className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded cursor-pointer" 127 + onClick={handleSubmit} 128 + > 129 + {post ? "Save Post" : "Create Post"} 130 + </button> 131 + </form> 132 + ); 133 + } 134 + 135 + export function NewPostLayout() { 136 + const searchParams = useSearchParams()[0]; 137 + const postId = parseInt(searchParams.get("postId") ?? "-1"); 138 + const post = 139 + postId < 0 || postId >= posts.length 140 + ? null 141 + : posts.find((p) => p.id === postId); 142 + 143 + return ( 144 + <div className="flex flex-col gap-4 items-center justify-center dark:bg-slate-700 p-10 h-screen"> 145 + <h1 className="text-2xl font-bold">New Post</h1> 146 + <BlogPostForm 147 + post={post} 148 + onSubmit={(post) => { 149 + if (post.id < posts.length) { 150 + posts[post.id] = post; 151 + } else { 152 + posts.push(post); 153 + } 154 + }} 155 + /> 156 + </div> 157 + ); 158 + }
+1 -1
react/src/components/BlogPostList.tsx
··· 15 15 .map((post, idx) => ( 16 16 <BlogPostItem 17 17 key={idx} 18 - idx={posts.length - idx - 1} 18 + idx={post.id} 19 19 title={post.title} 20 20 summary={post.summary} 21 21 datePosted={post.datePosted}
+34 -30
react/src/lib/post.ts
··· 1 1 export interface BlogPost { 2 - datePosted: string; 3 - title: string; 4 - author: string; 5 - summary: string; 6 - content: string; 2 + id: number; 3 + datePosted: string; 4 + title: string; 5 + author: string; 6 + summary: string; 7 + content: string; 7 8 } 8 9 9 10 export const posts: BlogPost[] = [ 10 - { 11 - datePosted: "2025-11-15", 12 - title: "My First Blog Post", 13 - author: "Samuel Shuert", 14 - summary: "First blog post", 15 - content: 16 - "This is my first blog post. I am excited to share my thoughts with the world! lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", 17 - }, 18 - { 19 - datePosted: "2025-11-17", 20 - title: "My Second Blog Post", 21 - author: "Samuel Shuert", 22 - summary: "Another post", 23 - content: 24 - "This is my second blog post. I am excited to share my thoughts with the world!", 25 - }, 26 - { 27 - datePosted: "2025-11-18", 28 - title: "My Third Blog Post", 29 - author: "Samuel Shuert", 30 - summary: 31 - "The third blog post lorem ipsum dolor sit amet consectetur adipiscing elit The third blog post lorem ipsum dolor sit amet consectetur adipiscing elit", 32 - content: 33 - "This is my third blog post. I am excited to share my thoughts with the world!", 34 - }, 11 + { 12 + id: 0, 13 + datePosted: "2025-11-15", 14 + title: "My First Blog Post", 15 + author: "Samuel Shuert", 16 + summary: "First blog post", 17 + content: 18 + "This is my first blog post. I am excited to share my thoughts with the world! lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", 19 + }, 20 + { 21 + id: 1, 22 + datePosted: "2025-11-17", 23 + title: "My Second Blog Post", 24 + author: "Samuel Shuert", 25 + summary: "Another post", 26 + content: 27 + "This is my second blog post. I am excited to share my thoughts with the world!", 28 + }, 29 + { 30 + id: 2, 31 + datePosted: "2025-11-18", 32 + title: "My Third Blog Post", 33 + author: "Samuel Shuert", 34 + summary: 35 + "The third blog post lorem ipsum dolor sit amet consectetur adipiscing elit The third blog post lorem ipsum dolor sit amet consectetur adipiscing elit", 36 + content: 37 + "This is my third blog post. I am excited to share my thoughts with the world!", 38 + }, 35 39 ];
+2
react/src/main.tsx
··· 4 4 import "./index.css"; 5 5 import { App } from "./App.tsx"; 6 6 import { BlogPostDetail, PostLayout } from "./components/BlogPostDetail.tsx"; 7 + import { NewPostLayout } from "./components/BlogPostForm.tsx"; 7 8 8 9 createRoot(document.getElementById("root")!).render( 9 10 <StrictMode> ··· 13 14 <Route path="entries" element={<PostLayout />}> 14 15 <Route path=":postId" element={<BlogPostDetail />} /> 15 16 </Route> 17 + <Route path="post" element={<NewPostLayout />} /> 16 18 </Routes> 17 19 </BrowserRouter> 18 20 </StrictMode>,