CMU Coding Bootcamp
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}