CMU Coding Bootcamp
at main 7.7 kB view raw
1import { posts, type BlogPost } from "../lib/post"; 2import { useRef, useState } from "react"; 3import { useNavigate } from "react-router"; 4import { useSearchParams } from "react-router"; 5import { 6 ContentState, 7 convertFromRaw, 8 convertToRaw, 9 Editor, 10 EditorState, 11 RichUtils, 12} from "draft-js"; 13import "draft-js/dist/Draft.css"; 14 15export function BlogPostForm({ 16 post, 17 onSubmit, 18}: { 19 post: BlogPost | null; 20 onSubmit: (post: BlogPost) => void; 21}) { 22 const [postState, setPostState] = useState({ 23 id: post?.id ?? posts.length, 24 title: post?.title ?? "", 25 summary: post?.summary ?? "", 26 content: post?.content ?? "", 27 author: post?.author ?? "", 28 datePosted: post?.datePosted ?? new Date().toISOString().split("T")[0], 29 }); 30 const [missing, setMissing] = useState<string[]>([]); 31 const [contentState, setContentState] = useState<EditorState>(() => { 32 if (post?.content) { 33 try { 34 const rawContent = JSON.parse(post.content); 35 return EditorState.createWithContent(convertFromRaw(rawContent)); 36 } catch { 37 // Fallback to plain text if JSON parsing fails 38 return EditorState.createWithContent( 39 ContentState.createFromText(post.content), 40 ); 41 } 42 } 43 return EditorState.createEmpty(); 44 }); 45 46 const editorRef = useRef<Editor>(null); 47 48 const navigate = useNavigate(); 49 50 const handleChange = ( 51 event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>, 52 ) => { 53 const { name, value } = event.target; 54 setPostState((prevState) => ({ ...prevState, [name]: value })); 55 }; 56 57 const handleMissing = () => { 58 const missingFields = Object.entries(postState) 59 .map(([key, value]) => (value === "" ? key : null)) 60 .filter((key) => key !== null); 61 setMissing(missingFields); 62 }; 63 64 const handleContentChange = (content: EditorState) => { 65 setContentState(content); 66 const rawContent = convertToRaw(content.getCurrentContent()); 67 setPostState((prevState) => ({ 68 ...prevState, 69 content: JSON.stringify(rawContent), 70 })); 71 }; 72 73 const handleInlineStyle = (style: string) => { 74 setContentState((prevState) => 75 RichUtils.toggleInlineStyle(prevState, style), 76 ); 77 }; 78 79 const handleSubmit = (event: React.MouseEvent<HTMLButtonElement>) => { 80 event.preventDefault(); 81 82 if ( 83 !postState.title || 84 !postState.summary || 85 !postState.content || 86 !postState.author 87 ) { 88 handleMissing(); 89 return; 90 } 91 92 onSubmit(postState); 93 navigate("/"); 94 }; 95 96 return ( 97 <form className="flex flex-col gap-4 dark:bg-slate-600 p-10 rounded-lg md:w-4xl w-md"> 98 <label className="md:grid md:grid-cols-6 flex flex-col w-full gap-2"> 99 Title: 100 <input 101 type="text" 102 name="title" 103 className="border-gray-400 md:col-start-2 md:col-span-5 border rounded h-8 py-1 px-2 w-full" 104 value={postState.title} 105 onChange={handleChange} 106 required 107 /> 108 </label> 109 {missing.includes("title") && ( 110 <p className="text-red-500">Title is required</p> 111 )} 112 <label className="md:grid md:grid-cols-6 flex flex-col w-full gap-2"> 113 Summary: 114 <textarea 115 name="summary" 116 className="border-gray-400 md:col-start-2 md:col-span-5 border rounded min-h-16 h-auto py-1 px-2 w-full" 117 value={postState.summary} 118 onChange={handleChange} 119 required 120 /> 121 </label> 122 {missing.includes("summary") && ( 123 <p className="text-red-500">Summary is required</p> 124 )} 125 <label className="md:grid md:grid-cols-6 flex flex-col w-full gap-2"> 126 Content: 127 </label> 128 <div className="md:grid md:grid-cols-6"> 129 <div className="md:col-start-2 md:col-span-5"> 130 <div className="flex gap-2 mb-2 border-b pb-2"> 131 <button 132 type="button" 133 onMouseDown={(e) => { 134 e.preventDefault(); 135 handleInlineStyle("BOLD"); 136 }} 137 className={`px-3 py-1 border rounded ${ 138 contentState.getCurrentInlineStyle().has("BOLD") 139 ? "bg-blue-500 text-white" 140 : "bg-gray-500" 141 }`} 142 > 143 <strong>B</strong> 144 </button> 145 <button 146 type="button" 147 onMouseDown={(e) => { 148 e.preventDefault(); 149 handleInlineStyle("ITALIC"); 150 }} 151 className={`px-3 py-1 border rounded ${ 152 contentState.getCurrentInlineStyle().has("ITALIC") 153 ? "bg-blue-500 text-white" 154 : "bg-gray-500" 155 }`} 156 > 157 <em>I</em> 158 </button> 159 <button 160 type="button" 161 onMouseDown={(e) => { 162 e.preventDefault(); 163 handleInlineStyle("UNDERLINE"); 164 }} 165 className={`px-3 py-1 border rounded ${ 166 contentState.getCurrentInlineStyle().has("UNDERLINE") 167 ? "bg-blue-500 text-white" 168 : "bg-gray-500" 169 }`} 170 > 171 <u>U</u> 172 </button> 173 </div> 174 175 {/* Editor */} 176 <div 177 className="border-gray-400 border rounded p-2 cursor-text min-h-48 pointer-events-auto select-text" 178 onMouseDown={(e) => { 179 if (e.target === e.currentTarget) { 180 e.preventDefault(); 181 editorRef.current?.focus(); 182 } 183 }} 184 > 185 <Editor 186 ref={editorRef} 187 editorState={contentState} 188 onChange={handleContentChange} 189 placeholder="Write your content here..." 190 /> 191 </div> 192 </div> 193 </div> 194 {missing.includes("content") && ( 195 <p className="text-red-500">Content is required</p> 196 )} 197 <label className="md:grid md:grid-cols-6 flex flex-col w-full gap-2"> 198 Author: 199 <input 200 type="text" 201 name="author" 202 className="border-gray-400 md:col-start-2 md:col-span-5 border rounded h-8 py-1 px-2 w-full" 203 value={postState.author} 204 onChange={handleChange} 205 required 206 /> 207 </label> 208 {missing.includes("author") && ( 209 <p className="text-red-500">Author is required</p> 210 )} 211 <label className="md:grid md:grid-cols-6 flex flex-col w-full gap-2"> 212 Date Posted: 213 <input 214 type="date" 215 name="datePosted" 216 value={postState.datePosted} 217 onChange={handleChange} 218 required 219 /> 220 </label> 221 <button 222 type="button" 223 className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded cursor-pointer" 224 onClick={handleSubmit} 225 > 226 {post ? "Save Post" : "Create Post"} 227 </button> 228 </form> 229 ); 230} 231 232export function NewPostLayout() { 233 const searchParams = useSearchParams()[0]; 234 const postId = parseInt(searchParams.get("postId") ?? "-1"); 235 const post = 236 postId < 0 || postId >= posts.length 237 ? null 238 : posts.find((p) => p.id === postId)!; 239 240 return ( 241 <div className="flex flex-col gap-4 items-center justify-center dark:bg-slate-700 p-10 h-screen"> 242 <h1 className="text-2xl font-bold">New Post</h1> 243 <BlogPostForm 244 post={post} 245 onSubmit={(post) => { 246 if (post.id < posts.length) { 247 posts[post.id] = post; 248 } else { 249 posts.push(post); 250 } 251 }} 252 /> 253 </div> 254 ); 255}