Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Customer review #19

Merged
merged 18 commits into from
Feb 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions components/Product/AddCommentToProduct.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import React, { useState } from 'react'
import axios from 'axios';
import { Button, Comment, Form, Header, Message } from 'semantic-ui-react';
import formatDate from '../../utils/formatDate';
import catchErrors from '../../utils/catchErrors';
import baseUrl from '../../utils/baseUrl';
import cookie from 'js-cookie';
import { useRouter } from 'next/router';

export default function AddCommentToProduct({ user, product, handleNewComment }) {
const [comment, setComment] = useState('');
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false);
const [error, setError] = React.useState("");

const router = useRouter();

function handleChange(event) {
const { value } = event.target;
setComment(value);
}

async function handleSubmit(event) {
event.preventDefault();
if (!user) {
router.push('/login');
return;
}

try {
setLoading(true);
setError("");
const url = `${baseUrl}/api/product`;
const token = cookie.get('token');
const payload = { comment, productId: product._id };
const headers = { Authorization: token };
const response = await axios.put(url, payload, { headers });
// { totalComments, product }
setComment('');
handleNewComment(response.data);
setSuccess(true);
} catch (error) {
catchErrors(error, setError);
setSuccess(false);
} finally {
setLoading(false);
}
}

return (
<>
<Comment.Group minimal>
<Form reply onSubmit={handleSubmit} loading={loading} error={Boolean(error)} success={success}>
<Message error header="Oops!" content={error} />
<Message success header="Success" content="Thank you for your precious comment!" />
<Form.TextArea name="content" onChange={handleChange} value={comment} />
{
user ?
<Button type="submit" content='Add Comment' labelPosition='left' icon='edit' primary disabled={loading || !comment.trim()} />
:
<Button color='orange' content="Login to Add Comments" icon='sign in' onClick={() => router.push('/login')} />
}

</Form>
{
product.comments && product.comments.length > 0 &&
<>
<Header as='h3' dividing>
Comments
</Header>
{
product.comments.map(comment => (
<Comment key={`comment_id ${comment._id}`}>
<Comment.Content>
<Comment.Author as='span'>{comment.user.name}</Comment.Author>
<Comment.Metadata>
<span>{formatDate(comment.updated_at)}</span>
</Comment.Metadata>
<Comment.Text>{comment.content}</Comment.Text>
</Comment.Content>
</Comment>
))
}
</>
}
</Comment.Group>
</>
)
}
22 changes: 22 additions & 0 deletions components/Product/CommentPagination.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useRouter } from "next/router";
import { Container, Pagination } from "semantic-ui-react";
import { redirectUser } from '../../utils/auth'

function CommentPagination({ productId, totalPages }, ctx) {
const router = useRouter();
return (
<Container textAlign="center" style={{ margin: "2em" }}>
<Pagination
defaultActivePage={1}
totalPages={totalPages}
onPageChange={(event, data) => {
data.activePage === 1
? redirectUser(ctx, `/product?_id=${productId}`)
: redirectUser(ctx, `/product?_id=${productId}&page=${data.activePage}`)
}}
/>
</Container>
);
}

export default CommentPagination;
16 changes: 16 additions & 0 deletions models/Product.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,21 @@ import shortid from 'shortid';

const { String, Number, ObjectId } = mongoose.Schema.Types;

const CommentSchema = new mongoose.Schema({
user: {
type: ObjectId,
ref: 'User',
},
content: {
type: String,
required: true,
},
updated_at: {
type: mongoose.Schema.Types.Date,
required: true,
},
});

