├── .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 | [](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 | 
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 |
434 | {flush() || null }
435 |
436 | ),
437 | };
438 | };
439 |
440 | export default MainDocument;
441 | ```
442 |
443 | Now we're ready to implement some pages with components.
444 |
445 | ## Step 6 Storybook Integration
446 |
447 | Storybook is a development environment for UI components. It allows you to browse a component library, view the different states of each component, and interactively develop and test components.
448 |
449 | Here you can find a [Startguide](https://storybook.js.org/basics/guide-react) for react.
450 |
451 | First of all, you need to add `@storybook/react` to your project. To do that, simply run:
452 |
453 | ```bash
454 | yarn add @storybook/react -D
455 | ```
456 |
457 | Then add the following script to your package json in order to start the storybook later in this guide:
458 |
459 | ```json
460 | {
461 | "scripts": {
462 | "storybook": "start-storybook -p 9001 -c .storybook"
463 | }
464 | }
465 | ```
466 |
467 | After that, create the config file.
468 |
469 | Storybook can be configured in several different ways. That’s why we need a config directory. We’ve added a -c option to the above script mentioning .storybook as the config directory.
470 |
471 | For the basic Storybook configuration file, you don’t need to do much, but simply tell Storybook where to find stories.
472 |
473 | For that, simply create a file at .storybook/config.js with the following content:
474 |
475 | ```js
476 | import { configure } from '@storybook/react';
477 |
478 | const req = require.context('../components', true, /stories\.js$/);
479 |
480 | function loadStories() {
481 | req.keys().forEach(req)
482 | }
483 |
484 | configure(loadStories, module);
485 | ```
486 |
487 | Here we use Webpack’s require.context to load modules dynamically. You can learn a lot of interesting things about how to use require.context if you take a look at the relevant Webpack docs.
488 |
489 | So, we need to add babel plugin to make it work.
490 |
491 | ```bash
492 | yarn add babel-plugin-require-context-hook -D
493 | ```
494 |
495 | Update babel config:
496 |
497 | `.babelrc`
498 |
499 | ```js
500 | "test": {
501 | "presets": ["react", "env", "stage-0"],
502 | "plugins": [
503 | "require-context-hook"
504 | ]
505 | }
506 | ```
507 |
508 | Initialize require context hook in testConfig:
509 |
510 | `lib/testConfig.js`
511 |
512 | ```js
513 | import { configure } from 'enzyme';
514 | import Adapter from 'enzyme-adapter-react-16';
515 | import registerRequireContextHook from 'babel-plugin-require-context-hook/register';
516 |
517 | registerRequireContextHook();
518 |
519 | export default configure({ adapter: new Adapter() });
520 | ```
521 |
522 | All files with .stories extension inside the src/components will be required dynamically.
523 |
524 | Create separate babel config for storybook to avoid conflicts with different environments.
525 |
526 | `.babelrc`
527 |
528 | ```js
529 | {
530 | "presets": ["env", "stage-0", "react"]
531 | }
532 | ```
533 |
534 | Adjust eslint config (so we don't see warnings when importing storybook packages):
535 |
536 | `.eslintrc.json`
537 |
538 | ```js
539 | "import/no-extraneous-dependencies": ["error", { "devDependencies": true }]
540 | ```
541 |
542 | Storybook is all about writing stories. Usually a story contains a single state of one of your components. That’s like a visual test case.
543 |
544 | Technically, a story is a function that returns a React element.
545 | We're going to use atomic design methodology for our app.
546 | Popularly known within the design world, Atomic Design helps to build consistent, solid and reusable design systems. Plus, in the world of React that stimulates the componentization, Atomic Design is used unconsciously; but when used in the right way, it becomes a powerful tool for developers.
547 |
548 | First of all create `index.js` entrypoint for components directory:
549 |
550 | `components/index.js`
551 |
552 | ```js
553 | const req = require.context('.', true, /\.\/[^/]+\/[^/]+\/index\.js$/);
554 |
555 | req.keys().forEach((key) => {
556 | const componentName = key.replace(/^.+\/([^/]+)\/index\.js/, '$1');
557 | module.exports[componentName] = req(key).default;
558 | });
559 | ```
560 |
561 | Atomic Design should be a solution, not another problem. If you want to create a component and don't know where to put it (`atoms`, `molecules`, `organisms` etc.), do not worry, do not think too much, just put it anywhere. After you realize what it is, just move the component folder to the right place. Everything else should work.
562 |
563 | This is possible because all components are dynamically exported on `components/index.js` and imported in a way that Atomic Design structure doesn't matter:
564 |
565 | Let's create our first atom - material Button.
566 |
567 | ## Step 7 - Create Atoms
568 |
569 | Atoms are the basic building blocks of matter. Applied to web interfaces, atoms are our HTML tags, such as a form label, an input or a button.
570 |
571 | Atoms can also include more abstract elements like color palettes, fonts and even more invisible aspects of an interface like animations.
572 |
573 | Like atoms in nature they’re fairly abstract and often not terribly useful on their own. However, they’re good as a reference in the context of a pattern library as you can see all your global styles laid out at a glance.
574 |
575 | #### Buttom atom
576 |
577 | `components/atoms/Button/index.js`
578 |
579 | ```js
580 | import React from 'react';
581 | import PropTypes from 'prop-types';
582 | import { Button as MaterialButton } from '@material-ui/core';
583 |
584 | const Button = (props) => {
585 | const { children, ...defaultProps } = props;
586 |
587 | return (
588 |
589 | {children}
590 |
591 | );
592 | };
593 |
594 | Button.propTypes = {
595 | children: PropTypes.node.isRequired,
596 | };
597 |
598 | export default Button;
599 | ```
600 |
601 | And then let’s write a story for this atom.
602 |
603 | `components/atoms/Button/index.stories.js`
604 |
605 | ```js
606 | import React from 'react';
607 | import { action } from '@storybook/addon-actions';
608 | import { storiesOf } from '@storybook/react';
609 | import { Button } from '../..';
610 |
611 | storiesOf('atoms/Button', module)
612 | .add('default', () => (
613 |
614 | Default
615 |
616 | ))
617 | .add('outlined primary', () => (
618 |
619 | Outline Primary
620 |
621 | ))
622 | .add('contained secondary', () => (
623 |
624 | Contained Secondary
625 |
626 | ))
627 | .add('circle button', () => (
628 |
629 | CB
630 |
631 | ))
632 | .add('disabled button', () => (
633 |
634 | Disabled Button
635 |
636 | ));
637 | ```
638 |
639 | Now, let’s run it:
640 |
641 | ```bash
642 | yarn storybook
643 | ```
644 |
645 | Then we will see this:
646 |
647 | 
648 |
649 | ## Step 8 - Add jest for testing
650 |
651 | You need to add Jest as development dependency
652 |
653 | ```bash
654 | yarn add jest -D
655 | ```
656 |
657 | Jest is a complete and ready to set-up JavaScript testing solution. Works out-of-the-box for any React project.
658 |
659 | Add enzyme
660 |
661 | ```bash
662 | yarn add enzyme enzyme-adapter-react-16 -D
663 | ```
664 |
665 | Enzyme is a JavaScript Testing utility for React that makes it easier to assert, manipulate, and traverse your React Components' output.
666 |
667 | We need different babel presets for test environment specifically for nextjs app.
668 |
669 | `.babelrc`
670 |
671 | ```js
672 | {
673 | "env": {
674 | "development": {
675 | "presets": ["next/babel"]
676 | },
677 | "production": {
678 | "presets": ["next/babel"]
679 | },
680 | "test": {
681 | "presets": ["react", "env", "stage-0"]
682 | }
683 | }
684 | }
685 | ```
686 |
687 | Enzyme expects an adapter to be configured
688 |
689 | `lib/testConfig.js`
690 |
691 | ```js
692 | import { configure } from 'enzyme';
693 | import Adapter from 'enzyme-adapter-react-16';
694 |
695 | export default configure({ adapter: new Adapter() });
696 | ```
697 |
698 | Connect test config:
699 |
700 | `package.json`
701 |
702 | ```js
703 | "jest": {
704 | "setupTestFrameworkScriptFile": "./lib/testConfig.js"
705 | }
706 | ```
707 |
708 | Add to `eslintrc.json`:
709 |
710 | ```js
711 | "env": {
712 | "jest": true
713 | },
714 | ```
715 |
716 | This will add all the jest related things to your environment, eliminating the linter errors/warnings
717 |
718 | Add to `package.json`:
719 |
720 | ```js
721 | {
722 | "scripts": {
723 | "test": "jest"
724 | }
725 | }
726 | ```
727 |
728 | It allows us to use `yarn test` to run all jest specs.
729 |
730 | Add simple test for our Button component:
731 |
732 | ```js
733 | import React from 'react';
734 | import { shallow } from 'enzyme';
735 | import Button from '.';
736 |
737 | describe('Button', () => {
738 | it('renders children when passed in', () => {
739 | const wrapper = shallow(Test );
740 | expect(wrapper.contains('Test')).toBe(true);
741 | });
742 | });
743 | ```
744 |
745 | Now if we run yarn test we should have 1 passed spec.
746 |
747 | Got it? Now, let's add more simple components (atoms) which we will use for our home page.
748 |
749 | #### AppBar atom
750 |
751 | `components/atoms/AppBar/index.js`
752 |
753 | ```js
754 | import React from 'react';
755 | import PropTypes from 'prop-types';
756 | import { AppBar as MaterialAppBar } from '@material-ui/core';
757 |
758 | const AppBar = (props) => {
759 | const { children, ...defaultProps } = props;
760 |
761 | return (
762 |
763 | {children}
764 |
765 | );
766 | };
767 |
768 | AppBar.propTypes = {
769 | children: PropTypes.node.isRequired,
770 | };
771 |
772 | export default AppBar;
773 | ```
774 |
775 | `components/atoms/AppBar/index.stories.js`
776 |
777 | ```js
778 | import React from 'react';
779 | import { storiesOf } from '@storybook/react';
780 | import { AppBar } from '../..';
781 |
782 | storiesOf('atoms/AppBar', module)
783 | .add('default', () => (
784 |
785 | Example of AppBar
786 |
787 | ))
788 | .add('secondary', () => (
789 |
790 | Secondary color
791 |
792 | ));
793 | ```
794 |
795 | `components/atoms/AppBar/index.test.js`
796 |
797 | ```js
798 | import React from 'react';
799 | import { shallow } from 'enzyme';
800 | import AppBar from '.';
801 |
802 | describe('AppBar', () => {
803 | it('renders children when passed in', () => {
804 | const wrapper = shallow(test
);
805 | expect(wrapper.contains(test
)).toBe(true);
806 | });
807 | });
808 | ```
809 |
810 | #### Card atom
811 |
812 | `components/atoms/Card/index.js`
813 |
814 | ```js
815 | import React from 'react';
816 | import PropTypes from 'prop-types';
817 | import { Card as MaterialCard } from '@material-ui/core';
818 |
819 | const Card = (props) => {
820 | const { children, ...defaultProps } = props;
821 |
822 | return (
823 |
824 | {children}
825 |
826 | );
827 | };
828 |
829 | Card.propTypes = {
830 | children: PropTypes.node.isRequired,
831 | };
832 |
833 | export default Card;
834 | ```
835 |
836 | `components/atoms/Card/index.stories.js`
837 |
838 | ```js
839 | import React from 'react';
840 | import { storiesOf } from '@storybook/react';
841 | import { Card } from '../..';
842 |
843 | storiesOf('atoms/Card', module)
844 | .add('default', () => (
845 |
846 | Default
847 |
848 | ));
849 | ```
850 |
851 | `components/atoms/Card/index.test.js`
852 |
853 | ```js
854 | import React from 'react';
855 | import { shallow } from 'enzyme';
856 | import Card from '.';
857 |
858 | describe('Card', () => {
859 | it('renders children when passed in', () => {
860 | const wrapper = shallow(
861 |
862 | Some text
863 | Test
864 | ,
865 | );
866 | expect(wrapper.contains('Test')).toBe(true);
867 | expect(wrapper.contains('Some text')).toBe(true);
868 | });
869 | });
870 | ```
871 |
872 | #### CardActions atom
873 |
874 | `components/atoms/CardActions/index.js`
875 |
876 | ```js
877 | import React from 'react';
878 | import PropTypes from 'prop-types';
879 | import { CardActions as MaterialCardActions } from '@material-ui/core';
880 |
881 | const CardActions = (props) => {
882 | const { children, ...defaultProps } = props;
883 |
884 | return (
885 |
886 | {children}
887 |
888 | );
889 | };
890 |
891 | CardActions.propTypes = {
892 | children: PropTypes.node.isRequired,
893 | };
894 |
895 | export default CardActions;
896 | ```
897 |
898 | `components/atoms/CardActions/index.stories.js`
899 |
900 | ```js
901 | import React from 'react';
902 | import { storiesOf } from '@storybook/react';
903 | import { CardActions, Button } from '../..';
904 |
905 | storiesOftoms/CardActions', module)
906 | .add('with button', () => (
907 |
908 | Button
909 |
910 | ));
911 | ```
912 |
913 | `components/atoms/CardActions/index.test.js`
914 |
915 | ```js
916 | import React from 'react';
917 | import { shallow } from 'enzyme';
918 | import CardActions from '.';
919 |
920 | describe('CardActions', () => {
921 | it('renders children when passed in', () => {
922 | const wrapper = shallow(
923 |
924 | Some text
925 | Test
926 | ,
927 | );
928 | expect(wrapper.contains('Test')).toBe(true);
929 | expect(wrapper.contains('Some text')).toBe(true);
930 | });
931 | });
932 | ```
933 |
934 | #### CardContent atom
935 |
936 | `components/atoms/CardContent/index.js`
937 |
938 | ```js
939 | import React from 'react';
940 | import PropTypes from 'prop-types';
941 | import { CardContent as MaterialCardContent } from '@material-ui/core';
942 |
943 | const CardContent = (props) => {
944 | const { children, ...defaultProps } = props;
945 |
946 | return (
947 |
948 | {children}
949 |
950 | );
951 | };
952 |
953 | CardContent.propTypes = {
954 | children: PropTypes.node.isRequired,
955 | };
956 |
957 | export default CardContent;
958 | ```
959 |
960 | `components/atoms/CardContent/index.stories.js`
961 |
962 | ```js
963 | import React from 'react';
964 | import { storiesOf } from '@storybook/react';
965 | import { CardContent } from '../..';
966 |
967 | storiesOfardContent', module)
968 | .add('default', () => (
969 |
970 | Lorem
971 | Lorem Ipsum
972 |
973 | ));
974 | ```
975 |
976 | `components/atoms/CardContent/index.test.js`
977 |
978 | ```js
979 | import React from 'react';
980 | import { shallow } from 'enzyme';
981 | import CardContent from '.';
982 |
983 | describe('CardContent', () => {
984 | it('renders children when passed in', () => {
985 | const wrapper = shallow(
986 |
987 | Lorem
988 | Ipsum
989 | ,
990 | );
991 | expect(wrapper.contains('Lorem')).toBe(true);
992 | expect(wrapper.contains('Ipsum')).toBe(true);
993 | });
994 | });
995 | ```
996 |
997 | We're gonna create few more atoms using same approach:
998 |
999 | - IconButton
1000 | - List
1001 | - ListItem
1002 | - ListItemIcon
1003 | - ListItemText
1004 | - Loader
1005 | - MenuIcon
1006 | - SwipeableDrawer
1007 | - Toolbar
1008 | - Typography
1009 | - Grid
1010 |
1011 | ## Step 9 - Creating Moleculus
1012 |
1013 | Things are getting more interesting and tangible when we start combining atoms together. Molecules are groups of atoms bonded together and are the smallest fundamental units of a compound (just like in real world). These molecules take on their own properties and serve as the backbone of our design systems.
1014 |
1015 | For example, a form label, input or button aren’t too useful by themselves, but combine them together as a form and now they can actually do something together.
1016 |
1017 | Building up to molecules from atoms encourages a “do one thing and do it well” mentality. While molecules can be complex, as a rule of thumb they are relatively simple combinations of atoms built for reuse.
1018 |
1019 | Now we need to create first molecule component. It will be SimpleCard that consists of atoms.
1020 |
1021 | #### SimpleCard Molecule
1022 |
1023 | `components/moleculus/SimpleCard/index.js`
1024 |
1025 | ```js
1026 | import React from 'react';
1027 | import PropTypes from 'prop-types';
1028 | import { withStyles } from '@material-ui/core/styles';
1029 |
1030 | import {
1031 | Card, CardContent, Typography, CardActions, Button,
1032 | } from 'components';
1033 |
1034 | const styles = {
1035 | card: {
1036 | minWidth: 100,
1037 | },
1038 | bullet: {
1039 | display: 'inline-block',
1040 | margin: '0 2px',
1041 | transform: 'scale(0.8)',
1042 | },
1043 | title: {
1044 | marginBottom: 16,
1045 | fontSize: 14,
1046 | },
1047 | pos: {
1048 | marginBottom: 12,
1049 | },
1050 | };
1051 |
1052 | const SimpleCard = (props) => {
1053 | const {
1054 | classes, title, description, url,
1055 | } = props;
1056 |
1057 | return (
1058 |
1059 |
1060 |
1061 | {title}
1062 |
1063 |
1064 | {description}
1065 |
1066 |
1067 |
1068 | Learn More
1069 |
1070 |
1071 | );
1072 | };
1073 |
1074 | SimpleCard.propTypes = {
1075 | classes: PropTypes.shape({}).isRequired,
1076 | title: PropTypes.string.isRequired,
1077 | description: PropTypes.string,
1078 | url: PropTypes.string,
1079 | };
1080 |
1081 | SimpleCard.defaultProps = {
1082 | description: 'No description',
1083 | url: null,
1084 | };
1085 |
1086 | export default withStyles(styles)(SimpleCard);
1087 | ```
1088 |
1089 | `components/moleculus/SimpleCard/index.stories.js`
1090 |
1091 | ```js
1092 | import React from 'react';
1093 | import { storiesOf } from '@storybook/react';
1094 | import { SimpleCard } from '../..';
1095 |
1096 | storiesOf('moleculus/SimpleCard', module)
1097 | .add('default', () => (
1098 |
1099 | ));
1100 | ```
1101 |
1102 | `components/moleculus/SimpleCard/index.test.js`
1103 |
1104 | ```js
1105 | import React from 'react';
1106 | import { mount } from 'enzyme';
1107 | import { SimpleCard, Typography } from '../..';
1108 |
1109 | describe('SimpleCard', () => {
1110 | it('renders header with correct title', () => {
1111 | const wrapper = mount( );
1112 | const typographyNodes = wrapper.find(Typography);
1113 | expect(typographyNodes.first().text()).toEqual('foo');
1114 | expect(typographyNodes.last().text()).toEqual('bar');
1115 | });
1116 | });
1117 | ```
1118 |
1119 | #### Create Header molecule
1120 |
1121 | `components/moleculus/Header/index.js`
1122 |
1123 | ```js
1124 | import React from 'react';
1125 | import PropTypes from 'prop-types';
1126 | import { withStyles } from '@material-ui/core/styles';
1127 |
1128 | import {
1129 | AppBar, IconButton,
1130 | MenuIcon, Toolbar, Typography,
1131 | } from '../..';
1132 |
1133 | const styles = {
1134 | root: {
1135 | flexGrow: 1,
1136 | },
1137 | flex: {
1138 | flexGrow: 1,
1139 | },
1140 | menuButton: {
1141 | marginLeft: -12,
1142 | marginRight: 20,
1143 | },
1144 | };
1145 |
1146 | const Header = (props) => {
1147 | const {
1148 | classes, swipeableMenu, loginButton, title, openMenu,
1149 | } = props;
1150 |
1151 | return (
1152 |
1153 |
1154 | {swipeableMenu}
1155 |
1156 |
1157 |
1158 |
1159 |
1160 | {title}
1161 |
1162 | {loginButton}
1163 |
1164 |
1165 |
1166 | );
1167 | };
1168 |
1169 | Header.propTypes = {
1170 | swipeableMenu: PropTypes.node,
1171 | loginButton: PropTypes.node,
1172 | classes: PropTypes.shape().isRequired,
1173 | title: PropTypes.string,
1174 | openMenu: PropTypes.func,
1175 | };
1176 |
1177 | Header.defaultProps = {
1178 | swipeableMenu: null,
1179 | loginButton: null,
1180 | title: null,
1181 | openMenu: null,
1182 | };
1183 |
1184 | export default withStyles(styles)(Header);
1185 | ```
1186 |
1187 | `components/moleculus/Header/index.stories.js`
1188 |
1189 | ```js
1190 | import React from 'react';
1191 | import { storiesOf } from '@storybook/react';
1192 | import { Header } from '../..';
1193 |
1194 | storiesOf('moleculus/Header', module)
1195 | .add('default', () => (
1196 |
1197 | ))
1198 | .add('with title', () => (
1199 |
1200 | ));
1201 | ```
1202 |
1203 | `components/moleculus/Header/index.test.js`
1204 |
1205 | ```js
1206 | import React from 'react';
1207 | import { mount } from 'enzyme';
1208 | import { Header, Typography } from '../..';
1209 |
1210 | describe('Header', () => {
1211 | it('renders header with correct title', () => {
1212 | const wrapper = mount();
1213 | const typographyNode = wrapper.find(Typography);
1214 |
1215 | expect(typographyNode.text()).toEqual('foo');
1216 | });
1217 | });
1218 | ```
1219 |
1220 | ## Step 10 - Build Organisms
1221 |
1222 | Organisms are groups of molecules joined together to form a relatively complex, distinct section of an interface.
1223 |
1224 | We’re starting to get increasingly concrete. A client might not be terribly interested in the molecules of a design system, but with organisms we can see the final interface beginning to take shape.
1225 |
1226 | Organisms can consist of similar and/or different molecule types. For example, a masthead organism might consist of diverse components like a logo, primary navigation, search form, and list of social media channels. But a “product grid” organism might consist of the same molecule (possibly containing a product image, product title and price) repeated over and over again.
1227 |
1228 | Building up from molecules to organisms encourages creating standalone, portable, reusable components.
1229 |
1230 | #### Let’s create a Header with swipeable menu organism:
1231 |
1232 | `components/organisms/HeaderWithSwipeableMenu/index.js`
1233 |
1234 | ```js
1235 | import React from 'react';
1236 | import PropTypes from 'prop-types';
1237 | import { Header, SwipeableMenu } from 'components';
1238 |
1239 | const HeaderWithSwipeableMenu = (props) => {
1240 | const {
1241 | closeMenu, openMenu, loginButtonContainer, leftMenuIsOpened,
1242 | } = props;
1243 |
1244 | const MENU_ITEMS = [
1245 | {
1246 | id: 1,
1247 | url: '/',
1248 | text: 'Home',
1249 | },
1250 | {
1251 | id: 2,
1252 | url: '/top_ruby',
1253 | text: 'Top Ruby Repositories',
1254 | },
1255 | {
1256 | id: 3,
1257 | url: '/top_js',
1258 | text: 'Top Javascript Repositories',
1259 | },
1260 | {
1261 | id: 4,
1262 | url: '/new_js',
1263 | text: 'New Javascript Repositories',
1264 | },
1265 | ];
1266 |
1267 | return (
1268 |
1278 | )}
1279 | loginButton={loginButtonContainer}
1280 | />
1281 | );
1282 | };
1283 |
1284 | HeaderWithSwipeableMenu.propTypes = {
1285 | leftMenuIsOpened: PropTypes.bool.isRequired,
1286 | openMenu: PropTypes.func.isRequired,
1287 | closeMenu: PropTypes.func.isRequired,
1288 | loginButtonContainer: PropTypes.node.isRequired,
1289 | };
1290 |
1291 | export default HeaderWithSwipeableMenu;
1292 | ```
1293 |
1294 | `components/organisms/HeaderWithSwipeableMenu/index.stories.js`
1295 |
1296 | ```js
1297 | import React from 'react';
1298 | import { storiesOf } from '@storybook/react';
1299 | import { HeaderWithSwipeableMenu } from 'components';
1300 | import { action } from '@storybook/addon-actions';
1301 |
1302 | storiesOf('organisms/HeaderWithSwipeableMenu', module)
1303 | .add('default with click event', () => (
1304 |
1305 | ))
1306 | .add('opened by default', () => (
1307 |
1308 | ));
1309 | ```
1310 |
1311 | `components/organisms/HeaderWithSwipeableMenu/index.test.js`
1312 |
1313 | ```js
1314 | import React from 'react';
1315 | import { mount } from 'enzyme';
1316 | import { HeaderWithSwipeableMenu } from 'components';
1317 |
1318 | describe('HeaderWithSwipeableMenu', () => {
1319 | it('renders header with swipeable menu', () => {
1320 | const mockedOpenMenu = jest.fn();
1321 | const mockedCloseMenu = jest.fn();
1322 |
1323 | const wrapper = mount(
1324 | }
1329 | />,
1330 | );
1331 |
1332 | const expectedMenuItems = [
1333 | 'Top Javascript Repositories',
1334 | 'Home',
1335 | 'Top Ruby Repositories',
1336 | 'New Javascript Repositories',
1337 | ];
1338 |
1339 | wrapper.find('ListItemText').find('Typography').forEach((node) => {
1340 | expect(expectedMenuItems).toContain(node.text());
1341 | });
1342 |
1343 | wrapper.find('button').simulate('click');
1344 | expect(mockedOpenMenu).toHaveBeenCalled();
1345 | });
1346 | });
1347 | ```
1348 |
1349 | ## Step 11 - Creating Templates
1350 |
1351 | Templates are page-level objects that place components into a layout and articulate the design’s underlying content structure. To build on our previous example, we can take the HeaderWithMenu organism and apply it to a home template.
1352 |
1353 | This Home template displays all the necessary page components functioning together, which provides context for these relatively abstract molecules and organisms. When crafting an effective design system, it’s critical to demonstrate how components look and function together in the context of a layout to prove the parts add up to a well-functioning whole.
1354 |
1355 | Another important characteristic of templates is that they focus on the page’s underlying content structure rather than the page’s final content. Design systems must account for the dynamic nature of content, so it’s very helpful to articulate important properties of components like image sizes and character lengths for headings and text passages.
1356 |
1357 | #### Let’s build Home template:
1358 |
1359 | `components/templates/Home/index.js`
1360 |
1361 | ```js
1362 | import React from 'react';
1363 | import PropTypes from 'prop-types';
1364 | import { Grid } from '@material-ui/core';
1365 |
1366 | const Home = (props) => {
1367 | const { header, footer, content } = props;
1368 |
1369 | return (
1370 |
1371 | {header}
1372 |
1373 |
1374 | {content}
1375 |
1376 |
1377 | {footer}
1378 |
1379 | );
1380 | };
1381 |
1382 | Home.propTypes = {
1383 | header: PropTypes.node,
1384 | content: PropTypes.node,
1385 | footer: PropTypes.string,
1386 | };
1387 |
1388 | Home.defaultProps = {
1389 | header: null,
1390 | content: null,
1391 | footer: null,
1392 | };
1393 |
1394 | export default (Home);
1395 | ```
1396 |
1397 | `components/templates/Home/index.stories.js`
1398 |
1399 | ```js
1400 | import React from 'react';
1401 | import { storiesOf } from '@storybook/react';
1402 | import { Home } from '../..';
1403 |
1404 | storiesOf('templates/Home', module)
1405 | .add('default', () => (
1406 |
1413 | ));
1414 | ```
1415 |
1416 | `components/templates/Home/index.test.js`
1417 |
1418 | ```js
1419 | import React from 'react';
1420 | import { mount } from 'enzyme';
1421 | import { Home, SimpleCard } from '../..';
1422 |
1423 | describe('Home', () => {
1424 | it('renders component with passed card components', () => {
1425 | const wrapper = mount(
1426 | ,
1429 | ,
1430 | ]}
1431 | />,
1432 | );
1433 |
1434 | expect(wrapper.find(SimpleCard)).toHaveLength(2);
1435 | });
1436 | });
1437 | ```
1438 |
1439 | ## Step 12 - Create Pages
1440 |
1441 | Now we can use nextjs pages as an entry point
1442 |
1443 | `pages/index.js`
1444 |
1445 | ```js
1446 | import React from 'react';
1447 | import { Home } from 'components';
1448 | import HeaderContainer from 'containers/HeaderContainer';
1449 | import ViewerRepoList from 'containers/ViewerRepoList';
1450 |
1451 | const Index = () => (
1452 | }
1454 | content={ }
1455 | />
1456 | );
1457 |
1458 | export default Index;
1459 | ```
1460 |
1461 | The file-system is the main API. Every .js file becomes a route that gets automatically processed and rendered. If we run yarn dev we can access this page on localhost:3000
1462 |
1463 | ## Step 13 - Implementing Authentication
1464 |
1465 | To communicate with github graphql api we need to create a github application first. Follow these steps to create your github app https://developer.github.com/apps/building-github-apps/creating-a-github-app/
1466 |
1467 | For development environment Homepage URL - http://localhost:3000, Authorization callback URL - http://localhost:3000/auth/github/callback
1468 |
1469 | To use github secret keys we will use dotenv package.
1470 |
1471 | create `next.config.js` with following commands:
1472 |
1473 | `next.config.js`
1474 |
1475 | ```js
1476 | require('dotenv').config();
1477 |
1478 | const path = require('path');
1479 | const Dotenv = require('dotenv-webpack');
1480 |
1481 | module.exports = {
1482 | webpack: (config) => {
1483 | config.plugins = config.plugins || [];
1484 |
1485 | config.plugins = [
1486 | ...config.plugins,
1487 |
1488 | // Read the .env file
1489 | new Dotenv({
1490 | path: path.join(__dirname, '.env'),
1491 | systemvars: true,
1492 | }),
1493 | ];
1494 |
1495 | return config;
1496 | },
1497 | };
1498 | ```
1499 |
1500 | Now, you need to add github app secret keys. We can use `.env` file for storing secret API keys, that will be available only on server side.
1501 |
1502 | `.env`
1503 |
1504 | ```js
1505 | GITHUB_CLIENT_ID = ''
1506 | GITHUB_CLIENT_SECRET = ''
1507 | ```
1508 |
1509 | Add this file to `.gitignore`
1510 |
1511 | #### Containers
1512 |
1513 | If we need to implement some component with it's own state management or side effects (in other words smart component) we place it in `containers` folder. All components with graphql/REST requests will be there.
1514 |
1515 | We can use graphql queries in our containers using `react-apollo` Query component.
1516 |
1517 | #### Github Login Button Container
1518 |
1519 | We need login button container to make request to github authentication endpoint and redirect to callback page.
1520 |
1521 | `containers/GithubLoginButtonContainer/index.js`
1522 |
1523 | ```js
1524 | import React from 'react';
1525 | import Router from 'next/router';
1526 | import { Query } from 'react-apollo';
1527 | import { Button, Loader } from 'components';
1528 | import Cookies from 'js-cookie';
1529 | import viewer from 'graphql/queries/viewer';
1530 |
1531 | class GithubLoginButtonContainer extends React.Component {
1532 | handleSignIn = () => {
1533 | Router.push({
1534 | pathname: 'https://github.com/login/oauth/authorize',
1535 | query: {
1536 | client_id: '55a16b6d3467b24fdde9',
1537 | },
1538 | });
1539 | };
1540 |
1541 | handleSignOut = () => {
1542 | Cookies.remove('access_token');
1543 | Router.push('/');
1544 | };
1545 |
1546 | render() {
1547 | const { handleSignIn, handleSignOut } = this;
1548 |
1549 | return (
1550 |
1551 | {({ loading, error, data }) => {
1552 | if (loading) {
1553 | return (
1554 |
1555 | );
1556 | }
1557 | if (error) {
1558 | return (
1559 |
1560 | Sign In
1561 |
1562 | );
1563 | }
1564 | return (
1565 |
1566 | {data.viewer.login}
1567 |
1568 | Sign Out
1569 |
1570 |
1571 | );
1572 | }}
1573 |
1574 | );
1575 | }
1576 | }
1577 |
1578 | export default GithubLoginButtonContainer;
1579 | ```
1580 |
1581 | Next, create tests for our container
1582 |
1583 | `containers/GithubLoginButtonContainer/index.test.js`
1584 |
1585 | ```js
1586 | import React from 'react';
1587 | import { shallow } from 'enzyme';
1588 | import GithubLoginButton from 'containers/GithubLoginButton';
1589 |
1590 | jest.mock('next/config', () => () => ({ publicRuntimeConfig: { GithubClientId: '123' } }));
1591 |
1592 | jest.mock('next/router', () => (
1593 | { push: () => ({}) }
1594 | ));
1595 |
1596 | describe('GithubLoginButton', () => {
1597 | it('renders children when passed in', () => {
1598 | const wrapper = shallow(Test );
1599 | expect(wrapper.contains('Test')).toBe(true);
1600 | });
1601 |
1602 | it('calls SignIn handler', () => {
1603 | const wrapper = shallow(Test );
1604 | const instance = wrapper.instance();
1605 | jest.spyOn(instance, 'handleSignIn');
1606 | instance.forceUpdate();
1607 | wrapper.find('Button').simulate('click');
1608 | expect(instance.handleSignIn).toHaveBeenCalled();
1609 | });
1610 | });
1611 | ```
1612 |
1613 | #### Callback page
1614 |
1615 | GitHub redirects to a callback url on your website (which you provided when registering the app with GitHub).
1616 |
1617 | We need to implement callback page which will be used to obtain `access_token`.
1618 |
1619 | Install `isomorphic-unfetch` (Tiny 500b `fetch` "barely-polyfill"). We will use this package for authentication REST requests only.
1620 |
1621 | ```bash
1622 | yarn add isomorphic-unfetch
1623 | ```
1624 |
1625 | We need to store `access_token` on a client side using cookies. A simple, lightweight JavaScript API for handling cookies.
1626 |
1627 | ```bash
1628 | yarn add js-cookie
1629 | ```
1630 |
1631 | `pages/auth/github/login.js`
1632 |
1633 | ```js
1634 | import React from 'react';
1635 | import Router, { withRouter } from 'next/router';
1636 | import fetch from 'isomorphic-unfetch';
1637 | import Cookies from 'js-cookie';
1638 | import PropTypes from 'prop-types';
1639 |
1640 | class Callback extends React.Component {
1641 | static propTypes = {
1642 | errorMessage: PropTypes.string,
1643 | accessToken: PropTypes.string,
1644 | };
1645 |
1646 | static defaultProps = {
1647 | errorMessage: undefined,
1648 | accessToken: undefined,
1649 | };
1650 |
1651 | componentDidMount() {
1652 | const { accessToken } = this.props;
1653 |
1654 | if (accessToken) {
1655 | Cookies.set('access_token', accessToken);
1656 | Router.push('/');
1657 | }
1658 | }
1659 |
1660 | static async getInitialProps({ query }) {
1661 | const bodyData = JSON.stringify({
1662 | client_id: process.env.GITHUB_CLIENT_ID,
1663 | client_secret: process.env.GITHUB_CLIENT_SECRET,
1664 | code: query.code,
1665 | });
1666 |
1667 | const res = await fetch('https://github.com/login/oauth/access_token', {
1668 | headers: {
1669 | Accept: 'application/json',
1670 | 'Content-Type': 'application/json',
1671 | },
1672 | method: 'POST',
1673 | body: bodyData,
1674 | });
1675 |
1676 | const json = await res.json();
1677 | const errorMessage = json.error_description;
1678 | return { errorMessage, accessToken: json.access_token };
1679 | }
1680 |
1681 | render() {
1682 | const { errorMessage } = this.props;
1683 |
1684 | if (errorMessage) {
1685 | return (
1686 | {errorMessage}
1687 | );
1688 | }
1689 |
1690 | return null;
1691 | }
1692 | }
1693 |
1694 | export default withRouter(Callback);
1695 | ```
1696 |
1697 | `getInitialProps` is an async static method. It can asynchronously fetch anything that resolves to a JavaScript plain Object, which populates props.
1698 |
1699 | Data returned from `getInitialProps` is serialized when server rendering, similar to a `JSON.stringify`. Make sure the returned object from `getInitialProps` is a plain Object and not using `Date`, `Map` or `Set`.
1700 |
1701 | For the initial page load, `getInitialProps` will execute on the server only. `getInitialProps` will only be executed on the client when navigating to a different route via the Link component or using the routing APIs.
1702 |
1703 | ## Step 14 - GraphQL with Apollo integration
1704 |
1705 | Apollo Client is the best way to use GraphQL to build client applications. The client is designed to help you quickly build a UI that fetches data with GraphQL, and can be used with any JavaScript front-end.
1706 |
1707 | With Apollo’s declarative approach to data fetching, all of the logic for retrieving your data, tracking loading and error states, and updating your UI is encapsulated in a single Query component. This encapsulation makes composing your Query components with your presentational components very easy.
1708 |
1709 | Let’s see how it looks like in practice with React Apollo.
1710 |
1711 | The easiest way to get started with Apollo Client is by using Apollo Boost. It's a starter kit that configures your client for you with recommended settings.
1712 |
1713 | Install packages:
1714 |
1715 | ```bash
1716 | yarn add apollo-boost react-apollo graphql graphql-tag
1717 | ```
1718 |
1719 | - `apollo-boost` Package containing everything you need to set up Apollo Client
1720 | - `react-apollo` View layer integration for React
1721 | - `graphql` Also parses your GraphQL queries
1722 | - `graphql-tag` Provides template literal tag you can use to concisely write a GraphQL query that is parsed into the standard GraphQL AST
1723 |
1724 | Now you have all the dependencies you need, let’s create your Apollo Client. The only thing you need to get started is the endpoint for your GraphQL server.
1725 |
1726 | If you don’t pass in uri directly, it defaults to the /graphql endpoint on the same host your app is served from.
1727 |
1728 |
1729 | #### GraphQL authentication
1730 |
1731 | Apollo Client uses the ultra flexible Apollo Link that includes several options for authentication.
1732 |
1733 | We will use cookies for storing github access_token and send it as an authorization header. It’s easy to add an authorization header to every HTTP request by adding `headers` to ApolloClient.
1734 |
1735 | It’s very easy to tell your network interface to send the cookie along with every request. You just need to pass the headers option. e.g. `headers: 'token'`
1736 |
1737 | In this example, we’ll pull the login token from cookies every time a request is sent:
1738 |
1739 | `pages/_app.js`
1740 |
1741 | ```js
1742 | const token = Cookies.get('access_token');
1743 |
1744 | const client = new ApolloClient({
1745 | uri: 'https://api.github.com/graphql',
1746 | headers: { authorization: `Bearer ${token}` },
1747 | });
1748 |
1749 | class MainApp extends App {
1750 | // ...
1751 | render() {
1752 | // ...
1753 | const token = Cookies.get('access_token');
1754 |
1755 | const client = new ApolloClient({
1756 | uri: 'https://api.github.com/graphql',
1757 | headers: { authorization: `Bearer ${token}` },
1758 | });
1759 |
1760 | return (
1761 |
1762 |
1763 | {/* ... */}
1764 |
1765 |
1766 | );
1767 | }
1768 | }
1769 | ```
1770 |
1771 | After that we can make requests to Github API using our token from cookies.
1772 |
1773 | #### GraphQL queries
1774 |
1775 | Let's implement our first graphql query
1776 |
1777 | This query finds last 50 repositories with more than 10000 stars
1778 |
1779 | `queries/searchTopRubyRepos.js`
1780 |
1781 | ```js
1782 | import gql from 'graphql-tag';
1783 |
1784 | const searchTopRubyRepos = gql`
1785 | {
1786 | search(query: "language:Ruby stars:>10000", first: 50, type: REPOSITORY) {
1787 | edges {
1788 | node {
1789 | ... on Repository {
1790 | name
1791 | description
1792 | url
1793 | }
1794 | }
1795 | }
1796 | }
1797 | }
1798 | `;
1799 |
1800 | export default searchTopRubyRepos;
1801 | ```
1802 |
1803 | `containers/SearchRepoList/index.js`
1804 |
1805 | ```js
1806 | import React from 'react';
1807 | import PropTypes from 'prop-types';
1808 | import {
1809 | SimpleCard, Loader, Grid, Typography,
1810 | } from 'components';
1811 | import { Query } from 'react-apollo';
1812 |
1813 | const SearchRepoList = ({ query }) => (
1814 |
1815 | {({ loading, error, data }) => {
1816 | if (loading) {
1817 | return (
1818 |
1819 |
1820 |
1821 | );
1822 | }
1823 | if (error) {
1824 | return (
1825 |
1826 | Please Sign In to fetch data
1827 |
1828 | );
1829 | }
1830 | return (
1831 |
1832 | {data.search.edges.map(repo => (
1833 |
1834 |
1839 |
1840 | ))}
1841 |
1842 | );
1843 | }}
1844 |
1845 | );
1846 |
1847 | SearchRepoList.propTypes = {
1848 | query: PropTypes.node.isRequired,
1849 | };
1850 |
1851 | export default SearchRepoList;
1852 | ```
1853 |
1854 | Now let's implement nextjs page
1855 |
1856 | ```js
1857 | import React from 'react';
1858 | import { Home } from 'components';
1859 | import HeaderWithMenu from 'containers/HeaderWithMenu';
1860 | import SearchRepoList from 'containers/SearchRepoList';
1861 | import searchTopRubyRepos from 'graphql/queries/searchTopRubyRepos';
1862 |
1863 | const TopRuby = () => (
1864 | }
1866 | content={ }
1867 | />
1868 | );
1869 |
1870 | export default TopRuby;
1871 | ```
1872 |
1873 | ## Step 15 - Creating Snapshot tests
1874 |
1875 | Snapshot testing is a very useful tool whenever you want to make sure your UI doesn’t change unexpectedly.
1876 |
1877 | A typical snapshot test case for a mobile app renders a UI component, takes a screenshot, then compares it to a reference image stored alongside the test. The test will fail if the two images don’t match: either the change is unexpected, or the screenshot needs to be updated to the new version of the UI component.
1878 |
1879 | To add react-test-render, write this command
1880 |
1881 | ```bash
1882 | yarn add react-test-renderer -D
1883 | ```
1884 |
1885 | This package provides a React renderer that can be used to render React components to pure JavaScript objects, without depending on the DOM or a native mobile environment.
1886 |
1887 | `__tests__/index.test.js`
1888 |
1889 | ```js
1890 | import React from 'react';
1891 | import renderer from 'react-test-renderer';
1892 | import Index from 'pages';
1893 | import { mount } from 'enzyme';
1894 | import viewerLast100Repositories from 'graphql/queries/viewerLast100Repositories';
1895 | import { MockedProvider } from 'react-apollo/test-utils';
1896 |
1897 | describe('Home Page', () => {
1898 | it('renders loading state initially', () => {
1899 | const component = renderer.create(
1900 |
1901 |
1902 | ,
1903 | );
1904 |
1905 | const tree = component.toJSON();
1906 | expect(tree).toMatchSnapshot();
1907 | });
1908 |
1909 | it('renders cards with all information on success', async () => {
1910 | const mocks = [
1911 | {
1912 | request: {
1913 | query: viewerLast100Repositories,
1914 | },
1915 | result: {
1916 | data: {
1917 | viewer: {
1918 | repositories: {
1919 | edges: [
1920 | { node: { id: '1', name: 'repo name', description: 'desc' } },
1921 | ],
1922 | },
1923 | },
1924 | },
1925 | },
1926 | },
1927 | ];
1928 |
1929 | const component = renderer.create(
1930 |
1931 |
1932 | ,
1933 | );
1934 | await new Promise(resolve => setTimeout(resolve));
1935 |
1936 | const tree = component.toJSON();
1937 | expect(tree).toMatchSnapshot();
1938 | });
1939 |
1940 | it('renders correct message on error', async () => {
1941 | const mock = {
1942 | request: {
1943 | query: viewerLast100Repositories,
1944 | },
1945 | error: new Error('error'),
1946 | };
1947 |
1948 | const component = renderer.create(
1949 |
1950 |
1951 | ,
1952 | );
1953 | await new Promise(resolve => setTimeout(resolve));
1954 |
1955 | const tree = component.toJSON();
1956 | expect(tree).toMatchSnapshot();
1957 | });
1958 | });
1959 | ```
1960 |
1961 | `__tests__/auth/github/callback.test.js`
1962 |
1963 | ```js
1964 | import React from 'react';
1965 | import renderer from 'react-test-renderer';
1966 | import Router from 'next/router';
1967 | import Callback from '../../../pages/auth/github/callback';
1968 |
1969 | describe('Github Callback Page', () => {
1970 |
1971 | it('matches snapshot', () => {
1972 | const mockedRouter = { push: () => {} };
1973 | Router.router = mockedRouter;
1974 | const component = renderer.create( );
1975 | const tree = component.toJSON();
1976 | expect(tree).toMatchSnapshot();
1977 | });
1978 | });
1979 | ```
1980 |
1981 | ## Demo App
1982 |
1983 | https://next-github.herokuapp.com/
1984 |
1985 | ## Source
1986 |
1987 | https://github.com/leksster/nextjs6-graphql-client-tutorial
--------------------------------------------------------------------------------
/__tests__/__snapshots__/index.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Home Page renders cards with all information on success 1`] = `
4 |
5 |
8 |
11 |
19 |
22 |
40 |
43 |
49 |
53 |
56 |
57 |
58 |
61 |
62 |
65 | Home
66 |
67 |
84 |
87 | Sign In
88 |
89 |
92 |
93 |
94 |
95 |
96 |
103 |
111 |
114 |
117 |
120 |
123 | repo name
124 |
125 |
128 | desc
129 |
130 |
131 |
161 |
162 |
163 |
164 |
165 |
166 | `;
167 |
168 | exports[`Home Page renders correct message on error 1`] = `
169 |
170 |
173 |
176 |
184 |
187 |
205 |
208 |
214 |
218 |
221 |
222 |
223 |
226 |
227 |
230 | Home
231 |
232 |
249 |
252 | Sign In
253 |
254 |
257 |
258 |
259 |
260 |
261 |
268 |
276 |
284 |
287 | Please Sign In to fetch data
288 |
289 |
290 |
291 |
292 |
293 | `;
294 |
295 | exports[`Home Page renders loading state initially 1`] = `
296 |
297 |
300 |
303 |
311 |
314 |
332 |
335 |
341 |
345 |
348 |
349 |
350 |
353 |
354 |
357 | Home
358 |
359 |
369 |
373 |
382 |
383 |
384 |
385 |
386 |
387 |
394 |
402 |
410 |
420 |
424 |
433 |
434 |
435 |
436 |
437 |
438 |
439 | `;
440 |
--------------------------------------------------------------------------------
/__tests__/auth/github/__snapshots__/callback.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Github Callback Page matches snapshot 1`] = `null`;
4 |
--------------------------------------------------------------------------------
/__tests__/auth/github/callback.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 | import Router from 'next/router';
4 | import Callback from '../../../pages/auth/github/callback';
5 |
6 | describe('Github Callback Page', () => {
7 | it('matches snapshot', () => {
8 | const mockedRouter = { push: () => {} };
9 | Router.router = mockedRouter;
10 | const component = renderer.create( );
11 | const tree = component.toJSON();
12 | expect(tree).toMatchSnapshot();
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/__tests__/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 | import Index from 'pages';
4 | import viewerLast100Repositories from 'graphql/queries/viewerLast100Repositories';
5 | import { MockedProvider } from 'react-apollo/test-utils';
6 |
7 | describe('Home Page', () => {
8 | it('renders loading state initially', () => {
9 | const component = renderer.create(
10 |
11 |
12 | ,
13 | );
14 |
15 | const tree = component.toJSON();
16 | expect(tree).toMatchSnapshot();
17 | });
18 |
19 | it('renders cards with all information on success', async () => {
20 | const mocks = [
21 | {
22 | request: {
23 | query: viewerLast100Repositories,
24 | },
25 | result: {
26 | data: {
27 | viewer: {
28 | repositories: {
29 | edges: [
30 | {
31 | node: {
32 | id: '1', url: 'url.com', name: 'repo name', description: 'desc',
33 | },
34 | },
35 | ],
36 | },
37 | },
38 | },
39 | },
40 | },
41 | ];
42 |
43 | const component = renderer.create(
44 |
45 |
46 | ,
47 | );
48 | await new Promise(resolve => setTimeout(resolve));
49 |
50 | const tree = component.toJSON();
51 | expect(tree).toMatchSnapshot();
52 | });
53 |
54 | it('renders correct message on error', async () => {
55 | const mock = {
56 | request: {
57 | query: viewerLast100Repositories,
58 | },
59 | error: new Error('error'),
60 | };
61 |
62 | const component = renderer.create(
63 |
64 |
65 | ,
66 | );
67 | await new Promise(resolve => setTimeout(resolve));
68 |
69 | const tree = component.toJSON();
70 | expect(tree).toMatchSnapshot();
71 | });
72 | });
73 |
--------------------------------------------------------------------------------
/components/atoms/AppBar/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { AppBar as MaterialAppBar } from '@material-ui/core';
4 |
5 | const AppBar = (props) => {
6 | const { children, ...defaultProps } = props;
7 |
8 | return (
9 |
10 | {children}
11 |
12 | );
13 | };
14 |
15 | AppBar.propTypes = {
16 | children: PropTypes.node.isRequired,
17 | };
18 |
19 | export default AppBar;
20 |
--------------------------------------------------------------------------------
/components/atoms/AppBar/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 | import { AppBar } from '../..';
4 |
5 | storiesOf('atoms/AppBar', module)
6 | .add('default', () => (
7 |
8 | Example of AppBar
9 |
10 | ))
11 | .add('secondary', () => (
12 |
13 | Secondary color
14 |
15 | ));
16 |
--------------------------------------------------------------------------------
/components/atoms/AppBar/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import AppBar from '.';
4 |
5 | describe('AppBar', () => {
6 | it('renders children when passed in', () => {
7 | const wrapper = shallow(test
);
8 | expect(wrapper.contains(test
)).toBe(true);
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/components/atoms/Button/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Button as MaterialButton } from '@material-ui/core';
4 |
5 | const Button = (props) => {
6 | const { children, ...defaultProps } = props;
7 |
8 | return (
9 |
10 | {children}
11 |
12 | );
13 | };
14 |
15 | Button.propTypes = {
16 | children: PropTypes.node.isRequired,
17 | };
18 |
19 | export default Button;
20 |
--------------------------------------------------------------------------------
/components/atoms/Button/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { action } from '@storybook/addon-actions';
3 | import { storiesOf } from '@storybook/react';
4 | import { Button } from '../..';
5 |
6 | storiesOf('atoms/Button', module)
7 | .add('default', () => (
8 |
9 | Default
10 |
11 | ))
12 | .add('outlined primary', () => (
13 |
14 | Outline Primary
15 |
16 | ))
17 | .add('contained secondary', () => (
18 |
19 | Contained Secondary
20 |
21 | ))
22 | .add('circle button', () => (
23 |
24 | CB
25 |
26 | ))
27 | .add('disabled button', () => (
28 |
29 | Disabled Button
30 |
31 | ));
32 |
--------------------------------------------------------------------------------
/components/atoms/Button/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import Button from '.';
4 |
5 | describe('Button', () => {
6 | it('renders children when passed in', () => {
7 | const wrapper = shallow(Test );
8 | expect(wrapper.contains('Test')).toBe(true);
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/components/atoms/Card/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Card as MaterialCard } from '@material-ui/core';
4 |
5 | const Card = (props) => {
6 | const { children, ...defaultProps } = props;
7 |
8 | return (
9 |
10 | {children}
11 |
12 | );
13 | };
14 |
15 | Card.propTypes = {
16 | children: PropTypes.node.isRequired,
17 | };
18 |
19 | export default Card;
20 |
--------------------------------------------------------------------------------
/components/atoms/Card/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 | import { Card } from '../..';
4 |
5 | storiesOf('atoms/Card', module)
6 | .add('default', () => (
7 |
8 | Title
9 | Default
10 |
11 | ));
12 |
--------------------------------------------------------------------------------
/components/atoms/Card/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import Card from '.';
4 |
5 | describe('Card', () => {
6 | it('renders children when passed in', () => {
7 | const wrapper = shallow(
8 |
9 | Some text
10 | Test
11 | ,
12 | );
13 | expect(wrapper.contains('Test')).toBe(true);
14 | expect(wrapper.contains('Some text')).toBe(true);
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/components/atoms/CardActions/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { CardActions as MaterialCardActions } from '@material-ui/core';
4 |
5 | const CardActions = (props) => {
6 | const { children, ...defaultProps } = props;
7 |
8 | return (
9 |
10 | {children}
11 |
12 | );
13 | };
14 |
15 | CardActions.propTypes = {
16 | children: PropTypes.node.isRequired,
17 | };
18 |
19 | export default CardActions;
20 |
--------------------------------------------------------------------------------
/components/atoms/CardActions/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 | import { CardActions, Button } from '../..';
4 |
5 | storiesOf('atoms/CardActions', module)
6 | .add('with button', () => (
7 |
8 | Button
9 | Button
10 |
11 | ));
12 |
--------------------------------------------------------------------------------
/components/atoms/CardActions/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import CardActions from '.';
4 |
5 | describe('CardActions', () => {
6 | it('renders children when passed in', () => {
7 | const wrapper = shallow(
8 |
9 | Some text
10 | Test
11 | ,
12 | );
13 | expect(wrapper.contains('Test')).toBe(true);
14 | expect(wrapper.contains('Some text')).toBe(true);
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/components/atoms/CardContent/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { CardContent as MaterialCardContent } from '@material-ui/core';
4 |
5 | const CardContent = (props) => {
6 | const { children, ...defaultProps } = props;
7 |
8 | return (
9 |
10 | {children}
11 |
12 | );
13 | };
14 |
15 | CardContent.propTypes = {
16 | children: PropTypes.node.isRequired,
17 | };
18 |
19 | export default CardContent;
20 |
--------------------------------------------------------------------------------
/components/atoms/CardContent/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 | import { CardContent } from '../..';
4 |
5 | storiesOf('atoms/CardContent', module)
6 | .add('default', () => (
7 |
8 | Lorem
9 | Lorem Ipsum
10 |
11 | ));
12 |
--------------------------------------------------------------------------------
/components/atoms/CardContent/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import CardContent from '.';
4 |
5 | describe('CardContent', () => {
6 | it('renders children when passed in', () => {
7 | const wrapper = shallow(
8 |
9 | Lorem
10 | Ipsum
11 | ,
12 | );
13 | expect(wrapper.contains('Lorem')).toBe(true);
14 | expect(wrapper.contains('Ipsum')).toBe(true);
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/components/atoms/Grid/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Grid as MaterialGrid } from '@material-ui/core';
4 |
5 | const Grid = (props) => {
6 | const { children, ...defaultProps } = props;
7 |
8 | return (
9 |
10 | {children}
11 |
12 | );
13 | };
14 |
15 | Grid.propTypes = {
16 | children: PropTypes.node.isRequired,
17 | };
18 |
19 | export default Grid;
20 |
--------------------------------------------------------------------------------
/components/atoms/Grid/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 | import { Grid } from '../..';
4 |
5 | storiesOf('atoms/Grid', module)
6 | .add('default', () => (
7 |
8 | test
9 |
10 | ));
11 |
--------------------------------------------------------------------------------
/components/atoms/Grid/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import Grid from '.';
4 |
5 | describe('Grid', () => {
6 | it('renders children when passed in', () => {
7 | const wrapper = shallow(Test
);
8 | expect(wrapper.contains(Test
)).toBe(true);
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/components/atoms/IconButton/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { IconButton as MaterialIconButton } from '@material-ui/core';
4 |
5 | const IconButton = (props) => {
6 | const { children, ...defaultProps } = props;
7 |
8 | return (
9 |
10 | {children}
11 |
12 | );
13 | };
14 |
15 | IconButton.propTypes = {
16 | children: PropTypes.node.isRequired,
17 | };
18 |
19 | export default IconButton;
20 |
--------------------------------------------------------------------------------
/components/atoms/IconButton/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 | import { IconButton } from '../..';
4 |
5 | storiesOf('atoms/IconButton', module)
6 | .add('default', () => (
7 |
8 | test
9 |
10 | ));
11 |
--------------------------------------------------------------------------------
/components/atoms/IconButton/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import IconButton from '.';
4 |
5 | describe('IconButton', () => {
6 | it('renders children when passed in', () => {
7 | const wrapper = shallow(Test
);
8 | expect(wrapper.contains(Test
)).toBe(true);
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/components/atoms/List/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { List as MaterialList } from '@material-ui/core';
4 |
5 | const List = (props) => {
6 | const { children, ...defaultProps } = props;
7 |
8 | return (
9 |
10 | {children}
11 |
12 | );
13 | };
14 |
15 | List.propTypes = {
16 | children: PropTypes.node.isRequired,
17 | };
18 |
19 | export default List;
20 |
--------------------------------------------------------------------------------
/components/atoms/List/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 | import { List } from '../..';
4 |
5 | storiesOf('atoms/List', module)
6 | .add('default', () => (
7 |
8 | test
9 | test
10 | test
11 | test
12 |
13 | ));
14 |
--------------------------------------------------------------------------------
/components/atoms/List/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import List from '.';
4 |
5 | describe('List', () => {
6 | it('renders children when passed in', () => {
7 | const wrapper = shallow(
8 |
9 | Lorem
10 | Ipsum
11 | Test
12 |
,
13 | );
14 | expect(wrapper.contains(Test
)).toBe(true);
15 | expect(wrapper.contains(Ipsum
)).toBe(true);
16 | expect(wrapper.contains(Lorem
)).toBe(true);
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/components/atoms/ListItem/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { ListItem as MaterialListItem } from '@material-ui/core';
4 |
5 | const ListItem = (props) => {
6 | const { children, ...defaultProps } = props;
7 |
8 | return (
9 |
10 | {children}
11 |
12 | );
13 | };
14 |
15 | ListItem.propTypes = {
16 | children: PropTypes.node.isRequired,
17 | };
18 |
19 | export default ListItem;
20 |
--------------------------------------------------------------------------------
/components/atoms/ListItem/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 | import { ListItem } from '../..';
4 |
5 | storiesOf('atoms/ListItem', module)
6 | .add('default', () => (
7 |
8 | test
9 |
10 | ))
11 | .add('button', () => (
12 |
13 | test
14 |
15 | ));
16 |
--------------------------------------------------------------------------------
/components/atoms/ListItem/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import ListItem from '.';
4 |
5 | describe('ListItem', () => {
6 | it('renders children when passed in', () => {
7 | const wrapper = shallow(
8 |
9 | Lorem
10 | Ipsum
11 | Test
12 | ,
13 | );
14 | expect(wrapper.contains(Test
)).toBe(true);
15 | expect(wrapper.contains(Ipsum
)).toBe(true);
16 | expect(wrapper.contains(Lorem
)).toBe(true);
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/components/atoms/ListItemIcon/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { ListItemIcon as MaterialListItemIcon } from '@material-ui/core';
4 |
5 | const ListItemIcon = (props) => {
6 | const { children, ...defaultProps } = props;
7 |
8 | return (
9 |
10 | {children}
11 |
12 | );
13 | };
14 |
15 | ListItemIcon.propTypes = {
16 | children: PropTypes.node.isRequired,
17 | };
18 |
19 | export default ListItemIcon;
20 |
--------------------------------------------------------------------------------
/components/atoms/ListItemIcon/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 | import StarIcon from '@material-ui/icons/Star';
4 | import SendIcon from '@material-ui/icons/Send';
5 | import MailIcon from '@material-ui/icons/Mail';
6 | import { ListItemIcon } from '../..';
7 |
8 | storiesOf('atoms/ListItemIcon', module)
9 | .add('star', () => (
10 |
11 |
12 |
13 | ))
14 | .add('send', () => (
15 |
16 |
17 |
18 | ))
19 | .add('mail', () => (
20 |
21 |
22 |
23 | ));
24 |
--------------------------------------------------------------------------------
/components/atoms/ListItemIcon/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import ListItemIcon from '.';
4 |
5 | describe('ListItemIcon', () => {
6 | it('renders children when passed in', () => {
7 | const wrapper = shallow(
8 |
9 | Lorem
10 | Ipsum
11 | Test
12 | ,
13 | );
14 | expect(wrapper.contains(Test
)).toBe(true);
15 | expect(wrapper.contains(Ipsum
)).toBe(true);
16 | expect(wrapper.contains(Lorem
)).toBe(true);
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/components/atoms/ListItemText/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { ListItemText as MaterialListItemText } from '@material-ui/core';
4 |
5 | const ListItemText = (props) => {
6 | const { children, ...defaultProps } = props;
7 |
8 | return (
9 |
10 | {children}
11 |
12 | );
13 | };
14 |
15 | ListItemText.propTypes = {
16 | children: PropTypes.node,
17 | };
18 |
19 | ListItemText.defaultProps = {
20 | children: null,
21 | };
22 |
23 | export default ListItemText;
24 |
--------------------------------------------------------------------------------
/components/atoms/ListItemText/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 | import ListItemText from '.';
4 |
5 | storiesOf('atoms/ListItemText', module)
6 | .add('default', () => (
7 |
8 | ));
9 |
--------------------------------------------------------------------------------
/components/atoms/ListItemText/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import ListItemText from '.';
4 |
5 | describe('ListItemText', () => {
6 | it('renders children when passed in', () => {
7 | const wrapper = shallow(
8 |
9 | Lorem
10 | Ipsum
11 | Test
12 | ,
13 | );
14 | expect(wrapper.contains(Test
)).toBe(true);
15 | expect(wrapper.contains(Ipsum
)).toBe(true);
16 | expect(wrapper.contains(Lorem
)).toBe(true);
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/components/atoms/Loader/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import CircularProgress from '@material-ui/core/CircularProgress';
3 |
4 | const Loader = props => (
5 |
6 | );
7 |
8 | export default Loader;
9 |
--------------------------------------------------------------------------------
/components/atoms/Loader/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 | import { Loader } from '../..';
4 |
5 | storiesOf('atoms/Loader', module)
6 | .add('default', () => (
7 |
8 | ))
9 | .add('big size', () => (
10 |
11 | ));
12 |
--------------------------------------------------------------------------------
/components/atoms/MenuIcon/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import MaterialMenuIcon from '@material-ui/icons/Menu';
3 |
4 | const MenuIcon = props => (
5 |
6 | );
7 |
8 | export default MenuIcon;
9 |
--------------------------------------------------------------------------------
/components/atoms/MenuIcon/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 | import { MenuIcon } from '../..';
4 |
5 | storiesOf('atoms/MenuIcon', module)
6 | .add('default', () => (
7 |
8 | ));
9 |
--------------------------------------------------------------------------------
/components/atoms/SwipeableDrawer/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { SwipeableDrawer as MaterialSwipeableDrawer } from '@material-ui/core';
4 |
5 | const SwipeableDrawer = (props) => {
6 | const { children, ...defaultProps } = props;
7 |
8 | return (
9 |
10 | {children}
11 |
12 | );
13 | };
14 |
15 | SwipeableDrawer.propTypes = {
16 | children: PropTypes.node.isRequired,
17 | };
18 |
19 | export default SwipeableDrawer;
20 |
--------------------------------------------------------------------------------
/components/atoms/SwipeableDrawer/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 | import { SwipeableDrawer } from '../..';
4 |
5 | storiesOf('atoms/SwipeableDrawer', module)
6 | .add('default', () => (
7 |
8 | Default
9 |
10 | ))
11 | .add('top', () => (
12 |
13 | Top
14 |
15 | ))
16 | .add('right', () => (
17 |
18 | Right
19 |
20 | ))
21 | .add('bottom', () => (
22 |
23 | Bottom
24 |
25 | ));
26 |
--------------------------------------------------------------------------------
/components/atoms/SwipeableDrawer/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import SwipeableDrawer from '.';
4 |
5 | describe('SwipeableDrawer', () => {
6 | it('renders children when passed in', () => {
7 | const wrapper = shallow(
8 |
9 | Lorem
10 | ,
11 | );
12 |
13 | expect(wrapper.contains(Lorem
)).toBe(true);
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/components/atoms/Toolbar/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Toolbar as MaterialToolbar } from '@material-ui/core';
4 |
5 | const Toolbar = (props) => {
6 | const { children, ...defaultProps } = props;
7 |
8 | return (
9 |
10 | {children}
11 |
12 | );
13 | };
14 |
15 | Toolbar.propTypes = {
16 | children: PropTypes.node.isRequired,
17 | };
18 |
19 | export default Toolbar;
20 |
--------------------------------------------------------------------------------
/components/atoms/Toolbar/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 | import { Toolbar } from '../..';
4 |
5 | storiesOf('atoms/Toolbar', module)
6 | .add('default', () => (
7 |
8 | tool 1
9 | tool 2
10 | tool 3
11 |
12 | ));
13 |
--------------------------------------------------------------------------------
/components/atoms/Toolbar/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import Toolbar from '.';
4 |
5 | describe('Toolbar', () => {
6 | it('renders children when passed in', () => {
7 | const wrapper = shallow(Test
);
8 | expect(wrapper.contains(Test
)).toBe(true);
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/components/atoms/Typography/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Typography as MaterialTypography } from '@material-ui/core';
4 |
5 | const Typography = (props) => {
6 | const { children, ...defaultProps } = props;
7 |
8 | return (
9 |
10 | {children}
11 |
12 | );
13 | };
14 |
15 | Typography.propTypes = {
16 | children: PropTypes.node.isRequired,
17 | };
18 |
19 | export default Typography;
20 |
--------------------------------------------------------------------------------
/components/atoms/Typography/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 | import Typography from '.';
4 |
5 | storiesOf('atoms/Typography', module)
6 | .add('default', () => (
7 |
8 | Some random text
9 |
10 | ));
11 |
--------------------------------------------------------------------------------
/components/atoms/Typography/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import Typography from '.';
4 |
5 | describe('Typography', () => {
6 | it('renders children when passed in', () => {
7 | const wrapper = shallow(Test );
8 | expect(wrapper.contains('Test')).toBe(true);
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/components/index.js:
--------------------------------------------------------------------------------
1 | const req = require.context('.', true, /\.\/[^/]+\/[^/]+\/index\.js$/);
2 |
3 | req.keys().forEach((key) => {
4 | const componentName = key.replace(/^.+\/([^/]+)\/index\.js/, '$1');
5 | module.exports[componentName] = req(key).default;
6 | });
7 |
--------------------------------------------------------------------------------
/components/moleculus/Header/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { withStyles } from '@material-ui/core/styles';
4 |
5 | import {
6 | AppBar, IconButton,
7 | MenuIcon, Toolbar, Typography,
8 | } from '../..';
9 |
10 | const styles = {
11 | root: {
12 | flexGrow: 1,
13 | },
14 | flex: {
15 | flexGrow: 1,
16 | },
17 | menuButton: {
18 | marginLeft: -12,
19 | marginRight: 20,
20 | },
21 | };
22 |
23 | const Header = (props) => {
24 | const {
25 | classes, swipeableMenu, loginButton, title, openMenu,
26 | } = props;
27 |
28 | return (
29 |
30 |
31 | {swipeableMenu}
32 |
33 |
34 |
35 |
36 |
37 | {title}
38 |
39 | {loginButton}
40 |
41 |
42 |
43 | );
44 | };
45 |
46 | Header.propTypes = {
47 | swipeableMenu: PropTypes.node,
48 | loginButton: PropTypes.node,
49 | classes: PropTypes.shape().isRequired,
50 | title: PropTypes.string,
51 | openMenu: PropTypes.func,
52 | };
53 |
54 | Header.defaultProps = {
55 | swipeableMenu: null,
56 | loginButton: null,
57 | title: null,
58 | openMenu: null,
59 | };
60 |
61 | export default withStyles(styles)(Header);
62 |
--------------------------------------------------------------------------------
/components/moleculus/Header/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 | import { Header } from '../..';
4 |
5 | storiesOf('moleculus/Header', module)
6 | .add('default', () => (
7 |
8 | ))
9 | .add('with title', () => (
10 |
11 | ));
12 |
--------------------------------------------------------------------------------
/components/moleculus/Header/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { mount } from 'enzyme';
3 | import { Header, Typography } from '../..';
4 |
5 | describe('Header', () => {
6 | it('renders header with correct title', () => {
7 | const wrapper = mount();
8 | const typographyNode = wrapper.find(Typography);
9 |
10 | expect(typographyNode.text()).toEqual('foo');
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/components/moleculus/SimpleCard/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { withStyles } from '@material-ui/core/styles';
4 |
5 | import {
6 | Card, CardContent, Typography, CardActions, Button,
7 | } from 'components';
8 |
9 | const styles = {
10 | card: {
11 | minWidth: 100,
12 | },
13 | bullet: {
14 | display: 'inline-block',
15 | margin: '0 2px',
16 | transform: 'scale(0.8)',
17 | },
18 | title: {
19 | marginBottom: 16,
20 | fontSize: 14,
21 | },
22 | pos: {
23 | marginBottom: 12,
24 | },
25 | };
26 |
27 | const SimpleCard = (props) => {
28 | const {
29 | classes, title, description, url,
30 | } = props;
31 |
32 | return (
33 |
34 |
35 |
36 | {title}
37 |
38 | {description && (
39 |
40 | {description}
41 |
42 | )}
43 |
44 |
45 |
46 | Learn More
47 |
48 |
49 |
50 | );
51 | };
52 |
53 | SimpleCard.propTypes = {
54 | classes: PropTypes.shape({}).isRequired,
55 | title: PropTypes.string.isRequired,
56 | description: PropTypes.string,
57 | url: PropTypes.string,
58 | };
59 |
60 | SimpleCard.defaultProps = {
61 | description: 'No description',
62 | url: null,
63 | };
64 |
65 | export default withStyles(styles)(SimpleCard);
66 |
--------------------------------------------------------------------------------
/components/moleculus/SimpleCard/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 | import { SimpleCard } from '../..';
4 |
5 | storiesOf('moleculus/SimpleCard', module)
6 | .add('default', () => (
7 |
8 | ));
9 |
--------------------------------------------------------------------------------
/components/moleculus/SimpleCard/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { mount } from 'enzyme';
3 | import { SimpleCard, Typography } from '../..';
4 |
5 | describe('SimpleCard', () => {
6 | it('renders header with correct title', () => {
7 | const wrapper = mount( );
8 | const typographyNodes = wrapper.find(Typography);
9 | expect(typographyNodes.first().text()).toEqual('foo');
10 | expect(typographyNodes.last().text()).toEqual('bar');
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/components/moleculus/SwipeableMenu/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import Link from 'next/link';
4 |
5 | import {
6 | List, ListItem, ListItemText, SwipeableDrawer,
7 | } from '../..';
8 |
9 | const SwipeableMenu = (props) => {
10 | const {
11 | menuItems, openMenu, closeMenu, isOpenedByDefault,
12 | } = props;
13 |
14 | return (
15 |
20 |
21 | {menuItems.map(item => (
22 |
23 |
24 |
25 |
26 |
27 | ))}
28 |
29 |
30 | );
31 | };
32 |
33 | SwipeableMenu.propTypes = {
34 | menuItems: PropTypes.arrayOf(PropTypes.shape()).isRequired,
35 | openMenu: PropTypes.func.isRequired,
36 | closeMenu: PropTypes.func.isRequired,
37 | isOpenedByDefault: PropTypes.bool,
38 | };
39 |
40 | SwipeableMenu.defaultProps = {
41 | isOpenedByDefault: false,
42 | };
43 |
44 | export default SwipeableMenu;
45 |
--------------------------------------------------------------------------------
/components/moleculus/SwipeableMenu/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 | import { SwipeableMenu } from '../..';
4 |
5 | storiesOf('moleculus/SwipeableMenu', module)
6 | .add('opened by default', () => (
7 |
8 | ));
9 |
--------------------------------------------------------------------------------
/components/moleculus/SwipeableMenu/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import SwipeableMenu from '.';
4 | import { ListItem, ListItemText, SwipeableDrawer } from '../..';
5 |
6 | describe('SwipeableMenu', () => {
7 | it('renders correct menu items using array of strings as props', () => {
8 | const mockedOpenMenu = jest.fn();
9 | mockedOpenMenu.mockReturnValueOnce(true);
10 |
11 | const mockedCloseMenu = jest.fn();
12 | mockedCloseMenu.mockReturnValueOnce(true);
13 |
14 | const wrapper = shallow(
15 | ,
24 | );
25 |
26 | expect(wrapper.find(ListItem)).toHaveLength(3);
27 |
28 | const swipeableDrawer = wrapper.find(SwipeableDrawer);
29 | expect(swipeableDrawer.props().onClose()).toEqual(true);
30 | expect(swipeableDrawer.props().onOpen()).toEqual(true);
31 |
32 | const props = wrapper.find(ListItemText).map(node => node.props().primary);
33 | expect(props).toEqual(['foo', 'bar', 'baz']);
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/components/organisms/HeaderWithSwipeableMenu/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Header, SwipeableMenu } from 'components';
4 |
5 | const HeaderWithSwipeableMenu = (props) => {
6 | const {
7 | closeMenu, openMenu, loginButtonContainer, leftMenuIsOpened, title,
8 | } = props;
9 |
10 | const MENU_ITEMS = [
11 | {
12 | id: 1,
13 | url: '/',
14 | text: 'Home',
15 | },
16 | {
17 | id: 2,
18 | url: '/top_ruby',
19 | text: 'Top Ruby Repositories',
20 | },
21 | {
22 | id: 3,
23 | url: '/top_js',
24 | text: 'Top Javascript Repositories',
25 | },
26 | {
27 | id: 4,
28 | url: '/new_js',
29 | text: 'New Javascript Repositories',
30 | },
31 | ];
32 |
33 | return (
34 |
44 | )}
45 | loginButton={loginButtonContainer}
46 | />
47 | );
48 | };
49 |
50 | HeaderWithSwipeableMenu.propTypes = {
51 | title: PropTypes.string,
52 | leftMenuIsOpened: PropTypes.bool.isRequired,
53 | openMenu: PropTypes.func.isRequired,
54 | closeMenu: PropTypes.func.isRequired,
55 | loginButtonContainer: PropTypes.node.isRequired,
56 | };
57 |
58 | HeaderWithSwipeableMenu.defaultProps = {
59 | title: 'Home',
60 | };
61 |
62 | export default HeaderWithSwipeableMenu;
63 |
--------------------------------------------------------------------------------
/components/organisms/HeaderWithSwipeableMenu/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 | import { HeaderWithSwipeableMenu } from 'components';
4 |
5 | storiesOf('organisms/HeaderWithSwipeableMenu', module)
6 | .add('default with click event', () => (
7 |
8 | ))
9 | .add('opened by default', () => (
10 |
11 | ));
12 |
--------------------------------------------------------------------------------
/components/organisms/HeaderWithSwipeableMenu/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { mount } from 'enzyme';
3 | import { HeaderWithSwipeableMenu } from 'components';
4 |
5 | describe('HeaderWithSwipeableMenu', () => {
6 | it('renders header with swipeable menu', () => {
7 | const mockedOpenMenu = jest.fn();
8 | const mockedCloseMenu = jest.fn();
9 |
10 | const wrapper = mount(
11 | }
16 | />,
17 | );
18 |
19 | const expectedMenuItems = [
20 | 'Top Javascript Repositories',
21 | 'Home',
22 | 'Top Ruby Repositories',
23 | 'New Javascript Repositories',
24 | ];
25 |
26 | wrapper.find('ListItemText').find('Typography').forEach((node) => {
27 | expect(expectedMenuItems).toContain(node.text());
28 | });
29 |
30 | wrapper.find('button').simulate('click');
31 | expect(mockedOpenMenu).toHaveBeenCalled();
32 | });
33 | });
34 |
--------------------------------------------------------------------------------
/components/templates/Home/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Grid } from '@material-ui/core';
4 |
5 | const Home = (props) => {
6 | const { header, footer, content } = props;
7 |
8 | return (
9 |
10 | {header}
11 |
12 |
13 | {content}
14 |
15 |
16 | {footer}
17 |
18 | );
19 | };
20 |
21 | Home.propTypes = {
22 | header: PropTypes.node,
23 | content: PropTypes.node,
24 | footer: PropTypes.string,
25 | };
26 |
27 | Home.defaultProps = {
28 | header: null,
29 | content: null,
30 | footer: null,
31 | };
32 |
33 | export default (Home);
34 |
--------------------------------------------------------------------------------
/components/templates/Home/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 | import { Home } from '../..';
4 |
5 | storiesOf('templates/Home', module)
6 | .add('default', () => (
7 |
14 | ));
15 |
--------------------------------------------------------------------------------
/components/templates/Home/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { mount } from 'enzyme';
3 | import { Home, SimpleCard } from '../..';
4 |
5 | describe('Home', () => {
6 | it('renders component with passed card components', () => {
7 | const wrapper = mount(
8 | ,
11 | ,
12 | ]}
13 | />,
14 | );
15 |
16 | expect(wrapper.find(SimpleCard)).toHaveLength(2);
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/containers/GithubLoginButtonContainer/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Router from 'next/router';
3 | import { Query } from 'react-apollo';
4 | import { Button, Loader } from 'components';
5 | import getConfig from 'next/config';
6 | import Cookies from 'js-cookie';
7 | import viewer from 'graphql/queries/viewer';
8 |
9 | class GithubLoginButtonContainer extends React.Component {
10 | handleSignIn = () => {
11 | const { publicRuntimeConfig: { githubClientId } } = getConfig();
12 |
13 | Router.push({
14 | pathname: 'https://github.com/login/oauth/authorize',
15 | query: {
16 | client_id: githubClientId,
17 | },
18 | });
19 | };
20 |
21 | handleSignOut = () => {
22 | Cookies.remove('access_token');
23 | Router.push('/');
24 | };
25 |
26 | render() {
27 | const { handleSignIn, handleSignOut } = this;
28 |
29 | return (
30 |
31 | {({ loading, error, data }) => {
32 | if (loading) {
33 | return ;
34 | }
35 | if (error) {
36 | return (
37 |
38 | Sign In
39 |
40 | );
41 | }
42 | return (
43 |
44 | {data.viewer.login}
45 |
46 | Sign Out
47 |
48 |
49 | );
50 | }}
51 |
52 | );
53 | }
54 | }
55 |
56 | export default GithubLoginButtonContainer;
57 |
--------------------------------------------------------------------------------
/containers/HeaderContainer/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { HeaderWithSwipeableMenu } from 'components';
4 | import GithubLoginButtonContainer from 'containers/GithubLoginButtonContainer';
5 |
6 | class HeaderContainer extends React.Component {
7 | static propTypes = {
8 | title: PropTypes.string.isRequired,
9 | };
10 |
11 | state = {
12 | leftMenuIsOpened: false,
13 | };
14 |
15 | toggleLeftMenuShow = leftMenuIsOpened => () => {
16 | this.setState({
17 | leftMenuIsOpened,
18 | });
19 | };
20 |
21 | render() {
22 | const {
23 | state: {
24 | leftMenuIsOpened,
25 | },
26 | props: {
27 | title,
28 | },
29 | toggleLeftMenuShow,
30 | } = this;
31 |
32 | return (
33 | }
39 | />
40 | );
41 | }
42 | }
43 |
44 | export default HeaderContainer;
45 |
--------------------------------------------------------------------------------
/containers/HeaderContainer/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import HeaderContainer from 'containers/HeaderContainer';
4 |
5 | describe('HeaderContainer', () => {
6 | it('renders component', () => {
7 | const wrapper = shallow(
8 | ,
9 | );
10 |
11 | const header = wrapper.find('HeaderWithSwipeableMenu');
12 | expect(header).toHaveLength(1);
13 | expect(header.props().title).toEqual('Home');
14 | expect(header.props().loginButtonContainer).toBeDefined();
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/containers/SearchRepoList/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {
4 | SimpleCard, Loader, Grid, Typography,
5 | } from 'components';
6 | import { Query } from 'react-apollo';
7 |
8 | const SearchRepoList = ({ query }) => (
9 |
10 | {({ loading, error, data }) => {
11 | if (loading) {
12 | return (
13 |
14 |
15 |
16 | );
17 | }
18 | if (error) {
19 | return (
20 |
21 | Please Sign In to fetch data
22 |
23 | );
24 | }
25 | return (
26 |
27 | {data.search.edges.map(repo => (
28 |
29 |
34 |
35 | ))}
36 |
37 | );
38 | }}
39 |
40 | );
41 |
42 | SearchRepoList.propTypes = {
43 | query: PropTypes.node.isRequired,
44 | };
45 |
46 | export default SearchRepoList;
47 |
--------------------------------------------------------------------------------
/containers/ViewerRepoList/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | SimpleCard, Loader, Grid, Typography,
4 | } from 'components';
5 | import { Query } from 'react-apollo';
6 | import viewerLast100Repositories from 'graphql/queries/viewerLast100Repositories';
7 |
8 | const ViewerRepoList = () => (
9 |
10 | {({ loading, error, data }) => {
11 | if (loading) {
12 | return (
13 |
14 |
15 |
16 | );
17 | }
18 | if (error) {
19 | return (
20 |
21 | Please Sign In to fetch data
22 |
23 | );
24 | }
25 | return (
26 |
27 | {data.viewer.repositories.edges.map(repo => (
28 |
29 |
34 |
35 | ))}
36 |
37 | );
38 | }}
39 |
40 | );
41 |
42 | export default ViewerRepoList;
43 |
--------------------------------------------------------------------------------
/graphql/queries/searchNewJsRepos.js:
--------------------------------------------------------------------------------
1 | import gql from 'graphql-tag';
2 |
3 | const searchNewJsRepos = gql`
4 | {
5 | search(query: "language:JavaScript, stars:>1 created:>=2018-07-01", first: 50, type: REPOSITORY) {
6 | edges {
7 | node {
8 | ... on Repository {
9 | name
10 | description
11 | url
12 | }
13 | }
14 | }
15 | }
16 | }
17 | `;
18 |
19 | export default searchNewJsRepos;
20 |
--------------------------------------------------------------------------------
/graphql/queries/searchNewRubyRepos.js:
--------------------------------------------------------------------------------
1 | import gql from 'graphql-tag';
2 |
3 | // const dateArray = new Date().toLocaleDateString().split('/').reverse();
4 |
5 | // const lastMonth = dateArray.map((elem, key) => (key === 1 ? elem - 1 : elem)).join('-');
6 |
7 | const searchNewJsRepos = gql`
8 | {
9 | search(query: "language:Ruby, stars:>1 created:>=2018-07-01", first: 50, type: REPOSITORY) {
10 | edges {
11 | node {
12 | ... on Repository {
13 | name
14 | description
15 | url
16 | }
17 | }
18 | }
19 | }
20 | }
21 | `;
22 |
23 | export default searchNewJsRepos;
24 |
--------------------------------------------------------------------------------
/graphql/queries/searchTopJsRepos.js:
--------------------------------------------------------------------------------
1 | import gql from 'graphql-tag';
2 |
3 | const searchTopJsRepos = gql`
4 | {
5 | search(query: "language:JavaScript stars:>10000", first: 50, type: REPOSITORY) {
6 | edges {
7 | node {
8 | ... on Repository {
9 | name
10 | description
11 | url
12 | }
13 | }
14 | }
15 | }
16 | }
17 | `;
18 |
19 | export default searchTopJsRepos;
20 |
--------------------------------------------------------------------------------
/graphql/queries/searchTopRubyRepos.js:
--------------------------------------------------------------------------------
1 | import gql from 'graphql-tag';
2 |
3 | const searchTopRubyRepos = gql`
4 | {
5 | search(query: "language:Ruby stars:>10000", first: 50, type: REPOSITORY) {
6 | edges {
7 | node {
8 | ... on Repository {
9 | name
10 | description
11 | url
12 | }
13 | }
14 | }
15 | }
16 | }
17 | `;
18 |
19 | export default searchTopRubyRepos;
20 |
--------------------------------------------------------------------------------
/graphql/queries/viewer.js:
--------------------------------------------------------------------------------
1 | import gql from 'graphql-tag';
2 |
3 | const viewer = gql`
4 | {
5 | viewer {
6 | login,
7 | avatarUrl
8 | }
9 | }
10 | `;
11 |
12 | export default viewer;
13 |
--------------------------------------------------------------------------------
/graphql/queries/viewerLast100Repositories.js:
--------------------------------------------------------------------------------
1 | import gql from 'graphql-tag';
2 |
3 | const viewerLast100Repositories = gql`
4 | {
5 | viewer {
6 | repositories(last:100) {
7 | edges {
8 | node {
9 | id
10 | name
11 | description
12 | url
13 | }
14 | }
15 | }
16 | }
17 | }
18 | `;
19 |
20 | export default viewerLast100Repositories;
21 |
--------------------------------------------------------------------------------
/lib/getPageContext.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-underscore-dangle */
2 |
3 | import { SheetsRegistry } from 'jss';
4 | import { createMuiTheme, createGenerateClassName } from '@material-ui/core/styles';
5 | import blue from '@material-ui/core/colors/blue';
6 | import orange from '@material-ui/core/colors/orange';
7 |
8 | // A theme with custom primary and secondary color.
9 | // It's optional.
10 | const theme = createMuiTheme({
11 | palette: {
12 | primary: blue,
13 | secondary: orange,
14 | },
15 | });
16 |
17 | function createPageContext() {
18 | return {
19 | theme,
20 | // This is needed in order to deduplicate the injection of CSS in the page.
21 | sheetsManager: new Map(),
22 | // This is needed in order to inject the critical CSS.
23 | sheetsRegistry: new SheetsRegistry(),
24 | // The standard class name generator.
25 | generateClassName: createGenerateClassName(),
26 | };
27 | }
28 |
29 | export default function getPageContext() {
30 | // Make sure to create a new context for every server-side request so that data
31 | // isn't shared between connections (which would be bad).
32 | if (!process.browser) {
33 | return createPageContext();
34 | }
35 |
36 | // Reuse context on the client-side.
37 | if (!global.__INIT_MATERIAL_UI__) {
38 | global.__INIT_MATERIAL_UI__ = createPageContext();
39 | }
40 |
41 | return global.__INIT_MATERIAL_UI__;
42 | }
43 |
--------------------------------------------------------------------------------
/lib/testConfig.js:
--------------------------------------------------------------------------------
1 | import { configure } from 'enzyme';
2 | import Adapter from 'enzyme-adapter-react-16';
3 | import registerRequireContextHook from 'babel-plugin-require-context-hook/register';
4 |
5 | registerRequireContextHook();
6 |
7 | export default configure({ adapter: new Adapter() });
8 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config({ silent: process.env.NODE_ENV === 'production' });
2 |
3 | module.exports = {
4 | serverRuntimeConfig: {
5 | githubClientId: process.env.GITHUB_CLIENT_ID,
6 | githubClientSecret: process.env.GITHUB_CLIENT_SECRET,
7 | },
8 | publicRuntimeConfig: {
9 | githubClientId: process.env.GITHUB_CLIENT_ID,
10 | },
11 | };
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nextjs6-graphql-client-tutorial",
3 | "version": "0.1.0",
4 | "description": "NextJS6 with apollo GraphQL app",
5 | "main": "index.js",
6 | "repository": "git@github.com:leksster/nextjs6-graphql-client-tutorial.git",
7 | "author": "Alex Bykov ",
8 | "license": "MIT",
9 | "jest": {
10 | "setupTestFrameworkScriptFile": "./lib/testConfig.js"
11 | },
12 | "scripts": {
13 | "storybook": "start-storybook -p 9001 -c .storybook",
14 | "dev": "next",
15 | "build": "next build",
16 | "start": "next start -p $PORT",
17 | "heroku-postbuild": "npm run build",
18 | "test": "jest",
19 | "lint": "eslint ."
20 | },
21 | "dependencies": {
22 | "@material-ui/core": "^1.5.1",
23 | "@material-ui/icons": "^2.0.3",
24 | "apollo-boost": "^0.1.15",
25 | "dotenv": "^6.0.0",
26 | "graphql": "^14.0.0",
27 | "graphql-tag": "^2.9.2",
28 | "isomorphic-unfetch": "^2.1.1",
29 | "js-cookie": "^2.2.0",
30 | "jss": "^9.8.7",
31 | "next": "^6.1.1",
32 | "prop-types": "^15.6.2",
33 | "react": "^16.4.2",
34 | "react-apollo": "^2.1.11",
35 | "react-dom": "^16.4.2",
36 | "react-jss": "^8.6.1",
37 | "styled-jsx": "^3.0.2"
38 | },
39 | "devDependencies": {
40 | "@storybook/addon-actions": "^3.4.11",
41 | "@storybook/react": "^3.4.10",
42 | "babel-core": "^6.26.3",
43 | "babel-eslint": "^8.2.6",
44 | "babel-plugin-module-resolver": "^3.1.1",
45 | "babel-plugin-require-context-hook": "^1.0.0",
46 | "enzyme": "^3.4.4",
47 | "enzyme-adapter-react-16": "^1.2.0",
48 | "eslint": "^5.4.0",
49 | "eslint-config-airbnb": "^17.1.0",
50 | "eslint-plugin-import": "^2.14.0",
51 | "eslint-plugin-jsx-a11y": "^6.1.1",
52 | "eslint-plugin-react": "^7.11.1",
53 | "jest": "^23.5.0",
54 | "react-test-renderer": "^16.4.2"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/pages/_app.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | import fetch from 'isomorphic-unfetch';
3 | /* eslint-enable no-unused-vars */
4 | import React from 'react';
5 | import App, { Container } from 'next/app';
6 | import { ApolloProvider } from 'react-apollo';
7 | import ApolloClient from 'apollo-boost';
8 | import Cookies from 'js-cookie';
9 | import { MuiThemeProvider } from '@material-ui/core/styles';
10 | import CssBaseline from '@material-ui/core/CssBaseline';
11 | import JssProvider from 'react-jss/lib/JssProvider';
12 | import getPageContext from '../lib/getPageContext';
13 |
14 | class MainApp extends App {
15 | constructor(props) {
16 | super(props);
17 | this.pageContext = getPageContext();
18 | }
19 |
20 | pageContext = null;
21 |
22 | componentDidMount() {
23 | // Remove the server-side injected CSS.
24 | const jssStyles = document.querySelector('#jss-server-side');
25 | if (jssStyles && jssStyles.parentNode) {
26 | jssStyles.parentNode.removeChild(jssStyles);
27 | }
28 | }
29 |
30 | render() {
31 | const { Component, pageProps } = this.props;
32 |
33 | const token = Cookies.get('access_token');
34 |
35 | const client = new ApolloClient({
36 | uri: 'https://api.github.com/graphql',
37 | headers: { authorization: `Bearer ${token}` },
38 | });
39 |
40 | return (
41 |
42 |
43 | {/* Wrap every page in Jss and Theme providers */}
44 |
48 | {/* MuiThemeProvider makes the theme available down the React
49 | tree thanks to React context. */}
50 |
54 | {/*
55 | CssBaseline kickstart an elegant,
56 | consistent, and simple baseline to build upon.
57 | */}
58 |
59 | {/* Pass pageContext to the _document though the renderPage enhancer
60 | to render collected styles on server side. */}
61 |
62 |
63 |
64 |
65 |
66 | );
67 | }
68 | }
69 |
70 | export default MainApp;
71 |
--------------------------------------------------------------------------------
/pages/_document.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import Document, { Head, Main, NextScript } from 'next/document';
4 | import flush from 'styled-jsx/server';
5 |
6 | class MainDocument extends Document {
7 | render() {
8 | const { pageContext } = this.props;
9 |
10 | return (
11 |
12 |
13 | NextJS with Apollo Graphql client
14 |
15 | {/* Use minimum-scale=1 to enable GPU rasterization */}
16 |
23 | {/* PWA primary color */}
24 |
25 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | );
36 | }
37 | }
38 |
39 | MainDocument.getInitialProps = (ctx) => {
40 | // Resolution order
41 | //
42 | // On the server:
43 | // 1. app.getInitialProps
44 | // 2. page.getInitialProps
45 | // 3. document.getInitialProps
46 | // 4. app.render
47 | // 5. page.render
48 | // 6. document.render
49 | //
50 | // On the server with error:
51 | // 1. document.getInitialProps
52 | // 2. app.render
53 | // 3. page.render
54 | // 4. document.render
55 | //
56 | // On the client
57 | // 1. app.getInitialProps
58 | // 2. page.getInitialProps
59 | // 3. app.render
60 | // 4. page.render
61 |
62 | // Render app and page and get the context of the page with collected side effects.
63 | let pageContext;
64 |
65 | const page = ctx.renderPage((Component) => {
66 | const WrappedComponent = (props) => {
67 | ({ pageContext } = props);
68 |
69 | return ;
70 | };
71 |
72 | WrappedComponent.propTypes = {
73 | pageContext: PropTypes.shape({}).isRequired,
74 | };
75 |
76 | return WrappedComponent;
77 | });
78 |
79 | return {
80 | ...page,
81 | pageContext,
82 | // Styles fragment is rendered after the app and page rendering finish.
83 | styles: (
84 |
85 |
90 | {flush() || null }
91 |
92 | ),
93 | };
94 | };
95 |
96 | export default MainDocument;
97 |
--------------------------------------------------------------------------------
/pages/auth/github/callback.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Router, { withRouter } from 'next/router';
3 | import fetch from 'isomorphic-unfetch';
4 | import Cookies from 'js-cookie';
5 | import getConfig from 'next/config';
6 | import PropTypes from 'prop-types';
7 |
8 | class Callback extends React.Component {
9 | static propTypes = {
10 | errorMessage: PropTypes.string,
11 | accessToken: PropTypes.string,
12 | };
13 |
14 | static defaultProps = {
15 | errorMessage: undefined,
16 | accessToken: undefined,
17 | };
18 |
19 | componentDidMount() {
20 | const { accessToken } = this.props;
21 |
22 | if (accessToken) {
23 | Cookies.set('access_token', accessToken);
24 | Router.push('/');
25 | }
26 | }
27 |
28 | static async getInitialProps({ query }) {
29 | const { serverRuntimeConfig: { githubClientId, githubClientSecret } } = getConfig();
30 |
31 | const bodyData = JSON.stringify({
32 | client_id: githubClientId,
33 | client_secret: githubClientSecret,
34 | code: query.code,
35 | });
36 |
37 | const res = await fetch('https://github.com/login/oauth/access_token', {
38 | headers: {
39 | Accept: 'application/json',
40 | 'Content-Type': 'application/json',
41 | },
42 | method: 'POST',
43 | body: bodyData,
44 | });
45 |
46 | const json = await res.json();
47 | const errorMessage = json.error_description;
48 | return { errorMessage, accessToken: json.access_token };
49 | }
50 |
51 | render() {
52 | const { errorMessage } = this.props;
53 |
54 | if (errorMessage) {
55 | return (
56 | {errorMessage}
57 | );
58 | }
59 |
60 | return null;
61 | }
62 | }
63 |
64 | export default withRouter(Callback);
65 |
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Home } from 'components';
3 | import HeaderContainer from 'containers/HeaderContainer';
4 | import ViewerRepoList from 'containers/ViewerRepoList';
5 |
6 | const Index = () => (
7 | }
9 | content={ }
10 | />
11 | );
12 |
13 | export default Index;
14 |
--------------------------------------------------------------------------------
/pages/new_js/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Home } from 'components';
3 | import HeaderContainer from 'containers/HeaderContainer';
4 | import SearchRepoList from 'containers/SearchRepoList';
5 | import searchNewJsRepos from 'graphql/queries/searchNewJsRepos';
6 |
7 | const NewJs = () => (
8 | }
10 | content={ }
11 | />
12 | );
13 |
14 | export default NewJs;
15 |
--------------------------------------------------------------------------------
/pages/new_ruby/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Home } from 'components';
3 | import HeaderContainer from 'containers/HeaderContainer';
4 | import SearchRepoList from 'containers/SearchRepoList';
5 | import searchNewRubyRepos from 'graphql/queries/searchNewRubyRepos';
6 |
7 | const NewRuby = () => (
8 | }
10 | content={ }
11 | />
12 | );
13 |
14 | export default NewRuby;
15 |
--------------------------------------------------------------------------------
/pages/top_js/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Home } from 'components';
3 | import HeaderContainer from 'containers/HeaderContainer';
4 | import SearchRepoList from 'containers/SearchRepoList';
5 | import searchTopJsRepos from 'graphql/queries/searchTopJsRepos';
6 |
7 | const TopRuby = () => (
8 | }
10 | content={ }
11 | />
12 | );
13 |
14 | export default TopRuby;
15 |
--------------------------------------------------------------------------------
/pages/top_ruby/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Home } from 'components';
3 | import HeaderContainer from 'containers/HeaderContainer';
4 | import SearchRepoList from 'containers/SearchRepoList';
5 | import searchTopRubyRepos from 'graphql/queries/searchTopRubyRepos';
6 |
7 | const TopRuby = () => (
8 | }
10 | content={ }
11 | />
12 | );
13 |
14 | export default TopRuby;
15 |
--------------------------------------------------------------------------------
/public/images/nextjs-404.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rubygarage/nextjs6-graphql-client-tutorial/fce9f92cba87b1203285f3db5350cf404e049224/public/images/nextjs-404.png
--------------------------------------------------------------------------------
/public/images/storybook-initial-old.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rubygarage/nextjs6-graphql-client-tutorial/fce9f92cba87b1203285f3db5350cf404e049224/public/images/storybook-initial-old.png
--------------------------------------------------------------------------------
/public/images/storybook-initial.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rubygarage/nextjs6-graphql-client-tutorial/fce9f92cba87b1203285f3db5350cf404e049224/public/images/storybook-initial.png
--------------------------------------------------------------------------------