Skip to content

Commit 96b30d4

Browse files
Merge pull request #12 from firstcontributions/post-editor
add editor component for story
2 parents 99466ab + 50e804e commit 96b30d4

File tree

15 files changed

+850
-83
lines changed

15 files changed

+850
-83
lines changed

package-lock.json

Lines changed: 311 additions & 29 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,15 @@
1313
"prepare": "husky install"
1414
},
1515
"dependencies": {
16+
"@editorjs/code": "^2.7.0",
17+
"@editorjs/editorjs": "^2.24.3",
18+
"@editorjs/header": "^2.6.2",
19+
"@editorjs/image": "^2.6.2",
20+
"@editorjs/list": "^1.7.0",
21+
"@editorjs/paragraph": "^2.8.0",
22+
"@editorjs/table": "^2.0.2",
1623
"@react-icons/all-files": "^4.1.0",
24+
"md5": "^2.3.0",
1725
"next": "12.0.10",
1826
"node-fetch": "^3.2.0",
1927
"react": "17.0.2",
@@ -24,6 +32,7 @@
2432
"simple-oauth2": "^4.3.0"
2533
},
2634
"devDependencies": {
35+
"@tailwindcss/typography": "^0.5.2",
2736
"@types/node": "17.0.15",
2837
"@types/react": "17.0.39",
2938
"@types/react-relay": "^13.0.1",

schema/schema.graphql

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ type Query {
1212
# The ID of an object
1313
id: ID!
1414
): Node
15+
feeds(first: Int, last: Int, after: String, before: String): StoriesConnection!
1516
}
1617