const ProductSchema = new mongoose.Schema({
name: {
type: String,
Expand All @@ -25,6 +40,7 @@ const ProductSchema = new mongoose.Schema({
type: String,
required: true,
},
comments: [CommentSchema],
category: {
type: String,
//required: true
Expand Down
84 changes: 74 additions & 10 deletions pages/api/product.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import Product from '../../models/Product';
import Cart from '../../models/Cart';
import User from '../../models/User';
import connectDb from '../../utils/connectDb';
import jwt from 'jsonwebtoken';
import mongoose from 'mongoose';
import Rating from '../../models/Rating';

connectDb();

const COMMENTS_PER_PAGE = 5;

export default async (req, res) => {
switch (req.method) {
case 'GET':
Expand All @@ -13,6 +18,9 @@ export default async (req, res) => {
case 'POST':
await handlePostRequest(req, res);
break;
case 'PUT':
await handlePutRequest(req, res);
break;
case 'DELETE':
await handleDeleteRequest(req, res);
break;
Expand All @@ -23,12 +31,24 @@ export default async (req, res) => {
};

async function handleGetRequest(req, res) {
const { _id } = req.query;
const product = await Product.findOne({ _id }).populate({
path: 'ratings',
model: Rating,
});
res.status(200).json(product);
const { _id, page } = req.query;
const startIndex =
page && !Number.isNaN(Number(page)) && page > 0 ? (page - 1) * COMMENTS_PER_PAGE : 0;
const product = await Product.findOne({ _id })
.populate({ path: 'comments.user', model: User })
.populate({
path: 'ratings',
model: Rating,
})
.slice('comments', startIndex, startIndex + COMMENTS_PER_PAGE);

// Get comments count
const [{ comments: count }] = await Product.aggregate()
.match({ _id: mongoose.Types.ObjectId(_id) })
.project({
comments: { $cond: [{ $ifNull: ['$comments', false] }, { $size: '$comments' }, 0] },
});
res.status(200).json({ totalComments: Math.ceil(count / COMMENTS_PER_PAGE), product });
}

async function handlePostRequest(req, res) {
Expand All @@ -50,6 +70,53 @@ async function handlePostRequest(req, res) {
}
}

async function handlePutRequest(req, res) {
// Check if the user is authorized
if (!('authorization' in req.headers)) {
return res.status(401).send('No authorization token');
}
// Get the required fields & check if they exist
const { comment, productId } = req.body;
if (!comment || !productId) {
return res.status(422).send('Comment and productId are required');
}
try {
// Verify the token
const { userId } = jwt.verify(req.headers.authorization, process.env.JWT_SECRET);
// Find the user
const user = await User.findOne({ _id: userId });
// If the user exists
if (user) {
// Create the comment
const newComment = { user: userId, content: comment, updated_at: Date.now() };
// Add the comment to the Product & get new Product
const updatedProduct = await Product.findOneAndUpdate(
{ _id: productId },
{ $push: { comments: { $each: [newComment], $position: 0 } } },
{ new: true },
)
.populate({ path: 'comments.user', model: User })
.slice('comments', COMMENTS_PER_PAGE);

// Get comments count
const [{ comments: count }] = await Product.aggregate()
.match({ _id: mongoose.Types.ObjectId(productId) })
.project({
comments: { $cond: [{ $ifNull: ['$comments', false] }, { $size: '$comments' }, 0] },
});

// Return comments count and the updated product
res
.status(200)
.json({ totalComments: Math.ceil(count / COMMENTS_PER_PAGE), product: updatedProduct });
} else {
res.status(404).send('User not found');
}
} catch (error) {
res.status(403).send(`Please login to add comments!`);
}
}

async function handleDeleteRequest(req, res) {
const { _id } = req.query;

Expand All @@ -58,10 +125,7 @@ async function handleDeleteRequest(req, res) {
await Product.findOneAndDelete({ _id });

// 2) Remove from all carts, referenced as "product"
await Cart.updateMany(
{ 'products.product': _id },
{ $pull: { products: { product: _id } } },
);
await Cart.updateMany({ 'products.product': _id }, { $pull: { products: { product: _id } } });
res.status(204).json({});
} catch (error) {
console.error(error);
Expand Down
37 changes: 29 additions & 8 deletions pages/product.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,53 @@
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import ProductSummary from '../components/Product/ProductSummary';
import ProductAttributes from '../components/Product/ProductAttributes';
import AddCommentToProduct from '../components/Product/AddCommentToProduct';
import CommentPagination from '../components/Product/CommentPagination';
import baseUrl from '../utils/baseUrl';
import { useEffect } from 'react';

function Product({ product, user }) {
function Product({ product, user, totalComments }) {
const [displayedProduct, setDisplayedProduct] = useState(product);
const [displayedTotalComments, setDisplayedTotalComments] = useState(totalComments);

function handleNewComment({ totalComments: newTotalComments, product: newProduct }) {
setDisplayedProduct(newProduct);
setDisplayedTotalComments(newTotalComments);
}

useEffect(() => {
async function incrementProductView() {
const url = `${baseUrl}/api/view`;
const payload = { productId: product._id };
await axios.post(url, payload);
}
incrementProductView();
}, [product]);

setDisplayedProduct(product);
setDisplayedTotalComments(totalComments);
}, [product, totalComments]);

return (
<>
<ProductSummary user={user} {...product} />
<ProductAttributes user={user} {...product} />
<ProductSummary user={user} {...displayedProduct} />
<ProductAttributes user={user} {...displayedProduct} />
<AddCommentToProduct
user={user}
product={displayedProduct}
handleNewComment={handleNewComment}
/>
{displayedTotalComments > 0 && (
<CommentPagination productId={product._id} totalPages={displayedTotalComments} />
)}
</>
);
}

Product.getInitialProps = async ({ query: { _id } }) => {
Product.getInitialProps = async ({ query: { _id, page } }) => {
const url = `${baseUrl}/api/product`;
const payload = { params: { _id } };
const payload = { params: { _id, page } };
const response = await axios.get(url, payload);
return { product: response.data };
return response.data;
};

export default Product;