From a8d6069838dc53ad7a9067e482e57259f4055b32 Mon Sep 17 00:00:00 2001 From: Tyler McGinnis Date: Sun, 21 Jul 2019 19:50:05 -0600 Subject: [PATCH 1/2] Solution --- app/components/Loading.js | 46 ++++++------ app/components/Nav.js | 66 +++++++++-------- app/components/Post.js | 128 +++++++++++++++++++-------------- app/components/PostMetaInfo.js | 24 +++---- app/components/Posts.js | 73 ++++++++++--------- app/components/User.js | 116 ++++++++++++++++++------------ app/contexts/theme.js | 7 +- app/index.js | 67 ++++++++--------- 8 files changed, 280 insertions(+), 247 deletions(-) diff --git a/app/components/Loading.js b/app/components/Loading.js index 13217e0..97a4d99 100644 --- a/app/components/Loading.js +++ b/app/components/Loading.js @@ -12,35 +12,29 @@ const styles = { } } -export default class Loading extends React.Component { - state = { content: this.props.text } - componentDidMount () { - const { speed, text } = this.props +export default function Loading ({ text='Loading', speed=300 }) { + const [content, setContent] = React.useState(text) - this.interval = window.setInterval(() => { - this.state.content === text + '...' - ? this.setState({ content: text }) - : this.setState(({ content }) => ({ content: content + '.' })) + React.useEffect(() => { + const id = window.setInterval(() => { + setContent((content) => { + return content === `${text}...` + ? text + : `${content}.` + }) }, speed) - } - componentWillUnmount () { - window.clearInterval(this.interval) - } - render() { - return ( -

- {this.state.content} -

- ) - } -} -Loading.propTypes = { - text: PropTypes.string.isRequired, - speed: PropTypes.number.isRequired, + return () => window.clearInterval(id) + }, [speed, text]) + + return ( +

+ {content} +

+ ) } -Loading.defaultProps = { - text: 'Loading', - speed: 300 +Loading.propTypes = { + text: PropTypes.string, + speed: PropTypes.number, } diff --git a/app/components/Nav.js b/app/components/Nav.js index eee174e..73731a8 100644 --- a/app/components/Nav.js +++ b/app/components/Nav.js @@ -1,44 +1,42 @@ import React from 'react' -import { ThemeConsumer } from '../contexts/theme' +import ThemeContext from '../contexts/theme' import { NavLink } from 'react-router-dom' const activeStyle = { color: 'rgb(187, 46, 31)' } -export default function Nav () { +export default function Nav ({ toggleTheme }) { + const theme = React.useContext(ThemeContext) + return ( - - {({ theme, toggleTheme }) => ( - - )} - + ) } \ No newline at end of file diff --git a/app/components/Post.js b/app/components/Post.js index 47bd8b4..b674da6 100644 --- a/app/components/Post.js +++ b/app/components/Post.js @@ -6,67 +6,91 @@ import PostMetaInfo from './PostMetaInfo' import Title from './Title' import Comment from './Comment' -export default class Post extends React.Component { - state = { - post: null, - loadingPost: true, - comments: null, - loadingComments: true, - error: null, +function postReducer (state, action) { + if (action.type === 'fetch') { + return { + ...state, + loadingPost: true, + loadingComments: true + } + } else if (action.type === 'post') { + return { + ...state, + loadingPost: false, + post: action.post, + } + } else if (action.type === 'comments') { + return { + ...state, + loadingComments: false, + comments: action.comments, + } + } else if (action.type === 'error') { + return { + ...state, + loadingComments: false, + loadingPost: false, + error: action.error + } + } else { + throw new Error(`That action type is not supported.`) } - componentDidMount() { - const { id } = queryString.parse(this.props.location.search) +} + +export default function Post ({ location }) { + const { id } = queryString.parse(location.search) + const [state, dispatch] = React.useReducer( + postReducer, + { post: null, loadingPost: true, comments: null, loadingComments: true, error: null } + ) + + const { post, loadingPost, comments, loadingComments, error } = state + + React.useEffect(() => { + dispatch({ type: 'fetch' }) fetchItem(id) .then((post) => { - this.setState({ post, loadingPost: false }) - + dispatch({ type: 'post', post }) return fetchComments(post.kids || []) }) - .then((comments) => this.setState({ - comments, - loadingComments: false - })) - .catch(({ message }) => this.setState({ + .then((comments) => dispatch({ type: 'comments', comments })) + .catch(({ message }) => dispatch({ + type: 'error', error: message, - loadingPost: false, - loadingComments: false })) - } - render() { - const { post, loadingPost, comments, loadingComments, error } = this.state + }, [id]) - if (error) { - return

{error}

- } + if (error) { + return

{error}

+ } - return ( - - {loadingPost === true - ? - : -

- - </h1> - <PostMetaInfo - by={post.by} - time={post.time} - id={post.id} - descendants={post.descendants} + return ( + <React.Fragment> + {loadingPost === true + ? <Loading text='Fetching post' /> + : <React.Fragment> + <h1 className='header'> + <Title url={post.url} title={post.title} id={post.id} /> + </h1> + <PostMetaInfo + by={post.by} + time={post.time} + id={post.id} + descendants={post.descendants} + /> + <p dangerouslySetInnerHTML={{__html: post.text}} /> + </React.Fragment>} + {loadingComments === true + ? loadingPost === false && <Loading text='Fetching comments' /> + : <React.Fragment> + {comments.map((comment) => + <Comment + key={comment.id} + comment={comment} /> - <p dangerouslySetInnerHTML={{__html: post.text}} /> - </React.Fragment>} - {loadingComments === true - ? loadingPost === false && <Loading text='Fetching comments' /> - : <React.Fragment> - {this.state.comments.map((comment) => - <Comment - key={comment.id} - comment={comment} - /> - )} - </React.Fragment>} - </React.Fragment> - ) - } + )} + </React.Fragment>} + </React.Fragment> + ) } \ No newline at end of file diff --git a/app/components/PostMetaInfo.js b/app/components/PostMetaInfo.js index e135575..9b1fe6e 100644 --- a/app/components/PostMetaInfo.js +++ b/app/components/PostMetaInfo.js @@ -2,23 +2,21 @@ import React from 'react' import { Link } from 'react-router-dom' import PropTypes from 'prop-types' import { formatDate } from '../utils/helpers' -import { ThemeConsumer } from '../contexts/theme' +import ThemeContext from '../contexts/theme' export default function PostMetaInfo ({ by, time, id, descendants }) { + const theme = React.useContext(ThemeContext) + return ( - <ThemeConsumer> - {({ theme }) => ( - <div className={`meta-info-${theme}`}> - <span>by <Link to={`/user?id=${by}`}>{by}</Link></span> - <span>on {formatDate(time)}</span> - {typeof descendants === 'number' && ( - <span> - with <Link to={`/post?id=${id}`}>{descendants}</Link> comments - </span> - )} - </div> + <div className={`meta-info-${theme}`}> + <span>by <Link to={`/user?id=${by}`}>{by}</Link></span> + <span>on {formatDate(time)}</span> + {typeof descendants === 'number' && ( + <span> + with <Link to={`/post?id=${id}`}>{descendants}</Link> comments + </span> )} - </ThemeConsumer> + </div> ) } diff --git a/app/components/Posts.js b/app/components/Posts.js index be6f9cd..179a14d 100644 --- a/app/components/Posts.js +++ b/app/components/Posts.js @@ -4,51 +4,54 @@ import { fetchMainPosts } from '../utils/api' import Loading from './Loading' import PostsList from './PostsList' -export default class Posts extends React.Component { - state = { - posts: null, - error: null, - loading: true, - } - componentDidMount() { - this.handleFetch() - } - componentDidUpdate(prevProps) { - if (prevProps.type !== this.props.type) { - this.handleFetch() - } - } - handleFetch () { - this.setState({ +function postsReducer (state, action) { + if (action.type === 'fetch') { + return { posts: null, error: null, loading: true - }) - - fetchMainPosts(this.props.type) - .then((posts) => this.setState({ - posts, - loading: false, - error: null - })) - .catch(({ message }) => this.setState({ - error: message, - loading: false - })) + } + } else if (action.type === 'success') { + return { + posts: action.posts, + error: null, + loading: false, + } + } else if (action.type === 'error') { + return { + posts: state.posts, + error: action.message, + loading: false + } + } else { + throw new Error(`That action type is not supported.`) } - render() { - const { posts, error, loading } = this.state +} + +export default function Posts ({ type }) { + const [state, dispatch] = React.useReducer( + postsReducer, + { posts: null, error: null, loading: true } + ) + + React.useEffect(() => { + dispatch({ type: 'fetch' }) - if (loading === true) { + fetchMainPosts(type) + .then((posts) => dispatch({ type: 'success', posts })) + .catch(({ message }) => dispatch({ type: 'error', error: message })) + }, [type]) + + + if (state.loading === true) { return <Loading /> } - if (error) { - return <p className='center-text error'>{error}</p> + if (state.error) { + return <p className='center-text error'>{state.error}</p> } - return <PostsList posts={posts} /> - } + return <PostsList posts={state.posts} /> } Posts.propTypes = { diff --git a/app/components/User.js b/app/components/User.js index 4e5b2ec..49b0a88 100644 --- a/app/components/User.js +++ b/app/components/User.js @@ -5,60 +5,82 @@ import Loading from './Loading' import { formatDate } from '../utils/helpers' import PostsList from './PostsList' -export default class Post extends React.Component { - state = { - user: null, - loadingUser: true, - posts: null, - loadingPosts: true, - error: null, +function postReducer (state, action) { + if (action.type === 'fetch') { + return { + ...state, + loadingUser: true, + loadingPosts: true, + } + } else if (action.type === 'user') { + return { + ...state, + user: action.user, + loadingUser: false + } + } else if (action.type === 'posts') { + return { + ...state, + posts: action.posts, + loadingPosts: false, + error: null, + } + } else if (action.type === 'error') { + return { + ...state, + error: action.message, + loadingPosts: false, + loadingUser: false + } + } else { + throw new Error(`That action type is not supported.`) } - componentDidMount() { - const { id } = queryString.parse(this.props.location.search) +} + +export default function User ({ location }) { + const { id } = queryString.parse(location.search) + + const [state, dispatch] = React.useReducer( + postReducer, + { user: null, loadingUser: true, posts: null, loadingPosts: true, error: null } + ) + + React.useEffect(() => { + dispatch({ type: 'fetch' }) fetchUser(id) .then((user) => { - this.setState({ user, loadingUser: false}) - + dispatch({ type: 'user', user }) return fetchPosts(user.submitted.slice(0, 30)) }) - .then((posts) => this.setState({ - posts, - loadingPosts: false, - error: null - })) - .catch(({ message }) => this.setState({ - error: message, - loadingUser: false, - loadingPosts: false - })) - } - render() { - const { user, posts, loadingUser, loadingPosts, error } = this.state + .then((posts) => dispatch({ type: 'posts', posts })) + .catch(({ message }) => dispatch({ type: 'error', message })) + }, [id]) - if (error) { - return <p className='center-text error'>{error}</p> - } + const { user, posts, loadingUser, loadingPosts, error } = state - return ( - <React.Fragment> - {loadingUser === true - ? <Loading text='Fetching User' /> - : <React.Fragment> - <h1 className='header'>{user.id}</h1> - <div className='meta-info-light'> - <span>joined <b>{formatDate(user.created)}</b></span> - <span>has <b>{user.karma.toLocaleString()}</b> karma</span> - </div> - <p dangerouslySetInnerHTML={{__html: user.about}} /> - </React.Fragment>} - {loadingPosts === true - ? loadingUser === false && <Loading text='Fetching posts'/> - : <React.Fragment> - <h2>Posts</h2> - <PostsList posts={posts} /> - </React.Fragment>} - </React.Fragment> - ) + if (error) { + return <p className='center-text error'>{error}</p> } + + return ( + <React.Fragment> + {loadingUser === true + ? <Loading text='Fetching User' /> + : <React.Fragment> + <h1 className='header'>{user.id}</h1> + <div className='meta-info-light'> + <span>joined <b>{formatDate(user.created)}</b></span> + <span>has <b>{user.karma.toLocaleString()}</b> karma</span> + </div> + <p dangerouslySetInnerHTML={{__html: user.about}} /> + </React.Fragment>} + {loadingPosts === true + ? loadingUser === false && <Loading text='Fetching posts'/> + : <React.Fragment> + <h2>Posts</h2> + <PostsList posts={posts} /> + </React.Fragment>} + </React.Fragment> + ) } \ No newline at end of file diff --git a/app/contexts/theme.js b/app/contexts/theme.js index 7e4742b..05fa5fb 100644 --- a/app/contexts/theme.js +++ b/app/contexts/theme.js @@ -1,6 +1,7 @@ import React from 'react' -const { Consumer, Provider } = React.createContext() +const ThemeContext = React.createContext() -export const ThemeConsumer = Consumer -export const ThemeProvider = Provider \ No newline at end of file +export default ThemeContext +export const ThemeConsumer = ThemeContext.Consumer +export const ThemeProvider = ThemeContext.Provider \ No newline at end of file diff --git a/app/index.js b/app/index.js index 5caeb22..29fc072 100644 --- a/app/index.js +++ b/app/index.js @@ -10,45 +10,38 @@ const Posts = React.lazy(() => import('./components/Posts')) const Post = React.lazy(() => import('./components/Post')) const User = React.lazy(() => import('./components/User')) -class App extends React.Component { - state = { - theme: 'light', - toggleTheme: () => { - this.setState(({ theme }) => ({ - theme: theme === 'light' ? 'dark' : 'light' - })) - } - } - render() { - return ( - <Router> - <ThemeProvider value={this.state}> - <div className={this.state.theme}> - <div className='container'> - <Nav /> +function App () { + const [theme, setTheme] = React.useState('light') + const toggleTheme = () => setTheme((t) => t === 'light' ? 'dark' : 'light') - <React.Suspense fallback={<Loading />}> - <Switch> - <Route - exact - path='/' - render={() => <Posts type='top' />} - /> - <Route - path='/new' - render={() => <Posts type='new' />} - /> - <Route path='/post' component={Post} /> - <Route path='/user' component={User} /> - <Route render={() => <h1>404</h1>} /> - </Switch> - </React.Suspense> - </div> + return ( + <Router> + <ThemeProvider value={theme}> + <div className={theme}> + <div className='container'> + <Nav toggleTheme={toggleTheme} /> + + <React.Suspense fallback={<Loading />}> + <Switch> + <Route + exact + path='/' + render={() => <Posts type='top' />} + /> + <Route + path='/new' + render={() => <Posts type='new' />} + /> + <Route path='/post' component={Post} /> + <Route path='/user' component={User} /> + <Route render={() => <h1>404</h1>} /> + </Switch> + </React.Suspense> </div> - </ThemeProvider> - </Router> - ) - } + </div> + </ThemeProvider> + </Router> + ) } ReactDOM.render( From b0b4ef8abedebf20f61bc77634b1ae66a52f6bac Mon Sep 17 00:00:00 2001 From: Tyler McGinnis <tylermcginnis33@gmail.com> Date: Thu, 9 Jul 2020 13:01:20 -0600 Subject: [PATCH 2/2] Update README.md --- README.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f2168be..7bbec29 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,19 @@ <h1 align="center"> - <a href="https://tylermcginnis.com"><img src="https://tylermcginnis.com/tylermcginnis_glasses-300.png" alt="TylerMcGinnis.com Logo" width="300"></a> - <br> + <a href="https://ui.dev"> + <img + src="https://ui.dev/images/logos/ui.png" + alt="UI.dev Logo" width="300" /> + </a> + <br /> </h1> -<h3 align="center">React Hooks Course Curriculum - <a href="https://tm.dev/react-course-curriculum/">Hacker News Clone</a></h3> +<h3 align="center">React Hooks Course Curriculum - <a href="http://hn.ui.dev/">Hacker News Clone</a></h3> ### Info -This is the repository for TylerMcGinnis.com's "React Hooks" course curriculum project. +This is the repository for UI.dev's "React Hooks" course curriculum project. -For more information on the course, visit __[tm.dev/courses/react-hooks](https://tm.dev/courses/react-hooks/)__. +For more information on the course, visit __[ui.dev/react-hooks/](https://ui.dev/react-hooks/)__. ### Assignment @@ -17,7 +21,7 @@ Clone this repo and refactor it to use React Hooks. The starter code is located ### Project -This is a (soft) "Hacker News" clone. You can view the final project at __[tm.dev/react-course-curriculum](https://tm.dev/react-course-curriculum/)__. +This is a (soft) "Hacker News" clone. You can view the final project at __[hn.ui.dev/](http://hn.ui.dev/)__. ### Solution