Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(#22): Setup markdown editor #40

Merged
merged 11 commits into from
Apr 18, 2021
1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"react-redux": "^7.2.0",
"react-router-dom": "^5.1.2",
"react-router-redux": "^4.0.8",
"react-rte": "^0.16.3",
"react-scripts": "^3.4.3",
"redux": "^4.0.5",
"redux-devtools-extension": "^2.13.8",
Expand Down
5 changes: 4 additions & 1 deletion client/src/components/PostItem/PostItem.component.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import {connect} from 'react-redux';
import PropTypes from 'prop-types';
import {Link} from 'react-router-dom';

import htmlSubstring from '../../services/htmlSubstring'
import injectEllipsis from '../../services/injectEllipsis'

import UserCard from '../UserCard/UserCard.component';
import TagBadge from '../TagBadge/TagBadge.component';

Expand Down Expand Up @@ -58,7 +61,7 @@ const PostItem = ({
<h3>
<Link to={`/questions/${id}`}>{title}</Link>
</h3>
<div className='brief'>{body.substring(0, 200)}...</div>
<div className='brief' dangerouslySetInnerHTML={{__html: injectEllipsis(htmlSubstring(body, 200))}}></div>
<TagBadge tag_name={tagname} size={'s-tag'} float={'left'} />
<UserCard
created_at={created_at}
Expand Down
73 changes: 73 additions & 0 deletions client/src/components/RichTextEditor/RichTextEditor.component.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React, {Component} from 'react';
import RichTextEditor from 'react-rte';
import './RichTextEditor.styles.scss';

class MyStatefulEditor extends Component {
state = {
value: RichTextEditor.createEmptyValue(),
};

cleanEditorState = () => {
this.setState({value: RichTextEditor.createEmptyValue()});
};

onChange = (value) => {
this.setState({value});
if (this.props.onChange) {
// Send the changes up to the parent component as an HTML string.
// This is here to demonstrate using `.toString()` but in a real app it
// would be better to avoid generating a string on each change.
this.props.onChange(value.toString('html'));
}
};

render() {
// The toolbarConfig object allows you to specify custom buttons, reorder buttons and to add custom css classes.
// Supported inline styles: https://github.com/facebook/draft-js/blob/master/docs/Advanced-Topics-Inline-Styles.md
// Supported block types: https://github.com/facebook/draft-js/blob/master/docs/Advanced-Topics-Custom-Block-Render.md#draft-default-block-render-map
const toolbarConfig = {
// Optionally specify the groups to display (displayed in the order listed).
display: [
'INLINE_STYLE_BUTTONS',
'BLOCK_TYPE_BUTTONS',
'LINK_BUTTONS',
// 'BLOCK_TYPE_DROPDOWN',
// 'HISTORY_BUTTONS',
],
INLINE_STYLE_BUTTONS: [
{label: 'Bold', style: 'BOLD', className: 'button-format'},
{label: 'Italic', style: 'ITALIC', className: 'button-format'},
{label: 'Underline', style: 'UNDERLINE', className: 'button-format'},
// {label: 'Monospace', style: 'CODE', className: 'button-format'},
],
// BLOCK_TYPE_DROPDOWN: [
// {label: 'Normal', style: 'unstyled'},
// {label: 'Heading Large', style: 'header-one'},
// {label: 'Heading Medium', style: 'header-two'},
// {label: 'Heading Small', style: 'header-three'},
// ],
BLOCK_TYPE_BUTTONS: [
{label: 'UL', style: 'unordered-list-item', className: 'button-format'},
{label: 'OL', style: 'ordered-list-item', className: 'button-format'},
{label: 'Blockquote', style: 'blockquote', className: 'button-format'},
{
label: 'Code Block',
style: 'code-block',
className: 'button-format code-block',
},
],
};
return (
<RichTextEditor
className='rich-text-editor-root'
toolbarClassName='rich-text-editor-toolbar'
editorClassName='rich-text-editor-editor'
toolbarConfig={toolbarConfig}
value={this.state.value}
onChange={this.onChange}
/>
);
}
}

export default MyStatefulEditor;
57 changes: 57 additions & 0 deletions client/src/components/RichTextEditor/RichTextEditor.styles.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
.rich-text-editor-root {
background-color: transparent;
border: none;
font-family: inherit;
font-size: inherit;

.rich-text-editor-toolbar {
border-bottom: 1px solid var(--black-200);
button {
width: 32px;
}
button.button-format {
background: var(--white);
span {
filter: invert(100%);
-webkit-filter: invert(100%);
}
}
button.button-format.code-block {
// code-block has not default icon in react-rte, so we gotta implement it on our own
span {
display: inline-block;
width: 22px;
height: 22px;
background-position: 50%;
background-repeat: no-repeat;
background-size: 18px;
background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pg0KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDE2LjAuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPg0KPCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iQ2FwYV8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCINCgkgd2lkdGg9Ijk0LjUwNHB4IiBoZWlnaHQ9Ijk0LjUwNHB4IiB2aWV3Qm94PSIwIDAgOTQuNTA0IDk0LjUwNCIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgOTQuNTA0IDk0LjUwNDsiDQoJIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPGc+DQoJPGc+DQoJCTxwYXRoIGQ9Ik05My45MTgsNDUuODMzTDY5Ljc5OSwyMS43MTRjLTAuNzUtMC43NS0yLjA3Ny0wLjc1LTIuODI3LDBsLTUuMjI5LDUuMjI5Yy0wLjc4MSwwLjc4MS0wLjc4MSwyLjA0NywwLDIuODI4DQoJCQlsMTcuNDc3LDE3LjQ3NUw2MS43NDQsNjQuNzI0Yy0wLjc4MSwwLjc4MS0wLjc4MSwyLjA0NywwLDIuODI4bDUuMjI5LDUuMjI5YzAuMzc1LDAuMzc1LDAuODg0LDAuNTg3LDEuNDE0LDAuNTg3DQoJCQljMC41MjksMCwxLjAzOS0wLjIxMiwxLjQxNC0wLjU4N2wyNC4xMTctMjQuMTE4Qzk0LjY5OSw0Ny44ODEsOTQuNjk5LDQ2LjYxNCw5My45MTgsNDUuODMzeiIvPg0KCQk8cGF0aCBkPSJNMzIuNzU5LDY0LjcyNEwxNS4yODUsNDcuMjQ4bDE3LjQ3Ny0xNy40NzVjMC4zNzUtMC4zNzUsMC41ODYtMC44ODMsMC41ODYtMS40MTRjMC0wLjUzLTAuMjEtMS4wMzktMC41ODYtMS40MTQNCgkJCWwtNS4yMjktNS4yMjljLTAuMzc1LTAuMzc1LTAuODg0LTAuNTg2LTEuNDE0LTAuNTg2Yy0wLjUzLDAtMS4wMzksMC4yMTEtMS40MTQsMC41ODZMMC41ODUsNDUuODMzDQoJCQljLTAuNzgxLDAuNzgxLTAuNzgxLDIuMDQ3LDAsMi44MjlMMjQuNzA0LDcyLjc4YzAuMzc1LDAuMzc1LDAuODg0LDAuNTg3LDEuNDE0LDAuNTg3YzAuNTMsMCwxLjAzOS0wLjIxMiwxLjQxNC0wLjU4N2w1LjIyOS01LjIyOQ0KCQkJQzMzLjU0Miw2Ni43NzEsMzMuNTQyLDY1LjUwNSwzMi43NTksNjQuNzI0eiIvPg0KCQk8cGF0aCBkPSJNNjAuOTY3LDEzLjZjLTAuMjU0LTAuNDY2LTAuNjgyLTAuODEyLTEuMTktMC45NjJsLTQuMjM5LTEuMjUxYy0xLjA1OC0wLjMxNC0yLjE3MiwwLjI5My0yLjQ4NCwxLjM1MkwzMy4zNzUsNzkuMzgyDQoJCQljLTAuMTUsMC41MDktMC4wOTIsMS4wNTYsMC4xNjEsMS41MjFjMC4yNTMsMC40NjcsMC42ODIsMC44MTIsMS4xOSwwLjk2M2w0LjIzOSwxLjI1MWMwLjE4OSwwLjA1NiwwLjM4LDAuMDgzLDAuNTY3LDAuMDgzDQoJCQljMC44NjMsMCwxLjY2LTAuNTY0LDEuOTE3LTEuNDM1bDE5LjY3OS02Ni42NDRDNjEuMjc4LDE0LjYxMiw2MS4yMjEsMTQuMDY1LDYwLjk2NywxMy42eiIvPg0KCTwvZz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjwvc3ZnPg0K');
}
}
button[title='Link'],
button[title='Remove Link'] {
span {
filter: invert(0%);
-webkit-filter: invert(0%);
}
}
button[title='Link']:disabled,
button[title='Remove Link']:disabled {
span {
filter: invert(100%);
-webkit-filter: invert(100%);
}
}
}

.rich-text-editor-editor {
height: 300px;
.DraftEditor-root {
pre {
padding: 5px;
background-color: var(--black-100);
color: var(--black-800);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React, {Fragment, useState} from 'react';
import React, {Fragment, useState, useRef} from 'react';
import {connect} from 'react-redux';
import PropTypes from 'prop-types';
import {addAnswer} from '../../../../redux/answers/answers.actions';

import LinkButton from '../../../../components/LinkButton/LinkButton.component';
import RichTextEditor from '../../../../components/RichTextEditor/RichTextEditor.component';

import './AnswerForm.styles.scss';

Expand All @@ -12,17 +13,21 @@ const AnswerForm = ({addAnswer, auth, postId}) => {
text: '',
});

const {text} = formData;
const richTextEditorRef = useRef(null);

const handleChange = (e) =>
setFormData({...formData, [e.target.name]: e.target.value});
const {text} = formData;

const handleSubmit = async (e) => {
e.preventDefault();
addAnswer(postId, {text});
setFormData({
text: '',
});
richTextEditorRef.current.cleanEditorState();
};

const updateConvertedContent = (htmlConvertedContent) => {
setFormData({...formData, text: htmlConvertedContent});
};

return (
Expand All @@ -32,7 +37,13 @@ const AnswerForm = ({addAnswer, auth, postId}) => {
<form className='answer-form' onSubmit={(e) => handleSubmit(e)}>
<div className='answer-grid'>
<label className=' fc-black-800'>Your Answer</label>
<textarea
<div className='s-textarea rich-text-editor-container'>
<RichTextEditor
ref={richTextEditorRef}
onChange={updateConvertedContent}
/>
</div>
{/* <textarea
className='s-textarea'
name='text'
cols='30'
Expand All @@ -41,7 +52,7 @@ const AnswerForm = ({addAnswer, auth, postId}) => {
onChange={(e) => handleChange(e)}
placeholder='Enter body with minimum 30 characters'
id='text'
/>
/> */}
<button className='s-btn s-btn__primary'>Post Your Answer</button>
</div>
</form>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,11 @@
width: 150px;
font-size: 14px;
}
}

.s-textarea.rich-text-editor-container {
padding: 0;
.rich-text-editor-toolbar {
padding: 0;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@ const AnswerItem = ({
</div>
</div>
<div className='answer-item'>
<div className='answer-content fc-black-800'>
<p>{body}</p>
<div className='answer-content fc-black-800' dangerouslySetInnerHTML={{__html: body}}>
</div>
<div className='answer-actions'>
<div className='action-btns'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const PostCell = ({
return (
<Fragment>
<div className='post-cell'>
<div className='post-text fc-black-800'>{post_body}</div>
<div className='post-text fc-black-800' dangerouslySetInnerHTML={{__html: post_body}}></div>
<div className='post-tags fc-black-800'>
<TagBadge tag_name={tagname} size={'s-tag'} float={'left'} />
</div>
Expand Down
20 changes: 17 additions & 3 deletions client/src/pages/PostForm/AskForm/AskForm.component.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React, {Fragment, useState} from 'react';
import React, {Fragment, useState, useRef} from 'react';
import {connect} from 'react-redux';
import PropTypes from 'prop-types';
import {addPost} from '../../../redux/posts/posts.actions';
import RichTextEditor from '../../../components/RichTextEditor/RichTextEditor.component';

import './AskForm.styles.scss';

Expand All @@ -12,6 +13,8 @@ const AskForm = ({addPost}) => {
tagname: '',
});

const richTextEditorRef = useRef(null);

const {title, body, tagname} = formData;

const onChange = (e) =>
Expand All @@ -25,6 +28,11 @@ const AskForm = ({addPost}) => {
body: '',
tagname: '',
});
richTextEditorRef.current.cleanEditorState();
};

const updateConvertedContent = (htmlConvertedContent) => {
setFormData({...formData, body: htmlConvertedContent});
};

return (
Expand Down Expand Up @@ -59,7 +67,13 @@ const AskForm = ({addPost}) => {
question
</p>
</label>
<textarea
<div className='s-textarea rich-text-editor-container'>
<RichTextEditor
ref={richTextEditorRef}
onChange={updateConvertedContent}
/>
</div>
{/* <textarea
className='s-textarea'
name='body'
cols='30'
Expand All @@ -69,7 +83,7 @@ const AskForm = ({addPost}) => {
placeholder='Enter body with minimum 30 characters'
id='body'
required
/>
/> */}
</div>
<div className='tag-grid'>
<label className='form-label s-label'>
Expand Down
19 changes: 11 additions & 8 deletions client/src/pages/PostForm/AskForm/AskForm.styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
margin: 0 2px 0 2px;
color: #fff;
background-color: #0095ff;
box-shadow: inset 0 1px 0 0 rgba(255,255,255,0.4);
padding: .8em;
box-shadow: inset 0 1px 0 0 rgba(255, 255, 255, 0.4);
padding: 0.8em;
border: 1px solid transparent;
border-radius: 3px;
outline: none;
Expand All @@ -37,7 +37,8 @@
}

.question-layout {
box-shadow: 0 10px 25px rgba(0,0,0,0.05), 0 20px 48px rgba(0,0,0,0.05), 0 1px 4px rgba(0,0,0,0.1);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.05),
0 20px 48px rgba(0, 0, 0, 0.05), 0 1px 4px rgba(0, 0, 0, 0.1);
padding: 16px !important;
background-color: #fff !important;
border-color: #d6d9dc !important;
Expand Down Expand Up @@ -78,7 +79,7 @@
-webkit-appearance: none;
width: 100%;
margin: 0;
padding: .6em .7em;
padding: 0.6em 0.7em;
border: 1px solid #bbc0c4;
border-radius: 3px;
background-color: #fff;
Expand Down Expand Up @@ -124,7 +125,7 @@
-webkit-appearance: none;
width: 100%;
margin: 0;
padding: .6em .7em;
padding: 0.6em 0.7em;
border: 1px solid #bbc0c4;
border-radius: 3px;
background-color: #fff;
Expand Down Expand Up @@ -168,14 +169,16 @@
clear: both;
}
}

.s-textarea.rich-text-editor-container {
padding: 0;
}
.body-input {
flex: 1 auto !important;
font-size: 13px;
-webkit-appearance: none;
width: 100%;
margin: 0;
padding: .6em .7em;
padding: 0.6em 0.7em;
border: 1px solid #bbc0c4;
border-radius: 3px;
background-color: #fff;
Expand All @@ -186,4 +189,4 @@
opacity: 0.6;
}
}
}
}
Loading