Skip to content

Commit b8cabe3

Browse files
committed
Refactor ArticleEditor component to improve state management and debounced save functionality for title and body. Update type definitions for props and enhance editor mode toggling. Adjust textarea references for better handling of null values.
1 parent 8ae4d06 commit b8cabe3

File tree

2 files changed

+142
-127
lines changed

2 files changed

+142
-127
lines changed

src/components/Editor/ArticleEditor.tsx

Lines changed: 141 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
HeadingIcon,
1414
ImageIcon,
1515
} from "@radix-ui/react-icons";
16-
import React, { useRef } from "react";
16+
import React, { useRef, useState, useCallback } from "react";
1717

1818
import { ArticleRepositoryInput } from "@/backend/services/inputs/article.input";
1919
import { useAutosizeTextArea } from "@/hooks/use-auto-resize-textarea";
@@ -32,30 +32,20 @@ import ArticleEditorDrawer from "./ArticleEditorDrawer";
3232
import EditorCommandButton from "./EditorCommandButton";
3333
import { useMarkdownEditor } from "./useMarkdownEditor";
3434

35-
interface Prop {
35+
interface ArticleEditorProps {
3636
uuid?: string;
3737
article?: Article;
3838
}
3939

40-
const ArticleEditor: React.FC<Prop> = ({ article, uuid }) => {
40+
const ArticleEditor: React.FC<ArticleEditorProps> = ({ article, uuid }) => {
4141
const { _t, lang } = useTranslation();
4242
const router = useRouter();
4343
const [isOpenSettingDrawer, toggleSettingDrawer] = useToggle();
4444
const appConfig = useAppConfirm();
45-
const titleRef = useRef<HTMLTextAreaElement>(null!);
45+
const titleRef = useRef<HTMLTextAreaElement>(null);
4646
const bodyRef = useRef<HTMLTextAreaElement | null>(null);
47-
const setDebouncedTitle = useDebouncedCallback(
48-
(title: string) => handleDebouncedSaveTitle(title),
49-
1000
50-
);
51-
const setDebouncedBody = useDebouncedCallback(
52-
(body: string) => handleDebouncedSaveBody(body),
53-
1000
54-
);
47+
const [editorMode, setEditorMode] = useState<"write" | "preview">("write");
5548

56-
const [editorMode, selectEditorMode] = React.useState<"write" | "preview">(
57-
"write"
58-
);
5949
const editorForm = useForm({
6050
defaultValues: {
6151
title: article?.title || "",
@@ -64,107 +54,169 @@ const ArticleEditor: React.FC<Prop> = ({ article, uuid }) => {
6454
resolver: zodResolver(ArticleRepositoryInput.updateArticleInput),
6555
});
6656

67-
useAutosizeTextArea(titleRef, editorForm.watch("title") ?? "");
57+
const watchedTitle = editorForm.watch("title");
58+
const watchedBody = editorForm.watch("body");
6859

69-
const editor = useMarkdownEditor({
70-
ref: bodyRef,
71-
onChange: handleBodyContentChange,
72-
});
60+
useAutosizeTextArea(titleRef, watchedTitle ?? "");
7361

7462
const updateMyArticleMutation = useMutation({
7563
mutationFn: (
7664
input: z.infer<typeof ArticleRepositoryInput.updateMyArticleInput>
77-
) => {
78-
return articleActions.updateMyArticle(input);
79-
},
80-
onSuccess: () => {
81-
router.refresh();
82-
},
83-
onError(err) {
84-
alert(err.message);
85-
},
65+
) => articleActions.updateMyArticle(input),
66+
onSuccess: () => router.refresh(),
67+
onError: (err) => alert(err.message),
8668
});
8769

8870
const articleCreateMutation = useMutation({
8971
mutationFn: (
9072
input: z.infer<typeof ArticleRepositoryInput.createMyArticleInput>
9173
) => articleActions.createMyArticle(input),
92-
onSuccess: (res) => {
93-
router.push(`/dashboard/articles/${res?.id}`);
94-
},
95-
onError(err) {
96-
alert(err.message);
97-
},
74+
onSuccess: (res) => router.push(`/dashboard/articles/${res?.id}`),
75+
onError: (err) => alert(err.message),
9876
});
9977

100-
const handleSaveArticleOnBlurTitle = (title: string) => {
101-
if (!uuid) {
102-
if (title) {
103-
articleCreateMutation.mutate({
104-
title: title ?? "",
105-
});
106-
}
107-
}
108-
};
109-
110-
const handleDebouncedSaveTitle = (title: string) => {
111-
if (uuid) {
112-
if (title) {
78+
const handleDebouncedSaveTitle = useCallback(
79+
(title: string) => {
80+
if (uuid && title) {
11381
updateMyArticleMutation.mutate({
114-
title: title ?? "",
82+
title,
11583
article_id: uuid,
11684
});
11785
}
118-
}
119-
};
86+
},
87+
[uuid, updateMyArticleMutation]
88+
);
89+
90+
const handleDebouncedSaveBody = useCallback(
91+
(body: string) => {
92+
if (!body) return;
12093

121-
const handleDebouncedSaveBody = (body: string) => {
122-
if (uuid) {
123-
if (body) {
94+
if (uuid) {
12495
updateMyArticleMutation.mutate({
12596
article_id: uuid,
12697
handle: article?.handle ?? "untitled",
12798
body,
12899
});
129-
}
130-
} else {
131-
if (body) {
100+
} else {
132101
articleCreateMutation.mutate({
133-
title: editorForm.watch("title")?.length
134-
? (editorForm.watch("title") ?? "untitled")
102+
title: watchedTitle?.length
103+
? (watchedTitle ?? "untitled")
135104
: "untitled",
136105
body,
137106
});
138107
}
139-
}
140-
};
108+
},
109+
[
110+
uuid,
111+
article?.handle,
112+
watchedTitle,
113+
updateMyArticleMutation,
114+
articleCreateMutation,
115+
]
116+
);
117+
118+
const setDebouncedTitle = useDebouncedCallback(
119+
handleDebouncedSaveTitle,
120+
1000
121+
);
122+
const setDebouncedBody = useDebouncedCallback(handleDebouncedSaveBody, 1000);
123+
124+
const handleSaveArticleOnBlurTitle = useCallback(
125+
(title: string) => {
126+
if (!uuid && title) {
127+
articleCreateMutation.mutate({
128+
title,
129+
});
130+
}
131+
},
132+
[uuid, articleCreateMutation]
133+
);
141134

142-
function handleBodyContentChange(
143-
e: React.ChangeEvent<HTMLTextAreaElement> | string
144-
) {
145-
const value = typeof e === "string" ? e : e.target.value;
146-
editorForm.setValue("body", value);
147-
setDebouncedBody(value);
148-
}
135+
const handleBodyContentChange = useCallback(
136+
(e: React.ChangeEvent<HTMLTextAreaElement> | string) => {
137+
const value = typeof e === "string" ? e : e.target.value;
138+
editorForm.setValue("body", value);
139+
setDebouncedBody(value);
140+
},
141+
[editorForm, setDebouncedBody]
142+
);
143+
144+
const editor = useMarkdownEditor({
145+
ref: bodyRef,
146+
onChange: handleBodyContentChange,
147+
});
148+
149+
const toggleEditorMode = useCallback(
150+
() => setEditorMode((mode) => (mode === "write" ? "preview" : "write")),
151+
[]
152+
);
153+
154+
const handlePublishToggle = useCallback(() => {
155+
appConfig.show({
156+
title: _t("Are you sure?"),
157+
labels: {
158+
confirm: _t("Yes"),
159+
cancel: _t("No"),
160+
},
161+
onConfirm: () => {
162+
if (uuid) {
163+
updateMyArticleMutation.mutate({
164+
article_id: uuid,
165+
is_published: !article?.is_published,
166+
});
167+
}
168+
},
169+
});
170+
}, [appConfig, _t, uuid, article?.is_published, updateMyArticleMutation]);
171+
172+
const handleTitleChange = useCallback(
173+
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
174+
const value = e.target.value;
175+
editorForm.setValue("title", value);
176+
setDebouncedTitle(value);
177+
},
178+
[editorForm, setDebouncedTitle]
179+
);
180+
181+
const renderEditorToolbar = () => (
182+
<div className="flex w-full gap-6 p-2 my-2 bg-muted">
183+
<EditorCommandButton
184+
onClick={() => editor?.executeCommand("heading")}
185+
Icon={<HeadingIcon />}
186+
/>
187+
<EditorCommandButton
188+
onClick={() => editor?.executeCommand("bold")}
189+
Icon={<FontBoldIcon />}
190+
/>
191+
<EditorCommandButton
192+
onClick={() => editor?.executeCommand("italic")}
193+
Icon={<FontItalicIcon />}
194+
/>
195+
<EditorCommandButton
196+
onClick={() => editor?.executeCommand("image")}
197+
Icon={<ImageIcon />}
198+
/>
199+
</div>
200+
);
149201

150202
return (
151203
<>
152204
<div className="flex bg-background gap-2 items-center justify-between mt-2 mb-10 sticky z-30 p-5">
153205
<div className="flex items-center gap-2 text-sm text-forground-muted">
154206
<div className="flex gap-4 items-center">
155-
<Link href={"/dashboard"} className=" text-forground">
207+
<Link href="/dashboard" className="text-forground">
156208
<ArrowLeftIcon width={20} height={20} />
157209
</Link>
158210
{updateMyArticleMutation.isPending ? (
159211
<p>{_t("Saving")}...</p>
160212
) : (
161-
<p>
162-
{article?.updated_at && (
213+
article?.updated_at && (
214+
<p>
163215
<span>
164-
({_t("Saved")} {formattedTime(article?.updated_at, lang)})
216+
({_t("Saved")} {formattedTime(article.updated_at, lang)})
165217
</span>
166-
)}
167-
</p>
218+
</p>
219+
)
168220
)}
169221
</div>
170222

@@ -187,30 +239,14 @@ const ArticleEditor: React.FC<Prop> = ({ article, uuid }) => {
187239
{uuid && (
188240
<div className="flex gap-2">
189241
<button
190-
onClick={() =>
191-
selectEditorMode(editorMode === "write" ? "preview" : "write")
192-
}
242+
onClick={toggleEditorMode}
193243
className="px-4 py-1 hidden md:block font-semibold transition-colors duration-200 rounded-sm hover:bg-muted"
194244
>
195245
{editorMode === "write" ? _t("Preview") : _t("Editor")}
196246
</button>
197247

198248
<button
199-
onClick={() => {
200-
appConfig.show({
201-
title: _t("Are you sure?"),
202-
labels: {
203-
confirm: _t("Yes"),
204-
cancel: _t("No"),
205-
},
206-
onConfirm: () => {
207-
updateMyArticleMutation.mutate({
208-
article_id: uuid,
209-
is_published: !article?.is_published,
210-
});
211-
},
212-
});
213-
}}
249+
onClick={handlePublishToggle}
214250
className={clsx(
215251
"transition-colors hidden md:block duration-200 px-4 py-1 font-semibold cursor-pointer",
216252
{
@@ -222,77 +258,56 @@ const ArticleEditor: React.FC<Prop> = ({ article, uuid }) => {
222258
>
223259
{article?.is_published ? _t("Unpublish") : _t("Publish")}
224260
</button>
225-
<button onClick={() => toggleSettingDrawer()}>
261+
<button onClick={toggleSettingDrawer}>
226262
<GearIcon className="w-5 h-5" />
227263
</button>
228264
</div>
229265
)}
230266
</div>
231267

232-
{/* Editor */}
233268
<div className="max-w-[750px] mx-auto p-4 md:p-0">
234269
<textarea
235270
placeholder={_t("Title")}
236271
tabIndex={1}
237272
autoFocus
238273
rows={1}
239-
value={editorForm.watch("title")}
274+
value={watchedTitle}
275+
disabled={articleCreateMutation.isPending}
240276
className="w-full text-2xl focus:outline-none bg-background resize-none"
241277
ref={titleRef}
242278
onBlur={(e) => handleSaveArticleOnBlurTitle(e.target.value)}
243-
onChange={(e) => {
244-
editorForm.setValue("title", e.target.value);
245-
setDebouncedTitle(e.target.value);
246-
}}
279+
onChange={handleTitleChange}
247280
/>
248281

249-
{/* Editor Toolbar */}
250282
<div className="flex flex-col justify-between md:items-center md:flex-row">
251-
<div className="flex w-full gap-6 p-2 my-2 bg-muted">
252-
<EditorCommandButton
253-
onClick={() => editor?.executeCommand("heading")}
254-
Icon={<HeadingIcon />}
255-
/>
256-
<EditorCommandButton
257-
onClick={() => editor?.executeCommand("bold")}
258-
Icon={<FontBoldIcon />}
259-
/>
260-
<EditorCommandButton
261-
onClick={() => editor?.executeCommand("italic")}
262-
Icon={<FontItalicIcon />}
263-
/>
264-
<EditorCommandButton
265-
onClick={() => editor?.executeCommand("image")}
266-
Icon={<ImageIcon />}
267-
/>
268-
</div>
283+
{renderEditorToolbar()}
269284
</div>
270-
{/* Editor Textarea */}
285+
271286
<div className="w-full">
272287
{editorMode === "write" ? (
273288
<textarea
274289
tabIndex={2}
275290
className="focus:outline-none h-[calc(100vh-120px)] bg-background w-full resize-none"
276291
placeholder={_t("Write something stunning...")}
277292
ref={bodyRef}
278-
value={editorForm.watch("body")}
293+
value={watchedBody}
279294
onChange={handleBodyContentChange}
280-
></textarea>
295+
/>
281296
) : (
282297
<div className="content-typography">
283-
{markdocParser(editorForm.watch("body") ?? "")}
298+
{markdocParser(watchedBody ?? "")}
284299
</div>
285300
)}
286301
</div>
287302
</div>
288303

289-
{uuid && (
304+
{uuid && article && (
290305
<ArticleEditorDrawer
291-
article={article!}
306+
article={article}
292307
open={isOpenSettingDrawer}
293308
onClose={toggleSettingDrawer}
294-
onSave={function (): void {
295-
throw new Error("Function not implemented.");
309+
onSave={() => {
310+
// Implementation needed
296311
}}
297312
/>
298313
)}

0 commit comments

Comments
 (0)