11"use client"
2- import React , { useState , useEffect } from 'react' ;
2+ import React , { useState , useEffect , useRef } from 'react' ;
33import { Button } from "@/components/ui/button" ;
44import { Input } from "@/components/ui/input" ;
5- import { Textarea } from "@/components/ui/textarea" ;
65import { Card , CardContent , CardHeader , CardTitle } from "@/components/ui/card" ;
76import { Badge } from "@/components/ui/badge" ;
87import { X } from "lucide-react" ;
98import { useToast } from "@/components/ui/use-toast" ;
109import FileUploadButton from '@/components/FileButton' ;
10+ import { Icons } from '@/components/icons' ;
11+ import dynamic from 'next/dynamic' ;
12+
13+ const EditorJS = dynamic ( ( ) => import ( '@editorjs/editorjs' ) , { ssr : false } ) ;
1114
1215const ChangelogEditor = ( { params } ) => {
1316 const { toast } = useToast ( ) ;
17+ const editorRef = useRef ( null ) ;
1418 const [ changelogData , setChangelogData ] = useState ( {
1519 title : '' ,
16- content : '' ,
20+ content : null ,
1721 coverImage : null ,
1822 tags : [ ] ,
1923 } ) ;
2024 const [ isDraft , setIsDraft ] = useState ( true ) ;
2125 const [ newTag , setNewTag ] = useState ( '' ) ;
26+ const [ uploading , setUploading ] = useState ( false ) ;
27+ const fileInputRef = useRef ( null ) ;
2228
2329 useEffect ( ( ) => {
2430 fetchChangelogData ( ) ;
2531 } , [ params . id , params . slug ] ) ;
2632
33+ useEffect ( ( ) => {
34+ if ( typeof window !== 'undefined' ) {
35+ initEditor ( ) ;
36+ }
37+ } , [ ] ) ;
38+
39+ const initEditor = async ( ) => {
40+ const EditorJS = ( await import ( '@editorjs/editorjs' ) ) . default ;
41+ const Header = ( await import ( '@editorjs/header' ) ) . default ;
42+ const List = ( await import ( '@editorjs/list' ) ) . default ;
43+ const Embed = ( await import ( '@editorjs/embed' ) ) . default ;
44+ const Image = ( await import ( '@editorjs/image' ) ) . default ;
45+ const Code = ( await import ( '@editorjs/code' ) ) . default ;
46+
47+ if ( ! editorRef . current ) {
48+ const editor = new EditorJS ( {
49+ holder : 'editorjs' ,
50+ tools : {
51+ header : Header ,
52+ list : List ,
53+ embed : Embed ,
54+ image : {
55+ class : Image ,
56+ config : {
57+ uploader : {
58+ uploadByFile ( file ) {
59+ // Implement your file upload logic here
60+ return Promise . resolve ( {
61+ success : 1 ,
62+ file : {
63+ url : 'https://example.com/image.png' ,
64+ }
65+ } ) ;
66+ }
67+ }
68+ }
69+ } ,
70+ code : Code ,
71+ } ,
72+ data : changelogData . content ,
73+ onChange : async ( ) => {
74+ const content = await editor . save ( ) ;
75+ setChangelogData ( prev => ( { ...prev , content } ) ) ;
76+ } ,
77+ } ) ;
78+
79+ editorRef . current = editor ;
80+ }
81+ } ;
82+
2783 const fetchChangelogData = async ( ) => {
2884 try {
2985 const response = await fetch ( `/api/auth/changelog/fetch-changelog?changelogId=${ params . id } ` , {
@@ -35,8 +91,13 @@ const ChangelogEditor = ({ params }) => {
3591 throw new Error ( 'Failed to fetch changelog data' ) ;
3692 }
3793 const data = await response . json ( ) ;
94+ data . tags = data . tags || [ ] ;
95+ data . content = JSON . parse ( data . content || '{}' ) ;
3896 setChangelogData ( data ) ;
39- setIsDraft ( data . draft ) ; // Assuming the API returns a 'draft' field
97+ setIsDraft ( data . draft ) ;
98+ if ( editorRef . current ) {
99+ editorRef . current . render ( data . content ) ;
100+ }
40101 } catch ( error ) {
41102 toast ( {
42103 title : "Error" ,
@@ -66,6 +127,13 @@ const ChangelogEditor = ({ params }) => {
66127 }
67128 } ;
68129
130+ const handleKeyPress = ( e ) => {
131+ if ( e . key === 'Enter' && newTag ) {
132+ e . preventDefault ( ) ;
133+ handleAddTag ( ) ;
134+ }
135+ } ;
136+
69137 const handleRemoveTag = ( tagToRemove ) => {
70138 setChangelogData ( prev => ( {
71139 ...prev ,
@@ -78,10 +146,11 @@ const ChangelogEditor = ({ params }) => {
78146 } ;
79147
80148 const handleSave = async ( saveAsDraft = true ) => {
149+ const content = await editorRef . current . save ( ) ;
81150 const payload = {
82151 changelogId : params . id ,
83152 title : changelogData . title ,
84- content : changelogData . content ,
153+ content : JSON . stringify ( content ) ,
85154 tags : changelogData . tags || [ ] ,
86155 draft : saveAsDraft ,
87156 coverImage : changelogData . coverImage
@@ -143,76 +212,95 @@ const ChangelogEditor = ({ params }) => {
143212 } ;
144213
145214 return (
146- < Card className = "w-full mx-auto" >
147- < CardHeader >
148- < CardTitle > Changelog Editor</ CardTitle >
149- </ CardHeader >
215+ < Card className = "w-full mx-auto mt-4 border-0 " >
216+ { /* <CardHeader> */ }
217+ { /* <CardTitle>Changelog Editor</CardTitle> */ }
218+ { /* </CardHeader> */ }
150219 < CardContent >
151- < div className = "space-y-4" >
220+ < div className = "flex justify-end space-x-2" >
221+ { isDraft ? (
222+ < >
223+ < Button variant = "outline" onClick = { ( ) => handleSave ( true ) } >
224+ Save as Draft
225+ </ Button >
226+ < Button onClick = { ( ) => handleSave ( false ) } >
227+ Publish
228+ </ Button >
229+ </ >
230+ ) : (
231+ < Button onClick = { ( ) => handleSave ( false ) } >
232+ Publish Changes
233+ </ Button >
234+ ) }
235+ < Button variant = "destructive" onClick = { handleDelete } >
236+ Delete
237+ </ Button >
238+ </ div >
239+ < div className = "space-y-6" >
152240 < Input
153241 type = "text"
154242 name = "title"
155243 placeholder = "Enter title"
156244 value = { changelogData . title }
157245 onChange = { handleInputChange }
158- className = "font-bold text-xl"
159- />
160- < div >
161- < label htmlFor = "cover-image" className = "block text-sm font-medium text-gray-700" >
162- Cover Image
163- </ label >
164- < FileUploadButton
165- uploading = { false }
166- setUploading = { ( ) => { } }
167- uploadedFileUrl = { changelogData . coverImage || '' }
168- setUploadedFileUrl = { handleImageChange }
169- />
170- </ div >
171- < Textarea
172- name = "content"
173- placeholder = "Enter changelog content (Markdown supported)"
174- value = { changelogData . content }
175- onChange = { handleInputChange }
176- rows = { 10 }
177- className = "font-mono"
246+ className = "font-bold text-2xl border-0 focus:ring-0 focus-visible:ring-0 px-0 mx-0 my-0 py-0 ring-offset-0 focus-visible:ring-offset-0"
178247 />
179- < div >
180- < Input
181- type = "text"
182- placeholder = "Add a tag"
183- value = { newTag }
184- onChange = { ( e ) => setNewTag ( e . target . value ) }
185- className = "mb-2"
186- />
187- < Button onClick = { handleAddTag } className = "mb-2" > Add Tag</ Button >
188- < div className = "flex flex-wrap gap-2" >
189- { changelogData . tags && changelogData . tags . map ( ( tag , index ) => (
190- < Badge key = { index } variant = "secondary" className = "flex items-center gap-1" >
191- { tag }
192- < X size = { 14 } onClick = { ( ) => handleRemoveTag ( tag ) } className = "cursor-pointer" />
193- </ Badge >
194- ) ) }
248+
249+ < div className = 'flex items-start justify-between space-x-4' >
250+ < div className = 'flex items-center space-x-4 cursor-pointer'
251+ onClick = { ( ) => fileInputRef ?. current ?. click ( ) }
252+ >
253+ < div className = "aspect-video bg-gray-100 rounded-lg overflow-hidden h-64" >
254+ { changelogData . coverImage ? (
255+ < img
256+ src = { changelogData . coverImage }
257+ alt = "Cover"
258+ className = "w-full h-full object-cover"
259+ />
260+ ) : (
261+ < div className = "w-full h-full flex items-center justify-center text-gray-400" >
262+ { uploading ? < Icons . spinner className = "h-6 w-6 animate-spin" /> : 'Cover image' }
263+ </ div >
264+ ) }
265+ </ div >
266+ < FileUploadButton
267+ innerRef = { fileInputRef }
268+ className = "hidden"
269+ uploading = { uploading }
270+ setUploading = { setUploading }
271+ uploadedFileUrl = { changelogData . coverImage || '' }
272+ setUploadedFileUrl = { handleImageChange }
273+ />
274+ </ div >
275+
276+ < div >
277+ < label className = "block text-sm font-medium text-gray-700 mb-2" > Tags</ label >
278+ < div className = "flex flex-wrap gap-2 mb-2" >
279+ { changelogData . tags && changelogData . tags . map ( ( tag , index ) => (
280+ < Badge key = { index } variant = "secondary" className = "flex items-center gap-1" >
281+ { tag }
282+ < X size = { 14 } onClick = { ( ) => handleRemoveTag ( tag ) } className = "cursor-pointer" />
283+ </ Badge >
284+ ) ) }
285+ </ div >
286+ < div className = "flex gap-2" >
287+ < Input
288+ type = "text"
289+ placeholder = "Add a tag"
290+ value = { newTag }
291+ onChange = { ( e ) => setNewTag ( e . target . value ) }
292+ onKeyPress = { handleKeyPress }
293+ />
294+ < Button onClick = { handleAddTag } > Add Tag</ Button >
295+ </ div >
195296 </ div >
196297 </ div >
197- < div className = "flex justify-end space-x-2" >
198- { isDraft ? (
199- < >
200- < Button variant = "outline" onClick = { ( ) => handleSave ( true ) } >
201- Save as Draft
202- </ Button >
203- < Button onClick = { ( ) => handleSave ( false ) } >
204- Publish
205- </ Button >
206- </ >
207- ) : (
208- < Button onClick = { ( ) => handleSave ( false ) } >
209- Publish Changes
210- </ Button >
211- ) }
212- < Button variant = "destructive" onClick = { handleDelete } >
213- Delete
214- </ Button >
298+
299+ { /* https://github.com/codex-team/editor.js/discussions/1910 */ }
300+ < div className = "prose" >
301+ < div id = "editorjs" className = "rounded-md w-full whateverNameYouWantHere editor-container" />
215302 </ div >
303+
216304 </ div >
217305 </ CardContent >
218306 </ Card >
0 commit comments