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

Fix #1459: Added Post and Posts to NextJS #1504

Merged
merged 6 commits into from
Jan 22, 2021
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
3 changes: 3 additions & 0 deletions src/frontend/next/src/components/AdminButtons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const AdminButtons = () => <h3> AdminButtons</h3>;

export default AdminButtons;
humphd marked this conversation as resolved.
Show resolved Hide resolved
58 changes: 58 additions & 0 deletions src/frontend/next/src/components/Posts/LoadAutoScroll.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useEffect, createRef } from 'react';
import { Container, Button, Grid, createStyles, Theme } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';

type Scroll = {
onScroll: Function;
};

const useStyles = makeStyles((theme: Theme) =>
createStyles({
content: {
'& > *': {
padding: theme.spacing(2),
bottom: theme.spacing(4),
},
},
})
);

function LoadAutoScroll({ onScroll }: Scroll) {
const classes = useStyles();
const $buttonRef = createRef<HTMLButtonElement>();
// This will make the automatic infinite scrolling feature
// Once the "button" is on the viewport(shown on the window),
// The new posts are updated(call onClick() -- setSize(size + 1) in Posts.jsx --)
useEffect(() => {
const options = {
root: null,
threshold: 1.0,
};

const observer = new IntersectionObserver(
(entries) =>
entries.forEach((entry) => {
if (entry.isIntersecting) {
onScroll();
}
}),
options
);
observer.observe($buttonRef.current!);
const buttonRefCopy = $buttonRef.current;

return () => {
observer.unobserve(buttonRefCopy as HTMLButtonElement);
};
}, [$buttonRef, onScroll]);

return (
<Container>
<Grid item xs={12} className={classes.content}>
<Button ref={$buttonRef}>Load More Posts</Button>
</Grid>
</Container>
);
}

export default LoadAutoScroll;
190 changes: 190 additions & 0 deletions src/frontend/next/src/components/Posts/Post.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import { useRef, useState } from 'react';
import useSWR from 'swr';
import { makeStyles, Theme } from '@material-ui/core/styles';
import { Box, Grid, Typography, ListSubheader, createStyles } from '@material-ui/core';
import ErrorRoundedIcon from '@material-ui/icons/ErrorRounded';
import { Post } from '../../interfaces';
import AdminButtons from '../AdminButtons';
import Spinner from '../Spinner';

type Props = {
postUrl: string;
};

const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
padding: 0,
fontSize: '1.5rem',
marginBottom: '4em',
},
header: {
backgroundColor: theme.palette.primary.main,
color: theme.palette.text.secondary,
padding: '2em 3em 2em 3em',
lineHeight: '1.3',
zIndex: 1100,
top: '-1.1em',
[theme.breakpoints.down(1440)]: {
paddingTop: '1.6em',
paddingBottom: '1.5em',
},
[theme.breakpoints.down(1065)]: {
position: 'static',
},
},
expandHeader: {
whiteSpace: 'normal',
cursor: 'pointer',
},
collapseHeader: {
whiteSpace: 'nowrap',
cursor: 'pointer',
},
title: {
fontSize: '3.5em',
fontWeight: 'bold',
overflow: 'hidden',
textOverflow: 'ellipsis',
[theme.breakpoints.between('xs', 'sm')]: {
fontSize: '2.5em',
},
},
author: {
fontSize: '1.5em',
fontWeight: 'bold',
color: theme.palette.primary.contrastText,
[theme.breakpoints.between('xs', 'sm')]: {
fontSize: '1.2em',
},
},
published: {
fontSize: '1.2em',
textDecoration: 'none',
color: theme.palette.primary.contrastText,
[theme.breakpoints.between('xs', 'sm')]: {
fontSize: '1em',
},
},
content: {
overflow: 'auto',
padding: '2em',
color: theme.palette.text.primary,
},
link: {
textDecoration: 'none',
color: theme.palette.primary.contrastText,
'&:hover': {
textDecorationLine: 'underline',
},
},
time: {
'&:hover': {
textDecorationLine: 'underline',
},
},
spinner: {
padding: '20px',
},
error: {
lineHeight: '1.00',
fontSize: '1em',
},
})
);

const formatPublishedDate = (dateString: string) => {
const date = new Date(dateString);
const options = { month: 'long', day: 'numeric', year: 'numeric' };
const formatted = new Intl.DateTimeFormat('en-CA', options).format(date);
return `Last Updated ${formatted}`;
};

