Skip to content

Commit b0746f1

Browse files
authored
Merge pull request #42 from imranhsayed/feature/post-comments
Add Post comments feature
2 parents c14c083 + 9e63804 commit b0746f1

File tree

16 files changed

+778
-6
lines changed

16 files changed

+778
-6
lines changed

.nvmrc

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
16.18.1

next.config.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ const allowedImageWordPressDomain = new URL( process.env.NEXT_PUBLIC_WORDPRESS_S
33

44
module.exports = {
55
trailingSlash: false,
6-
webpackDevMiddleware: config => {
6+
webpack: config => {
77
config.watchOptions = {
88
poll: 1000,
99
aggregateTimeout: 300
@@ -20,6 +20,6 @@ module.exports = {
2020
* @see https://nextjs.org/docs/basic-features/image-optimization#domains
2121
*/
2222
images: {
23-
domains: [ allowedImageWordPressDomain, 'via.placeholder.com' ],
23+
domains: [ allowedImageWordPressDomain, 'via.placeholder.com', 'secure.gravatar.com' ],
2424
},
2525
}

pages/blog/[slug].js

+7-3
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ import Layout from '../../src/components/layout';
1212
import { FALLBACK, handleRedirectsAndReturnData } from '../../src/utils/slug';
1313
import { getFormattedDate, sanitize } from '../../src/utils/miscellaneous';
1414
import { HEADER_FOOTER_ENDPOINT } from '../../src/utils/constants/endpoints';
15-
import { getPost, getPosts } from '../../src/utils/blog';
15+
import { getComments, getPost, getPosts } from '../../src/utils/blog';
1616
import Image from '../../src/components/image';
1717
import PostMeta from '../../src/components/post-meta';
18+
import Comments from '../../src/components/comments';
1819

19-
const Post = ( { headerFooter, postData } ) => {
20+
const Post = ( { headerFooter, postData, commentsData } ) => {
2021
const router = useRouter();
2122

2223
/**
@@ -43,6 +44,7 @@ const Post = ( { headerFooter, postData } ) => {
4344
<PostMeta date={ getFormattedDate( postData?.date ?? '' ) } authorName={ postData?._embedded?.author?.[0]?.name ?? '' }/>
4445
<h1 dangerouslySetInnerHTML={ { __html: sanitize( postData?.title?.rendered ?? '' ) } }/>
4546
<div dangerouslySetInnerHTML={ { __html: sanitize( postData?.content?.rendered ?? '' ) } }/>
47+
<Comments comments={ commentsData } postId={ postData?.id ?? '' }/>
4648
</div>
4749
</Layout>
4850
);
@@ -53,11 +55,13 @@ export default Post;
5355
export async function getStaticProps( { params } ) {
5456
const { data: headerFooterData } = await axios.get( HEADER_FOOTER_ENDPOINT );
5557
const postData = await getPost( params?.slug ?? '' );
58+
const commentsData = await getComments( postData?.[0]?.id ?? 0 );
5659

5760
const defaultProps = {
5861
props: {
5962
headerFooter: headerFooterData?.data ?? {},
60-
postData: postData?.[0] ?? {}
63+
postData: postData?.[0] ?? {},
64+
commentsData: commentsData || []
6165
},
6266
/**
6367
* Revalidate means that if a new request comes to server, then every 1 sec it will check
+233
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
/**
2+
* External Dependencies.
3+
*/
4+
import { useEffect, useState } from 'react';
5+
import cx from 'classnames';
6+
7+
/**
8+
* Internal Dependencies.
9+
*/
10+
import validateAndSanitizeCommentsForm from '../../validator/comments';
11+
import TextArea from '../form-elements/text-area';
12+
import Input from '../form-elements/input';
13+
import { postComment } from '../../utils/blog';
14+
import { sanitize } from '../../utils/miscellaneous';
15+
import Loading from '../loading';
16+
17+
const CommentForm = ( { postId, replyCommentID } ) => {
18+
19+
/**
20+
* Initialize Input State.
21+
*
22+
* @type {{date: Date, postId: number, wp_comment_cookies_consent: boolean}}
23+
*/
24+
const initialInputState = {
25+
postId: postId || 0,
26+
date: new Date(),
27+
parent: replyCommentID || 0,
28+
}
29+
30+
const [ input, setInput ] = useState( initialInputState );
31+
const [ commentPostSuccess, setCommentPostSuccess ] = useState( false );
32+
const [ commentPostError, setCommentPostError ] = useState( '' );
33+
const [ clearFormValues, setClearFormValues ] = useState( false );
34+
const [ loading, setLoading ] = useState( false );
35+
const submitBtnClasses = cx(
36+
'text-white hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-16px uppercase w-full sm:w-auto px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800',
37+
{
38+
'cursor-pointer bg-blue-700': ! loading,
39+
'bg-blue-400 dark:bg-blue-500 cursor-not-allowed': loading,
40+
},
41+
);
42+
43+
/**
44+
* When the reply Comment id gets updated
45+
* then update the input.
46+
*/
47+
useEffect( () => {
48+
setInput( {
49+
...input,
50+
parent: replyCommentID || 0,
51+
} );
52+
}, [ replyCommentID ] );
53+
54+
/**
55+
* If 'clearFormValues' becomes true,
56+
* reset the input value to initialInputState
57+
*/
58+
useEffect( () => {
59+
if ( clearFormValues ) {
60+
setInput( initialInputState );
61+
}
62+
}, [ clearFormValues ] );
63+
64+
/**
65+
* If 'commentPostSuccess' is set to true, set to false after
66+
* few seconds so the message disappears.
67+
*/
68+
useEffect( () => {
69+
if ( commentPostSuccess ) {
70+
const intervalId = setTimeout( () => {
71+
setCommentPostSuccess( false )
72+
}, 10000 )
73+
74+
// Unsubscribe from the interval.
75+
return () => {
76+
clearInterval( intervalId );
77+
};
78+
}
79+
}, [ commentPostSuccess ] )
80+
81+
/**
82+
* Handle form submit.
83+
*
84+
* @param {Event} event Event.
85+
*
86+
* @return {null}
87+
*/
88+
const handleFormSubmit = ( event ) => {
89+
event.preventDefault();
90+
91+
const commentFormValidationResult = validateAndSanitizeCommentsForm( input );
92+
93+
setInput( {
94+
...input,
95+
errors: commentFormValidationResult.errors,
96+
} );
97+
98+
// If there are any errors, return.
99+
if ( ! commentFormValidationResult.isValid ) {
100+
return null;
101+
}
102+
103+
// Set loading to true.
104+
setLoading( true );
105+
106+
// Make a POST request to post comment.
107+
const response = postComment( 377, input );
108+
109+
/**
110+
* The postComment() returns a promise,
111+
* When the promise gets resolved, i.e. request is complete,
112+
* then handle the success or error messages.
113+
*/
114+
response.then(( res ) => {
115+
setLoading( false );
116+
if ( res.success ) {
117+
setCommentPostSuccess( true );
118+
setClearFormValues( true );
119+
} else {
120+
setCommentPostError( res.error ?? 'Something went wrong. Please try again' );
121+
}
122+
})
123+
}
124+
125+
/*
126+
* Handle onchange input.
127+
*
128+
* @param {Object} event Event Object.
129+
*
130+
* @return {void}
131+
*/
132+
const handleOnChange = ( event ) => {
133+
134+
// Reset the comment post success and error messages, first.
135+
if ( commentPostSuccess ) {
136+
setCommentPostSuccess( false );
137+
}
138+
if ( commentPostError ) {
139+
setCommentPostError( '' );
140+
}
141+
142+
const { target } = event || {};
143+
const newState = { ...input, [ target.name ]: target.value };
144+
setInput( newState );
145+
};
146+
147+
return (
148+
<form action="/" noValidate onSubmit={ handleFormSubmit } id="comment-form">
149+
<h2>Leave a comment</h2>
150+
<p className="comment-notes">
151+
<span id="email-notes">Your email address will not be published.</span>
152+
<span className="required-field-message">Required fields are marked <span className="required">*</span></span>
153+
</p>
154+
<TextArea
155+
id="comment"
156+
containerClassNames="comment-form-comment mb-2"
157+
name="comment"
158+
label="Comment"
159+
cols="45"
160+
rows="5"
161+
required
162+
textAreaValue={ input?.comment ?? '' }
163+
handleOnChange={ handleOnChange }
164+
errors={ input?.errors ?? {} }
165+
/>
166+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-2">
167+
<Input
168+
name="author"
169+
inputValue={ input?.author }
170+
required
171+
handleOnChange={ handleOnChange }
172+
label="Name"
173+
errors={ input?.errors ?? {} }
174+
containerClassNames="comment-form-author"
175+
/>
176+
<Input
177+
name="email"
178+
inputValue={ input?.email }
179+
required
180+
handleOnChange={ handleOnChange }
181+
label="Email"
182+
errors={ input?.errors ?? {} }
183+
containerClassNames="comment-form-email mb-2"
184+
/>
185+
</div>
186+
<Input
187+
name="url"
188+
inputValue={ input?.url ?? '' }
189+
handleOnChange={ handleOnChange }
190+
label="Website"
191+
errors={ input?.errors ?? {} }
192+
containerClassNames="comment-form-url mb-2"
193+
/>
194+
<div className="form-submit py-4">
195+
<input
196+
name="submit"
197+
type="submit"
198+
id="submit"
199+
className={ submitBtnClasses }
200+
value="Post Comment"
201+
disabled={ loading }
202+
/>
203+
</div>
204+
{
205+
commentPostSuccess && ! loading ?
206+
(
207+
<div
208+
className="p-4 mb-4 text-sm text-green-800 rounded-lg bg-green-50 dark:bg-gray-800 dark:text-green-400"
209+
role="alert">
210+
<span className="font-medium">Success!</span> Your comment has been submitted for approval.
211+
It will be posted after admin's approval.
212+
</div>
213+
) : null
214+
}
215+
{
216+
commentPostError && ! loading ?
217+
(
218+
<div
219+
className="p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-gray-800 dark:text-red-400"
220+
role="alert">
221+
<span className="font-medium">Error! </span>
222+
<div className="inline-block" dangerouslySetInnerHTML={ { __html: sanitize( commentPostError ) } } />
223+
</div>
224+
) : null
225+
}
226+
{
227+
loading ? <Loading text="Processing..."/>: null
228+
}
229+
</form>
230+
)
231+
}
232+
233+
export default CommentForm;

src/components/comments/comment.js

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { isEmpty } from 'lodash';
2+
import { getFormattedDate, sanitize } from '../../utils/miscellaneous';
3+
import Image from '../image';
4+
5+
const Comment = ( { comment, handleReplyButtonClick } ) => {
6+
7+
if ( isEmpty( comment ) ) {
8+
return null;
9+
}
10+
11+
return (
12+
<article className="p-6 mb-6 text-base bg-white border-t border-gray-200 dark:border-gray-700 dark:bg-gray-900">
13+
<footer className="flex justify-between items-center mb-4">
14+
<div className="flex items-center">
15+
<div className="inline-flex items-center mr-3 text-sm text-gray-900 dark:text-white">
16+
<Image
17+
sourceUrl={ comment?.author_avatar_urls?.['48'] ?? '' }
18+
title={ comment?.author_name ?? '' }
19+
width="24"
20+
height="24"
21+
layout="fill"
22+
containerClassNames="mr-2 w-9 h-9"
23+
style={{borderRadius: '50%', overflow: 'hidden'}}
24+
/>
25+
{ comment?.author_name ?? '' }
26+
</div>
27+
<div className="text-sm text-gray-600 dark:text-gray-400">
28+
<time dateTime={ comment?.date ?? '' } title="March 12th, 2022">{ getFormattedDate( comment?.date ?? '' ) }</time>
29+
</div>
30+
</div>
31+
</footer>
32+
<div
33+
className="text-gray-500 dark:text-gray-400"
34+
dangerouslySetInnerHTML={ { __html: sanitize( comment?.content?.rendered ?? '' ) } }
35+
/>
36+
<div className="flex items-center mt-4 space-x-4">
37+
<button
38+
type="button"
39+
className="flex items-center text-sm text-gray-500 hover:underline dark:text-gray-400"
40+
onClick={ ( event ) => handleReplyButtonClick( event, comment.id ) }
41+
>
42+
<svg aria-hidden="true" className="mr-1 w-4 h-4" fill="none" stroke="currentColor"
43+
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
44+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
45+
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z">
46+
</path>
47+
</svg>
48+
Reply
49+
</button>
50+
</div>
51+
</article>
52+
)
53+
}
54+
55+
export default Comment;

src/components/comments/index.js

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { isArray, isEmpty } from 'lodash';
2+
import Comment from './comment';
3+
import CommentForm from './comment-form';
4+
import { useRef, useState } from 'react';
5+
import { smoothScroll } from '../../utils/miscellaneous';
6+
7+
const Comments = ( { comments, postId } ) => {
8+
9+
if ( isEmpty( comments ) || ! isArray( comments ) ) {
10+
return null;
11+
}
12+
13+
/**
14+
* Initialize.
15+
*/
16+
const commentFormEl = useRef( null );
17+
const [ replyCommentID, setReplyCommentID ] = useState( 0 );
18+
19+
/**
20+
* Handle Reply Button Click.
21+
*
22+
* @param {Event} event Event.
23+
* @param {number} commentId Comment Id.
24+
*/
25+
const handleReplyButtonClick = ( event, commentId ) => {
26+
setReplyCommentID( commentId );
27+
smoothScroll( commentFormEl.current, 20 );
28+
}
29+
30+
return (
31+
<div className="mt-20">
32+
<h2>{ comments.length } Comments</h2>
33+
{
34+
comments.map( ( comment, index ) => {
35+
return (
36+
<div
37+
key={ `${ comment?.id ?? '' }-${ index }` ?? '' }
38+
className="comment"
39+
>
40+
<Comment comment={ comment } handleReplyButtonClick={ handleReplyButtonClick }/>
41+
</div>
42+
);
43+
} )
44+
}
45+
<div ref={ commentFormEl }>
46+
<CommentForm postId={ postId } replyCommentID={ replyCommentID } />
47+
</div>
48+
</div>
49+
)
50+
}
51+
52+
export default Comments;

0 commit comments

Comments
 (0)