├── .babelrc ├── .env.sample ├── .eslintrc.json ├── .gitignore ├── .storybook ├── .babelrc └── config.js ├── .travis.yml ├── README.md ├── __tests__ ├── __snapshots__ │ └── index.test.js.snap ├── auth │ └── github │ │ ├── __snapshots__ │ │ └── callback.test.js.snap │ │ └── callback.test.js └── index.test.js ├── components ├── atoms │ ├── AppBar │ │ ├── index.js │ │ ├── index.stories.js │ │ └── index.test.js │ ├── Button │ │ ├── index.js │ │ ├── index.stories.js │ │ └── index.test.js │ ├── Card │ │ ├── index.js │ │ ├── index.stories.js │ │ └── index.test.js │ ├── CardActions │ │ ├── index.js │ │ ├── index.stories.js │ │ └── index.test.js │ ├── CardContent │ │ ├── index.js │ │ ├── index.stories.js │ │ └── index.test.js │ ├── Grid │ │ ├── index.js │ │ ├── index.stories.js │ │ └── index.test.js │ ├── IconButton │ │ ├── index.js │ │ ├── index.stories.js │ │ └── index.test.js │ ├── List │ │ ├── index.js │ │ ├── index.stories.js │ │ └── index.test.js │ ├── ListItem │ │ ├── index.js │ │ ├── index.stories.js │ │ └── index.test.js │ ├── ListItemIcon │ │ ├── index.js │ │ ├── index.stories.js │ │ └── index.test.js │ ├── ListItemText │ │ ├── index.js │ │ ├── index.stories.js │ │ └── index.test.js │ ├── Loader │ │ ├── index.js │ │ └── index.stories.js │ ├── MenuIcon │ │ ├── index.js │ │ └── index.stories.js │ ├── SwipeableDrawer │ │ ├── index.js │ │ ├── index.stories.js │ │ └── index.test.js │ ├── Toolbar │ │ ├── index.js │ │ ├── index.stories.js │ │ └── index.test.js │ └── Typography │ │ ├── index.js │ │ ├── index.stories.js │ │ └── index.test.js ├── index.js ├── moleculus │ ├── Header │ │ ├── index.js │ │ ├── index.stories.js │ │ └── index.test.js │ ├── SimpleCard │ │ ├── index.js │ │ ├── index.stories.js │ │ └── index.test.js │ └── SwipeableMenu │ │ ├── index.js │ │ ├── index.stories.js │ │ └── index.test.js ├── organisms │ └── HeaderWithSwipeableMenu │ │ ├── index.js │ │ ├── index.stories.js │ │ └── index.test.js └── templates │ └── Home │ ├── index.js │ ├── index.stories.js │ └── index.test.js ├── containers ├── GithubLoginButtonContainer │ └── index.js ├── HeaderContainer │ ├── index.js │ └── index.test.js ├── SearchRepoList │ └── index.js └── ViewerRepoList │ └── index.js ├── graphql └── queries │ ├── searchNewJsRepos.js │ ├── searchNewRubyRepos.js │ ├── searchTopJsRepos.js │ ├── searchTopRubyRepos.js │ ├── viewer.js │ └── viewerLast100Repositories.js ├── lib ├── getPageContext.js └── testConfig.js ├── next.config.js ├── package.json ├── pages ├── _app.js ├── _document.js ├── auth │ └── github │ │ └── callback.js ├── index.js ├── new_js │ └── index.js ├── new_ruby │ └── index.js ├── top_js │ └── index.js └── top_ruby │ └── index.js ├── public └── images │ ├── nextjs-404.png │ ├── storybook-initial-old.png │ └── storybook-initial.png └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "development": { 4 | "presets": ["next/babel"], 5 | "plugins": [ 6 | ["module-resolver", { 7 | "root": ["./"], 8 | "alias": { 9 | "components": "./components", 10 | "containers": "./containers", 11 | "queries": "./graphql/queries" 12 | } 13 | }] 14 | ] 15 | }, 16 | "production": { 17 | "presets": ["next/babel"], 18 | "plugins": [ 19 | ["module-resolver", { 20 | "root": ["./"], 21 | "alias": { 22 | "components": "./components", 23 | "containers": "./containers", 24 | "pages": "./pages", 25 | "queries": "./graphql/queries" 26 | } 27 | }] 28 | ] 29 | }, 30 | "test": { 31 | "presets": ["react", "env", "stage-0"], 32 | "plugins": [ 33 | "require-context-hook", 34 | ["module-resolver", { 35 | "root": ["./"], 36 | "alias": { 37 | "components": "./components", 38 | "containers": "./containers", 39 | "pages": "./pages", 40 | "queries": "./graphql/queries" 41 | } 42 | }] 43 | ] 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | GITHUB_CLIENT_ID = '' 2 | GITHUB_CLIENT_SECRET = '' -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "jest": true, 5 | "browser": true 6 | }, 7 | "settings": { 8 | "import/resolver": { 9 | "node": { 10 | "paths": ["./"] 11 | }, 12 | "babel-module": {} 13 | } 14 | }, 15 | "rules": { 16 | "react/jsx-filename-extension": ["error", { "extensions": [".js", ".jsx"] }], 17 | "import/no-extraneous-dependencies": [ 18 | "error", 19 | { "devDependencies": true } 20 | ] 21 | }, 22 | "extends": "airbnb" 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | .env 4 | .vscode -------------------------------------------------------------------------------- /.storybook/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env", "stage-0", "react"], 3 | "plugins": [ 4 | ["module-resolver", { 5 | "root": ["../"], 6 | "alias": { 7 | "components": "./components", 8 | "containers": "./containers", 9 | "pages": "./pages", 10 | "queries": "./graphql/queries" 11 | } 12 | }] 13 | ] 14 | } -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from '@storybook/react'; 2 | 3 | const req = require.context('../components', true, /stories\.js$/); 4 | 5 | function loadStories() { 6 | req.keys().forEach(req) 7 | } 8 | 9 | configure(loadStories, module); 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "10" 5 | cache: yarn 6 | script: 7 | - yarn lint || travis_terminate 1 8 | - yarn test || travis_terminate 1 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.com/leksster/nextjs6-graphql-client-tutorial.svg?branch=master)](https://travis-ci.com/leksster/nextjs6-graphql-client-tutorial) 2 | 3 | # Nextjs6 with Apollo Graphql and Material-UI Tutorial 4 | 5 | ## Step 1 - Yarn installation 6 | 7 | There are two options how to install Yarn. The first option is to use npm: 8 | 9 | ```bash 10 | npm install -g yarn 11 | ``` 12 | 13 | Another option is to go to the official download page and get the installer for your operating system and run it. 14 | 15 | The other method would be to go to [the official download page](https://yarnpkg.com/en/docs/install) and get the installer for your operating system and run it. 16 | 17 | ## Step 2 - Project initialization 18 | 19 | To start, create a sample project by running the following commands: 20 | 21 | ```bash 22 | mkdir github-client 23 | cd github-client 24 | yarn init 25 | yarn add react react-dom prop-types next 26 | mkdir pages 27 | ``` 28 | 29 | Then open the "package.json" in the github-client directory and add the following script. 30 | 31 | ``` 32 | { 33 | "scripts": { 34 | "dev": "next" 35 | } 36 | } 37 | ``` 38 | 39 | Everything is ready. To start the dev server, you need to run the following command: 40 | 41 | ```bash 42 | yarn dev 43 | ``` 44 | 45 | When you run `localhost:3000` you will see this page: 46 | 47 | ![404](public/images/nextjs-404.png "404 Page screenshot") 48 | 49 | ## Step 3 - Babel Installation 50 | 51 | Babel is a toolchain that is mainly used to convert ECMAScript 2015+ code into a backwards compatible version of JavaScript in old browsers or environments. 52 | 53 | To install babel compiler core use command: 54 | 55 | ```bash 56 | yarn add babel-core -D 57 | ``` 58 | 59 | #### Babel module resolver 60 | 61 | A Babel plugin to add a new resolver for your modules when compiling your code using Babel. This plugin allows you to add new "root" directories that contain your modules. It also allows you to setup a custom alias for directories, specific files, or even other npm modules. 62 | 63 | Let's install babel module resolver. 64 | 65 | ```bash 66 | yarn add babel-plugin-module-resolver -D 67 | ``` 68 | 69 | After that we need to update `.babelrc` config. So we can import dependencies without declaring related path. 70 | 71 | `.babelrc` 72 | 73 | ```js 74 | "plugins": [ 75 | ["module-resolver", { 76 | "root": ["./"], 77 | "alias": { 78 | "components": "./components", 79 | "containers": "./containers", 80 | "queries": "./graphql/queries" 81 | } 82 | }] 83 | ] 84 | ``` 85 | 86 | This allows us to make this: 87 | 88 | ```js 89 | import { Home } from 'components'; 90 | ``` 91 | 92 | Instead of this: 93 | 94 | ```js 95 | import { Home } from '../../components'; 96 | ``` 97 | 98 | ## Step 4 Linters configuration 99 | 100 | To avoid big refactoring in the future, you need to integrate linters to your app. For that, add eslint as development dependency: 101 | 102 | ```bash 103 | yarn add eslint -D 104 | ``` 105 | 106 | You need a wrapper for babel’s parser used for eslint. Use yarn to install package: 107 | 108 | ```bash 109 | yarn add babel-eslint -D 110 | ``` 111 | 112 | Babel-eslint allows you to lint ALL valid Babel code 113 | 114 | There are few dependencies that you have to install. 115 | This package provides Airbnb's .eslintrc as an extensible shared config. 116 | 117 | ```bash 118 | yarn add eslint-config-airbnb -D 119 | ``` 120 | 121 | Install ESLint plugin with rules that help validate proper imports. 122 | 123 | ```bash 124 | yarn add eslint-plugin-import -D 125 | ``` 126 | 127 | Install Static AST checker for accessibility rules on JSX elements. 128 | 129 | ```bash 130 | yarn add eslint-plugin-jsx-a11y -D 131 | ``` 132 | 133 | Install eslint plugin for React. 134 | 135 | ```bash 136 | yarn add eslint-plugin-react -D 137 | ``` 138 | 139 | Initialize eslint config. 140 | 141 | ```bash 142 | yarn run eslint --init 143 | ``` 144 | 145 | We're going to use airbnb eslint config. 146 | 147 | Choose the following settings: 148 | 149 | How would you like to configure ESLint? `Use a popular style guide` 150 | Which style guide do you want to follow? `Airbnb` 151 | Do you use React? `Yes` 152 | What format do you want your config file to be in? `JSON` 153 | Would you like to install them now with npm? `No` 154 | 155 | Now we have `.eslintrc.json` with the following configuration 156 | 157 | ```js 158 | { 159 | "extends": "airbnb" 160 | } 161 | ``` 162 | 163 | We're going to use `.js` extensions instead of `.jsx` because JSX is not standard JS, and is not likely to ever be. So we explicitly add this option to the `.eslintc.json`: 164 | 165 | ```js 166 | { 167 | "parser": "babel-eslint", 168 | "rules": { 169 | "react/jsx-filename-extension": ["error", { "extensions": [".js", ".jsx"] }] 170 | }, 171 | "extends": "airbnb" 172 | } 173 | ``` 174 | 175 | ## Step 5 - Material UI integration 176 | 177 | In a nutshell, Material-UI is an open-source project that features React components that implement Google’s Material Design. 178 | 179 | It kick-started in 2014, not long after React came out to the public, and has grown in popularity ever since. With over 35,000 stars on GitHub, Material-UI is one of the top user interface libraries for React out there. 180 | 181 | There are few additional steps that we need to apply before start using material with NextJS framework 182 | 183 | First of all install some additional packages. 184 | 185 | #### Install JSS: 186 | 187 | ``` 188 | yarn add jss 189 | ``` 190 | 191 | JSS is a more powerful abstraction than CSS. It uses JavaScript as a language to describe styles in a declarative and maintainable way. It is a high performance JS to CSS compiler which works at runtime and server-side. 192 | 193 | #### Install react-jss: 194 | 195 | ``` 196 | yarn add react-jss 197 | ``` 198 | 199 | We use React-JSS because it provides components for JSS as a layer of abstraction and has the following range of benefits compared to lower level core: 200 | 201 | - Theming support. 202 | - Critical CSS extraction. 203 | - Lazy evaluation - sheet is created only when the component will mount. 204 | - Auto attach/detach - sheet will be rendered to the DOM when the component is about to mount, and will be removed when no element needs it. 205 | - A Style Sheet gets shared between all elements. 206 | - Function values and rules are updated automatically with props. 207 | 208 | #### Install styled-jsx package for server-side rendering: 209 | 210 | ``` 211 | yarn add styled-jsx 212 | ``` 213 | 214 | Styled-jsx is a full, scoped and component-friendly CSS support for JSX (rendered on the server or the client). 215 | 216 | #### Install material core with icons 217 | 218 | ``` 219 | yarn add @material-ui/core @material-ui/icons 220 | ``` 221 | 222 | @material-ui/core is a set of react components that implement Google's Material Design. 223 | @materail-ui/icons is a set of components with svg icons 224 | 225 | There is an example app that shows how to properly integrate material-ui specifically for nextjs framework. https://github.com/mui-org/material-ui/tree/master/examples/nextjs 226 | 227 | Now create `lib/getPageContext.js` 228 | 229 | ```js 230 | /* eslint-disable no-underscore-dangle */ 231 | 232 | import { SheetsRegistry } from 'jss'; 233 | import { createMuiTheme, createGenerateClassName } from '@material-ui/core/styles'; 234 | 235 | // A theme with custom primary and secondary color. 236 | // It's optional. 237 | const theme = createMuiTheme(); 238 | 239 | function createPageContext() { 240 | return { 241 | theme, 242 | // This is needed in order to deduplicate the injection of CSS in the page. 243 | sheetsManager: new Map(), 244 | // This is needed in order to inject the critical CSS. 245 | sheetsRegistry: new SheetsRegistry(), 246 | // The standard class name generator. 247 | generateClassName: createGenerateClassName(), 248 | }; 249 | } 250 | 251 | export default function getPageContext() { 252 | // Make sure to create a new context for every server-side request so that data 253 | // isn't shared between connections (which would be bad). 254 | if (!process.browser) { 255 | return createPageContext(); 256 | } 257 | 258 | // Reuse context on the client-side. 259 | if (!global.__INIT_MATERIAL_UI__) { 260 | global.__INIT_MATERIAL_UI__ = createPageContext(); 261 | } 262 | 263 | return global.__INIT_MATERIAL_UI__; 264 | } 265 | ``` 266 | 267 | Next.js uses the App component to initialize pages. You can override it and control the page initialization. Which allows you to do amazing things like: 268 | 269 | - Persisting layout between page changes 270 | - Keeping state when navigating pages 271 | - Custom error handling using componentDidCatch 272 | - Inject additional data into pages (for example by processing GraphQL queries) 273 | 274 | To override, create the `./pages/_app.js` file and override the App class as shown below: 275 | 276 | ```js 277 | import React from 'react'; 278 | import App, { Container } from 'next/app'; 279 | import { MuiThemeProvider } from '@material-ui/core/styles'; 280 | import CssBaseline from '@material-ui/core/CssBaseline'; 281 | import JssProvider from 'react-jss/lib/JssProvider'; 282 | import getPageContext from '../lib/getPageContext'; 283 | 284 | class MainApp extends App { 285 | constructor(props) { 286 | super(props); 287 | this.pageContext = getPageContext(); 288 | } 289 | 290 | pageContext = null; 291 | 292 | componentDidMount() { 293 | // Remove the server-side injected CSS. 294 | const jssStyles = document.querySelector('#jss-server-side'); 295 | if (jssStyles && jssStyles.parentNode) { 296 | jssStyles.parentNode.removeChild(jssStyles); 297 | } 298 | } 299 | 300 | render() { 301 | const { Component, pageProps } = this.props; 302 | return ( 303 | 304 | {/* Wrap every page in Jss and Theme providers */} 305 | 309 | {/* MuiThemeProvider makes the theme available down the React 310 | tree thanks to React context. */} 311 | 315 | {/* 316 | CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. 317 | */} 318 | 319 | {/* 320 | Pass pageContext to the _document though the renderPage enhancer to render collected styles on server side. 321 | */} 322 | 323 | 324 | 325 | 326 | ); 327 | } 328 | } 329 | 330 | export default MainApp; 331 | ``` 332 | 333 | You need to add this line to `.eslintrc.json` to avoid `document is not defined` error: 334 | 335 | ``` 336 | "env": { 337 | "browser": true 338 | ``` 339 | 340 | Pages in Next.js skip the definition of the surrounding document's markup. For example, you never include ``, ``, etc. To override that default behavior, you must create a file at `./pages/_document.js`, where you can extend the Document class. 341 | 342 | You need to use codebase from the material-ui official repo example with nextjs: 343 | 344 | ```js 345 | import React from 'react'; 346 | import PropTypes from 'prop-types'; 347 | import Document, { Head, Main, NextScript } from 'next/document'; 348 | import flush from 'styled-jsx/server'; 349 | 350 | class MainDocument extends Document { 351 | render() { 352 | const { pageContext } = this.props; 353 | 354 | return ( 355 | 356 | 357 | Github Client 358 | 359 | {/* Use minimum-scale=1 to enable GPU rasterization */} 360 | 367 | {/* PWA primary color */} 368 | 369 | 373 | 374 | 375 |
376 | 377 | 378 | 379 | ); 380 | } 381 | } 382 | 383 | MainDocument.getInitialProps = (ctx) => { 384 | // Resolution order 385 | // 386 | // On the server: 387 | // 1. app.getInitialProps 388 | // 2. page.getInitialProps 389 | // 3. document.getInitialProps 390 | // 4. app.render 391 | // 5. page.render 392 | // 6. document.render 393 | // 394 | // On the server with error: 395 | // 1. document.getInitialProps 396 | // 2. app.render 397 | // 3. page.render 398 | // 4. document.render 399 | // 400 | // On the client 401 | // 1. app.getInitialProps 402 | // 2. page.getInitialProps 403 | // 3. app.render 404 | // 4. page.render 405 | 406 | // Render app and page and get the context of the page with collected side effects. 407 | let pageContext; 408 | 409 | const page = ctx.renderPage((Component) => { 410 | const WrappedComponent = (props) => { 411 | ({ pageContext } = props); 412 | 413 | return ; 414 | }; 415 | 416 | WrappedComponent.propTypes = { 417 | pageContext: PropTypes.shape({}).isRequired, 418 | }; 419 | 420 | return WrappedComponent; 421 | }); 422 | 423 | return { 424 | ...page, 425 | pageContext, 426 | // Styles fragment is rendered after the app and page rendering finish. 427 | styles: ( 428 | 429 |