const PostComponent = ({ postUrl }: Props) => {
const classes = useStyles();
// We need a ref to our post content, which we inject into a <section> below.
const sectionEl = useRef<HTMLElement>(null);
// Grab the post data from our backend so we can render it
const { data: post, error } = useSWR<Post>(postUrl);
const [expandHeader, setExpandHeader] = useState(false);

if (error) {
console.error(`Error loading post at ${postUrl}`, error);
return (
<Box className={classes.root} boxShadow={2}>
<ListSubheader className={classes.header}>
<AdminButtons />
<Typography variant="h1" className={classes.title}>
<Grid container className={classes.error}>
<Grid item>
<ErrorRoundedIcon className={classes.error} />
</Grid>{' '}
- Post Failed to Load
</Grid>
</Typography>
</ListSubheader>
</Box>
);
}

if (!post) {
return (
<Box className={classes.root} boxShadow={2}>
<ListSubheader className={classes.header}>
<AdminButtons />
<Typography variant="h1" className={classes.title}>
Loading Blog...
</Typography>
</ListSubheader>

<Grid container justify="center">
<Grid item className={classes.spinner}>
<Spinner />
</Grid>
</Grid>
</Box>
);
}

return (
<Box className={classes.root} boxShadow={2}>
<ListSubheader className={classes.header}>
<AdminButtons />
<Typography variant="h1" title={post.title} id={post.id} className={classes.title}>
<span
role="button"
tabIndex={0}
onClick={() => setExpandHeader(!expandHeader)}
onKeyDown={() => setExpandHeader(!expandHeader)}
className={expandHeader ? classes.expandHeader : classes.collapseHeader}
>
{post.title}
</span>
</Typography>
<Typography className={classes.author}>
&nbsp;By&nbsp;
<a className={classes.link} href={post.feed.url}>
{post.feed.author}
</a>
</Typography>
<a href={post.url} rel="bookmark" className={classes.published}>
<time className={classes.time} dateTime={post.updated}>
{` ${formatPublishedDate(post.updated)}`}
</time>
</a>
</ListSubheader>

<Grid container>
<Grid item xs={12} className={classes.content}>
<section
ref={sectionEl}
className="telescope-post-content"
dangerouslySetInnerHTML={{ __html: post.html }}
/>
</Grid>
</Grid>
</Box>
);
};

export default PostComponent;
58 changes: 58 additions & 0 deletions src/frontend/next/src/components/Posts/Timeline.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { ReactElement } from 'react';
import { Container, createStyles, Grid } from '@material-ui/core';
import { makeStyles, Theme } from '@material-ui/core/styles';
import PostComponent from './Post';
import { Post } from '../../interfaces';
import Spinner from '../Spinner';
import LoadAutoScroll from './LoadAutoScroll';
import useSiteMetaData from '../../hooks/use-site-metadata';
humphd marked this conversation as resolved.
Show resolved Hide resolved

type Props = {
pages: Post[][] | undefined;
nextPage: Function;
};

const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
padding: 0,
maxWidth: '785px',
},
activeCircle: {
borderRadius: '4rem',
transition: 'all linear 250ms',
color: theme.palette.primary.main,
},
humphd marked this conversation as resolved.
Show resolved Hide resolved
})
);

const Timeline = ({ pages, nextPage }: Props) => {
const classes = useStyles();
const { telescopeUrl } = useSiteMetaData();

if (!(pages && pages.length)) {
return (
<Grid container spacing={0} direction="column" alignItems="center" justify="center">
<Spinner />
</Grid>
);
}

// Iterate over all the pages (an array of arrays) and then convert all post
// elements to <Post>
const postsTimeline = pages.map((page: Post[]): Array<ReactElement> | ReactElement =>
page.map(({ id, url }: Post) => <PostComponent postUrl={`${telescopeUrl}${url}`} key={id} />)
);

// Add a "Load More" button at the end of the timeline. Give it a unique
// key each time, based on page (i.e., size), so we remove the previous one
if (nextPage) {
postsTimeline.push(
<LoadAutoScroll onScroll={() => nextPage()} key={`load-more-button-${pages.length}`} />
);
}

return <Container className={classes.root}>{postsTimeline}</Container>;
};

export default Timeline;
Loading