diff --git a/src/App.css b/src/App.css index 9d1052b..5bb6340 100644 --- a/src/App.css +++ b/src/App.css @@ -2,6 +2,7 @@ text-align: center; width: 100vw; height: 100vh; + font-family: arial; } * { @@ -204,6 +205,76 @@ header { height: 30px; } +#commentsBox { + width: 100vw; + min-height: 300px; + background-color: #f4f2f1; +} + +#commentLoadingSpinner { + left: 50%; + margin-left: -40px; +} + +#commentsTitle { + margin-top: 20px; + text-align: center; + font-size: medium; + font-weight: bold; + color: #414d52; + font-family: arial; +} + +.commentCard { + display: grid; + grid-template-columns: 40px auto 100px; + grid-template-rows: 40px auto 25px; + grid-template-areas: + "commentAvatar commentAuthor commentVotes" + "commentBody commentBody commentBody" + "commentDate commentDate commentDate"; + background-color: #f9f2ec; + border: 1px solid #a2baab; + border-radius: 10px; + margin: 10px 20px 10px 20px; + box-shadow: 0px 0px 3px #414d52; +} + +.commentAvatar { + grid-area: commentAvatar; + width: 30px; + height: 30px; + border-radius: 50%; + margin: auto; +} + +.commentAuthor { + grid-area: commentAuthor; + line-height: 40px; + font-style: italic; +} + +.commentVotes { + grid-area: commentVotes; + line-height: 40px; + text-align: right; + margin-right: 20px; +} + +.commentBody { + grid-area: commentBody; + text-align: justify; + margin: 10px; +} + +.commentDate { + grid-area: commentDate; + text-align: right; + margin: 0px 20px 0px 0px; + font-size: small; + color: #414d52; +} + #footerBox { position: fixed; left: 0; @@ -225,7 +296,7 @@ header { font-weight: bold; } -#articleCount { +#footerRightText { font-weight: bold; grid-column: 3; text-align: right; @@ -233,14 +304,8 @@ header { line-height: 28px; } -#wordCount { - font-weight: bold; - grid-column: 2; - line-height: 28px; -} - @media (max-width: 500px) { - #articleCount { + #footerRightText { display: none; } } diff --git a/src/App.js b/src/App.js index 7b85913..31fac5f 100644 --- a/src/App.js +++ b/src/App.js @@ -7,8 +7,9 @@ import Article from "./Article"; import { useState } from "react"; function App() { - const [numArticles, setNumArticles] = useState(null); + const [numItems, setNumItems] = useState(null); const [pageNumber, setPageNumber] = useState(1); + const [commentPageNumber, setCommentPageNumber] = useState(1); const [articlePerPage, setArticlePerPage] = useState(10); const [articleWordCount, setArticleWordCount] = useState(null); @@ -19,21 +20,27 @@ function App() { + } /> } + element={ +
+ } />
diff --git a/src/Article.jsx b/src/Article.jsx index 800c68f..33bf0d2 100644 --- a/src/Article.jsx +++ b/src/Article.jsx @@ -1,20 +1,29 @@ import { useEffect, useState } from "react"; -import axios from "axios"; import { useParams } from "react-router-dom"; import { formatDate, wordCount } from "./utils"; import LoadingSpinner from "./LoadingSpinner"; import { getArticle } from "./apiFunctions"; +import Comment from "./Comment"; -const Article = ({ setArticleWordCount }) => { +const Article = ({ + setArticleWordCount, + setNumItems, + setCommentPageNumber, + commentPageNumber, +}) => { const { articleid } = useParams(); const [article, setArticle] = useState(null); const [isLoading, setIsLoading] = useState(true); + const [commentCount, setCommentCount] = useState(null); useEffect(() => { setIsLoading(true); + setNumItems(null); + setCommentPageNumber(1); setArticleWordCount(null); getArticle(articleid).then((article) => { setArticle(article); + setCommentCount(article.comment_count); setArticleWordCount(wordCount(article.body)); setIsLoading(false); }); @@ -36,6 +45,12 @@ const Article = ({ setArticleWordCount }) => {

{article.author}

{formatDate(article.created_at)}

{article.body}

+
); }; diff --git a/src/ArticleList.jsx b/src/ArticleList.jsx index f85a269..be6ed5f 100644 --- a/src/ArticleList.jsx +++ b/src/ArticleList.jsx @@ -1,18 +1,17 @@ import { useState, useEffect } from "react"; import { getArticles } from "./apiFunctions"; import ArticleItem from "./ArticleItem"; -import { formatDate } from "./utils"; -import Footer from "./Footer"; import LoadingSpinner from "./LoadingSpinner"; -const ArticleList = ({ setNumArticles, pageNumber }) => { +const ArticleList = ({ setNumItems, pageNumber }) => { const [isLoading, setIsLoading] = useState(true); const [articles, setArticles] = useState([]); useEffect(() => { setIsLoading(true); + setNumItems(null); getArticles(pageNumber).then((articles) => { - setNumArticles(articles.total_count); + setNumItems(articles.total_count); setArticles(articles.articles); setIsLoading(false); }); diff --git a/src/Comment.jsx b/src/Comment.jsx new file mode 100644 index 0000000..e3b2f49 --- /dev/null +++ b/src/Comment.jsx @@ -0,0 +1,54 @@ +import { useState, useEffect } from "react"; +import LoadingSpinner from "./LoadingSpinner"; +import { getComments, getAuthorAvatar } from "./apiFunctions"; +import CommentHistory from "./CommentHistory"; + +const Comment = ({ + articleid, + commentCount, + setNumItems, + commentPageNumber, +}) => { + const [isLoading, setIsLoading] = useState(true); + const [comments, setComments] = useState(null); + const [authorAvatars, setAuthorAvatars] = useState({}); + + useEffect(() => { + setIsLoading(true); + setNumItems(null); + getComments(articleid, commentPageNumber).then((comments) => { + setComments(comments); + const authorList = [ + ...new Set(comments.map((comment) => comment.author)), + ]; + const promises = []; + authorList.forEach((author) => + promises.push(getAuthorAvatar(author)) + ); + Promise.all(promises).then((data) => { + setAuthorAvatars( + data.reduce((authors, item) => { + authors[item[0]] = item[1]; + return authors; + }, {}) + ); + setNumItems(commentCount); + setIsLoading(false); + }); + }); + }, [commentPageNumber]); + + return isLoading ? ( +
+ +
+ ) : ( + + ); +}; + +export default Comment; diff --git a/src/CommentCard.jsx b/src/CommentCard.jsx new file mode 100644 index 0000000..4b8ade9 --- /dev/null +++ b/src/CommentCard.jsx @@ -0,0 +1,19 @@ +import { formatDate } from "./utils"; + +const CommentCard = ({ comment, authorAvatars }) => { + return ( +
+ {comment.author} +

{comment.author}

+

{comment.votes} likes

+

{comment.body}

+

{formatDate(comment.created_at)}

+
+ ); +}; + +export default CommentCard; diff --git a/src/CommentHistory.jsx b/src/CommentHistory.jsx new file mode 100644 index 0000000..a35577c --- /dev/null +++ b/src/CommentHistory.jsx @@ -0,0 +1,23 @@ +import CommentCard from "./CommentCard"; + +const CommentHistory = ({ comments, authorAvatars, commentCount }) => { + return comments.length === 0 ? ( +
+

No comments

+
+ ) : ( +
+

Recent comments ({commentCount})

+ {comments.map((comment) => ( + + ))} +
+
+ ); +}; + +export default CommentHistory; diff --git a/src/Footer.jsx b/src/Footer.jsx index c317910..0b85e2e 100644 --- a/src/Footer.jsx +++ b/src/Footer.jsx @@ -3,34 +3,36 @@ import { useLocation } from "react-router-dom"; const Footer = ({ pageNumber, setPageNumber, - numArticles, + commentPageNumber, + setCommentPageNumber, + numItems, articlesPerPage, articleWordCount, }) => { const { pathname: path } = useLocation(); const articleView = /\/articles\/[0-9]+/i.test(path); - const totalPages = Math.ceil(numArticles / articlesPerPage); + const totalPages = Math.ceil(numItems / articlesPerPage); const pageNumbers = createPageNumberButtons( - pageNumber, + articleView ? commentPageNumber : pageNumber, totalPages, - setPageNumber + setPageNumber, + setCommentPageNumber, + articleView ); - return articleView ? ( -
-
- {articleWordCount ? `${articleWordCount} words` : null} -
-
- ) : numArticles ? ( + return numItems ? (
Page: {pageNumbers}
-
- {`${numArticles} article${numArticles === 1 ? "" : "s"}`} +
+ {articleView + ? articleWordCount + ? `${articleWordCount} words` + : null + : `${numItems} article${numItems === 1 ? "" : "s"}`}
) : ( @@ -38,7 +40,13 @@ const Footer = ({ ); }; -const createPageNumberButtons = (pageNumber, totalPages, setPageNumber) => { +const createPageNumberButtons = ( + pageNumber, + totalPages, + setPageNumber, + setCommentPageNumber, + articleView +) => { const pageNumbers = []; let start = pageNumber - 2; @@ -58,7 +66,9 @@ const createPageNumberButtons = (pageNumber, totalPages, setPageNumber) => { className="brandedButton pageNumberButton" key={n} disabled={n === pageNumber ? true : false} - onClick={() => setPageNumber(n)} + onClick={() => { + articleView ? setCommentPageNumber(n) : setPageNumber(n); + }} > {n} diff --git a/src/Header.jsx b/src/Header.jsx index 387ed69..49a7f15 100644 --- a/src/Header.jsx +++ b/src/Header.jsx @@ -1,5 +1,4 @@ import { useLocation, Link } from "react-router-dom"; -import ArticleList from "./ArticleList"; const Header = () => { const { pathname: path } = useLocation(); diff --git a/src/LoadingSpinner.jsx b/src/LoadingSpinner.jsx index ef60a32..f76ba26 100644 --- a/src/LoadingSpinner.jsx +++ b/src/LoadingSpinner.jsx @@ -1,6 +1,6 @@ -const LoadingSpinner = () => { +const LoadingSpinner = ({ id }) => { return ( -
+
diff --git a/src/apiFunctions.js b/src/apiFunctions.js index 3566049..a3c2816 100644 --- a/src/apiFunctions.js +++ b/src/apiFunctions.js @@ -13,3 +13,20 @@ export function getArticle(articleid) { .get(`https://news-app-backend.onrender.com/api/articles/${articleid}`) .then(({ data: { article } }) => article); } + +export function getComments(articleid, pageNumber) { + return axios + .get( + `https://news-app-backend.onrender.com/api/articles/${articleid}/comments`, + { + params: { p: pageNumber - 1 }, + } + ) + .then(({ data: { comments } }) => comments); +} + +export function getAuthorAvatar(author) { + return axios + .get(`https://news-app-backend.onrender.com/api/users/${author}`) + .then(({ data: { user } }) => [author, user.avatar_url]); +}