Skip to content

Commit

Permalink
Add Post comments (#128)
Browse files Browse the repository at this point in the history
* Add post page

* Add comment

* Remove dead code

* Fix texts

* Update text
  • Loading branch information
hoangvvo authored Sep 18, 2021
1 parent 4fb320a commit 300c164
Show file tree
Hide file tree
Showing 18 changed files with 360 additions and 40 deletions.
27 changes: 27 additions & 0 deletions api-lib/db/comment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export async function findComments(db, postId, from, limit) {
return db
.collection('comments')
.find({
postId,
...(from && {
createdAt: {
$lte: from,
},
}),
})
.sort({ $natural: -1 })
.limit(limit)
.toArray();
}

export async function insertComment(db, postId, { content, creatorId }) {
const comment = {
content,
postId,
creatorId,
createdAt: new Date(),
};
const { insertedId } = await db.collection('comments').insertOne(comment);
comment._id = insertedId;
return comment;
}
10 changes: 7 additions & 3 deletions api-lib/db/post.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { nanoid } from 'nanoid';

export async function getPosts(db, from = new Date(), by, limit) {
export async function findPostById(db, id) {
return db.collection('posts').findOne({ _id: id });
}

export async function findPosts(db, from, by, limit = 10) {
return db
.collection('posts')
.find({
Expand All @@ -12,8 +16,8 @@ export async function getPosts(db, from = new Date(), by, limit) {
}),
...(by && { creatorId: by }),
})
.sort({ createdAt: -1 })
.limit(limit || 10)
.sort({ $natural: -1 })
.limit(limit)
.toArray();
}

Expand Down
8 changes: 4 additions & 4 deletions api-lib/middlewares/all.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ncOpts } from "@/api-lib/nc";
import nc from "next-connect";
import auth from "./auth";
import database from "./database";
import { ncOpts } from '@/api-lib/nc';
import nc from 'next-connect';
import auth from './auth';
import database from './database';

const all = nc(ncOpts);

Expand Down
1 change: 0 additions & 1 deletion api-lib/middlewares/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export { default as all } from './all';
export { default as auth } from './auth';
export { default as database } from './database';

92 changes: 92 additions & 0 deletions components/comment/comments.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { useCommentPages } from '@/lib/comment';
import { defaultProfilePicture } from '@/lib/default';
import { useUser } from '@/lib/user';
import Link from 'next/link';

export function Comment({ comment }) {
const user = useUser(comment.creatorId);
return (
<>
<style jsx>
{`
.comment {
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.12);
padding: 1.5rem;
margin-bottom: 0.5rem;
transition: box-shadow 0.2s ease 0s;
display: block;
}
small {
color: #777;
}
`}
</style>

<div className="comment">
{user && (
<Link href={`/user/${user._id}`}>
<a style={{ display: 'inline-flex', alignItems: 'center' }}>
<img
width="27"
height="27"
style={{
borderRadius: '50%',
objectFit: 'cover',
marginRight: '0.3rem',
}}
src={user.profilePicture || defaultProfilePicture(user._id)}
alt={user.name}
/>
<b>{user.name}</b>
</a>
</Link>
)}
<p>{comment.content}</p>
<small>{new Date(comment.createdAt).toLocaleString()}</small>
</div>
</>
);
}

const PAGE_SIZE = 10;

export default function Comments({ postId }) {
const { data, error, size, setSize } = useCommentPages({
postId,
limit: PAGE_SIZE,
});
const comments = data
? data.reduce((acc, val) => [...acc, ...val.comments], [])
: [];
const isLoadingInitialData = !data && !error;
const isLoadingMore =
isLoadingInitialData ||
(size > 0 && data && typeof data[size - 1] === 'undefined');
const isEmpty = data?.[0]?.length === 0;
const isReachingEnd =
isEmpty || (data && data[data.length - 1]?.comments?.length < PAGE_SIZE);

return (
<div>
{comments.map((comment) => (
<Comment key={comment._id} comment={comment} />
))}
{!isEmpty && (
<button
style={{
background: 'transparent',
color: '#000',
}}
onClick={() => setSize(size + 1)}
disabled={isLoadingMore || isReachingEnd}
>
{isLoadingMore
? 'loading...'
: isReachingEnd
? 'no more comments'
: 'load more'}
</button>
)}
</div>
);
}
57 changes: 57 additions & 0 deletions components/comment/editor.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useCurrentUser } from '@/lib/user';
import Link from 'next/link';
import { useState } from 'react';

