├── .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 |

2 | 3 | UI.dev Logo 6 | 7 |
8 |

9 | 10 |

React Hooks Course Project - Github Battle App

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 | ![](https://user-images.githubusercontent.com/2933430/53439193-c39e1f00-39be-11e9-848f-d327f0ef5f53.png) ![](https://user-images.githubusercontent.com/2933430/53439196-c39e1f00-39be-11e9-875e-6f4aea52f099.png) ![](https://user-images.githubusercontent.com/2933430/53439197-c39e1f00-39be-11e9-8d17-d303692e5dd2.png) | ![](https://user-images.githubusercontent.com/2933430/53439194-c39e1f00-39be-11e9-8302-dcea6dae726a.png) ![](https://user-images.githubusercontent.com/2933430/53439195-c39e1f00-39be-11e9-9d10-488311266460.png) ![](https://user-images.githubusercontent.com/2933430/53439198-c39e1f00-39be-11e9-8bb8-d12687113a2e.png) 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 |
  1. 18 |

    Enter two Github users

    19 | 20 |
  2. 21 |
  3. 22 |

    Battle

    23 | 24 |
  4. 25 |
  5. 26 |

    See the winners

    27 | 28 |
  6. 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 |
55 | 58 |
59 | 68 | 75 |
76 |
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 |
96 | {`Avatar 101 | 104 | {username} 105 | 106 |
107 | 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 | {`Avatar 18 | {subheader && ( 19 |

20 | {subheader} 21 |

22 | )} 23 |

24 | 25 | {name} 26 | 27 |

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 | 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 | 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 | 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 | 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 |
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 | } --------------------------------------------------------------------------------