@@ -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
1818import { ArticleRepositoryInput } from "@/backend/services/inputs/article.input" ;
1919import { useAutosizeTextArea } from "@/hooks/use-auto-resize-textarea" ;
@@ -32,30 +32,20 @@ import ArticleEditorDrawer from "./ArticleEditorDrawer";
3232import EditorCommandButton from "./EditorCommandButton" ;
3333import { 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