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 3 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;
190 changes: 190 additions & 0 deletions src/frontend/next/src/components/Post/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import { useRef, useState } from 'react';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like you can just move this Posts.tsx component inside the Posts folder.

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/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;
57 changes: 57 additions & 0 deletions src/frontend/next/src/components/Posts/Timeline.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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,
},
})
);

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[]): any =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:any isn't ideal. Does it need typing (can it not infer it here)?

Not critical, but we could improve here or in follow-up.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@humphd : What does this mean in TS? I thought page already have type Post[]. The return? If so it would return a list of , no idea how to express that in syntax

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like typescript doesn't like it without :any:
image
Not sure what to do.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rogercyyu : I would say keep any for now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the type here is

const postsTimeline = pages.map((page: Post[]): JSX.Element[] | JSX.Element =>
...

We should avoid using any. The whole point of using typescript is having type checking, but using any defeats that purpose.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Problem is for JSX.Element[], I get 'JSX' is not defined.eslintno-undef unless we can use // eslint-disable-next-line no-undef to get rid of that.
image
hmmm...

Copy link
Contributor

@tonyvugithub tonyvugithub Jan 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rogercyyu : Can you try sth like this Array<ReactElement>?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ReactElement basically JSX.Element

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tonyvugithub thanks, that fixed it.

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