Skip to content

Commit

Permalink
Add user posts (#64)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
hoangvvo committed May 24, 2020
1 parent 10d7fcd commit 092b7c5
Show file tree
Hide file tree
Showing 15 changed files with 256 additions and 20 deletions.
3 changes: 2 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ A full-fledged app made with [**Next.js**](https://github.com/zeit/next.js/) and
<div align="center">

- [x] Other user profile
- [ ] Posting
- [x] Posting
- [ ] PM?

</div>
Expand Down
48 changes: 48 additions & 0 deletions components/post/editor.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div style={{ color: '#555', textAlign: 'center' }}>
Please sign in to post
</div>
);
}

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 (
<>
<form onSubmit={hanldeSubmit} style={{ flexDirection: 'row' }} autoComplete="off">
<label htmlFor="name">
<input
name="content"
type="text"
placeholder="Write something..."
/>
</label>
<button type="submit" style={{ marginLeft: '0.5rem' }}>Post</button>
</form>
</>
);
}
95 changes: 95 additions & 0 deletions components/post/posts.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<style jsx>
{`
div {
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);
}
small {
color: #777;
}
`}
</style>
<div>
{user && (
<Link href="/user/[userId]" as={`/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} alt={user.name} />
<b>{user.name}</b>
</a>
</Link>
)}
<p>
{post.content}
</p>
<small>{new Date(post.createdAt).toLocaleString()}</small>
</div>
</>
);
}

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 <p>loading</p>;
return posts.map((post) => <Post key={post._id} post={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 (
<div>
{pages}
{!isReachingEnd && (
<button
type="button"
style={{
background: 'transparent',
color: '#000',
}}
onClick={loadMore}
disabled={isReachingEnd || isLoadingMore}
>
{isLoadingMore ? '. . .' : 'load more'}
</button>
)}
</div>
);
}
8 changes: 3 additions & 5 deletions lib/db.js
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
1 change: 1 addition & 0 deletions lib/fetch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default function fetcher(url) { return fetch(url).then((r) => r.json()); }
10 changes: 7 additions & 3 deletions lib/hooks.jsx
Original file line number Diff line number Diff line change
@@ -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;
}
5 changes: 2 additions & 3 deletions lib/passport.js
Original file line number Diff line number Diff line change
@@ -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));
});

Expand Down
6 changes: 4 additions & 2 deletions middlewares/database.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
47 changes: 47 additions & 0 deletions pages/api/posts/index.js
Original file line number Diff line number Diff line change
@@ -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;
9 changes: 8 additions & 1 deletion pages/api/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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) => {
Expand Down
15 changes: 15 additions & 0 deletions pages/api/users/[userId]/index.js
Original file line number Diff line number Diff line change
@@ -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;
16 changes: 15 additions & 1 deletion pages/index.jsx
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -12,9 +14,12 @@ const IndexPage = () => {
text-align: center;
color: #888;
}
h3 {
color: #555;
}
`}
</style>
<div>
<div style={{ marginBottom: '2rem' }}>
<h2>
Hello,
{' '}
Expand All @@ -23,6 +28,15 @@ const IndexPage = () => {
</h2>
<p>Have a wonderful day.</p>
</div>
<div>
<h3>
All posts from the Web
{' '}
<span role="img" aria-label="Earth">🌎</span>
</h3>
<PostEditor />
<Posts />
</div>
</>
);
};
Expand Down

1 comment on commit 092b7c5

@vercel
Copy link

@vercel vercel bot commented on 092b7c5 May 24, 2020

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.