Skip to content

Commit

Permalink
Fix Seneca-CDOT#1459: Added Post and Posts to NextJS (Seneca-CDOT#1504)
Browse files Browse the repository at this point in the history
* Port Post & Posts

* Fixed eslint issues (hopefully) & PR changes

* Renamed Post/PostComponent.tsx to Post/index.tsx. Fixed AdminButtons import

* Fixed import statements & spacing

* Moved Post/index.tsx -> Posts/Post.tsx & fixed TS error

* Fixed eslint warning
  • Loading branch information
rogercyyu committed Jan 22, 2021
1 parent 0c9c86e commit 8f22e42
Show file tree
Hide file tree
Showing 6 changed files with 416 additions and 1 deletion.
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;
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';

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,
},
})
);

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

0 comments on commit 8f22e42

Please sign in to comment.