├── .gitignore
├── README.md
├── _redirects
├── app
├── components
│ ├── Battle.js
│ ├── Card.js
│ ├── Hover.js
│ ├── Loading.js
│ ├── Nav.js
│ ├── Popular.js
│ ├── Results.js
│ └── Tooltip.js
├── contexts
│ └── theme.js
├── index.css
├── index.html
├── index.js
└── utils
│ └── api.js
├── package-lock.json
├── package.json
└── webpack.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 | ### Info
13 |
14 | This is the repository for UI.dev's "React Hooks" course project.
15 |
16 | For more information on the course, visit __[ui.dev/react-hooks/](https://ui.dev/react-hooks/)__.
17 |
18 | ### Project
19 |
20 | This project is a "Github Battle" app. You'll be able to see the most popular repos for a variety of languages as well as battle two Github users to see who has the better profile.
21 |
22 | You can view the final project at __[github-battle.ui.dev/](http://github-battle.ui.dev/)__
23 |
24 | ### Branches
25 |
26 | Every (Project) video in the course coincides with a branch. If you want to compare your code with Tyler's or you just want to play around with the code, check out the different branches.
27 |
28 |
29 |
30 | ### Project Preview
31 |
32 | Light Mode | Dark Mode
33 | :-------------------------:|:-------------------------:
34 |    |   
35 |
36 | ### [Tyler McGinnis](https://twitter.com/tylermcginnis)
37 |
--------------------------------------------------------------------------------
/_redirects:
--------------------------------------------------------------------------------
1 | /* /index.html 200
--------------------------------------------------------------------------------
/app/components/Battle.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { FaUserFriends, FaFighterJet, FaTrophy, FaTimesCircle } from 'react-icons/fa'
3 | import PropTypes from 'prop-types'
4 | import Results from './Results'
5 | import { ThemeConsumer } from '../contexts/theme'
6 | import { Link } from 'react-router-dom'
7 |
8 | function Instructions () {
9 | return (
10 |
11 | {({ theme }) => (
12 |
13 |
14 | Instructions
15 |
16 |
17 |
18 | Enter two Github users
19 |
20 |
21 |
22 | Battle
23 |
24 |
25 |
26 | See the winners
27 |
28 |
29 |
30 |
31 | )}
32 |
33 | )
34 | }
35 |
36 | class PlayerInput extends React.Component {
37 | state = {
38 | username: ''
39 | }
40 | handleSubmit = (event) => {
41 | event.preventDefault()
42 |
43 | this.props.onSubmit(this.state.username)
44 | }
45 | handleChange = (event) => {
46 | this.setState({
47 | username: event.target.value
48 | })
49 | }
50 | render() {
51 | return (
52 |
53 | {({ theme }) => (
54 |
77 | )}
78 |
79 | )
80 | }
81 | }
82 |
83 | PlayerInput.propTypes = {
84 | onSubmit: PropTypes.func.isRequired,
85 | label: PropTypes.string.isRequired
86 | }
87 |
88 | function PlayerPreview ({ username, onReset, label }) {
89 | return (
90 |
91 | {({ theme }) => (
92 |
93 |
{label}
94 |
95 |
107 |
108 |
109 |
110 |
111 |
112 | )}
113 |
114 | )
115 | }
116 |
117 | PlayerPreview.propTypes = {
118 | username: PropTypes.string.isRequired,
119 | onReset: PropTypes.func.isRequired,
120 | label: PropTypes.string.isRequired
121 | }
122 |
123 | export default class Battle extends React.Component {
124 | state = {
125 | playerOne: null,
126 | playerTwo: null,
127 | }
128 | handleSubmit = (id, player) => {
129 | this.setState({
130 | [id]: player
131 | })
132 | }
133 | handleReset = (id) => {
134 | this.setState({
135 | [id]: null
136 | })
137 | }
138 | render() {
139 | const { playerOne, playerTwo } = this.state
140 |
141 | return (
142 |
143 |
144 |
145 |
146 |
Players
147 |
148 | {playerOne === null
149 | ?
this.handleSubmit('playerOne', player)}
152 | />
153 | : this.handleReset('playerOne')}
157 | />
158 | }
159 |
160 | {playerTwo === null
161 | ? this.handleSubmit('playerTwo', player)}
164 | />
165 | : this.handleReset('playerTwo')}
169 | />
170 | }
171 |
172 |
173 |
174 | {playerOne && playerTwo && (
175 |
182 | Battle
183 |
184 | )}
185 |
186 |
187 | )
188 | }
189 | }
--------------------------------------------------------------------------------
/app/components/Card.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { ThemeConsumer } from '../contexts/theme'
4 |
5 | export default function Card ({ header, subheader, avatar, href, name, children }) {
6 | return (
7 |
8 | {({ theme }) => (
9 |
10 |
11 | {header}
12 |
13 |
18 | {subheader && (
19 |
20 | {subheader}
21 |
22 | )}
23 |
28 | {children}
29 |
30 | )}
31 |
32 | )
33 | }
34 |
35 | Card.propTypes = {
36 | header: PropTypes.string.isRequired,
37 | subheader: PropTypes.string,
38 | avatar: PropTypes.string.isRequired,
39 | href: PropTypes.string.isRequired,
40 | name: PropTypes.string.isRequired,
41 | }
--------------------------------------------------------------------------------
/app/components/Hover.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default class Hover extends React.Component {
4 | state = { hovering: false }
5 | mouseOver = () => this.setState({ hovering: true })
6 | mouseOut = () => this.setState({ hovering: false })
7 | render () {
8 | return (
9 |
10 | {this.props.children(this.state.hovering)}
11 |
12 | )
13 | }
14 | }
--------------------------------------------------------------------------------
/app/components/Loading.js:
--------------------------------------------------------------------------------
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 |
32 | {this.state.content}
33 |
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:
--------------------------------------------------------------------------------
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 |
12 | {({ theme, toggleTheme }) => (
13 |
14 |
15 |
16 |
21 | Popular
22 |
23 |
24 |
25 |
29 | Battle
30 |
31 |
32 |
33 |
38 | {theme === 'light' ? '🔦' : '💡'}
39 |
40 |
41 | )}
42 |
43 | )
44 | }
--------------------------------------------------------------------------------
/app/components/Popular.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { fetchPopularRepos } from '../utils/api'
4 | import { FaUser, FaStar, FaCodeBranch, FaExclamationTriangle } from 'react-icons/fa'
5 | import Card from './Card'
6 | import Loading from './Loading'
7 | import Tooltip from './Tooltip'
8 |
9 | function LangaugesNav ({ selected, onUpdateLanguage }) {
10 | const languages = ['All', 'JavaScript', 'Ruby', 'Java', 'CSS', 'Python']
11 |
12 | return (
13 |
14 | {languages.map((language) => (
15 |
16 | onUpdateLanguage(language)}>
20 | {language}
21 |
22 |
23 | ))}
24 |
25 | )
26 | }
27 |
28 | LangaugesNav.propTypes = {
29 | selected: PropTypes.string.isRequired,
30 | onUpdateLanguage: PropTypes.func.isRequired
31 | }
32 |
33 | function ReposGrid ({ repos }) {
34 | return (
35 |
36 | {repos.map((repo, index) => {
37 | const { name, owner, html_url, stargazers_count, forks, open_issues } = repo
38 | const { login, avatar_url } = owner
39 |
40 | return (
41 |
42 |
48 |
49 |
50 |
51 |
52 |
53 | {login}
54 |
55 |
56 |
57 |
58 |
59 | {stargazers_count.toLocaleString()} stars
60 |
61 |
62 |
63 | {forks.toLocaleString()} forks
64 |
65 |
66 |
67 | {open_issues.toLocaleString()} open
68 |
69 |
70 |
71 |
72 | )
73 | })}
74 |
75 | )
76 | }
77 |
78 | ReposGrid.propTypes = {
79 | repos: PropTypes.array.isRequired
80 | }
81 |
82 | export default class Popular extends React.Component {
83 | state = {
84 | selectedLanguage: 'All',
85 | repos: {},
86 | error: null,
87 | }
88 | componentDidMount () {
89 | this.updateLanguage(this.state.selectedLanguage)
90 | }
91 | updateLanguage = (selectedLanguage) => {
92 | this.setState({
93 | selectedLanguage,
94 | error: null,
95 | })
96 |
97 | if (!this.state.repos[selectedLanguage]) {
98 | fetchPopularRepos(selectedLanguage)
99 | .then((data) => {
100 | this.setState(({ repos }) => ({
101 | repos: {
102 | ...repos,
103 | [selectedLanguage]: data
104 | }
105 | }))
106 | })
107 | .catch(() => {
108 | console.warn('Error fetching repos: ', error)
109 |
110 | this.setState({
111 | error: `There was an error fetching the repositories.`
112 | })
113 | })
114 | }
115 | }
116 | isLoading = () => {
117 | const { selectedLanguage, repos, error } = this.state
118 |
119 | return !repos[selectedLanguage] && error === null
120 | }
121 | render() {
122 | const { selectedLanguage, repos, error } = this.state
123 |
124 | return (
125 |
126 |
130 |
131 | {this.isLoading() && }
132 |
133 | {error && {error}
}
134 |
135 | {repos[selectedLanguage] && }
136 |
137 | )
138 | }
139 | }
--------------------------------------------------------------------------------
/app/components/Results.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { battle } from '../utils/api'
3 | import { FaCompass, FaBriefcase, FaUsers, FaUserFriends, FaCode, FaUser } from 'react-icons/fa'
4 | import Card from './Card'
5 | import PropTypes from 'prop-types'
6 | import Loading from './Loading'
7 | import Tooltip from './Tooltip'
8 | import queryString from 'query-string'
9 | import { Link } from 'react-router-dom'
10 |
11 | function ProfileList ({ profile }) {
12 | return (
13 |
14 |
15 |
16 | {profile.name}
17 |
18 | {profile.location && (
19 |
20 |
21 |
22 | {profile.location}
23 |
24 |
25 | )}
26 | {profile.company && (
27 |
28 |
29 |
30 | {profile.company}
31 |
32 |
33 | )}
34 |
35 |
36 | {profile.followers.toLocaleString()} followers
37 |
38 |
39 |
40 | {profile.following.toLocaleString()} following
41 |
42 |
43 | )
44 | }
45 |
46 | ProfileList.propTypes = {
47 | profile: PropTypes.object.isRequired,
48 | }
49 |
50 | export default class Results extends React.Component {
51 | state = {
52 | winner: null,
53 | loser: null,
54 | error: null,
55 | loading: true
56 | }
57 | componentDidMount () {
58 | const { playerOne, playerTwo } = queryString.parse(this.props.location.search)
59 |
60 | battle([ playerOne, playerTwo ])
61 | .then((players) => {
62 | this.setState({
63 | winner: players[0],
64 | loser: players[1],
65 | error: null,
66 | loading: false
67 | })
68 | }).catch(({ message }) => {
69 | this.setState({
70 | error: message,
71 | loading: false
72 | })
73 | })
74 | }
75 | render() {
76 | const { winner, loser, error, loading } = this.state
77 |
78 | if (loading === true) {
79 | return
80 | }
81 |
82 | if (error) {
83 | return (
84 | {error}
85 | )
86 | }
87 |
88 | return (
89 |
90 |
91 |
98 |
99 |
100 |
107 |
108 |
109 |
110 |
113 | Reset
114 |
115 |
116 | )
117 | }
118 | }
--------------------------------------------------------------------------------
/app/components/Tooltip.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import Hover from './Hover'
4 |
5 | const styles = {
6 | container: {
7 | position: 'relative',
8 | display: 'flex'
9 | },
10 | tooltip: {
11 | boxSizing: 'border-box',
12 | position: 'absolute',
13 | width: '160px',
14 | bottom: '100%',
15 | left: '50%',
16 | marginLeft: '-80px',
17 | borderRadius: '3px',
18 | backgroundColor: 'hsla(0, 0%, 20%, 0.9)',
19 | padding: '7px',
20 | marginBottom: '5px',
21 | color: '#fff',
22 | textAlign: 'center',
23 | fontSize: '14px',
24 | }
25 | }
26 |
27 | export default function Tooltip ({ text, children }) {
28 | return (
29 |
30 | {(hovering) => (
31 |
32 | {hovering === true &&
{text}
}
33 | {children}
34 |
35 | )}
36 |
37 | )
38 | }
39 |
40 | Tooltip.propTypes = {
41 | text: PropTypes.string.isRequired,
42 | }
--------------------------------------------------------------------------------
/app/contexts/theme.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const { Consumer, Provider } = React.createContext()
4 |
5 | export const ThemeConsumer = Consumer
6 | export const ThemeProvider = Provider
--------------------------------------------------------------------------------
/app/index.css:
--------------------------------------------------------------------------------
1 | html, body, #app {
2 | margin: 0;
3 | height: 100%;
4 | width: 100%;
5 | }
6 |
7 | body {
8 | font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif;
9 | }
10 |
11 | ul {
12 | padding: 0;
13 | }
14 |
15 | li {
16 | list-style-type: none;
17 | }
18 |
19 | .container {
20 | max-width: 1200px;
21 | margin: 0 auto;
22 | padding: 50px;
23 | }
24 |
25 | .dark {
26 | color: #DADADA;
27 | background: #1c2022;
28 | min-height: 100%;
29 | }
30 |
31 | .flex-center {
32 | display: flex;
33 | justify-content: center;
34 | align-items: center;
35 | }
36 |
37 | .btn-clear {
38 | border: none;
39 | background: transparent;
40 | }
41 |
42 | .nav-link {
43 | font-size: 18px;
44 | font-weight: bold;
45 | text-decoration: none;
46 | color: inherit;
47 | }
48 |
49 | .nav li {
50 | margin-right: 10px;
51 | }
52 |
53 | .grid {
54 | display: flex;
55 | flex-wrap: wrap;
56 | }
57 |
58 | .space-around {
59 | justify-content: space-around;
60 | }
61 |
62 | .space-between {
63 | justify-content: space-between;
64 | }
65 |
66 | .header-lg {
67 | font-size: 35px;
68 | font-weight: 300;
69 | margin: 20px;
70 | }
71 |
72 | .header-sm {
73 | font-size: 28px;
74 | font-weight: 300;
75 | margin: 10px;
76 | }
77 |
78 | .avatar {
79 | width: 150px;
80 | height: 150px;
81 | border-radius: 3px;
82 | margin: 0 auto;
83 | display: block;
84 | }
85 |
86 | .center-text {
87 | text-align: center;
88 | }
89 |
90 | .link {
91 | color: rgb(187, 46, 31);
92 | text-decoration: none;
93 | font-weight: bold;
94 | }
95 |
96 | .card-list {
97 | margin: 20px 0;
98 | font-size: 20px;
99 | }
100 |
101 | .card-list li {
102 | display: flex;
103 | align-items: center;
104 | margin: 10px;
105 | }
106 |
107 | .card-list svg {
108 | margin-right: 10px;
109 | }
110 |
111 | .card-list a {
112 | font-weight: 500;
113 | color: inherit;
114 | }
115 |
116 | .bg-light {
117 | background: rgba(0, 0, 0, 0.08);
118 | border-radius: 3px;
119 | }
120 |
121 | .card {
122 | margin: 10px 0;
123 | width: 250px;
124 | padding: 20px;
125 | }
126 |
127 | .card a {
128 | text-decoration: none;
129 | }
130 |
131 | .card img {
132 | margin-bottom: 8px;
133 | }
134 |
135 | .instructions-container {
136 | margin: 100px 0;
137 | }
138 |
139 | .container-sm {
140 | width: 80%;
141 | margin: 0 auto;
142 | }
143 |
144 | .battle-instructions {
145 | padding: 0;
146 | font-size: 25px;
147 | }
148 |
149 | .battle-instructions li {
150 | flex: 1;
151 | min-width: 300px
152 | }
153 |
154 | .battle-instructions svg {
155 | padding: 40px;
156 | border-radius: 3px;
157 | }
158 |
159 | .column {
160 | display: flex;
161 | flex-direction: column;
162 | }
163 |
164 | .row {
165 | display: flex;
166 | flex-direction: row;
167 | }
168 |
169 | .player {
170 | flex: 1;
171 | margin: 0 20px;
172 | padding: 10px;
173 | }
174 |
175 | .player-label {
176 | font-size: 20px;
177 | margin: 5px 0;
178 | font-weight: 300;
179 | }
180 |
181 | .player-inputs input {
182 | padding: 8px;
183 | font-size: 16px;
184 | flex: 2;
185 | border-radius: 3px;
186 | border: none;
187 | box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15);
188 | }
189 |
190 | .player-inputs .input-light {
191 | background: rgba(0, 0, 0, 0.02);
192 | }
193 |
194 | .player-inputs button {
195 | flex: 1;
196 | margin-left: 10px;
197 | }
198 |
199 | .btn {
200 | padding: 10px;
201 | text-decoration: uppercase;
202 | letter-spacing: .25em;
203 | border-radius: 3px;
204 | border: none;
205 | font-size: 16px;
206 | display: flex;
207 | justify-content: center;
208 | align-items: center;
209 | cursor: pointer;
210 | text-decoration: none;
211 | max-width: 200px;
212 | }
213 |
214 | .dark-btn {
215 | color: #e6e6e6;
216 | background: #141414;
217 | }
218 |
219 | .dark-btn:disabled {
220 | background: #f2f2f2;
221 | color: #c7c7c7;
222 | }
223 |
224 | .players-container {
225 | margin: 100px 0;
226 | }
227 |
228 | .avatar-small {
229 | width: 55px;
230 | height: 55px;
231 | border-radius: 50%;
232 | }
233 |
234 | .player-info {
235 | display: flex;
236 | flex: 1;
237 | align-items: center;
238 | font-size: 20px;
239 | padding: 10px;
240 | }
241 |
242 | .player-info .link {
243 | margin-left: 10px;
244 | }
245 |
246 | .btn-space {
247 | margin: 40px auto;
248 | }
249 |
250 | .error {
251 | color: #ff1616;
252 | font-size: 20px;
253 | margin: 50px 0;
254 | }
255 |
256 | .bg-dark {
257 | background: rgb(36, 40, 42);
258 | border-radius: 3px;
259 | }
260 |
261 | .player-inputs .input-dark {
262 | color: #DADADA;
263 | background: rgba(0, 0, 0, 0.3);
264 | }
265 |
266 | .light-btn {
267 | color: #000;
268 | background: #aaa8a8;
269 | }
270 |
271 | .light-btn:disabled {
272 | background: #292929;
273 | color: #4a4a4a;
274 | }
--------------------------------------------------------------------------------
/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Github Battle (Hooks)
5 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import './index.css'
4 | import { ThemeProvider } from './contexts/theme'
5 | import Nav from './components/Nav'
6 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'
7 | import Loading from './components/Loading'
8 |
9 | const Popular = React.lazy(() => import('./components/Popular'))
10 | const Battle = React.lazy(() => import('./components/Battle'))
11 | const Results = React.lazy(() => import('./components/Results'))
12 |
13 | class App extends React.Component {
14 | state = {
15 | theme: 'light',
16 | toggleTheme: () => {
17 | this.setState(({ theme }) => ({
18 | theme: theme === 'light' ? 'dark' : 'light'
19 | }))
20 | }
21 | }
22 | render() {
23 | return (
24 |
25 |
26 |
27 |
28 |
29 |
30 | } >
31 |
32 |
33 |
34 |
35 | 404 } />
36 |
37 |
38 |
39 |
40 |
41 |
42 | )
43 | }
44 | }
45 |
46 | ReactDOM.render(
47 | ,
48 | document.getElementById('app')
49 | )
--------------------------------------------------------------------------------
/app/utils/api.js:
--------------------------------------------------------------------------------
1 | const id = "YOUR_CLIENT_ID"
2 | const sec = "YOUR_SECRET_ID"
3 | const params = `?client_id=${id}&client_secret=${sec}`
4 |
5 | function getErrorMsg (message, username) {
6 | if (message === 'Not Found') {
7 | return `${username} doesn't exist`
8 | }
9 |
10 | return message
11 | }
12 |
13 | function getProfile (username) {
14 | return fetch(`https://api.github.com/users/${username}${params}`)
15 | .then((res) => res.json())
16 | .then((profile) => {
17 | if (profile.message) {
18 | throw new Error(getErrorMsg(profile.message, username))
19 | }
20 |
21 | return profile
22 | })
23 | }
24 |
25 | function getRepos (username) {
26 | return fetch(`https://api.github.com/users/${username}/repos${params}&per_page=100`)
27 | .then((res) => res.json())
28 | .then((repos) => {
29 | if (repos.message) {
30 | throw new Error(getErrorMsg(repos.message, username))
31 | }
32 |
33 | return repos
34 | })
35 | }
36 |
37 | function getStarCount (repos) {
38 | return repos.reduce((count, { stargazers_count }) => count + stargazers_count , 0)
39 | }
40 |
41 | function calculateScore (followers, repos) {
42 | return (followers * 3) + getStarCount(repos)
43 | }
44 |
45 | function getUserData (player) {
46 | return Promise.all([
47 | getProfile(player),
48 | getRepos(player)
49 | ]).then(([ profile, repos ]) => ({
50 | profile,
51 | score: calculateScore(profile.followers, repos)
52 | }))
53 | }
54 |
55 | function sortPlayers (players) {
56 | return players.sort((a, b) => b.score - a.score)
57 | }
58 |
59 | export function battle (players) {
60 | return Promise.all([
61 | getUserData(players[0]),
62 | getUserData(players[1])
63 | ]).then((results) => sortPlayers(results))
64 | }
65 |
66 | export function fetchPopularRepos (language) {
67 | const endpoint = window.encodeURI(`https://api.github.com/search/repositories?q=stars:>1+language:${language}&sort=stars&order=desc&type=Repositories`)
68 |
69 | return fetch(endpoint)
70 | .then((res) => res.json())
71 | .then((data) => {
72 | if (!data.items) {
73 | throw new Error(data.message)
74 | }
75 |
76 | return data.items
77 | })
78 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "github-battle-hooks",
3 | "version": "1.0.0",
4 | "description": "Project for TylerMcGinnis.com's React Hooks course.",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "webpack-dev-server",
8 | "build": "NODE_ENV='production' webpack",
9 | "build-for-windows": "SET NODE_ENV='production' && webpack"
10 | },
11 | "babel": {
12 | "presets": [
13 | "@babel/preset-env",
14 | "@babel/preset-react"
15 | ],
16 | "plugins": [
17 | "@babel/plugin-proposal-class-properties",
18 | "syntax-dynamic-import"
19 | ]
20 | },
21 | "keywords": [],
22 | "author": "",
23 | "license": "ISC",
24 | "dependencies": {
25 | "prop-types": "^15.7.2",
26 | "query-string": "^6.8.1",
27 | "react": "^16.8.6",
28 | "react-dom": "^16.8.6",
29 | "react-icons": "^3.7.0",
30 | "react-router-dom": "^5.0.1"
31 | },
32 | "devDependencies": {
33 | "@babel/core": "^7.5.5",
34 | "@babel/plugin-proposal-class-properties": "^7.5.5",
35 | "@babel/preset-env": "^7.5.5",
36 | "@babel/preset-react": "^7.0.0",
37 | "babel-loader": "^8.0.6",
38 | "babel-plugin-syntax-dynamic-import": "^6.18.0",
39 | "copy-webpack-plugin": "^5.0.3",
40 | "css-loader": "^3.1.0",
41 | "html-webpack-plugin": "^3.2.0",
42 | "style-loader": "^0.23.1",
43 | "webpack": "^4.36.1",
44 | "webpack-cli": "^3.3.6",
45 | "webpack-dev-server": "^3.7.2"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const HtmlWebpackPlugin = require('html-webpack-plugin')
3 | const CopyPlugin = require('copy-webpack-plugin')
4 |
5 | module.exports = {
6 | entry: './app/index.js',
7 | output: {
8 | path: path.resolve(__dirname, 'dist'),
9 | filename: 'index_bundle.js',
10 | publicPath: '/'
11 | },
12 | module: {
13 | rules: [
14 | { test: /\.(js)$/, use: 'babel-loader' },
15 | { test: /\.css$/, use: [ 'style-loader', 'css-loader' ]}
16 | ]
17 | },
18 | mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
19 | plugins: [
20 | new HtmlWebpackPlugin({
21 | template: 'app/index.html'
22 | }),
23 | new CopyPlugin([
24 | { from : '_redirects' }
25 | ])
26 | ],
27 | devServer: {
28 | historyApiFallback: true
29 | }
30 | }
--------------------------------------------------------------------------------