Skip to content

Commit b09a444

Browse files
committed
feat: refactor post using tiptap
1 parent 5777f72 commit b09a444

File tree

12 files changed

+1416
-214
lines changed

12 files changed

+1416
-214
lines changed

apps/web/@/actions/protect/postAction.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
"use server"
22

33
import { revalidatePath } from "next/cache"
4-
5-
import { PostOnUserType, PostStatus, TPostItem } from "database"
6-
import { updatePostStatus } from "database/src/posts/queries"
4+
import { redirect } from "next/navigation"
5+
6+
import {
7+
createPost,
8+
PostOnUserType,
9+
PostStatus,
10+
TCreatePostInput,
11+
TPostItem,
12+
updatePost,
13+
updatePostStatus,
14+
} from "database"
715
import { toast } from "react-toastify"
816

17+
import APP_ROUTES from "@/constants/routes"
918
import { TUserItem, userSelect } from "@/types/users"
1019
import { getServerSession } from "@/utils/auth"
1120

@@ -173,3 +182,28 @@ export const onTogglePost = async ({ post }: { post: TPostItem }) => {
173182
revalidatePath(`/post/${post.slug}`)
174183
}
175184
}
185+
186+
export const handleCreateUpdatePost = async ({
187+
postId,
188+
data,
189+
userId,
190+
}: {
191+
postId: string
192+
data: TCreatePostInput
193+
userId: string
194+
}) => {
195+
let newPostId = postId
196+
try {
197+
if (postId) {
198+
await updatePost(postId, data, userId)
199+
} else {
200+
const post = await createPost(data, userId)
201+
newPostId = post?.data?.slug
202+
}
203+
} catch (error) {
204+
toast.error(error)
205+
} finally {
206+
revalidatePath(APP_ROUTES.POST.replace(":postId", newPostId))
207+
redirect(APP_ROUTES.POST.replace(":postId", newPostId))
208+
}
209+
}

apps/web/@/messages/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
"save": "Save",
8787
"cancel": "Cancel",
8888
"create": "Create",
89+
"update": "Update",
8990
"save_as_draft": "Save as draft",
9091
"preview": "Preview",
9192
"no_items_founded": "There are no items founded.",
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
.tiptap {
2+
}
3+
4+
.ProseMirror:focus {
5+
outline: none;
6+
}
7+
8+
.ProseMirror p.is-editor-empty:first-child::before {
9+
color: #adb5bd;
10+
content: attr(data-placeholder);
11+
float: left;
12+
height: 0;
13+
pointer-events: none;
14+
}
15+
16+
.editor {
17+
background-color: #fff;
18+
color: #0d0d0d;
19+
display: flex;
20+
flex-direction: column;
21+
/* max-height: 26rem; */
22+
23+
&__header {
24+
align-items: center;
25+
background: #fff;
26+
border-top-left-radius: 0.25rem;
27+
border-top-right-radius: 0.25rem;
28+
display: flex;
29+
flex: 0 0 auto;
30+
flex-wrap: wrap;
31+
padding: 0.25rem;
32+
}
33+
34+
&__content {
35+
flex: 1 1 auto;
36+
overflow-x: hidden;
37+
overflow-y: auto;
38+
padding: 1.25rem 1rem;
39+
-webkit-overflow-scrolling: touch;
40+
}
41+
42+
&__footer {
43+
align-items: center;
44+
border-top: 3px solid #0d0d0d;
45+
color: #0d0d0d;
46+
display: flex;
47+
flex: 0 0 auto;
48+
font-size: 12px;
49+
flex-wrap: wrap;
50+
font-weight: 600;
51+
justify-content: space-between;
52+
padding: 0.25rem 0.75rem;
53+
white-space: nowrap;
54+
}
55+
}
56+
57+
/* Give a remote user a caret */
58+
.collaboration-cursor__caret {
59+
border-left: 1px solid #0d0d0d;
60+
border-right: 1px solid #0d0d0d;
61+
margin-left: -1px;
62+
margin-right: -1px;
63+
pointer-events: none;
64+
position: relative;
65+
word-break: normal;
66+
}
67+
68+
/* Render the username above the caret */
69+
.collaboration-cursor__label {
70+
border-radius: 3px 3px 3px 0;
71+
color: #0d0d0d;
72+
font-size: 12px;
73+
font-style: normal;
74+
font-weight: 600;
75+
left: -1px;
76+
line-height: normal;
77+
padding: 0.1rem 0.3rem;
78+
position: absolute;
79+
top: -1.4em;
80+
user-select: none;
81+
white-space: nowrap;
82+
}
83+
84+
.divider {
85+
background-color: #adb5bd;
86+
height: 32px;
87+
margin-left: 0.5rem;
88+
margin-right: 0.75rem;
89+
width: 1px;
90+
}
91+
92+
/* .menu-item {
93+
background-color: transparent;
94+
border-radius: 0.4rem;
95+
color: #adb5bd;
96+
cursor: pointer;
97+
height: 1.75rem;
98+
width: 1.75rem;
99+
margin-right: 0.25rem;
100+
padding: 0.25rem;
101+
display: flex;
102+
justify-content: center;
103+
align-items: center;
104+
105+
svg {
106+
fill: currentColor;
107+
height: 100%;
108+
width: 100%;
109+
}
110+
111+
&:hover,
112+
&.is-active {
113+
background-color: #616161;
114+
}
115+
} */
Lines changed: 145 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,161 @@
11
"use client"
22

3-
import React, { useEffect, useRef } from "react"
3+
import "./index.css"
44