export default function CommentEditor({ postId }) {
const [user] = useCurrentUser();

const [msg, setMsg] = useState(null);

if (!user) {
return (
<div style={{ color: '#555', textAlign: 'center', marginBottom: 12 }}>
Please{' '}
<Link href="/login">
<a>sign in</a>
</Link>{' '}
to comment
</div>
);
}

async function hanldeSubmit(e) {
e.preventDefault();
const body = {
content: e.currentTarget.content.value,
};
if (!e.currentTarget.content.value) return;
e.currentTarget.content.value = '';
const res = await fetch(`/api/posts/${postId}/comments`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (res.ok) {
setMsg('Posted!');
setTimeout(() => setMsg(null), 5000);
}
}

return (
<>
<p style={{ color: '#0070f3', textAlign: 'center' }}>{msg}</p>
<form
onSubmit={hanldeSubmit}
style={{ flexDirection: 'row' }}
autoComplete="off"
>
<label htmlFor="name">
<input name="content" type="text" placeholder="Comment the post" />
</label>
<button type="submit" style={{ marginLeft: '0.5rem' }}>
Comment
</button>
</form>
</>
);
}
2 changes: 2 additions & 0 deletions components/comment/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as Comments } from './comments';
export { default as CommentEditor } from './editor';
9 changes: 7 additions & 2 deletions components/post/editor.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useCurrentUser } from '@/lib/user';
import Link from 'next/link';
import { useState } from 'react';

export default function PostEditor() {
Expand All @@ -8,8 +9,12 @@ export default function PostEditor() {

if (!user) {
return (
<div style={{ color: '#555', textAlign: 'center' }}>
Please sign in to post
<div style={{ color: '#555', textAlign: 'center', marginBottom: 12 }}>
Please{' '}
<Link href="/login">
<a>sign in</a>
</Link>{' '}
to post
</div>
);
}
Expand Down
2 changes: 2 additions & 0 deletions components/post/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as PostEditor } from './editor';
export { default as Posts, Post } from './posts';
20 changes: 13 additions & 7 deletions components/post/posts.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,26 @@ import { usePostPages } from '@/lib/post';
import { useUser } from '@/lib/user';
import Link from 'next/link';

function Post({ post }) {
export function Post({ post, hideLink }) {
const user = useUser(post.creatorId);
return (
<>
<style jsx>
{`
div {
.post {
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.12);
padding: 1.5rem;
margin-bottom: 0.5rem;
transition: box-shadow 0.2s ease 0s;
}
div:hover {
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
display: block;
}
small {
color: #777;
}
`}
</style>
<div>

<div className="post">
{user && (
<Link href={`/user/${user._id}`}>
<a style={{ display: 'inline-flex', alignItems: 'center' }}>
Expand All @@ -44,6 +43,13 @@ function Post({ post }) {
)}
<p>{post.content}</p>
<small>{new Date(post.createdAt).toLocaleString()}</small>
{!hideLink && (
<div>
<Link href={`/user/${post.creatorId}/post/${post._id}`}>
<a>View post</a>
</Link>
</div>
)}
</div>
</>
);
Expand Down Expand Up @@ -84,7 +90,7 @@ export default function Posts({ creatorId }) {
{isLoadingMore
? 'loading...'
: isReachingEnd
? 'no more issues'
? 'no more posts'
: 'load more'}
</button>
)}
Expand Down
34 changes: 34 additions & 0 deletions lib/comment/hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { fetcher } from '@/lib/fetch';
import useSWRInfinite from 'swr/infinite';

export function useCommentPages({ postId, limit = 10 } = {}) {
return useSWRInfinite(
(index, previousPageData) => {
// reached the end
if (previousPageData && previousPageData.comments.length === 0)
return null;

const searchParams = new URLSearchParams();
searchParams.set('limit', limit);

if (index !== 0) {
const from = new Date(
new Date(
previousPageData.comments[
previousPageData.comments.length - 1
].createdAt
).getTime() - 1
);

searchParams.set('from', from.toJSON());
}

return `/api/posts/${postId}/comments?${searchParams.toString()}`;
},
fetcher,
{
refreshInterval: 10000,
revalidateAll: false,
}
);
}
1 change: 1 addition & 0 deletions lib/comment/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './hooks';
52 changes: 52 additions & 0 deletions pages/api/posts/[postId]/comments/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { findPostById } from '@/api-lib/db';
import { findComments, insertComment } from '@/api-lib/db/comment';
import { all } from '@/api-lib/middlewares';
import { ncOpts } from '@/api-lib/nc';
import nc from 'next-connect';

const handler = nc(ncOpts);

handler.use(all);

handler.get(async (req, res) => {
const post = await findPostById(req.db, req.query.postId);

if (!post) {
return res.status(404).send('post not found');
}

const comments = await findComments(
req.db,
req.query.postId,
req.query.from ? new Date(req.query.from) : undefined,
req.query.limit ? parseInt(req.query.limit, 10) : undefined
);

return res.send({ comments });
});

handler.post(async (req, res) => {
if (!req.user) {
return res.status(401).send('unauthenticated');
}

const content = req.body.content;
if (!content) {
return res.status(400).send('You must write something');
}

const post = await findPostById(req.db, req.query.postId);

if (!post) {
return res.status(404).send('post not found');
}

const comment = await insertComment(req.db, post._id, {
creatorId: req.user._id,
content,
});

return res.json({ comment });
});

export default handler;
Loading

1 comment on commit 300c164

@vercel
Copy link

@vercel vercel bot commented on 300c164 Sep 18, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.