1718
interface Node {
@@ -45,6 +46,35 @@ type BadgeEdge {
4546
cursor: String!
4647
}
4748

49+
type Comment implements Node {
50+
abstractContent: String!
51+
contentJson: String!
52+
createdBy: User!
53+
id: ID!
54+
reactions(first: Int, last: Int, after: String, before: String): ReactionsConnection!
55+
timeCreated: Time!
56+
timeUpdated: Time!
57+
}
58+
59+
input CommentInput {
60+
abstractContent: String!
61+
contentJson: String!
62+
storyID: ID!
63+
}
64+
input UpdateCommentInput {
65+
id: ID!
66+
}
67+
68+
type CommentsConnection {
69+
edges: [CommentEdge]!
70+
pageInfo: PageInfo!
71+
}
72+
73+
type CommentEdge {
74+
node: Comment!
75+
cursor: String!
76+
}
77+
4878
type GitContributionStats {
4979
issues: Int!
5080
pullRequests: Int!
@@ -78,6 +108,31 @@ type IssueEdge {
78108
cursor: String!
79109
}
80110

111+
type Reaction implements Node {
112+
createdBy: User!
113+
id: ID!
114+
timeCreated: Time!
115+
timeUpdated: Time!
116+
}
117+
118+
input ReactionInput {
119+
commentID: ID!
120+
storyID: ID!
121+
}
122+
input UpdateReactionInput {
123+
id: ID!
124+
}
125+
126+
type ReactionsConnection {
127+
edges: [ReactionEdge]!
128+
pageInfo: PageInfo!
129+
}
130+
131+
type ReactionEdge {
132+
node: Reaction!
133+
cursor: String!
134+
}
135+
81136
type Reputation {
82137
value: Float!
83138
}
@@ -86,6 +141,41 @@ input ReputationInput {
86141
value: Float!
87142
}
88143

144+
type Story implements Node {
145+
abstractContent: String!
146+
comments(first: Int, last: Int, after: String, before: String): CommentsConnection!
147+
contentJson: String!
148+
createdBy: User!
149+
id: ID!
150+
reactions(first: Int, last: Int, after: String, before: String): ReactionsConnection!
151+
thumbnail: String!
152+
timeCreated: Time!
153+
timeUpdated: Time!
154+
title: String!
155+
urlSuffix: String!
156+
}
157+
158+
input StoryInput {
159+
abstractContent: String!
160+
contentJson: String!
161+
thumbnail: String!
162+
title: String!
163+
urlSuffix: String!
164+
}
165+
input UpdateStoryInput {
166+
id: ID!
167+
}
168+
169+
type StoriesConnection {
170+
edges: [StoryEdge]!
171+
pageInfo: PageInfo!
172+
}
173+
174+
type StoryEdge {
175+
node: Story!
176+
cursor: String!
177+
}
178+
89179
type User implements Node {
90180
avatar: String!
91181
badges(first: Int, last: Int, after: String, before: String): BadgesConnection!
@@ -98,6 +188,7 @@ type User implements Node {
98188
name: String!
99189
relevantIssues(first: Int, last: Int, after: String, before: String): IssuesConnection!
100190
reputation: Reputation!
191+
stories(first: Int, last: Int, after: String, before: String): StoriesConnection!
101192
timeCreated: Time!
102193
timeUpdated: Time!
103194
}
@@ -112,13 +203,20 @@ input UserInput {
112203
}
113204
input UpdateUserInput {
114205
id: ID!
206+
avatar: String
115207
bio: String
116208
gitContributionStats: GitContributionStatsInput
117209
name: String
118210
reputation: ReputationInput
119211
}
120212

121213
type Mutation {
214+
createComment(comment:CommentInput!): Comment!
215+
updateComment(comment: UpdateCommentInput!): Comment!
216+
createReaction(reaction:ReactionInput!): Reaction!
217+
updateReaction(reaction: UpdateReactionInput!): Reaction!
218+
createStory(story:StoryInput!): Story!
219+
updateStory(story: UpdateStoryInput!): Story!
122220
createUser(user:UserInput!): User!
123221
updateUser(user: UpdateUserInput!): User!
124222
}

src/components/Button.tsx

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
1-
import { ReactNode } from "react"
1+
import { ReactNode } from 'react'
22

33
type ButtonProps = {
4-
children: ReactNode
5-
handleClick?: () => void
4+
children: ReactNode
5+
onClick?: () => void
66
}
77

8-
const Button = ({children, handleClick}: ButtonProps) => {
9-
return(
10-
<button
11-
type="button"
12-
className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center mr-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
13-
onClick={handleClick}
14-
>
15-
{children}
16-
</button>
17-
)
8+
const Button = ({ children, onClick: onClick }: ButtonProps) => {
9+
return (
10+
<button
11+
type="button"
12+
className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center mr-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
13+
onClick={onClick}
14+
>
15+
{children}
16+
</button>
17+
)
1818
}
1919

20-
export default Button
20+
export default Button

src/components/Layout.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
import React, { ReactNode } from "react";
2+
import Navbar from "./navbar/Navbar";
23

34
type LayoutProps = {
4-
sidebarContentLeft: ReactNode
5+
sidebarContentLeft?: ReactNode
56
children: ReactNode
67
sidebarContentRight: ReactNode
78
}
89

910
export default function Layout ({sidebarContentLeft, children, sidebarContentRight}: LayoutProps) {
1011
return (
11-
<div className="mx-auto bg-gray-100 p-4 px-20">
12-
<div className="grid grid-cols-9 gap-10">
12+
<div className="mx-auto bg-gray-100 min-h-screen">
13+
<Navbar />
14+
<div className="grid grid-cols-9 gap-10 mt-10 px-20 pt-10">
15+
{sidebarContentLeft && (
1316
<aside className="col-span-2">
1417
{sidebarContentLeft}
15-
</aside>
16-
<main className="col-span-5">
18+
</aside>)}
19+
<main className={ sidebarContentLeft ? "col-span-5": "col-span-7"}>
1720
{children}
1821
</main>
1922
<aside className="col-span-2">

src/components/navbar/Navbar.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export default function Navbar () {
2+
return( <div className="flex fixed p-4 w-full bg-blue-400 top-0 z-10">
3+
Logo
4+
<div>bu</div>
5+
</div>)
6+
}

src/components/story/StoryEditor.tsx

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import EditorJS, { LogLevels, OutputData } from '@editorjs/editorjs'
2+
import { Http2ServerRequest } from 'http2'
3+
import React, { useEffect, useRef, useState } from 'react'
4+
import { graphql, useMutation } from 'react-relay'
5+
import Button from '../Button'
6+
import Card from '../Card'
7+
import { EDITOR_JS_TOOLS } from './editorTools'
8+
9+
const DEFAULT_INITIAL_DATA = (): OutputData => {
10+
return {
11+
time: new Date().getTime(),
12+
blocks: [
13+
{
14+
type: 'header',
15+
data: {
16+
text: 'This is my awesome editor!',
17+
level: 1,
18+
},
19+
},
20+
],
21+
}
22+
}
23+
24+
const getAbstract = (content: OutputData) => {
25+
let abstract = ''
26+
for (let i = 0; i < content.blocks.length; i++) {
27+
if (
28+
content.blocks[i].type == 'header' ||
29+
content.blocks[i].type == 'paragraph'
30+
) {
31+
abstract += content.blocks[i].data.text
32+
}
33+
if (abstract.length >= 200) break
34+
}
35+
return abstract
36+
}
37+
38+
const EDITTOR_HOLDER_ID = 'editorjs'
39+
40+
type EditorProps = {
41+
title?: string
42+
body?: string
43+
}
44+
45+
export default function Editor({ title, body }: EditorProps) {
46+
const [postTitle, setPostTitle] = useState(title || 'Your Title Goes Here')
47+
const ejInstance = useRef<EditorJS | null>()
48+
49+
const [editorData, setEditorData] = React.useState(
50+
(body && JSON.parse(decodeURI(body))) || DEFAULT_INITIAL_DATA
51+
)
52+
useEffect(() => {
53+
if (!ejInstance.current) {
54+
initEditor()
55+
}
56+
return () => {
57+
ejInstance?.current?.destroy()
58+
ejInstance.current = null
59+
}
60+
}, [])
61+
62+
const initEditor = () => {
63+
const editor = new EditorJS({
64+
holder: EDITTOR_HOLDER_ID,
65+
data: editorData,
66+
onReady: () => {
67+
ejInstance.current = editor
68+
},
69+
onChange: async () => {
70+
const content = await editor.saver.save()
71+
setEditorData(content)
72+
},
73+
autofocus: true,
74+
tools: EDITOR_JS_TOOLS,
75+
inlineToolbar: true,
76+
})
77+
}
78+
const [commitMutation, isMutationInFlight] = useMutation(
79+
graphql`
80+
mutation StoryEditorCreateMutation($input: StoryInput!) {
81+
createStory(story: $input) {
82+
id
83+
}
84+
}
85+
`
86+
)
87+
const handleStorySubmit = () => {
88+
commitMutation({
89+
variables: {
90+
input: {
91+
title: postTitle,
92+
contentJson: JSON.stringify(editorData),
93+
abstractContent: getAbstract(editorData),
94+
urlSuffix: postTitle.toLowerCase().replace(' ', '-').substring(0, 32),
95+
thumbnail: '',
96+
},
97+
},
98+
})
99+
}
100+
return (
101+
<Card classes="prose max-w-none">
102+
<div>
103+
<div id={EDITTOR_HOLDER_ID}> </div>
104+
</div>
105+
<style jsx>
106+
{`
107+
[contenteditable]:focus {
108+
outline: 0px solid transparent;
109+
border-bottom: 1px dashed #aaa;
110+
}
111+
`}
112+
</style>
113+
<Button onClick={() => handleStorySubmit()}>Submit </Button>
114+
</Card>
115+
)
116+
}

src/components/story/editorTools.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import Table from '@editorjs/table'
2+
import List from '@editorjs/list'
3+
import Code from '@editorjs/code'
4+
import Image from '@editorjs/image'
5+
import Header from '@editorjs/header'
6+
7+
export const EDITOR_JS_TOOLS = {
8+
table: Table,
9+
list: List,
10+
code: Code,
11+
image: Image,
12+
header: Header,
13+
}

src/index.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
declare module '@editorjs/table'
2+
declare module '@editorjs/list'
3+
declare module '@editorjs/code'
4+
declare module '@editorjs/image'
5+
declare module '@editorjs/header'

0 commit comments

Comments
 (0)