├── .gitignore ├── README.md ├── package.json ├── public ├── favicon.ico └── index.html └── src ├── components ├── App.jsx ├── Search.jsx └── User.jsx ├── index.css └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React + GitHub API project 2 | 3 | In this project, we're going to take a small, existing React application and add new features to it. 4 | 5 | Here's what the application will look like once you are done: 6 | 7 | ![react github project](http://i.imgur.com/cSckwUo.gif) 8 | 9 | The code you are given for the project implements the search form and the loading of basic user info. You'll have to do all the rest. 10 | 11 | Let's take a look at the code that's already there. Many of the starter files should already be familiar to you if you completed [the previous workshop](https://github.com/ziad-saab/react-intro-workshop). 12 | 13 | * `package.json`: Configuration file for NPM, contains dependencies and project metadata 14 | * `.gitignore`: Files that should be ignored by Git. `node_modules` can always be regenerated 15 | * `public/index.html`: File that gets served thru Webpack after having been filled in 16 | * `src/index.js`: This file is the entry point for the app. It puts our app on the screen! 17 | * `src/components/*`: All the components of our application. 18 | * `src/index.css`: The styles for our app. Check it out to see how your starter app is being styled, and add to it to complete the project. Notice that we don't `` this CSS from the index? How does this work?? Make sure you understand!!! 19 | 20 | **To get started coding on this project, remember the following steps**: 21 | 22 | 1. `npm install` the first time you clone this repo 23 | 2. `npm start` anytime you want to start developing. This will watch your JS files and re-run webpack when there are changes 24 | 3. Start coding! 25 | 26 | In `index.js` we have the following route structure: 27 | 28 | ```javascript 29 | 30 | 31 | 32 | 33 | ``` 34 | 35 | The top route says to load the `App` component. Looking at the code of `App.jsx`, you'll see that its `render` method outputs `{this.props.children}`. If the URL happens to be only `/`, then React Router will render an `` instance, and will pass it a `` as its child. If the route happens to be `/user/:username`, React Router will display `` but will pass it `` as a child. 36 | 37 | When the `Search` component is displayed, it has a form and a button. When the form is submitted, we use React Router's `browserHistory` to **programmatically change the URL**. Look at the `Search`'s `_handleSubmit` method to see how that happens. 38 | 39 | Once we navigate to the new URL, React Router will render a `User` component. Looking at the `componentDidMount` method of the `User`, you'll see that it does an AJAX call using `this.props.params.username`. The reason why it has access to this prop is because the Router passed it when it mounted the component. 40 | 41 | The AJAX call is made to `https://api.github.com/users/{USERNAME}` and returns the following information: 42 | 43 | ```json 44 | { 45 | "login": "gaearon", 46 | "id": 810438, 47 | "avatar_url": "https://avatars.githubusercontent.com/u/810438?v=3", 48 | "gravatar_id": "", 49 | "url": "https://api.github.com/users/gaearon", 50 | "html_url": "https://github.com/gaearon", 51 | "followers_url": "https://api.github.com/users/gaearon/followers", 52 | "following_url": "https://api.github.com/users/gaearon/following{/other_user}", 53 | "gists_url": "https://api.github.com/users/gaearon/gists{/gist_id}", 54 | "starred_url": "https://api.github.com/users/gaearon/starred{/owner}{/repo}", 55 | "subscriptions_url": "https://api.github.com/users/gaearon/subscriptions", 56 | "organizations_url": "https://api.github.com/users/gaearon/orgs", 57 | "repos_url": "https://api.github.com/users/gaearon/repos", 58 | "events_url": "https://api.github.com/users/gaearon/events{/privacy}", 59 | "received_events_url": "https://api.github.com/users/gaearon/received_events", 60 | "type": "User", 61 | "site_admin": false, 62 | "name": "Dan Abramov", 63 | "company": "Facebook", 64 | "blog": "http://twitter.com/dan_abramov", 65 | "location": "London, UK", 66 | "email": "dan.abramov@me.com", 67 | "hireable": null, 68 | "bio": "Created: Redux, React Hot Loader, React DnD. Now helping make @reactjs better at @facebook.", 69 | "public_repos": 176, 70 | "public_gists": 48, 71 | "followers": 10338, 72 | "following": 171, 73 | "created_at": "2011-05-25T18:18:31Z", 74 | "updated_at": "2016-07-28T14:41:02Z" 75 | } 76 | ``` 77 | [GitHub API documentation for Users](https://developer.github.com/v3/users/) 78 | 79 | In the `render` method of the `User` component, we are displaying the user info based on the received result, and we have three links that don't lead anywhere for the moment: 80 | 81 | ![links](http://i.imgur.com/3CFG1ir.png) 82 | 83 | If you click on followers, notice that the URL of the page changes to `/users/:username/followers`. If you have your dev tools open, React Router will give you an error message telling you that this route does not exist. 84 | 85 | **The goal of this workshop** is to implement the three links above. To do this, we'll start by implementing the followers page together with step by step instructions. Then, your job will be to implement the two remaining screens and fix any bugs. 86 | 87 | ## Implementing the Followers page 88 | When clicking on the followers link in the UI, notice that the URL changes to `/user/:username/followers`. Currently this results in a "not found" route. Let's fix this. 89 | 90 | ![followers page](http://i.imgur.com/IwkBOUc.png) 91 | 92 | ### Step 1: adding the route 93 | In `index.js`, you currently have your user route setup like this: 94 | 95 | ```javascript 96 | 97 | ``` 98 | 99 | Let's change it to a route with a nested route 100 | 101 | ```javascript 102 | 103 | 104 | 105 | ``` 106 | 107 | For this to do anything, we first have to implement the `Followers` component. 108 | 109 | ### Step 2: adding the `Followers` component 110 | Create a component called `Followers`. Since this component is also a route component, it will receive the same `this.props.params.username`. In this component, we're eventually going to do an AJAX call to grab the followers of the user. 111 | 112 | For the moment, create the component only with a `render` function. In there, use your props to return the following: 113 | 114 | ```html 115 |
116 |

Followers of USERNAME

117 |
118 | ``` 119 | 120 | ### Step 3: displaying the nested component inside its parent 121 | When the URL changes to `followers`, we want to display the followers alongside the current `User` component. **This is why we are nesting the followers route inside the user route.** 122 | 123 | To reflect this nesting in our tree of components, we have to add a `{this.props.children}` output to our `User` component. 124 | 125 | Modify the `User` component to make it display its children just before the closing `` in the `render` method. 126 | 127 | When this is done, go back to your browser. Search for a user, and click on FOLLOWERS. The followers component should be displayed below the user info. 128 | 129 | ### Step 4: loading GitHub data in the `Followers` component: 130 | We want to load the followers of the current user as soon as the `Followers` component is mounted in the DOM. In the `componentDidMount` of `Followers`, use `fetch` to make a request to GitHub's API for the followers. Simply add `/followers` to the GitHub API URL for the user e.g. https://api.github.com/users/ziad-saab/followers 131 | 132 | In the callback to your AJAX request, use `setState` to set a `followers` state on your component. 133 | 134 | ### Step 5: displaying the followers data in the `Followers` component: 135 | Using the `this.state.followers` in your `render` method, display the followers that you receive from GitHub. We'll do this in a few steps. 136 | 137 | 1. Create a new pure component called `GithubUser`. It should receive a `user` prop, and use its `avatar_url` and `login` properties to display one GitHub user. The whole display should link back to that user's page in your app, using React Router's `Link` component. Here's what a sample output of your `GithubUser` component should look like: 138 | 139 | ```javascript 140 | 141 | 142 | ziad-saab 143 | 144 | ``` 145 | 146 | And here's a visual example of four `GithubUser` instances (you can use `vertical-align` in your CSS to align the image and the name): 147 | 148 | ![GithubUser component](http://i.imgur.com/dWp7NIc.png) 149 | 150 | 2. In `Followers`, import your `GithubUser` component. 151 | 3. In the `render` method of `Followers`, use `map` to take the array at `this.state.followers`, and map it to an array of `` elements, passing the `user` prop. The code of `Followers`' `render` method should look like this: 152 | 153 | ```javascript 154 | if (!this.state.followers) { 155 | return
LOADING FOLLOWERS...
156 | } 157 | 158 | return ( 159 |
160 |

Followers of {this.props.params.username}

161 |
    162 | {this.state.followers.map(/* INSERT CODE HERE TO RETURN A NEW */)} 163 |
164 |
165 | ); 166 | ``` 167 | 168 | Having done this, you should have a full `Followers` component ready to go. 169 | 170 | ### Step 6: :warning: A wild bug has appeared! 171 | Try to click on a follower in the followers list. Notice that the URL changes to match the user you clicked, but the display does not change to reflect that. [We had the same problem in the previous workshop](https://github.com/ziad-saab/react-intro-workshop#advanced-inter-component-communication). If you recall, it was due to us fetching the data in `componentDidMount`, but sometimes a component's props change while it's still mounted. 172 | 173 | Here's what's happening in this case: 174 | 175 | 1. User is on `/` and does a search for "gaearon" 176 | 2. User gets redirected to `/user/gaearon` and React Router **mounts** an instance of the `User` component, passing it "gaearon" as `this.props.params.username`. The `User` component's `componentDidMount` method kicks in and fetches data with AJAX 177 | 3. User clicks on FOLLOWERS, gets redirected to `/users/gaearon/followers`. React Router keeps the instance of `User` mounted, and passes it a new instance of `Followers` as `this.props.children`. The `Followers` instance is mounted and its `componentDidMount` kicks in, fetching the followers data. 178 | 4. User clicks on one follower called "alexkuz" and the URL changes to `/users/alexkuz`. React Router **does not mount** a new `User` instance. Instead, it changes the `params` prop of the existing `User` instance to make it `{username: "alexkuz"}`. 179 | 5. Since `componentDidMount` of `User` is not called, no AJAX call occurs. 180 | 181 | To fix this bug, follow the same instructions you did in yesterday's workshop: 182 | 183 | 1. Move the logic from `componentDidMount` to another method called `fetchData` 184 | 2. Call `fetchData` from `componentDidMount` 185 | 3. Implement `componentDidUpdate` and call `fetchData` again but **conditionally**, only if the `username` prop has changed. 186 | 187 | :warning: `componentDidUpdate` gets called **frequently**, whether the props or the state changed. That's why it's important to always check the new vs. old state/props before calling `setState` again. 188 | 189 | ## Implementing the following page 190 | Implementing the following page is an exact copy of the followers page. The only differences are: 191 | 192 | 1. Use `/following` instead of `/followers` in your AJAX call 193 | 2. The title of the page and its URL will be different 194 | 195 | When displaying the following list, note that you can -- and *should* -- reuse the same `GithubUser` presentational component. 196 | 197 | ![following page](http://i.imgur.com/1bFxwc7.png) 198 | 199 | ## Implementing the repos page 200 | Implementing the repos page is similar to the other two pages you implemented. The only differences are: 201 | 202 | 1. Use `/repos` in your AJAX call 203 | 2. Title and URL are different 204 | 3. Instead of using a `` element to link to the repo, use a regular `` since you're linking to an external resource. 205 | 4. You'll need a new `GithubRepo` component that will act similar to the `GithubUser` component you used to display the followers/following. 206 | 207 | ![repos page](http://i.imgur.com/kxvnCun.png) 208 | 209 | When you finish everything, your end-result should look and behave like this: 210 | 211 | ![react github project](http://i.imgur.com/cSckwUo.gif) 212 | 213 | 214 | # Challenge: infinite scrolling! 215 | :warning: If you're going to do this challenge, I suggest you start it in a separate branch and commit often. This way you always have somewhere to go back to when "things were working". 216 | 217 | For this challenge, we're going to use the [`react-infinite`](https://github.com/seatgeek/react-infinite) component to load extra data from the GitHub API. 218 | 219 | Right now, if you look at a profile with a lot of followers, you'll notice that GitHub API only returns the first 25 followers. The API has a `per_page` query string parameter that you can set, but the maximum number of items per page is still 100. If someone has more than 100 followers, you'd have to do many requests to get all of them. 220 | 221 | React Infinite will take care of most of the heavy lifting for us. First of all, it's never a good idea to have thousands of elements on the page if the user is only seeing a handful. React Infinite will be efficient in showing only the elements that are in the viewport. Second, React Infinite will detect the scroll position and can fire off a callback when the scrolling reaches the edge of your container. 222 | 223 | All you have to do is provide React Infinite with the callback function that will load your data, and pass your items to the `` component. 224 | 225 | Let's do it step by step for **followers** and then you can reproduce it for the other pages. This is what your app will look like once you are done: 226 | 227 | ![infinite scroll](http://i.imgur.com/Y4D2d37.gif) 228 | 229 | ## Step 0: :eyeglasses: reading the documentation! 230 | [Read the documentation for React Infinite](https://github.com/seatgeek/react-infinite#react-infinite) to get an idea of what's going on. Once you have read the documentation, make sure to install the `react-infinite` package from NPM. 231 | 232 | ## Step 1: modifying the `Followers` component 233 | Your `Followers` component currently loads its data on `componentDidMount`. It turns out that if you mount an `` component without any data, it will automatically call your callback function to fetch more data. 234 | 235 | ### Step 1.1: Adding new state data to `Followers` 236 | In the `constructor` method of `Followers`, let's add a few more pieces of state. Let's add a `page` state and initialize it to `1`. Add another state called `loading` and set it to `false`. Finally, add a `followers` state and set it to an empty array. 237 | 238 | ### Step 1.2: Change the `componentDidMount` method name to `fetchData`. In your AJAX call, add two query string parameters to the GitHub API URL: `page` will come from your state, and `per_page` can be set to anything between 1 and 100. Set it to 50. Your URL should look like this: 239 | 240 | ``` 241 | https://api.github.com/users/USER/followers?access_token=XXX&page=1&per_page=50 242 | ``` 243 | 244 | ### Step 1.3: Loading... 245 | Before doing the AJAX call in `fetchData`, set the `loading` state to `true`. 246 | 247 | ### Step 1.4: Change the callback to the AJAX call 248 | In the callback of the AJAX call, you're currently setting the `followers` state to the response you receive from the GitHub server. Instead, since you already have a followers array, use the `concat` method to add the new items to your existing `this.state.followers` array. Additionally, set the `loading` state to `false`, and the `page` state to whatever it currently is `+ 1`. 249 | 250 | ### Step 1.5: Importing the library 251 | Load `react-infinite` in your `Followers` component, and assign it to the variable `Infinite`. 252 | 253 | ### Step 1.6: Change the `render` method 254 | In the `render` method, we're currently checking if `this.state.followers` is truthy. We don't need to do that anymore, because we'll always have a list of followers. 255 | 256 | Replace your container with an `` container, and pass it the following props: 257 | 258 | * `isInfiniteLoading`: take it from your `loading` state 259 | * `onInfiniteLoad`: point to your `fetchData` method 260 | * `useWindowAsScrollContainer`: this prop doesn't have a value! It will be set to `true` automatically 261 | * `elementHeight`: to scroll efficiently, React Infinite needs to know the height of an element. Use your browser's inspector to find the approximate height of your `GithubUser` elements. It's not perfect, but it'll do for now. 262 | * `infiniteLoadBeginEdgeOffset`: this sets the amount of pixels from the edge of your container at which more data will be loaded. Set it to `100` so that the data starts loading before you reach the edge (bottom) of the window. 263 | 264 | Your `render` code should have the following in it now: 265 | 266 | ```javascript 267 | 268 | {this.state.followers.map(...)} 269 | 270 | ``` 271 | 272 | After you've done all these changes, your infinite scroll should be working. React Infinite will call your `fetchData` method as often as needed to display new elements. Every time a new page is fetched, you're incrementing the `page` state so that future page fetches will fetch the next page. Since you're `concat`ing followers, your list will keep growing until there is no more data. 273 | 274 | ## Optional step: adding a loading indicator 275 | React Infinite lets you use a [`loadingSpinnerDelegate`](https://github.com/seatgeek/react-infinite#react-node-loadingspinnerdelegate). It's basically a React element that will be displayed below the list when `loading` is `true`. 276 | 277 | You can do this as simply as `loadingSpinnerDelegate={
LOADING
}` or you can go for a CSS animation, or even a GIF. 278 | 279 | ## Finally 280 | When you are done, make sure to add infinite scrolling to the following and repos pages. They should work exactly the same way :) 281 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bleh", 3 | "version": "0.1.0", 4 | "private": true, 5 | "devDependencies": { 6 | "react-scripts": "0.9.0" 7 | }, 8 | "dependencies": { 9 | "react": "^15.4.2", 10 | "react-dom": "^15.4.2", 11 | "react-router": "^3.0.2" 12 | }, 13 | "scripts": { 14 | "start": "react-scripts start", 15 | "build": "react-scripts build", 16 | "test": "react-scripts test --env=jsdom", 17 | "eject": "react-scripts eject" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ziad-saab/react-github-api-project/a9188018ce7d06f36a402689ccd9e58fe12a5f4a/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 | GitHub API Project 17 | 18 | 19 |
20 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | /* 5 | This is the layout component. It's displayed by the top-level Route 6 | this.props.children will correspond to the current URL's component. 7 | 8 | If the URL is only / then the IndexRoute's component will be the child (Search component) 9 | If the URL is /user/:username then the User component will be displayed. 10 | */ 11 | class App extends React.Component { 12 | render() { 13 | return ( 14 |
15 |
16 |

React GitHub Project

17 |
18 |
19 | {this.props.children} 20 |
21 |
22 | ); 23 | } 24 | }; 25 | 26 | export default App; 27 | -------------------------------------------------------------------------------- /src/components/Search.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { browserHistory as history } from 'react-router'; 3 | 4 | /* 5 | This component displays a form where the user can enter a GitHub username 6 | When they submit the form either by pressing ENTER or clicking the button, 7 | we will use react-router's history.push function to push a new URL to the history. 8 | 9 | This will have as an effect to navigate to a new URL, which will display the User component 10 | Why are we doing this instead of using a ? The answer is straightforward, but make sure you understand!!! 11 | */ 12 | class Search extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | 16 | // Why do we need to do this?? Make sure you understand!!! 17 | this._handleSubmit = this._handleSubmit.bind(this); 18 | } 19 | _handleSubmit(e) { 20 | e.preventDefault(); 21 | history.push(`/user/${this.refs.userInput.value}`) 22 | } 23 | 24 | render() { 25 | return ( 26 |
27 |

Enter a GitHub username

28 |
29 | 30 | 31 |
32 |
33 | ); 34 | } 35 | }; 36 | 37 | export default Search; 38 | -------------------------------------------------------------------------------- /src/components/User.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | class User extends React.Component { 5 | constructor() { 6 | super(); 7 | this.state = {}; 8 | } 9 | 10 | /* 11 | This method will be called by React after the first render. It's a perfect place to load 12 | data with AJAX. This User component gets mounted in the DOM as soon as the URL is /user/:username 13 | 14 | When that happens, react-router will pass a `params` prop containing every parameter in the URL, just like 15 | when we get URL parameters in Express with req.params. Here, it's this.props.params. Since we called our route 16 | parameter `username`, it's available under this.props.params.username 17 | 18 | We're using it to make an API call to GitHub to fetch the user data for the username in the URL. Once we receive 19 | the data -- in the callback -- we call `setState` to put the user data in our state. This will trigger a re-render. 20 | When `render` gets called again, `this.state.user` exists and we get the user info display instead of "LOADING..." 21 | */ 22 | componentDidMount() { 23 | fetch(`https://api.github.com/users/${this.props.params.username}`) 24 | .then(response => response.json()) 25 | .then( 26 | user => { 27 | // How can we use `this` inside a callback without binding it?? 28 | // Make sure you understand this fundamental difference with arrow functions!!! 29 | this.setState({ 30 | user: user 31 | }); 32 | } 33 | ); 34 | } 35 | 36 | /* 37 | This method is used as a mapping function. Eventually this could be factored out to its own component. 38 | */ 39 | renderStat(stat) { 40 | return ( 41 |
  • 42 | 43 |

    {stat.value}

    44 |

    {stat.name}

    45 | 46 |
  • 47 | ); 48 | } 49 | 50 | render() { 51 | // If the state doesn't have a user key, it means the AJAX didn't complete yet. Simply render a LOADING indicator. 52 | if (!this.state.user) { 53 | return (
    LOADING...
    ); 54 | } 55 | 56 | // If we get to this part of `render`, then the user is loaded 57 | const user = this.state.user; 58 | 59 | // Gather up some number stats about the user, to be used in a map below 60 | const stats = [ 61 | { 62 | name: 'Public Repos', 63 | value: user.public_repos, 64 | url: `/user/${this.props.params.username}/repos` 65 | }, 66 | { 67 | name: 'Followers', 68 | value: user.followers, 69 | url: `/user/${this.props.params.username}/followers` 70 | }, 71 | { 72 | name: 'Following', 73 | value: user.following, 74 | url: `/user/${this.props.params.username}/following` 75 | } 76 | ]; 77 | 78 | // Look in index.css for the styles that make this look like it does 79 | return ( 80 |
    81 |
    82 | 83 | {`${user.login} 84 |

    {user.login} ({user.name})

    85 |

    {user.bio}

    86 | 87 | 88 |
      89 | {stats.map(this.renderStat)} 90 |
    91 |
    92 |
    93 | ); 94 | } 95 | }; 96 | 97 | export default User; 98 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Helvetica; 3 | margin: 0; 4 | } 5 | 6 | h1,h2,h3,h4,h5,h6 { 7 | margin-top: 0; 8 | font-weight: 400; 9 | } 10 | 11 | .main-header { 12 | background-color: #222; 13 | color: #fff; 14 | overflow: hidden; 15 | padding: 0.5rem; 16 | } 17 | 18 | .main-header h1 { 19 | line-height: 1; 20 | margin: 0; 21 | font-weight: 400; 22 | } 23 | 24 | .main-header a { 25 | color: white; 26 | text-decoration: none; 27 | } 28 | 29 | .main-content { 30 | padding: 0.5rem; 31 | } 32 | 33 | 34 | 35 | 36 | 37 | .search-page { 38 | text-align: center; 39 | } 40 | 41 | .search-page__input { 42 | border: 1px solid black; 43 | padding: 0.2em; 44 | margin-right: 1em; 45 | font-size: 20px; 46 | } 47 | .search-page__button { 48 | border: 1px solid black; 49 | background-color: #fff; 50 | font-size: 20px; 51 | } 52 | 53 | 54 | 55 | 56 | 57 | .user-info { 58 | display: flex; 59 | flex-wrap: wrap; 60 | } 61 | 62 | .user-info__avatar { 63 | border-radius: 50%; 64 | width: 50px; 65 | height: 50px; 66 | margin-right: 1em; 67 | float: left; 68 | } 69 | 70 | .user-info__text { 71 | text-decoration: none; 72 | flex: 1; 73 | } 74 | 75 | .user-info__title { 76 | font-size: 20px; 77 | } 78 | 79 | .user-info__title, .user-info__bio { 80 | overflow: hidden; 81 | text-overflow: ellipsis; 82 | white-space: nowrap; 83 | } 84 | 85 | .user-info__stats { 86 | list-style: none; 87 | padding: 0; 88 | flex: 1; 89 | display: flex; 90 | } 91 | 92 | .user-info__stats a { 93 | color: #000; 94 | text-decoration: none; 95 | text-transform: lowercase; 96 | font-variant: small-caps; 97 | text-align: center; 98 | } 99 | 100 | .user-info__stat { 101 | flex: 1; 102 | } 103 | 104 | .user-info__stat p { 105 | margin: 0; 106 | white-space: nowrap; 107 | } 108 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | What is this sorcery?? Importing a CSS file in JavaScript? 3 | Make sure you understand what's going on here!!! 4 | */ 5 | import './index.css'; 6 | 7 | import React from 'react'; 8 | import ReactDOM from 'react-dom'; 9 | import { Router, Route, IndexRoute, browserHistory } from 'react-router'; 10 | 11 | import App from './components/App'; 12 | import Search from './components/Search'; 13 | import User from './components/User'; 14 | 15 | /* 16 | Rendering a router will output the right component tree based on the current URL. 17 | Nested routes' components will be passed down to the parent as `this.props.children` 18 | 19 | If the URL is /, then will be rendered, and this.props.children will be (this is the IndexRoute) 20 | If the URL is /user/ziad-saab then will be rendered, and this.props.children will be 21 | The instance will be passed a prop called `params`. It will be an object with `{username: 'ziad-saab'}` 22 | */ 23 | const routes = ( 24 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | 32 | ReactDOM.render(routes, document.getElementById('root')); 33 | --------------------------------------------------------------------------------