├── .gitignore
├── README.md
├── api
├── src
│ ├── db
│ │ ├── db.json
│ │ ├── index.js
│ │ ├── pet.js
│ │ ├── schema.json
│ │ └── user.js
│ ├── resolvers.js
│ ├── schema.js
│ └── server.js
└── tests
│ ├── mutations.test.js
│ ├── queries.test.js
│ └── resolvers.test.js
├── client
├── .babelrc
├── index.html
└── src
│ ├── client.js
│ ├── components
│ ├── App.js
│ ├── Error.js
│ ├── Header.js
│ ├── Loader.js
│ ├── NewPet.js
│ └── PetBox.js
│ ├── index.css
│ ├── index.js
│ └── pages
│ └── Pets.js
├── db.json
├── package.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
3 | # Logs
4 | logs
5 | *.log
6 | npm-debug.log*
7 | yarn-debug.log*
8 | yarn-error.log*
9 |
10 | # Runtime data
11 | pids
12 | *.pid
13 | *.seed
14 | *.pid.lock
15 |
16 | # Directory for instrumented libs generated by jscoverage/JSCover
17 | lib-cov
18 |
19 | # Coverage directory used by tools like istanbul
20 | coverage
21 |
22 | # nyc test coverage
23 | .nyc_output
24 |
25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
26 | .grunt
27 |
28 | # Bower dependency directory (https://bower.io/)
29 | bower_components
30 |
31 | # node-waf configuration
32 | .lock-wscript
33 |
34 | # Compiled binary addons (https://nodejs.org/api/addons.html)
35 | build/Release
36 |
37 | # Dependency directories
38 | node_modules/
39 | jspm_packages/
40 |
41 | # TypeScript v1 declaration files
42 | typings/
43 |
44 | # Optional npm cache directory
45 | .npm
46 |
47 | # Optional eslint cache
48 | .eslintcache
49 |
50 | # Optional REPL history
51 | .node_repl_history
52 |
53 | # Output of 'npm pack'
54 | *.tgz
55 |
56 | # Yarn Integrity file
57 | .yarn-integrity
58 |
59 | # dotenv environment variables file
60 | .env
61 |
62 | # next.js build output
63 | .next
64 | dist
65 | coverage
66 | .cache
67 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Fullstack GraphQL
2 | > Learn how to use GraphQL with Node and React
3 |
4 | This repository contains code used in two [Frontend Masters](https://frontendmasters.com) courses: [Server-Side GraphQL in Node.js](https://frontendmasters.com/courses/server-graphql-nodejs/) and [Client-Side GraphQL in React](https://frontendmasters.com/courses/client-graphql-react/)
5 |
6 | ## What you'll need
7 | * Node version 8.17.0
8 |
9 | ## Client-Side GraphQL in React
10 | After cloning the repository, make sure to switch to the `client` branch before running the application:
11 | ```bash
12 | git clone https://github.com/FrontendMasters/fullstack-graphql.git
13 | git checkout client --
14 | npm install
15 | npx yarn app
16 | ```
17 |
18 | ## Solutions
19 | The solution branch has the completed course fo reference. There is no one way to finish this course.
20 | `git checkout solution`
21 |
--------------------------------------------------------------------------------
/api/src/db/db.json:
--------------------------------------------------------------------------------
1 | {
2 | "user": {
3 | "id": "jBWMVGjm50l6LGwepDoty",
4 | "username": "frontendmaster"
5 | },
6 | "pet": []
7 | }
8 |
--------------------------------------------------------------------------------
/api/src/db/index.js:
--------------------------------------------------------------------------------
1 | const low = require('lowdb')
2 | const FileSync = require('lowdb/adapters/FileSync')
3 |
4 | const adapter = new FileSync('api/src/db/db.json')
5 | const db = low(adapter)
6 |
7 | const createPetModel = require('./pet')
8 | const createUserModel = require('./user')
9 |
10 | module.exports = {
11 | models: {
12 | Pet: createPetModel(db),
13 | User: createUserModel(db),
14 | },
15 | db
16 | }
17 |
--------------------------------------------------------------------------------
/api/src/db/pet.js:
--------------------------------------------------------------------------------
1 | const nanoid = require('nanoid')
2 |
3 | const createPetModel = db => {
4 | return {
5 | findMany(filter) {
6 | return db.get('pet')
7 | .filter(filter)
8 | .value()
9 | },
10 |
11 | findOne(filter) {
12 | return db.get('pet')
13 | .find(filter)
14 | .value()
15 | },
16 |
17 | create(pet) {
18 | const newPet = {id: nanoid(), createdAt: Date.now(), ...pet}
19 |
20 | db.get('pet')
21 | .push(newPet)
22 | .write()
23 |
24 | return newPet
25 | }
26 | }
27 | }
28 |
29 | module.exports = createPetModel
30 |
--------------------------------------------------------------------------------
/api/src/db/schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "user": {
3 | "id": "string",
4 | "username": "string"
5 | },
6 | "pet":{
7 | "id": "string",
8 | "createdAt": "number",
9 | "name": "string",
10 | "type": "string"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/api/src/db/user.js:
--------------------------------------------------------------------------------
1 | const nanoid = require('nanoid')
2 |
3 | const createUserModel = db => {
4 | return {
5 | findOne() {
6 | return db.get('user')
7 | .value()
8 | },
9 |
10 | create(user) {
11 | const newUser = {id: nanoid(), createdAt: Date.now(), ...user}
12 | db.set('user', newUser)
13 | .write()
14 |
15 | return newUser
16 | }
17 | }
18 | }
19 |
20 | module.exports = createUserModel
21 |
--------------------------------------------------------------------------------
/api/src/resolvers.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Here are your Resolvers for your Schema. They must match
3 | * the type definitions in your scheama
4 | */
5 |
6 | module.exports = {
7 | Query: {
8 |
9 | },
10 | Mutation: {
11 |
12 | },
13 | Pet: {
14 | img(pet) {
15 | return pet.type === 'DOG'
16 | ? 'https://placedog.net/300/300'
17 | : 'http://placekitten.com/300/300'
18 | }
19 | },
20 | User: {
21 |
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/api/src/schema.js:
--------------------------------------------------------------------------------
1 | const { gql } = require('apollo-server')
2 |
3 | /**
4 | * Type Definitions for our Schema using the SDL.
5 | */
6 | const typeDefs = gql`
7 |
8 |
9 | `;
10 |
11 | module.exports = typeDefs
12 |
--------------------------------------------------------------------------------
/api/src/server.js:
--------------------------------------------------------------------------------
1 | const { ApolloServer } = require('apollo-server')
2 | const typeDefs = require('./schema')
3 | const resolvers = require('./resolvers')
4 | const {models, db} = require('./db')
5 |
6 | const server = new ApolloServer()
7 |
8 | server.listen().then(({ url }) => {
9 | console.log(`🚀 Server ready at ${url}`);
10 | })
11 |
--------------------------------------------------------------------------------
/api/tests/mutations.test.js:
--------------------------------------------------------------------------------
1 | describe('mutations', () => {
2 | test('hello', () => {
3 | expect(1).toBe(1)
4 | })
5 | })
6 |
--------------------------------------------------------------------------------
/api/tests/queries.test.js:
--------------------------------------------------------------------------------
1 | describe('queries', () => {
2 | test('hello', () => {
3 | expect(1).toBe(1)
4 | })
5 | })
6 |
--------------------------------------------------------------------------------
/api/tests/resolvers.test.js:
--------------------------------------------------------------------------------
1 | describe('resolvers', () => {
2 | test('hello', () => {
3 | expect(1).toBe(1)
4 | })
5 | })
6 |
--------------------------------------------------------------------------------
/client/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["env", "react"],
3 | "plugins": ["emotion", "transform-object-rest-spread"]
4 | }
5 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | React starter app
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/client/src/client.js:
--------------------------------------------------------------------------------
1 | import { ApolloClient } from 'apollo-client'
2 | import { InMemoryCache } from 'apollo-cache-inmemory'
3 | import { HttpLink } from 'apollo-link-http'
4 | import gql from 'graphql-tag'
5 |
6 |
7 | const client = new ApolloClient()
8 |
9 | export default client
10 |
--------------------------------------------------------------------------------
/client/src/components/App.js:
--------------------------------------------------------------------------------
1 | import { Switch, Route } from 'react-router-dom'
2 | import React, {Fragment} from 'react'
3 | import Header from './Header'
4 | import Pets from '../pages/Pets'
5 |
6 | const App = () => (
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | )
16 |
17 | export default App
18 |
--------------------------------------------------------------------------------
/client/src/components/Error.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FrontendMasters/fullstack-graphql/d687926802b19ab25bc58c700c74c5c56b68192d/client/src/components/Error.js
--------------------------------------------------------------------------------
/client/src/components/Header.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Link } from 'react-router-dom'
3 | import { withRouter } from 'react-router'
4 |
5 | const Header = () =>
6 |
7 |
8 |
9 |
10 | Home
11 |
12 |
13 |
14 |
15 |
16 | export default withRouter(Header)
17 |
--------------------------------------------------------------------------------
/client/src/components/Loader.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ClipLoader from 'react-spinners/ClipLoader'
3 |
4 | const Loader = () =>
5 |
6 |
12 |
13 |
14 | export default Loader
15 |
--------------------------------------------------------------------------------
/client/src/components/NewPet.js:
--------------------------------------------------------------------------------
1 | import React, {useState} from 'react'
2 | import Select from 'react-select'
3 |
4 | const options = [
5 | { value: 'CAT', label: 'Cat' },
6 | { value: 'DOG', label: 'Dog' }
7 | ]
8 |
9 | export default function NewPet({onSubmit, onCancel}) {
10 | const [type, setType] = useState('DOG')
11 | const [name, setName] = useState('')
12 |
13 | const activeOption = options.find(o => o.value === type)
14 |
15 | const submit = e => {
16 | e.preventDefault()
17 | onSubmit({name, type})
18 | }
19 |
20 | const cancel = e => {
21 | e.preventDefault()
22 | onCancel()
23 | }
24 |
25 | return (
26 |
50 | )
51 | }
52 |
--------------------------------------------------------------------------------
/client/src/components/PetBox.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const PetBox = ({pet}) => (
4 |
5 |
6 |
7 |
8 |
{pet.name}
9 |
{pet.type}
10 |
11 | )
12 |
13 | export default PetBox
14 |
--------------------------------------------------------------------------------
/client/src/index.css:
--------------------------------------------------------------------------------
1 | @import 'flexboxgrid';
2 |
3 | :root {
4 | --bgColor: #EFEFEF;
5 | --primaryColor: #3454D1;
6 | --secondaryColor: #34D1BF;
7 | --textColor: #070707;
8 | --errorColor: #D1345B;
9 | --lightTextColor: #fff;
10 | }
11 |
12 | body {
13 | margin: 0;
14 | padding: 0;
15 | font-family: 'Helvetica Neue', 'Segoe UI', Roboto, -apple-system, system-ui, BlinkMacSystemFont, Arial, sans-serif;
16 | background: var(--bgColor);
17 | }
18 |
19 | body * {
20 | box-sizing: border-box;
21 | }
22 |
23 | html {
24 | font-size: calc(0.6em + 1vw);
25 | color: var(--textColor);
26 | }
27 |
28 | .page {
29 | padding: 3em 6em;
30 | }
31 |
32 | .row {
33 | margin: 0px;
34 | }
35 |
36 | header {
37 | height: 65px;
38 | width: 100vw;
39 | background: var(--primaryColor);
40 | padding: 10px 30px;
41 | }
42 |
43 | .link, a {
44 | text-decoration: none;
45 | color: var(--secondaryColor);
46 | }
47 |
48 | .full-page-loader {
49 | position: absolute;
50 | top: 0;
51 | left: 0;
52 | background: rgba(255,255,255, .8);
53 | width: 100vw;
54 | height: 100vh;
55 | display: flex;
56 | justify-content: center;
57 | align-items: center;
58 | }
59 |
60 | .full-page {
61 | height: 100vh;
62 | width: 100vw;
63 | }
64 |
65 | .full-height {
66 | height: 100%;
67 | }
68 |
69 | .form-box {
70 | background: white;
71 | padding: 1.5em;
72 | border-radius: 3px;
73 | }
74 |
75 | .box {
76 | background: white;
77 | padding: 1.5em;
78 | border-radius: 4px;
79 | border: 1px solid lightgrey;
80 | }
81 |
82 | .input {
83 | padding: .5em;
84 | outline: none;
85 | border: 1px solid lightgrey;
86 | border-radius: 3px;
87 | height: 1.6rem;
88 | font-size: 1rem;
89 | width: 100%;
90 | margin: 1em 0;
91 | }
92 |
93 | .input:focus {
94 | outline: none;
95 | }
96 |
97 | button,.button {
98 | outline: none;
99 | border: none;
100 | color: var(--lightTextColor);
101 | font-size: .8rem;
102 | text-transform: uppercase;
103 | padding: .5rem;
104 | background: var(--primaryColor);
105 | border-radius: 3px;
106 | cursor: pointer;
107 | }
108 |
109 | button.error, .button.error {
110 | background: var(--errorColor);
111 | }
112 |
113 | .col {
114 | padding-top: .5rem;
115 | padding-bottom: .5rem
116 | }
117 |
118 | figure {
119 | width: 200px;
120 | height: 200px;
121 | }
122 |
123 | figure img {
124 | width: 100%;
125 | }
126 |
--------------------------------------------------------------------------------
/client/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import { BrowserRouter } from 'react-router-dom'
4 | import { ApolloProvider } from '@apollo/react-hooks'
5 | import App from './components/App'
6 | import client from './client'
7 | import './index.css'
8 |
9 | const Root = () => (
10 |
11 |
12 |
13 | )
14 |
15 | ReactDOM.render(, document.getElementById('app'))
16 |
17 | if (module.hot) {
18 | module.hot.accept()
19 | }
20 |
--------------------------------------------------------------------------------
/client/src/pages/Pets.js:
--------------------------------------------------------------------------------
1 | import React, {useState} from 'react'
2 | import gql from 'graphql-tag'
3 | import PetBox from '../components/PetBox'
4 | import NewPet from '../components/NewPet'
5 | import { useQuery, useMutation } from '@apollo/react-hooks'
6 | import Loader from '../components/Loader'
7 |
8 | export default function Pets () {
9 | const [modal, setModal] = useState(false)
10 |
11 | const onSubmit = input => {
12 | setModal(false)
13 | }
14 |
15 | const petsList = pets.data.pets.map(pet => (
16 |
21 | ))
22 |
23 | if (modal) {
24 | return (
25 |
26 |
27 | setModal(false)}/>
28 |
29 |
30 | )
31 | }
32 |
33 | return (
34 |
35 |
36 |
37 |
38 |
Pets
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | {petsList}
49 |
50 |
51 |
52 | )
53 | }
54 |
--------------------------------------------------------------------------------
/db.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@apollo/react-hooks": "^3.1.2",
7 | "apollo-cache-inmemory": "^1.6.3",
8 | "apollo-client": "^2.6.4",
9 | "apollo-link-http": "^1.5.16",
10 | "apollo-server": "^2.9.6",
11 | "babel-plugin-emotion": "^10.0.21",
12 | "flexboxgrid": "^6.3.1",
13 | "graphql": "^14.5.8",
14 | "graphql-tag": "^2.10.1",
15 | "json5": "^2.1.0",
16 | "lowdb": "^1.0.0",
17 | "nanoid": "^2.1.1",
18 | "react": "^16.10.1",
19 | "react-dom": "^16.10.1",
20 | "react-router": "^5.1.2",
21 | "react-router-dom": "^5.1.2",
22 | "react-select": "^3.0.8",
23 | "react-spinners": "^0.6.1"
24 | },
25 | "scripts": {
26 | "server": "node api/src/server.js",
27 | "app": "parcel client/index.html --out-dir client/dist",
28 | "test-server": "jest api/tests"
29 | },
30 | "browserslist": {
31 | "production": [
32 | ">0.2%",
33 | "not dead",
34 | "not op_mini all"
35 | ],
36 | "development": [
37 | "last 1 chrome version",
38 | "last 1 firefox version",
39 | "last 1 safari version"
40 | ]
41 | },
42 | "devDependencies": {
43 | "@playlyfe/gql": "^2.6.2",
44 | "babel-core": "^6.26.3",
45 | "babel-plugin-transform-object-rest-spread": "^6.26.0",
46 | "babel-preset-env": "^1.7.0",
47 | "babel-preset-react": "^6.24.1",
48 | "jest": "^24.9.0",
49 | "parcel": "1.12.3"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------