Skip to content

Commit

Permalink
Port Post & Posts
Browse files Browse the repository at this point in the history
  • Loading branch information
rogercyyu committed Jan 21, 2021
1 parent d0260c6 commit 2a2d2b7
Show file tree
Hide file tree
Showing 5 changed files with 424 additions and 0 deletions.
192 changes: 192 additions & 0 deletions src/frontend/next/src/components/Post/PostComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
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/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!);

return () => {
observer.unobserve($buttonRef.current!);
};
}, []);

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

export default LoadAutoScroll;
107 changes: 107 additions & 0 deletions src/frontend/next/src/components/Posts/Posts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { useState, useEffect } from 'react';

import { useSWRInfinite } from 'swr';
import { Container, createStyles, Grid } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import { usePrevious } from 'react-use';
import SentimentDissatisfiedRoundedIcon from '@material-ui/icons/SentimentDissatisfiedRounded';
import Timeline from './Timeline';
import useSiteMetaData from '../../hooks/use-site-metadata';
import useFaviconBadge from '../../hooks/use-favicon-badge';

interface PostData {
[key: string]: any;
}

const useStyles = makeStyles(() =>
createStyles({
root: {
padding: 0,
maxWidth: '785px',
},
error: {
color: '#B5B5B5',
fontFamily: 'Roboto',
fontSize: '5rem',
paddingBottom: '30px',
},
errorIcon: {
color: '#B5B5B5',
fontSize: '10rem',
paddingBottom: 0,
},
})
);

// Refresh post data every 5 mins
const REFRESH_INTERVAL = 5 * 60 * 1000;

const Posts = () => {
const classes = useStyles();
const { telescopeUrl } = useSiteMetaData();
const [currentPostId, setCurrentPostId] = useState();

const { data, size, setSize, error } = useSWRInfinite<PostData>(
(index: number) => `${telescopeUrl}/posts?page=${index + 1}`,
async (url: string) => {
const res = await fetch(url);
return res.json();
},
{
refreshInterval: REFRESH_INTERVAL,
refreshWhenHidden: true,
onSuccess(newData: PostData[]) {
const safelyExtractId = () => {
try {
return newData[0][0].id;
} catch (err) {
return null;
}
};

// Get the id of the top post in the current and prev data sets
const id = safelyExtractId();
setCurrentPostId(id);
},
}
);

const prevPostId = usePrevious(currentPostId);

// Manage the favicon badge, depending on whether we have new data or not
const setBadgeHint = useFaviconBadge();
useEffect(() => {
if (currentPostId && currentPostId !== prevPostId) {
setBadgeHint();
}
}, [currentPostId, prevPostId, setBadgeHint]);

// TODO: need proper error handling
if (error) {
console.error('Error loading post data', error);
return (
<Container className={classes.root}>
<Grid
container
className={classes.error}
justify="center"
alignItems="center"
direction="column"
>
<Grid item>
<SentimentDissatisfiedRoundedIcon className={classes.errorIcon} />
</Grid>
<Grid item>Blog Timeline Failed to Load!</Grid>
</Grid>
</Container>
);
}

return (
<Container className={classes.root}>
<Timeline pages={data} nextPage={() => setSize(size + 1)} />
</Container>
);
};

export default Posts;
Loading

0 comments on commit 2a2d2b7

Please sign in to comment.