5-
import EditorJS from "@editorjs/editorjs"
6-
import Header from "@editorjs/header"
7-
import List from "@editorjs/list"
5+
import React, { useCallback } from "react"
86

9-
interface EditorJSProps {
10-
onChange: (data: any) => void
11-
data?: any
7+
import Blockquote from "@tiptap/extension-blockquote"
8+
import Bold from "@tiptap/extension-bold"
9+
import BulletList from "@tiptap/extension-bullet-list"
10+
import CharacterCount from "@tiptap/extension-character-count"
11+
import CodeBlock from "@tiptap/extension-code-block"
12+
import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight"
13+
import Document from "@tiptap/extension-document"
14+
import Heading from "@tiptap/extension-heading"
15+
import Link from "@tiptap/extension-link"
16+
import ListItem from "@tiptap/extension-list-item"
17+
import OrderedList from "@tiptap/extension-ordered-list"
18+
import Paragraph from "@tiptap/extension-paragraph"
19+
import Placeholder from "@tiptap/extension-placeholder"
20+
import Text from "@tiptap/extension-text"
21+
import Underline from "@tiptap/extension-underline"
22+
import { EditorContent, mergeAttributes, useEditor } from "@tiptap/react"
23+
import StarterKit from "@tiptap/starter-kit"
24+
import { common, createLowlight } from "lowlight"
25+
26+
import MenuBar from "./menu-bar"
27+
28+
type EditorProps = {
29+
content?: string
30+
placeholder?: string
31+
name: string
32+
onChange: (content: string) => void
1233
}
1334

14-
const Editor: React.FC<EditorJSProps> = ({ onChange, data }) => {
15-
const editorRef = useRef<EditorJS | null>(null)
35+
const Editor = ({
36+
content = "",
37+
placeholder = "Write your story...",
38+
name,
39+
onChange,
40+
...props
41+
}: EditorProps) => {
42+
const MyHeading = Heading.extend({
43+
levels: [2, 3, 4],
44+
renderHTML({ node, HTMLAttributes }) {
45+
const level = this.options.levels.includes(node.attrs.level)
46+
? node.attrs.level
47+
: this.options.levels[0]
1648

17-
useEffect(() => {
18-
if (!editorRef.current) {
19-
editorRef.current = new EditorJS({
20-
holder: "editorjs",
21-
tools: {
22-
header: Header,
23-
list: List,
24-
},
25-
data: data,
26-
placeholder: "Write your post here...",
27-
onChange: async () => {
28-
const content = await editorRef.current?.save()
49+
const classes = {
50+
2: "text-3xl font-bold dark:text-white",
51+
3: "text-2xl font-bold",
52+
4: "text-xl font-bold",
53+
}
2954

30-
onChange(content)
55+
return [
56+
`h${level}`,
57+
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
58+
class: `${classes[level]}`,
59+
}),
60+
0,
61+
]
62+
},
63+
}).configure({
64+
levels: [2, 3, 4],
65+
})
66+
67+
const lowlight = createLowlight(common)
68+
69+
const editor = useEditor({
70+
extensions: [
71+
Bold,
72+
Document,
73+
Paragraph,
74+
Text,
75+
CodeBlock,
76+
MyHeading,
77+
CharacterCount.configure({
78+
limit: 10000,
79+
}),
80+
CodeBlockLowlight.configure({
81+
lowlight,
82+
}),
83+
Blockquote.configure({
84+
HTMLAttributes: {
85+
class: "mt-6 border-l-2 pl-6 italic",
86+
},
87+
}),
88+
Placeholder.configure({
89+
placeholder,
90+
}),
91+
StarterKit.configure({
92+
heading: false,
93+
listItem: false,
94+
bulletList: false,
95+
orderedList: false,
96+
blockquote: false,
97+
}),
98+
BulletList.configure({
99+
HTMLAttributes: {
100+
class: "list-disc ml-6",
31101
},
32-
})
102+
}),
103+
OrderedList.configure({
104+
HTMLAttributes: {
105+
class: "list-decimal ml-6",
106+
},
107+
}),
108+
Link.configure({
109+
openOnClick: false,
110+
HTMLAttributes: {
111+
class: "text-blue-500 underline",
112+
},
113+
}),
114+
ListItem,
115+
Underline,
116+
],
117+
content,
118+
onUpdate: ({ editor }) => {
119+
onChange(editor.getHTML())
120+
},
121+
})
122+
123+
const setLink = useCallback(() => {
124+
const previousUrl = editor.getAttributes("link").href
125+
const url = window.prompt("URL", previousUrl)
126+
127+
// cancelled
128+
if (url === null) {
129+
return
33130
}
34131

35-
return () => {
36-
if (editorRef.current && editorRef.current.destroy) {
37-
editorRef.current.destroy()
38-
}
132+
// empty
133+
if (url === "") {
134+
editor.chain().focus().extendMarkRange("link").unsetLink().run()
135+
136+
return
39137
}
40-
}, [])
41138

42-
return <div id="editorjs" />
139+
// update link
140+
editor.chain().focus().extendMarkRange("link").setLink({ href: url }).run()
141+
}, [editor])
142+
143+
return (
144+
<div className="editor h-full w-full">
145+
{editor && (
146+
<MenuBar
147+
editor={editor}
148+
setLink={setLink}
149+
/>
150+
)}
151+
<EditorContent
152+
{...props}
153+
className="h-full w-full bg-transparent"
154+
name={name}
155+
editor={editor}
156+
/>
157+
</div>
158+
)
43159
}
44160

45161
export default Editor

0 commit comments

Comments
 (0)