diff --git a/src/frontend/src/components/Posts/Posts.jsx b/src/frontend/src/components/Posts/Posts.jsx index 65e24f1591..6a9667f39d 100644 --- a/src/frontend/src/components/Posts/Posts.jsx +++ b/src/frontend/src/components/Posts/Posts.jsx @@ -1,37 +1,217 @@ -import React from 'react'; +import React, { useState, useEffect, useCallback, useRef } from 'react'; import PropTypes from 'prop-types'; -import { Container } from '@material-ui/core'; +import { Container, Button, Grid } from '@material-ui/core'; import { makeStyles } from '@material-ui/core/styles'; - +import parse from 'parse-link-header'; +import useSiteMetaData from '../../hooks/use-site-metadata'; import Post from '../Post/Post.jsx'; +import CustomizedSnackBar from '../SnackBar/SnackBar.jsx'; +import Spinner from '../Spinner/Spinner.jsx'; -const useStyles = makeStyles({ +const useStyles = makeStyles((theme) => ({ root: { padding: 0, maxWidth: '785px', }, -}); + content: { + '& > *': { + /** + * We, the implementors of this CSS realize how morally wrong it is + * to use !important in any case. That does not excuse the hour long + * Fight while finding other ways + */ + color: `${theme.palette.secondary.light} !important`, + borderColor: `${theme.palette.secondary.light} !important`, + padding: theme.spacing(2), + bottom: theme.spacing(4), + fontSize: '2rem', + transition: 'all linear 250ms', + + [theme.breakpoints.between('xs', 'sm')]: { + bottom: theme.spacing(8), + }, + }, + }, + activeCircle: { + borderRadius: '4rem', + transition: 'all linear 250ms', + color: theme.palette.primary.light, + }, +})); -const Posts = ({ posts }) => { +const Posts = () => { const classes = useStyles(); + const savedCallback = useRef(); + const [initNumPosts, setInitNumPosts] = useState(0); + const [currentNumPosts, setCurrentNumPosts] = useState(0); + const [shouldCheckForNewPosts, setShouldCheckForNewPosts] = useState(false); + const [alert, setAlert] = useState(false); + const [loading, setLoading] = useState(false); + const { telescopeUrl } = useSiteMetaData(); + const [posts, setPosts] = useState([]); + const [numPages, setNumPages] = useState(1); + const [endOfPosts, setEndOfPosts] = useState(false); + const snackbarMessage = 'There is new content available!'; + + // Pagination + const [nextPageLink, setNextPageLink] = useState(`/posts?page=${numPages}`); + + useEffect(() => { + async function getPosts() { + try { + setLoading(true); + const res = await fetch(`${telescopeUrl}${nextPageLink}`); + + if (!res.ok) { + throw new Error(res.statusText); + } + + const postUrls = await res.json(); + const links = parse(res.headers.get('Link')); + const postsData = await Promise.all( + postUrls.map(({ url }) => fetch(`${telescopeUrl}${url}`).then((resp) => resp.json())) + ); + + setPosts([...posts, ...postsData]); + setNextPageLink(links.next.url); + + // When we reach the end, disable the next-button UI + setEndOfPosts(links.next.url === links.last.url); + } catch (error) { + console.log('Something went wrong when fetching data', error); + } finally { + setLoading(false); + } + } + + getPosts(); + // Disabling the eslint check as nextPageLink and posts will cause the page to not render properly + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [telescopeUrl, numPages]); + + function getNewPosts() { + setLoading(true); + setNumPages(numPages + 1); + } + + const getPostsCount = useCallback(async () => { + if (!shouldCheckForNewPosts) { + return null; + } + try { + const res = await fetch(`${telescopeUrl}/posts`, { method: 'HEAD' }); + if (!res.ok) { + throw new Error(res.statusText); + } + return res.headers.get('x-total-count'); + } catch (error) { + console.log(error); + } finally { + setShouldCheckForNewPosts(false); + } + return null; + }, [telescopeUrl, shouldCheckForNewPosts]); + + const callback = useCallback(async () => { + setCurrentNumPosts(await getPostsCount()); + }, [getPostsCount]); + + useEffect(() => { + savedCallback.current = callback; + }); + + useEffect(() => { + async function setPostsInfo() { + try { + setInitNumPosts(await getPostsCount()); + } catch (error) { + console.log({ error }); + } + } + setPostsInfo(); + }, [getPostsCount, currentNumPosts]); + + useEffect(() => { + function getCurrentNumPosts() { + setShouldCheckForNewPosts(true); + savedCallback.current(); + } + savedCallback.current = callback; + // Polls every 5 minutes + const interval = setInterval(() => { + getCurrentNumPosts(); + }, 5 * 60 * 1000); + return () => clearInterval(interval); + }, [callback]); + + useEffect(() => { + function getCurrentNumPosts() { + setShouldCheckForNewPosts(true); + savedCallback.current(); + } + savedCallback.current = callback; + // Polls every 5 minutes + const interval = setInterval(getCurrentNumPosts, 5 * 60 * 1000); + return () => clearInterval(interval); + }, [callback]); + + useEffect(() => { + // Prevents alert from appearing upon page loading. + // Also checks whether there are new posts available + if (!loading && currentNumPosts !== initNumPosts && currentNumPosts !== 0) { + setAlert(true); + } else { + setAlert(false); + } + }, [currentNumPosts, initNumPosts, loading]); + + function GenerateLoadButtonContent() { + if (endOfPosts) { + return 'No more posts. Your turn! Add your feed...'; + } + return 'Load More Posts'; + } return posts.length > 0 ? ( - - {posts.map(({ id, feed, html, title, url, updated }) => ( - - ))} + + + {posts.map(({ id, feed, html, title, url, updated }) => ( + + ))} + + + + + + + + + {alert ? : null} + ) : ( - <> + <> + + + + ); }; diff --git a/src/frontend/src/pages/index.js b/src/frontend/src/pages/index.js index 8687a4398c..1f32b265a7 100644 --- a/src/frontend/src/pages/index.js +++ b/src/frontend/src/pages/index.js @@ -1,189 +1,17 @@ -import React, { useEffect, useState, useRef, useCallback } from 'react'; -import { makeStyles } from '@material-ui/core/styles'; -import { Button, Grid } from '@material-ui/core'; -import parse from 'parse-link-header'; +import React from 'react'; import PageBase from './PageBase'; import Banner from '../components/Banner'; import Posts from '../components/Posts'; import ScrollToTop from '../components/ScrollToTop'; -import useSiteMetaData from '../hooks/use-site-metadata'; -import CustomizedSnackBar from '../components/SnackBar'; -import Spinner from '../components/Spinner'; - -const useStyles = makeStyles((theme) => ({ - content: { - '& > *': { - /** - * We, the implementors of this CSS realize how morally wrong it is - * to use !important in any case. That does not excuse the hour long - * Fight while finding other ways - */ - color: `${theme.palette.secondary.light} !important`, - borderColor: `${theme.palette.secondary.light} !important`, - padding: theme.spacing(2), - bottom: theme.spacing(4), - fontSize: '2rem', - transition: 'all linear 250ms', - - [theme.breakpoints.between('xs', 'sm')]: { - bottom: theme.spacing(8), - }, - }, - }, - activeCircle: { - borderRadius: '4rem', - transition: 'all linear 250ms', - color: theme.palette.primary.light, - }, -})); export default function IndexPage() { - const [numPages, setNumPages] = useState(1); - const [posts, setPosts] = useState([]); - const [initNumPosts, setInitNumPosts] = useState(0); - const [currentNumPosts, setCurrentNumPosts] = useState(0); - const [endOfPosts, setEndOfPosts] = useState(false); - const [alert, setAlert] = useState(false); - const [shouldCheckForNewPosts, setShouldCheckForNewPosts] = useState(false); - const { telescopeUrl } = useSiteMetaData(); - const savedCallback = useRef(); - const snackbarMessage = 'There is new content available!'; - - // Pagination - const [nextPageLink, setNextPageLink] = useState(`/posts?page=${numPages}`); - const [loading, setLoading] = useState(false); - const classes = useStyles(); - - useEffect(() => { - async function getPosts() { - try { - setLoading(true); - const res = await fetch(`${telescopeUrl}${nextPageLink}`); - - if (!res.ok) { - throw new Error(res.statusText); - } - - const postUrls = await res.json(); - const links = parse(res.headers.get('Link')); - const postsData = await Promise.all( - postUrls.map(({ url }) => fetch(`${telescopeUrl}${url}`).then((resp) => resp.json())) - ); - - setPosts([...posts, ...postsData]); - setNextPageLink(links.next.url); - - // When we reach the end, disable the next-button UI - setEndOfPosts(links.next.url === links.last.url); - } catch (error) { - console.log('Something went wrong when fetching data', error); - } finally { - setLoading(false); - } - } - - getPosts(); - // Disabling the eslint check as nextPageLink and posts will cause the page to not render properly - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [telescopeUrl, numPages]); - - function getNewPosts() { - setLoading(true); - setNumPages(numPages + 1); - } - - const getPostsCount = useCallback(async () => { - if (!shouldCheckForNewPosts) { - return null; - } - try { - const res = await fetch(`${telescopeUrl}/posts`, { method: 'HEAD' }); - if (!res.ok) { - throw new Error(res.statusText); - } - return res.headers.get('x-total-count'); - } catch (error) { - console.log(error); - } finally { - setShouldCheckForNewPosts(false); - } - return null; - }, [telescopeUrl, shouldCheckForNewPosts]); - - const callback = useCallback(async () => { - setCurrentNumPosts(await getPostsCount()); - }, [getPostsCount]); - - useEffect(() => { - savedCallback.current = callback; - }); - - // Get the initial posts count when currentNumPosts value changes (re-render) - useEffect(() => { - async function setPostsInfo() { - try { - setInitNumPosts(await getPostsCount()); - } catch (error) { - console.log({ error }); - } - } - setPostsInfo(); - }, [getPostsCount, currentNumPosts]); - - useEffect(() => { - function getCurrentNumPosts() { - setShouldCheckForNewPosts(true); - savedCallback.current(); - } - savedCallback.current = callback; - // Polls every 5 minutes - const interval = setInterval(getCurrentNumPosts, 5 * 60 * 1000); - return () => clearInterval(interval); - }, [callback]); - - useEffect(() => { - // Prevents alert from appearing upon page loading. - // Also checks whether there are new posts available - if (!loading && currentNumPosts !== initNumPosts && currentNumPosts !== 0) { - setAlert(true); - } else { - setAlert(false); - } - }, [currentNumPosts, initNumPosts, loading]); - - function GenerateLoadButtonContent() { - if (endOfPosts) { - return 'No more posts. Your turn! Add your feed...'; - } - if (loading) { - return ; - } - return 'Load More Posts'; - } - return (
- - - - - - - - - {alert ? : null} +
);