From 300c1648f9811be2766cde066e69f1da8782f13f Mon Sep 17 00:00:00 2001 From: Hoang Date: Sun, 19 Sep 2021 02:24:48 +0700 Subject: [PATCH] Add Post comments (#128) * Add post page * Add comment * Remove dead code * Fix texts * Update text --- api-lib/db/comment.js | 27 +++++++ api-lib/db/post.js | 10 ++- api-lib/middlewares/all.js | 8 +- api-lib/middlewares/index.js | 1 - components/comment/comments.jsx | 92 ++++++++++++++++++++++ components/comment/editor.jsx | 57 ++++++++++++++ components/comment/index.js | 2 + components/post/editor.jsx | 9 ++- components/post/index.js | 2 + components/post/posts.jsx | 20 +++-- lib/comment/hooks.js | 34 ++++++++ lib/comment/index.js | 1 + pages/api/posts/[postId]/comments/index.js | 52 ++++++++++++ pages/api/posts/index.js | 18 ++--- pages/api/users/[userId]/index.js | 3 - pages/index.jsx | 3 +- pages/user/[userId]/index.jsx | 15 ++-- pages/user/[userId]/post/[postId].jsx | 46 +++++++++++ 18 files changed, 360 insertions(+), 40 deletions(-) create mode 100644 api-lib/db/comment.js create mode 100644 components/comment/comments.jsx create mode 100644 components/comment/editor.jsx create mode 100644 components/comment/index.js create mode 100644 components/post/index.js create mode 100644 lib/comment/hooks.js create mode 100644 lib/comment/index.js create mode 100644 pages/api/posts/[postId]/comments/index.js create mode 100644 pages/user/[userId]/post/[postId].jsx diff --git a/api-lib/db/comment.js b/api-lib/db/comment.js new file mode 100644 index 0000000..3edf3e3 --- /dev/null +++ b/api-lib/db/comment.js @@ -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; +} diff --git a/api-lib/db/post.js b/api-lib/db/post.js index 2ab94e0..f2912e1 100644 --- a/api-lib/db/post.js +++ b/api-lib/db/post.js @@ -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({ @@ -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(); } diff --git a/api-lib/middlewares/all.js b/api-lib/middlewares/all.js index 53d266a..c541308 100644 --- a/api-lib/middlewares/all.js +++ b/api-lib/middlewares/all.js @@ -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); diff --git a/api-lib/middlewares/index.js b/api-lib/middlewares/index.js index 6512744..5bb1954 100644 --- a/api-lib/middlewares/index.js +++ b/api-lib/middlewares/index.js @@ -1,4 +1,3 @@ export { default as all } from './all'; export { default as auth } from './auth'; export { default as database } from './database'; - diff --git a/components/comment/comments.jsx b/components/comment/comments.jsx new file mode 100644 index 0000000..c220085 --- /dev/null +++ b/components/comment/comments.jsx @@ -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 ( + <> + + +
+ {user && ( + + + {user.name} + {user.name} + + + )} +

{comment.content}

+ {new Date(comment.createdAt).toLocaleString()} +
+ + ); +} + +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 ( +
+ {comments.map((comment) => ( + + ))} + {!isEmpty && ( + + )} +
+ ); +} diff --git a/components/comment/editor.jsx b/components/comment/editor.jsx new file mode 100644 index 0000000..ca5d931 --- /dev/null +++ b/components/comment/editor.jsx @@ -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 ( +
+ Please{' '} + + sign in + {' '} + to comment +
+ ); + } + + 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 ( + <> +

{msg}

+
+ + +
+ + ); +} diff --git a/components/comment/index.js b/components/comment/index.js new file mode 100644 index 0000000..7871915 --- /dev/null +++ b/components/comment/index.js @@ -0,0 +1,2 @@ +export { default as Comments } from './comments'; +export { default as CommentEditor } from './editor'; diff --git a/components/post/editor.jsx b/components/post/editor.jsx index bcd2395..833e82e 100644 --- a/components/post/editor.jsx +++ b/components/post/editor.jsx @@ -1,4 +1,5 @@ import { useCurrentUser } from '@/lib/user'; +import Link from 'next/link'; import { useState } from 'react'; export default function PostEditor() { @@ -8,8 +9,12 @@ export default function PostEditor() { if (!user) { return ( -
- Please sign in to post +
+ Please{' '} + + sign in + {' '} + to post
); } diff --git a/components/post/index.js b/components/post/index.js new file mode 100644 index 0000000..bb32d9b --- /dev/null +++ b/components/post/index.js @@ -0,0 +1,2 @@ +export { default as PostEditor } from './editor'; +export { default as Posts, Post } from './posts'; diff --git a/components/post/posts.jsx b/components/post/posts.jsx index 72a49e2..a0d90d5 100644 --- a/components/post/posts.jsx +++ b/components/post/posts.jsx @@ -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 ( <> -
+ +
{user && ( @@ -44,6 +43,13 @@ function Post({ post }) { )}

{post.content}

{new Date(post.createdAt).toLocaleString()} + {!hideLink && ( +
+ + View post + +
+ )}
); @@ -84,7 +90,7 @@ export default function Posts({ creatorId }) { {isLoadingMore ? 'loading...' : isReachingEnd - ? 'no more issues' + ? 'no more posts' : 'load more'} )} diff --git a/lib/comment/hooks.js b/lib/comment/hooks.js new file mode 100644 index 0000000..5bc0294 --- /dev/null +++ b/lib/comment/hooks.js @@ -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, + } + ); +} diff --git a/lib/comment/index.js b/lib/comment/index.js new file mode 100644 index 0000000..4cc90d0 --- /dev/null +++ b/lib/comment/index.js @@ -0,0 +1 @@ +export * from './hooks'; diff --git a/pages/api/posts/[postId]/comments/index.js b/pages/api/posts/[postId]/comments/index.js new file mode 100644 index 0000000..5e9f929 --- /dev/null +++ b/pages/api/posts/[postId]/comments/index.js @@ -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; diff --git a/pages/api/posts/index.js b/pages/api/posts/index.js index 6d8afad..5660ef0 100644 --- a/pages/api/posts/index.js +++ b/pages/api/posts/index.js @@ -1,30 +1,24 @@ -import { getPosts, insertPost } from '@/api-lib/db'; -import { all, database } from '@/api-lib/middlewares'; +import { findPosts, insertPost } from '@/api-lib/db'; +import { all } from '@/api-lib/middlewares'; import { ncOpts } from '@/api-lib/nc'; import nc from 'next-connect'; const handler = nc(ncOpts); -const maxAge = 1 * 24 * 60 * 60; +handler.use(all); -handler.get(database, async (req, res) => { - const posts = await getPosts( +handler.get(async (req, res) => { + const posts = await findPosts( req.db, req.query.from ? new Date(req.query.from) : undefined, req.query.by, req.query.limit ? parseInt(req.query.limit, 10) : undefined ); - if (req.query.from && posts.length > 0) { - // This is safe to cache because from defines - // a concrete range of posts - res.setHeader('cache-control', `public, max-age=${maxAge}`); - } - res.send({ posts }); }); -handler.post(all, async (req, res) => { +handler.post(async (req, res) => { if (!req.user) { return res.status(401).send('unauthenticated'); } diff --git a/pages/api/users/[userId]/index.js b/pages/api/users/[userId]/index.js index bff490d..747d9f9 100644 --- a/pages/api/users/[userId]/index.js +++ b/pages/api/users/[userId]/index.js @@ -8,11 +8,8 @@ const handler = nc(ncOpts); handler.use(database); -const maxAge = 4 * 60 * 60; // 4 hours - handler.get(async (req, res) => { const user = extractUser(await findUserById(req.db, req.query.userId)); - if (user) res.setHeader('cache-control', `public, max-age=${maxAge}`); res.send({ user }); }); diff --git a/pages/index.jsx b/pages/index.jsx index 39a041d..c90b4e3 100644 --- a/pages/index.jsx +++ b/pages/index.jsx @@ -1,5 +1,4 @@ -import PostEditor from '@/components/post/editor'; -import Posts from '@/components/post/posts'; +import { PostEditor, Posts } from '@/components/post'; import { useCurrentUser } from '@/lib/user'; const IndexPage = () => { diff --git a/pages/user/[userId]/index.jsx b/pages/user/[userId]/index.jsx index eb73023..62aa8cc 100644 --- a/pages/user/[userId]/index.jsx +++ b/pages/user/[userId]/index.jsx @@ -1,10 +1,10 @@ import { findUserById } from '@/api-lib/db'; -import { all } from '@/api-lib/middlewares'; +import { database } from '@/api-lib/middlewares'; import { extractUser } from '@/api-lib/user'; -import Posts from '@/components/post/posts'; +import { Posts } from '@/components/post'; import { defaultProfilePicture } from '@/lib/default'; import { useCurrentUser } from '@/lib/user'; -import Error from 'next/error'; +import nc from 'next-connect'; import Head from 'next/head'; import Link from 'next/link'; @@ -12,7 +12,6 @@ export default function UserPage({ user }) { const { name, email, bio, profilePicture, _id } = user || {}; const [currentUser] = useCurrentUser(); const isCurrentUser = currentUser?._id === user._id; - if (!user) return ; return ( <>