Skip to content

Commit b28b09c

Browse files
committed
Starter code
0 parents  commit b28b09c

21 files changed

+8287
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules
2+
.DS_Store
3+
dist

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<h1 align="center">
2+
<a href="https://tylermcginnis.com"><img src="https://tylermcginnis.com/tylermcginnis_glasses-300.png" alt="TylerMcGinnis.com Logo" width="300"></a>
3+
<br>
4+
</h1>
5+
6+
<h3 align="center">React Hooks Course Curriculum - <a href="https://tm.dev/react-course-curriculum/">Hacker News Clone</a></h3>
7+
8+
### Info
9+
10+
This is the repository for TylerMcGinnis.com's "React Hooks" course curriculum project.
11+
12+
For more information on the course, visit __[tm.dev/courses/react-hooks](https://tm.dev/courses/react-hooks/)__.
13+
14+
### Assignment
15+
16+
Clone this repo and refactor it to use React Hooks. The starter code is located on the "master" branch.
17+
18+
### Project
19+
20+
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/)__.
21+
22+
### Solution
23+
24+
If you get stuck, you can view my solution by checking out the `solution` branch.
25+
26+
### Project Preview
27+
28+
Light Mode | Dark Mode
29+
:-------------------------:|:-------------------------:
30+
![](https://user-images.githubusercontent.com/2933430/55523754-c1775200-5647-11e9-9394-387cd49a012c.png) ![](https://user-images.githubusercontent.com/2933430/55523752-c0debb80-5647-11e9-91e0-cd2dd38b3255.png) ![](https://user-images.githubusercontent.com/2933430/55523749-c0debb80-5647-11e9-9575-80262d951938.png) | ![](https://user-images.githubusercontent.com/2933430/55523751-c0debb80-5647-11e9-865e-fc829b2566f8.png) ![](https://user-images.githubusercontent.com/2933430/55523753-c1775200-5647-11e9-8230-db5ea02e7333.png) ![](https://user-images.githubusercontent.com/2933430/55523750-c0debb80-5647-11e9-835b-79530775d1b9.png)
31+
32+
### [Tyler McGinnis](https://twitter.com/tylermcginnis)

_redirects

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/* /index.html 200

app/components/Comment.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import React from 'react'
2+
import PropTypes from 'prop-types'
3+
import PostMetaInfo from './PostMetaInfo'
4+
5+
export default function Comment ({ comment }) {
6+
return (
7+
<div className='comment'>
8+
<PostMetaInfo
9+
comment={true}
10+
by={comment.by}
11+
time={comment.time}
12+
id={comment.id}
13+
/>
14+
<p dangerouslySetInnerHTML={{__html: comment.text}} />
15+
</div>
16+
)
17+
}

app/components/Loading.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import React from 'react'
2+
import PropTypes from 'prop-types'
3+
4+
const styles = {
5+
content: {
6+
fontSize: '35px',
7+
position: 'absolute',
8+
left: '0',
9+
right: '0',
10+
marginTop: '20px',
11+
textAlign: 'center',
12+
}
13+
}
14+
15+
export default class Loading extends React.Component {
16+
state = { content: this.props.text }
17+
componentDidMount () {
18+
const { speed, text } = this.props
19+
20+
this.interval = window.setInterval(() => {
21+
this.state.content === text + '...'
22+
? this.setState({ content: text })
23+
: this.setState(({ content }) => ({ content: content + '.' }))
24+
}, speed)
25+
}
26+
componentWillUnmount () {
27+
window.clearInterval(this.interval)
28+
}
29+
render() {
30+
return (
31+
<p style={styles.content}>
32+
{this.state.content}
33+
</p>
34+
)
35+
}
36+
}
37+
38+
Loading.propTypes = {
39+
text: PropTypes.string.isRequired,
40+
speed: PropTypes.number.isRequired,
41+
}
42+
43+
Loading.defaultProps = {
44+
text: 'Loading',
45+
speed: 300
46+
}

app/components/Nav.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import React from 'react'
2+
import { ThemeConsumer } from '../contexts/theme'
3+
import { NavLink } from 'react-router-dom'
4+
5+
const activeStyle = {
6+
color: 'rgb(187, 46, 31)'
7+
}
8+
9+
export default function Nav () {
10+
return (
11+
<ThemeConsumer>
12+
{({ theme, toggleTheme }) => (
13+
<nav className='row space-between'>
14+
<ul className='row nav'>
15+
<li>
16+
<NavLink
17+
to='/'
18+
exact
19+
activeStyle={activeStyle}
20+
className='nav-link'>
21+
Top
22+
</NavLink>
23+
</li>
24+
<li>
25+
<NavLink
26+
to='/new'
27+
activeStyle={activeStyle}
28+
className='nav-link'>
29+
New
30+
</NavLink>
31+
</li>
32+
</ul>
33+
<button
34+
style={{fontSize: 30}}
35+
className='btn-clear'
36+
onClick={toggleTheme}
37+
>
38+
{theme === 'light' ? '🔦' : '💡'}
39+
</button>
40+
</nav>
41+
)}
42+
</ThemeConsumer>
43+
)
44+
}

app/components/Post.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import React from 'react'
2+
import queryString from 'query-string'
3+
import { fetchItem, fetchPosts, fetchComments } from '../utils/api'
4+
import Loading from './Loading'
5+
import PostMetaInfo from './PostMetaInfo'
6+
import Title from './Title'
7+
import Comment from './Comment'
8+
9+
export default class Post extends React.Component {
10+
state = {
11+
post: null,
12+
loadingPost: true,
13+
comments: null,
14+
loadingComments: true,
15+
error: null,
16+
}
17+
componentDidMount() {
18+
const { id } = queryString.parse(this.props.location.search)
19+
20+
fetchItem(id)
21+
.then((post) => {
22+
this.setState({ post, loadingPost: false })
23+
24+
return fetchComments(post.kids || [])
25+
})
26+
.then((comments) => this.setState({
27+
comments,
28+
loadingComments: false
29+
}))
30+
.catch(({ message }) => this.setState({
31+
error: message,
32+
loadingPost: false,
33+
loadingComments: false
34+
}))
35+
}
36+
render() {
37+
const { post, loadingPost, comments, loadingComments, error } = this.state
38+
39+
if (error) {
40+
return <p className='center-text error'>{error}</p>
41+
}
42+
43+
return (
44+
<React.Fragment>
45+
{loadingPost === true
46+
? <Loading text='Fetching post' />
47+
: <React.Fragment>
48+
<h1 className='header'>
49+
<Title url={post.url} title={post.title} id={post.id} />
50+
</h1>
51+
<PostMetaInfo
52+
by={post.by}
53+
time={post.time}
54+
id={post.id}
55+
descendants={post.descendants}
56+
/>
57+
<p dangerouslySetInnerHTML={{__html: post.text}} />
58+
</React.Fragment>}
59+
{loadingComments === true
60+
? loadingPost === false && <Loading text='Fetching comments' />
61+
: <React.Fragment>
62+
{this.state.comments.map((comment) =>
63+
<Comment
64+
key={comment.id}
65+
comment={comment}
66+
/>
67+
)}
68+
</React.Fragment>}
69+
</React.Fragment>
70+
)
71+
}
72+
}

app/components/PostMetaInfo.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import React from 'react'
2+
import { Link } from 'react-router-dom'
3+
import PropTypes from 'prop-types'
4+
import { formatDate } from '../utils/helpers'
5+
import { ThemeConsumer } from '../contexts/theme'
6+
7+
export default function PostMetaInfo ({ by, time, id, descendants }) {
8+
return (
9+
<ThemeConsumer>
10+
{({ theme }) => (
11+
<div className={`meta-info-${theme}`}>
12+
<span>by <Link to={`/user?id=${by}`}>{by}</Link></span>
13+
<span>on {formatDate(time)}</span>
14+
{typeof descendants === 'number' && (
15+
<span>
16+
with <Link to={`/post?id=${id}`}>{descendants}</Link> comments
17+
</span>
18+
)}
19+
</div>
20+
)}
21+
</ThemeConsumer>
22+
)
23+
}
24+
25+
PostMetaInfo.propTypes = {
26+
by: PropTypes.string.isRequired,
27+
time: PropTypes.number.isRequired,
28+
id: PropTypes.number.isRequired,
29+
descendants: PropTypes.number,
30+
}

app/components/Posts.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import React from 'react'
2+
import PropTypes from 'prop-types'
3+
import { fetchMainPosts } from '../utils/api'
4+
import Loading from './Loading'
5+
import PostsList from './PostsList'
6+
7+
export default class Posts extends React.Component {
8+
state = {
9+
posts: null,
10+
error: null,
11+
loading: true,
12+
}
13+
componentDidMount() {
14+
this.handleFetch()
15+
}
16+
componentDidUpdate(prevProps) {
17+
if (prevProps.type !== this.props.type) {
18+
this.handleFetch()
19+
}
20+
}
21+
handleFetch () {
22+
this.setState({
23+
posts: null,
24+
error: null,
25+
loading: true
26+
})
27+
28+
fetchMainPosts(this.props.type)
29+
.then((posts) => this.setState({
30+
posts,
31+
loading: false,
32+
error: null
33+
}))
34+
.catch(({ message }) => this.setState({
35+
error: message,
36+
loading: false
37+
}))
38+
}
39+
render() {
40+
const { posts, error, loading } = this.state
41+
42+
if (loading === true) {
43+
return <Loading />
44+
}
45+
46+
if (error) {
47+
return <p className='center-text error'>{error}</p>
48+
}
49+
50+
return <PostsList posts={posts} />
51+
}
52+
}
53+
54+
Posts.propTypes = {
55+
type: PropTypes.oneOf(['top', 'new'])
56+
}

app/components/PostsList.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import React from 'react'
2+
import PropTypes from 'prop-types'
3+
import PostMetaInfo from './PostMetaInfo'
4+
import Title from './Title'
5+
6+
export default function PostsList ({ posts }) {
7+
if (posts.length === 0) {
8+
return (
9+
<p className='center-text'>
10+
This user hasn't posted yet
11+
</p>
12+
)
13+
}
14+
15+
return (
16+
<ul>
17+
{posts.map((post) => {
18+
return (
19+
<li key={post.id} className='post'>
20+
<Title url={post.url} title={post.title} id={post.id} />
21+
<PostMetaInfo
22+
by={post.by}
23+
time={post.time}
24+
id={post.id}
25+
descendants={post.descendants}
26+
/>
27+
</li>
28+
)
29+
})}
30+
</ul>
31+
)
32+
}
33+
34+
PostsList.propTypes = {
35+
posts: PropTypes.array.isRequired
36+
}

0 commit comments

Comments
 (0)