From 092b7c54f17020b94730745f42ffcf536e378999 Mon Sep 17 00:00:00 2001
From: Hoang Vo <40987398+hoangvvo@users.noreply.github.com>
Date: Sun, 24 May 2020 13:50:25 -0400
Subject: [PATCH] Add user posts (#64)
* Use nanoid for better looking id
* create database indexes
* eslint
* move fetcher
* fix
* Add initial implimentation
* styling
* Add user info to post
* Typing
* Add filter posts by user
* Add some comment
* styling
* update roadmap
---
.eslintrc | 3 +-
README.md | 2 +-
components/post/editor.jsx | 48 ++++++++++++++++
components/post/posts.jsx | 95 +++++++++++++++++++++++++++++++
lib/db.js | 8 +--
lib/fetch.js | 1 +
lib/hooks.jsx | 10 +++-
lib/passport.js | 5 +-
middlewares/database.js | 6 +-
package.json | 1 +
pages/api/posts/index.js | 47 +++++++++++++++
pages/api/users.js | 9 ++-
pages/api/users/[userId]/index.js | 15 +++++
pages/index.jsx | 16 +++++-
pages/user/[userId]/index.jsx | 10 +++-
15 files changed, 256 insertions(+), 20 deletions(-)
create mode 100644 components/post/editor.jsx
create mode 100644 components/post/posts.jsx
create mode 100644 lib/fetch.js
create mode 100644 pages/api/posts/index.js
create mode 100644 pages/api/users/[userId]/index.js
diff --git a/.eslintrc b/.eslintrc
index 4cb52e9..5243825 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -34,6 +34,7 @@
"react/prop-types": "off",
"react/jsx-props-no-spreading": "off",
"import/prefer-default-export": "off",
- "no-param-reassign": "off"
+ "no-param-reassign": "off",
+ "no-nested-ternary": "off"
}
}
diff --git a/README.md b/README.md
index 2bd3510..768bdc8 100644
--- a/README.md
+++ b/README.md
@@ -51,7 +51,7 @@ A full-fledged app made with [**Next.js**](https://github.com/zeit/next.js/) and
- [x] Other user profile
-- [ ] Posting
+- [x] Posting
- [ ] PM?
diff --git a/components/post/editor.jsx b/components/post/editor.jsx
new file mode 100644
index 0000000..f9bbeba
--- /dev/null
+++ b/components/post/editor.jsx
@@ -0,0 +1,48 @@
+import React from 'react';
+import { useCurrentUser } from '../../lib/hooks';
+import { usePostPages } from './posts';
+
+export default function PostEditor() {
+ const [user] = useCurrentUser();
+ const { revalidate } = usePostPages();
+
+ if (!user) {
+ return (
+
+ Please sign in to post
+
+ );
+ }
+
+ async function hanldeSubmit(e) {
+ e.preventDefault();
+ const body = {
+ content: e.currentTarget.content.value,
+ };
+ if (!e.currentTarget.content.value) return;
+ e.currentTarget.content.value = '';
+ await fetch('/api/posts', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(body),
+ });
+ // revalidate the `post-pages` key in usePostPages
+ revalidate();
+ // Perhaps show a dialog box informing the post has been posted
+ }
+
+ return (
+ <>
+
+ >
+ );
+}
diff --git a/components/post/posts.jsx b/components/post/posts.jsx
new file mode 100644
index 0000000..c664186
--- /dev/null
+++ b/components/post/posts.jsx
@@ -0,0 +1,95 @@
+import React from 'react';
+import useSWR, { useSWRPages } from 'swr';
+import Link from 'next/link';
+import { useUser } from '../../lib/hooks';
+import fetcher from '../../lib/fetch';
+
+function Post({ post }) {
+ const user = useUser(post.creatorId);
+ return (
+ <>
+
+
+ {user && (
+
+
+
+ {user.name}
+
+
+ )}
+
+ {post.content}
+
+
{new Date(post.createdAt).toLocaleString()}
+
+ >
+ );
+}
+
+export const usePostPages = ({ creatorId } = {}) => {
+ const pageKey = `post-pages-${creatorId || 'all'}`;
+ const limit = 10;
+
+ const hookProps = useSWRPages(
+ pageKey,
+ ({ offset, withSWR }) => {
+ const { data: { posts } = {} } = withSWR(useSWR(`/api/posts?from=${offset || ''}&limit=${limit}&by=${creatorId || ''}`, fetcher));
+ if (!posts) return loading
;
+ return posts.map((post) => );
+ },
+ ({ data }) => (data.posts && data.posts.length >= 10
+ ? data.posts[data.posts.length - 1].createdAt // offset by date
+ : null),
+ [],
+ );
+
+ function revalidate() {
+ // We do not have any way to revalidate all pages right now
+ // Tracking at https://github.com/zeit/swr/issues/189
+
+ // TODO: How do we do this?
+ }
+
+ return { ...hookProps, revalidate };
+};
+
+export default function Posts({ creatorId }) {
+ const {
+ pages, isLoadingMore, isReachingEnd, loadMore,
+ } = usePostPages({ creatorId });
+
+ return (
+
+ {pages}
+ {!isReachingEnd && (
+
+ )}
+
+ );
+}
diff --git a/lib/db.js b/lib/db.js
index 70238d0..a7cc28f 100644
--- a/lib/db.js
+++ b/lib/db.js
@@ -1,16 +1,14 @@
-import { ObjectId } from 'mongodb';
-
export async function getUser(req, userId) {
const user = await req.db.collection('users').findOne({
- _id: ObjectId(userId),
+ _id: userId,
});
if (!user) return null;
const {
_id, name, email, bio, profilePicture, emailVerified,
} = user;
- const isAuth = _id.toString() === req.user?._id.toString();
+ const isAuth = _id === req.user?._id;
return {
- _id: _id.toString(),
+ _id,
name,
email: isAuth ? email : null,
bio,
diff --git a/lib/fetch.js b/lib/fetch.js
new file mode 100644
index 0000000..4beb89f
--- /dev/null
+++ b/lib/fetch.js
@@ -0,0 +1 @@
+export default function fetcher(url) { return fetch(url).then((r) => r.json()); }
diff --git a/lib/hooks.jsx b/lib/hooks.jsx
index 913fc54..4095e95 100644
--- a/lib/hooks.jsx
+++ b/lib/hooks.jsx
@@ -1,9 +1,13 @@
import useSWR from 'swr';
-
-const fetcher = (url) => fetch(url).then((r) => r.json());
+import fetcher from './fetch';
export function useCurrentUser() {
const { data, mutate } = useSWR('/api/user', fetcher);
- const user = data && data.user;
+ const user = data?.user;
return [user, { mutate }];
}
+
+export function useUser(id) {
+ const { data } = useSWR(`/api/users/${id}`, fetcher);
+ return data?.user;
+}
diff --git a/lib/passport.js b/lib/passport.js
index 9bce765..5f39baa 100644
--- a/lib/passport.js
+++ b/lib/passport.js
@@ -1,17 +1,16 @@
import passport from 'passport';
import bcrypt from 'bcryptjs';
import { Strategy as LocalStrategy } from 'passport-local';
-import { ObjectId } from 'mongodb';
passport.serializeUser((user, done) => {
- done(null, user._id.toString());
+ done(null, user._id);
});
// passport#160
passport.deserializeUser((req, id, done) => {
req.db
.collection('users')
- .findOne(ObjectId(id))
+ .findOne({ _id: id })
.then((user) => done(null, user));
});
diff --git a/middlewares/database.js b/middlewares/database.js
index 383b36b..cd03fa3 100644
--- a/middlewares/database.js
+++ b/middlewares/database.js
@@ -6,9 +6,11 @@ const client = new MongoClient(process.env.MONGODB_URI, {
});
export async function setUpDb(db) {
- await db
+ db
.collection('tokens')
- .createIndex('expireAt', { expireAfterSeconds: 0 });
+ .createIndex({ expireAt: -1 }, { expireAfterSeconds: 0 });
+ db.collection('posts').createIndex({ createdAt: -1 });
+ db.collection('users').createIndex({ email: 1 }, { unique: true });
}
export default async function database(req, res, next) {
diff --git a/package.json b/package.json
index d9cf7e1..be25bc4 100644
--- a/package.json
+++ b/package.json
@@ -27,6 +27,7 @@
"dotenv": "^8.2.0",
"mongodb": "^3.5.5",
"multer": "^1.4.2",
+ "nanoid": "^3.1.5",
"next": "^9.3.1",
"next-connect": "^0.6.1",
"next-session": "^3.0.1",
diff --git a/pages/api/posts/index.js b/pages/api/posts/index.js
new file mode 100644
index 0000000..ed34172
--- /dev/null
+++ b/pages/api/posts/index.js
@@ -0,0 +1,47 @@
+import nextConnect from 'next-connect';
+import { nanoid } from 'nanoid';
+import middleware from '../../../middlewares/middleware';
+
+const handler = nextConnect();
+
+handler.use(middleware);
+
+handler.get(async (req, res) => {
+ // Pagination: Fetch posts from before the input date or fetch from newest
+ const from = req.query.from ? new Date(req.query.from) : new Date();
+ const creatorId = req.query.by;
+ const posts = await req.db
+ .collection('posts')
+ .find({
+ createdAt: {
+ $lte: from,
+ },
+ ...(creatorId && { creatorId }),
+ })
+ .sort({ createdAt: -1 })
+ .limit(parseInt(req.query.limit, 10) || 10)
+ .toArray();
+ res.send({ posts });
+});
+
+handler.post(async (req, res) => {
+ if (!req.user) {
+ return res.status(401).send('unauthenticated');
+ }
+
+ const { content } = req.body;
+
+ if (!content) return res.status(400).send('You must write something');
+
+ const post = {
+ _id: nanoid(),
+ content,
+ createdAt: new Date(),
+ creatorId: req.user._id,
+ };
+
+ await req.db.collection('posts').insertOne(post);
+ return res.send(post);
+});
+
+export default handler;
diff --git a/pages/api/users.js b/pages/api/users.js
index 5792848..0e1c8b3 100644
--- a/pages/api/users.js
+++ b/pages/api/users.js
@@ -2,6 +2,7 @@ import nextConnect from 'next-connect';
import isEmail from 'validator/lib/isEmail';
import normalizeEmail from 'validator/lib/normalizeEmail';
import bcrypt from 'bcryptjs';
+import { nanoid } from 'nanoid';
import middleware from '../../middlewares/middleware';
import { extractUser } from '../../lib/api-helpers';
@@ -28,7 +29,13 @@ handler.post(async (req, res) => {
const user = await req.db
.collection('users')
.insertOne({
- email, password: hashedPassword, name, emailVerified: false, bio: '', profilePicture: null,
+ _id: nanoid(12),
+ email,
+ password: hashedPassword,
+ name,
+ emailVerified: false,
+ bio: '',
+ profilePicture: null,
})
.then(({ ops }) => ops[0]);
req.logIn(user, (err) => {
diff --git a/pages/api/users/[userId]/index.js b/pages/api/users/[userId]/index.js
new file mode 100644
index 0000000..06027d1
--- /dev/null
+++ b/pages/api/users/[userId]/index.js
@@ -0,0 +1,15 @@
+
+import nextConnect from 'next-connect';
+import middleware from '../../../../middlewares/middleware';
+import { getUser } from '../../../../lib/db';
+
+const handler = nextConnect();
+
+handler.use(middleware);
+
+handler.get(async (req, res) => {
+ const user = await getUser(req, req.query.userId);
+ res.send({ user });
+});
+
+export default handler;
diff --git a/pages/index.jsx b/pages/index.jsx
index 6b08484..eb5a346 100644
--- a/pages/index.jsx
+++ b/pages/index.jsx
@@ -1,5 +1,7 @@
import React from 'react';
import { useCurrentUser } from '../lib/hooks';
+import PostEditor from '../components/post/editor';
+import Posts from '../components/post/posts';
const IndexPage = () => {
const [user] = useCurrentUser();
@@ -12,9 +14,12 @@ const IndexPage = () => {
text-align: center;
color: #888;
}
+ h3 {
+ color: #555;
+ }
`}
-
+
Hello,
{' '}
@@ -23,6 +28,15 @@ const IndexPage = () => {
Have a wonderful day.
+
+
+ All posts from the Web
+ {' '}
+ 🌎
+
+
+
+
>
);
};
diff --git a/pages/user/[userId]/index.jsx b/pages/user/[userId]/index.jsx
index d321926..38f3f75 100644
--- a/pages/user/[userId]/index.jsx
+++ b/pages/user/[userId]/index.jsx
@@ -4,6 +4,7 @@ import Link from 'next/link';
import Error from 'next/error';
import middleware from '../../../middlewares/middleware';
import { useCurrentUser } from '../../../lib/hooks';
+import Posts from '../../../components/post/posts';
import { getUser } from '../../../lib/db';
export default function UserPage({ user }) {
@@ -30,11 +31,10 @@ export default function UserPage({ user }) {
border-radius: 50%;
box-shadow: rgba(0, 0, 0, 0.05) 0 10px 20px 1px;
margin-right: 1.5rem;
+ background-color: #f3f3f3;
}
div {
color: #777;
- display: flex;
- align-items: center;
}
p {
font-family: monospace;
@@ -49,7 +49,7 @@ export default function UserPage({ user }) {
{name}
-
+
@@ -68,6 +68,10 @@ export default function UserPage({ user }) {
+
>
);
}