├── LICENSE
├── README.md
├── Section1
└── README.md
├── Section2
├── .gitignore
├── README.md
└── server
│ ├── .babelrc
│ ├── .graphqlconfig.yml
│ ├── README.md
│ ├── TrelloWrapper.md
│ ├── datamodel.prisma
│ ├── docker-compose.yml
│ ├── package.json
│ ├── prisma.yml
│ └── src
│ ├── apollo-server.js
│ ├── express-graphql.js
│ ├── graphql-yoga-server.js
│ └── schema.js
├── Section3
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── README.md
├── package.json
├── public
│ ├── index.html
│ └── manifest.json
├── server
│ ├── .graphqlconfig.yml
│ ├── README.md
│ ├── datamodel.prisma
│ ├── docker-compose.yml
│ ├── package.json
│ └── prisma.yml
└── src
│ ├── App.css
│ ├── App.js
│ ├── components.js
│ ├── components
│ ├── Card.js
│ └── CardList.js
│ ├── dummyData.js
│ ├── index.css
│ ├── index.js
│ ├── registerServiceWorker.js
│ └── schema.js
├── Section4
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── README.md
├── package.json
├── public
│ ├── index.html
│ └── manifest.json
├── server
│ ├── .graphqlconfig.yml
│ ├── README.md
│ ├── datamodel.prisma
│ ├── docker-compose.yml
│ ├── package.json
│ └── prisma.yml
└── src
│ ├── App.css
│ ├── App.js
│ ├── components
│ ├── BoardContainer.js
│ ├── Card.js
│ ├── CardList.js
│ ├── Constants.js
│ └── CoolBoard.js
│ ├── dummyData.js
│ ├── index.css
│ ├── index.js
│ ├── registerServiceWorker.js
│ └── schema.js
├── Section5
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── README.md
├── package.json
├── public
│ ├── index.html
│ └── manifest.json
├── server
│ ├── .graphqlconfig.yml
│ ├── README.md
│ ├── datamodel.prisma
│ ├── docker-compose.yml
│ ├── package.json
│ └── prisma.yml
└── src
│ ├── App.css
│ ├── App.js
│ ├── components
│ ├── BoardContainer.js
│ ├── Card.js
│ ├── CardList.js
│ ├── Constants.js
│ └── CoolBoard.js
│ ├── dummyData.js
│ ├── index.css
│ ├── index.js
│ ├── registerServiceWorker.js
│ └── schema.js
├── Section6
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── README.md
├── package.json
├── public
│ ├── index.html
│ └── manifest.json
├── server
│ ├── .editorconfig
│ ├── .eslintignore
│ ├── .eslintrc.js
│ ├── .gitignore
│ ├── .graphqlconfig.yml
│ ├── .prettierrc
│ ├── README.md
│ ├── database
│ │ ├── datamodel.prisma
│ │ ├── prisma.yml
│ │ └── seed.graphql
│ ├── docker-compose.yml
│ ├── package.json
│ └── src
│ │ ├── generated
│ │ └── prisma.graphql
│ │ ├── index.js
│ │ ├── resolvers
│ │ ├── AuthPayload.js
│ │ ├── Mutation
│ │ │ ├── auth.js
│ │ │ ├── board.js
│ │ │ ├── card.js
│ │ │ └── list.js
│ │ ├── Query.js
│ │ ├── Subscription.js
│ │ └── index.js
│ │ ├── schema.graphql
│ │ └── utils.js
└── src
│ ├── App.css
│ ├── App.js
│ ├── components
│ ├── AuthForm.js
│ ├── BoardContainer.js
│ ├── Boards.js
│ ├── Card.js
│ ├── CardList.js
│ ├── Constants.js
│ ├── CoolBoard.js
│ ├── CreateBoardModal.js
│ ├── FullVerticalContainer.js
│ ├── LoginForm.js
│ ├── ProfileHeader.js
│ └── SignupForm.js
│ ├── dummyData.js
│ ├── index.css
│ ├── index.js
│ ├── registerServiceWorker.js
│ └── schema.js
└── Section7
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── README.md
├── package.json
├── public
├── index.html
└── manifest.json
├── server
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .graphqlconfig.yml
├── .prettierrc
├── README.md
├── database
│ ├── datamodel.graphql
│ ├── datamodel.prisma
│ ├── prisma.yml
│ └── seed.graphql
├── docker-compose.yml
├── package.json
└── src
│ ├── generated
│ └── prisma.graphql
│ ├── index.js
│ ├── resolvers
│ ├── AuthPayload.js
│ ├── Mutation
│ │ ├── auth.js
│ │ ├── board.js
│ │ ├── card.js
│ │ └── list.js
│ ├── Query.js
│ ├── Subscription.js
│ └── index.js
│ ├── schema.graphql
│ └── utils.js
└── src
├── App.css
├── App.js
├── authentication
├── AuthForm.js
├── LoginForm.js
└── SignupForm.js
├── common
├── FullVerticalContainer.js
├── GeneralErrorHandler.js
└── ProfileHeader.js
├── components
├── BoardContainer.js
├── Boards.js
├── Card.js
├── CardList.js
├── CoolBoard.js
└── CreateBoardModal.js
├── dummyData.js
├── index.css
├── index.js
├── registerServiceWorker.js
└── schema.js
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Packt
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | **Note:** 📣 After more than 4 years there are some important❗️ and cool 😎 updates, like e.g.
2 | * React Hooks ⚛️
3 | * simpler, functional React components
4 | * Apollo Client 3
5 | * more 🎁 to come... 🚀
6 |
7 | **➔ Just head over to the *latest* version at [lowsky/-Hands-on-Application-Building-with-GraphQL](https://github.com/lowsky/-Hands-on-Application-Building-with-GraphQL)**
8 |
9 | # Hands-on Application Building with GraphQL [Video]
10 | This is the code repository for [Hands-on Application Building with GraphQL [Video]](https://www.packtpub.com/product/hands-on-application-building-with-graphql-video/9781788991865?utm_source=github&utm_medium=repository&utm_campaign=9781788991865), published by [Packt](https://www.packtpub.com/?utm_source=github). It contains all the supporting project files necessary to work through the video course from start to finish.
11 |
12 | ## About the Video Course
13 | > GraphQL is a data-fetching API developed by Facebook, which has been using it for five years; it powers millions of devices and most components of the Facebook and Instagram website. In this course, you will get an introduction into GraphQL as a bridge for React client application to communicate with servers as the missing data-fetching or query language.
14 |
15 | In this course, you will learn how to build your own Trello-like web application using GraphQL. The course starts by teaching you GraphQL basics and comparing it with REST;
16 | you will then learn to run queries and specify types in its schema system. The course then shows you how to build a Graphql server and a client UI and connect this Apollo-based client to the server.
17 | You will then learn to add features to your board such as adding or editing a task. You will then see how to implement the shared whiteboard functionality by populating the changes into others sessions and how to solve the conflicts in this real-world scenario with concurrent changes from different users.
18 | The course then shows you how to add authentication to your application to prevent unwanted access to it and user centric web service
19 | Finally, you will learn troubleshooting typical problems that may occur while running your app, and how to fine-tune the schema and communication of client-server. By the end of the course, you will be able to build your own applications using GraphQL
20 |
21 |
What You Will Learn
22 |
23 |
24 | Build complete, effective web apps that interact with a backend via GraphQL queries.
25 | Construct a schema for your project on GraphQL
26 | Create your own server for your application in GraphQL
27 | Work with your own local server for your application
28 | Set up a React application to build your application
29 | Connect prisma/graph.cool as a cloud-based realtime GraphQL database
30 | Implement an UI to add, move, and edit cards on the server
31 | Add user authentication and user management to your application
32 | Learn tooling to troubleshoot issues that may occur while using your own or any other GraphQL server.
33 |
34 | ## Instructions and Navigation
35 | ### Assumed Knowledge
36 | To fully benefit from the coverage included in this course, you will need:
37 | A go-to resource for programmers keen to building applications in a relatively fast and easy way. You should already have some basic knowledge of creating a web application with React. By the end of this course, you'll be ready to create your own real-life app with GraphQL.
38 | ### Technical Requirements
39 | This course has the following software requirements:
40 |
41 | OS: Windows 7 or Windows 10, 11, Mac OS, Linux
42 | Browser: Firefox, Safari, Google Chrome, Latest Version
43 |
44 | ## Related Products
45 |
46 | **Code Repository** for
47 | [Hands-on Application Building with GraphQL](https://www.packtpub.com/product/hands-on-application-building-with-graphql-video/9781788991865), Published by [Packt](https://www.packtpub.com/)
48 |
49 | **Author**: Robert Hostlowsky (robert.hostlowsky) [on fediverse: @lowsky](https://mastodontech.de/@lowsky)
50 |
51 | There is this **live demo** available at [https://www.coolboard.fun/](https://www.coolboard.fun/)
52 |
53 | 
54 |
--------------------------------------------------------------------------------
/Section1/README.md:
--------------------------------------------------------------------------------
1 | # Hands-on Application Building with GraphQL
2 |
3 | ## Section 1: GraphQL basics / Getting Started with GraphQL
4 |
5 | ### Videos
6 |
7 | 1. The Course Overview - 00:06:25
8 | 1. Comparing GraphQL to REST: Trello Rest API - 00:16:48
9 | 1. Starting a Project on Graphcool - 00:03:57
10 | 1. Building GraphQL Schema for the project - 00:07:21
11 | 1. Working with GraphQL Queries and Types - 00:08:24
12 |
13 | ### Addons:
14 |
15 | Video 1.4: Our basic GraphQL schema
16 |
17 | ```
18 | type Board @model {
19 | id: ID! @isUnique
20 | lists: [List!]! @relation(name: "BoardOnList")
21 | name: String!
22 | }
23 | type List @model {
24 | board: Board @relation(name: "BoardOnList")
25 | cards: [Card!]! @relation(name: "CardOnList")
26 | id: ID! @isUnique
27 | name: String!
28 | }
29 | type Card @model {
30 | id: ID! @isUnique
31 | list: List @relation(name: "CardOnList")
32 | name: String!
33 | }
34 | ```
35 |
--------------------------------------------------------------------------------
/Section2/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | package-lock.json
3 | yarn.lock
4 |
--------------------------------------------------------------------------------
/Section2/README.md:
--------------------------------------------------------------------------------
1 | # Hands-on Application Building with GraphQL
2 |
3 | ## Section 2: Creating Your Own GraphQL Server
4 |
5 | 1. Using the Built-in GraphQL for Analyzing and Verifying the Schema - 00:12:28
6 | 1. Adding Some Mocked Data in Your Application - 00:05:27
7 | 1. Using Real Trello Data with a REST API - 00:10:18
8 | 1. Running Our Own Server Locally - 00:08:29
9 | 1. Local GraphQL Server with Database - 00:14:09
10 |
11 |
12 | ## ~Tutorial in Apollo launchpad (soon-to-be-updated):~
13 | ## Tutorial in Codesandbox
14 |
15 | _Note:_ "Apollo launchpad" was sunsetted and is not longer available. Therefore we migrated to Codesandbox with a similar service and look and feel!
16 |
17 | ### 2.1 coolboard-lists-cards-simple
18 |
19 | Initial:
20 | https://codesandbox.io/s/section21-initial-kmmp8
21 |
22 | Final:
23 | https://codesandbox.io/s/section21-part-2-c2pys
24 |
25 | ### 2.2 coolboard-lists-cards-simple-mocks
26 |
27 | Initial:
28 | https://codesandbox.io/s/section22-init-i7esz
29 |
30 | Final:
31 | https://codesandbox.io/s/section22-final-psxbl
32 |
33 | ### 2.3 Trello REST wrapper
34 | https://codesandbox.io/s/section23-nvsuj
35 |
36 | ### 2.4 Developing and running the server locally
37 |
38 | Initial:
39 | https://codesandbox.io/s/section24-initial-bh29w
40 |
41 | Final:
42 | https://codesandbox.io/s/section24-final-23rj0
43 |
44 | Also, [server/TrelloWrapper.md](./server/TrelloWrapper.md) for details how to start the trello-rest wrapper/proxy
45 |
46 | ### 2.5 local server
47 | More details in [server/README.md](./server/README.md) for running the local server based on prisma.
48 |
--------------------------------------------------------------------------------
/Section2/server/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "env"
4 | ],
5 | "plugins": [
6 | "transform-runtime",
7 | "transform-async-generator-functions",
8 | "transform-object-rest-spread"
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/Section2/server/.graphqlconfig.yml:
--------------------------------------------------------------------------------
1 | projects:
2 | database:
3 | extensions:
4 | prisma: prisma.yml
5 |
--------------------------------------------------------------------------------
/Section2/server/README.md:
--------------------------------------------------------------------------------
1 | # Local GraphQL server based on Prisma
2 |
3 | To run you local server, you will have to run these commands in a
4 | terminal in this sub-folder (after a `cd server`):
5 |
6 | * After having docker running on you local machine,
7 | you can **start up** the prisma server with `docker-compose up -d`.
8 |
9 | * By running `npm install` or `yarn`, the prisma tooling will be installed and available in the next step:
10 |
11 | * You then run `npx prisma deploy` to **deploy the schema**.
12 |
13 | * Finally, **check** it by opening this page: [http://localhost:4466/](http://localhost:4466/) which shows the graphiql or graphql playground.
14 | ---
15 | * Later, you can **stop** the prisma server via `docker-compose stop` ,
16 | `docker-compose kill` ,
17 |
18 | * For **completely removing** these docker containers you will need to run `docker-compose kill` and `docker-compose rm` which will destroy all its stored data!
19 |
--------------------------------------------------------------------------------
/Section2/server/TrelloWrapper.md:
--------------------------------------------------------------------------------
1 | # Trello-REST-Api wrapper - quick start
2 |
3 | To run you local server, you will have to run these commands in a
4 | terminal in this sub-folder (after a `cd server`).
5 |
6 | You will first need to install all libraries per `npm install` or `yarn`.
7 |
8 | ## run apollo-express-server
9 |
10 | ```bash
11 | yarn run start
12 | ```
13 | and open [http://localhost:3000/graphiql](http://localhost:3000/graphiql?query=%7B%0A%20%20Member(username%3A%22taco%22)%20%7B%0A%20%20%20%20id%0A%20%20%20%20url%0A%20%20%20%20username%0A%20%20%20%20boards%20%7B%0A%20%20%20%20%20%20id%0A%20%20%20%20%20%20name%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D)
14 | for this query:
15 | ```
16 | {
17 | Member(username:"taco") {
18 | id
19 | url
20 | username
21 | boards {
22 | id
23 | name
24 | }
25 | }
26 | }
27 | ```
28 |
29 | ## run express-graphql
30 |
31 | ```bash
32 | yarn run start-express
33 | ```
34 | and open [http://localhost:4000](http://localhost:4000/?query=%7B%0A%20%20Member(username%3A%22taco%22)%20%0A%20%20%7B%0A%20%20%20%20id%0A%20%20%20%20username%0A%20%20%20%20url%0A%20%20%20%20boards%20%7B%0A%20%20%20%20%20%20id%0A%20%20%20%20%20%20name%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A)
35 |
36 | ## run graphql-yoga
37 | ```bash
38 | yarn run start-yoga
39 | ```
40 | and open [http://localhost:4000](http://localhost:4000/?query=%7B%0A%20%20Member(username%3A%22taco%22)%20%7B%0A%20%20%20%20id%0A%20%20%20%20url%0A%20%20%20%20username%0A%20%20%20%20boards%20%7B%0A%20%20%20%20%20%20id%0A%20%20%20%20%20%20name%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D) playground
41 |
42 |
43 | You can for example run this query:
44 | ```
45 | {
46 | Member(username:"taco")
47 | {
48 | id
49 | username
50 | url
51 | boards {
52 | id
53 | name
54 | }
55 | }
56 | }
57 | ```
58 |
59 |
--------------------------------------------------------------------------------
/Section2/server/datamodel.prisma:
--------------------------------------------------------------------------------
1 | type Board {
2 | id: ID! @id
3 | lists: [List!]!
4 | name: String!
5 | }
6 | type List {
7 | cards: [Card!]!
8 | id: ID! @id
9 | name: String!
10 | }
11 | type Card {
12 | id: ID! @id
13 | name: String!
14 | description: String @default(value: "")
15 | }
16 | type User {
17 | id: ID! @id
18 | email: String! @unique
19 | password: String!
20 | name: String!
21 | avatarUrl: String @default(value:"")
22 | }
23 |
--------------------------------------------------------------------------------
/Section2/server/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | prisma-handson-course:
4 | image: prismagraphql/prisma:1.31
5 | restart: always
6 | ports:
7 | - "4466:4466"
8 | environment:
9 | PRISMA_CONFIG: |
10 | port: 4466
11 | # uncomment the next line and provide the env var PRISMA_MANAGEMENT_API_SECRET=my-secret to activate cluster security
12 | # managementApiSecret: my-secret
13 | databases:
14 | default:
15 | connector: mysql
16 | host: mysql-handson-course
17 | user: root
18 | password: prisma
19 | rawAccess: true
20 | port: 3306
21 | migrations: true
22 | mysql-handson-course:
23 | image: mysql:5.7
24 | restart: always
25 | # Uncomment the next two lines to connect to your your database from outside the Docker environment, e.g. using a database GUI like Workbench
26 | # ports:
27 | # - "3306:3306"
28 | environment:
29 | MYSQL_ROOT_PASSWORD: prisma
30 | volumes:
31 | - mysql:/var/lib/mysql
32 | volumes:
33 | mysql:
34 |
--------------------------------------------------------------------------------
/Section2/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "trello_rest_wrapper",
3 | "version": "0.1.0",
4 | "description": "A GraphQL server wrapping Trello REST API",
5 | "main": "./src/server.js",
6 | "scripts": {
7 | "start": "nodemon ./src/apollo-server.js --exec babel-node -e js",
8 | "start-yoga": "nodemon ./src/graphql-yoga-server.js --exec babel-node -e js",
9 | "start-express": "nodemon ./src/express-graphql.js --exec babel-node -e js",
10 | "test": "echo \"Error: no test specified\" && exit 1"
11 | },
12 | "author": "Robert Hostlowsky",
13 | "private": true,
14 | "devDependencies": {},
15 | "dependencies": {
16 | "apollo-server-express": "2.14.2",
17 | "babel-cli": "6.26.0",
18 | "babel-plugin-transform-async-generator-functions": "6.24.1",
19 | "babel-plugin-transform-object-rest-spread": "6.26.0",
20 | "babel-plugin-transform-runtime": "6.23.0",
21 | "babel-preset-env": "1.7.0",
22 | "casual": "1.6.0",
23 | "express": "4.17.0",
24 | "express-graphql": "0.8.0",
25 | "graphql": "14.3.1",
26 | "graphql-tools": "4.0.4",
27 | "graphql-yoga": "1.17.4",
28 | "node-fetch": "2.6.1",
29 | "nodemon": "1.19.0",
30 | "prisma": "1.33.0"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Section2/server/prisma.yml:
--------------------------------------------------------------------------------
1 | endpoint: http://localhost:4466
2 |
3 | datamodel: datamodel.prisma
4 |
5 | generate:
6 | - generator: graphql-schema
7 | output: schema.graphql
8 |
9 |
--------------------------------------------------------------------------------
/Section2/server/src/apollo-server.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 |
3 | const {ApolloServer, gql} = require('apollo-server-express');
4 |
5 | import {schema} from './schema';
6 |
7 | const PORT = 3000;
8 |
9 | const server = express();
10 |
11 | const apolloServer = new ApolloServer({ schema,
12 | debug: true,
13 | tracing: true,
14 | playground: true
15 | });
16 |
17 | apolloServer.applyMiddleware({
18 | path: '/graphql',
19 | app: server });
20 |
21 | server.listen({ port: PORT }, () => console.log(`GraphQL Server is now running on http://localhost:${PORT}${apolloServer.graphqlPath}`)
22 | )
23 |
--------------------------------------------------------------------------------
/Section2/server/src/express-graphql.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const graphqlHTTP = require('express-graphql');
3 |
4 | const schema = require('./schema');
5 |
6 | const app = express();
7 |
8 | app.use('/', graphqlHTTP({
9 | schema: schema.schema,
10 | graphiql: true
11 | }));
12 |
13 | app.listen(4000);
14 |
--------------------------------------------------------------------------------
/Section2/server/src/graphql-yoga-server.js:
--------------------------------------------------------------------------------
1 | import {GraphQLServer} from 'graphql-yoga'
2 | import {schema} from './schema';
3 |
4 | const server = new GraphQLServer({schema});
5 |
6 | let config = {
7 | port: 4000
8 | };
9 | let logServerStarted = (options) =>
10 | console.log(
11 | `Server is running on localhost:${options.port}`);
12 |
13 | server.start(
14 | config,
15 | (runOptions) => logServerStarted(runOptions)
16 | );
17 |
--------------------------------------------------------------------------------
/Section2/server/src/schema.js:
--------------------------------------------------------------------------------
1 | import {
2 | makeExecutableSchema
3 | } from 'graphql-tools';
4 | import casual from 'casual';
5 | import fetch from 'node-fetch'
6 |
7 | // Our schema for Trello
8 | const typeDefs = `
9 | type Board {
10 | id: ID!
11 | lists: [List!]!
12 | name: String!
13 | }
14 | type List {
15 | cards: [Card!]!
16 | id: ID!
17 | name: String!
18 | }
19 | type Card {
20 | id: ID!
21 | name: String!
22 | }
23 | type Member {
24 | id: ID!
25 | username: String!
26 | url: String!
27 | boards(first:Int): [Board!]!
28 | }
29 | type Query {
30 | Board(id: String): Board
31 | Member(username: String!): Member
32 | }`;
33 |
34 | let fetchBoardsListCardsById =
35 | async(listId) => {
36 | const response = await fetch(
37 | `https://api.trello.com/1/lists/${listId}/cards`);
38 | return response.json();
39 | };
40 |
41 | let fetchBoardsListsById =
42 | async(id) => {
43 | const response = await fetch(
44 | `https://api.trello.com/1/boards/${id}/lists`);
45 | const data = await response.json();
46 | return data.map(list => ({
47 | ...list,
48 | cards: () => fetchBoardsListCardsById(
49 | list.id)
50 | }));
51 | };
52 |
53 | let fetchBoardsById = async(id) => {
54 | const response = await fetch(`https://api.trello.com/1/boards/${id}`);
55 | const data = await response.json();
56 | return {
57 | ...data,
58 | lists: () => fetchBoardsListsById(id)
59 | };
60 | };
61 |
62 | let fetchMemberByName =
63 | async(username) => {
64 | const response = await
65 | fetch(`https://api.trello.com/1/members/${username}`, {});
66 | if(response.status !== 200) {
67 | throw new Error(response.statusText);
68 | }
69 | const data = await
70 | response.json();
71 |
72 | return {
73 | ...data,
74 | username: data.username + " per REST",
75 | boards: () => {
76 | return (data.idBoards || [])
77 | .map(boardId =>
78 | fetchBoardsById(
79 | boardId))
80 | }
81 | };
82 | };
83 |
84 | const resolvers = {
85 | Query: {
86 | Member(parent, args) {
87 | return fetchMemberByName(
88 | args.username);
89 | },
90 | Board(parent, args) {
91 | return fetchBoardsById(
92 | args.id);
93 | }
94 | }
95 | };
96 |
97 | // Export the GraphQL.js schema object as "schema"
98 | export const schema =
99 | makeExecutableSchema({
100 | resolvers,
101 | typeDefs
102 | });
103 |
104 | // Add mocking
105 | import {addMockFunctionsToSchema} from 'graphql-tools';
106 |
107 | const mocks = {
108 | String: () => casual.color_name
109 | };
110 | addMockFunctionsToSchema({
111 | schema, mocks,
112 | preserveResolvers: true
113 | });
114 |
--------------------------------------------------------------------------------
/Section3/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: http://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | # Unix-style newlines with a newline ending every file
7 | [*]
8 | end_of_line = lf
9 | insert_final_newline = true
10 | indent_style = space
11 | indent_size = 2
12 | trim_trailing_whitespace = true
13 | charset = utf-8
14 |
15 | # Matches the exact files either package.json or .travis.yml
16 | [package.json]
17 | indent_size = 2
18 |
19 | [*.md]
20 | trim_trailing_whitespace = false
21 |
--------------------------------------------------------------------------------
/Section3/.eslintignore:
--------------------------------------------------------------------------------
1 | src/registerServiceWorker.js
2 |
--------------------------------------------------------------------------------
/Section3/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | },
5 | parser: 'babel-eslint',
6 | plugins: ['react', 'prettier'],
7 | extends: [
8 | 'eslint:recommended',
9 | 'plugin:react/recommended',
10 | 'plugin:prettier/recommended',
11 | ],
12 | rules: {
13 | // already defined with prettier:
14 | 'prettier/prettier': ['warn'],
15 | indent: ['off', 2],
16 | quotes: ['off', 'single'],
17 | semi: ['off', 'always'],
18 | },
19 | };
20 |
--------------------------------------------------------------------------------
/Section3/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/Section3/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "singleQuote": true,
4 | "jsxBracketSameLine": true,
5 | "printWidth": 55
6 | }
7 |
--------------------------------------------------------------------------------
/Section3/README.md:
--------------------------------------------------------------------------------
1 | # Hands-on Application Building with GraphQL (and React)
2 |
3 | ## Section 3 : Building the UI Client with a Server Connection
4 |
5 | 1. Setting Up a React Application - 00:08:48
6 | 1. Creating the UI Components - 00:12:40
7 | 1. Integrating Apollo Framework/Apollo Provider - 00:15:27
8 | 1. Implementing the GraphQL Fragments - 00:09:56
9 | 1. Connecting to Graphcool Cloud-Based Storage Backend - 00:14:05
10 |
--------------------------------------------------------------------------------
/Section3/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "coolboard",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "apollo-cache-inmemory": "1.6.0",
7 | "apollo-client": "2.5.1",
8 | "apollo-link": "1.2.11",
9 | "apollo-link-http": "1.5.14",
10 | "apollo-link-schema": "1.2.2",
11 | "graphql": "14.3.1",
12 | "graphql-tag": "2.10.1",
13 | "graphql-tools": "4.0.4",
14 | "prop-types": "15.7.2",
15 | "react": "16.8.6",
16 | "react-apollo": "2.5.6",
17 | "react-dom": "16.8.6",
18 | "react-scripts": "3.0.1",
19 | "styled-components": "4.2.0"
20 | },
21 | "scripts": {
22 | "start": "react-scripts start",
23 | "build": "react-scripts build",
24 | "test": "react-scripts test --env=jsdom",
25 | "eject": "react-scripts eject"
26 | },
27 | "devDependencies": {
28 | "babel-eslint": "10.0.1",
29 | "eslint": "^5.16.0",
30 | "eslint-config-prettier": "4.3.0",
31 | "eslint-config-react-app": "4.0.1",
32 | "eslint-plugin-prettier": "3.1.0",
33 | "eslint-plugin-react": "7.13.0",
34 | "prettier": "1.17.1"
35 | },
36 | "browserslist": [
37 | ">0.2%",
38 | "not dead",
39 | "not ie < 11",
40 | "not op_mini all"
41 | ]
42 | }
43 |
--------------------------------------------------------------------------------
/Section3/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | React App
23 |
24 |
25 |
26 | You need to enable JavaScript to run this app.
27 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/Section3/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/Section3/server/.graphqlconfig.yml:
--------------------------------------------------------------------------------
1 | projects:
2 | database:
3 | extensions:
4 | prisma: prisma.yml
5 |
--------------------------------------------------------------------------------
/Section3/server/README.md:
--------------------------------------------------------------------------------
1 | # Local GraphQL server based on Prisma
2 |
3 | To run you local server, you will have to run these commands in a
4 | terminal in this sub-folder (after a `cd server`):
5 |
6 | * After having docker running on you local machine,
7 | you can **start up** the prisma server with `docker-compose up -d`.
8 |
9 | * By running `npm install` or `yarn`, the prisma tooling will be installed and available in the next step:
10 |
11 | * You then run `npx prisma deploy` to **deploy the schema**.
12 |
13 | * Finally, **check** it by opening this page: [http://localhost:4466/](http://localhost:4466/) which shows the graphiql or graphql playground.
14 | ---
15 | * Later, you can **stop** the prisma server via `docker-compose stop` ,
16 | `docker-compose kill` ,
17 |
18 | * For **completely removing** these docker containers you will need to run `docker-compose kill` and `docker-compose rm` which will destroy all its stored data!
19 |
--------------------------------------------------------------------------------
/Section3/server/datamodel.prisma:
--------------------------------------------------------------------------------
1 | type Board {
2 | id: ID! @id
3 | lists: [List!]!
4 | name: String!
5 | }
6 | type List {
7 | cards: [Card!]!
8 | id: ID! @id
9 | name: String!
10 | }
11 | type Card {
12 | id: ID! @id
13 | name: String!
14 | description: String @default(value: "")
15 | }
16 | type User {
17 | id: ID! @id
18 | email: String! @unique
19 | password: String!
20 | name: String!
21 | avatarUrl: String @default(value:"")
22 | }
23 |
--------------------------------------------------------------------------------
/Section3/server/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | prisma-handson-course:
4 | image: prismagraphql/prisma:1.31
5 | restart: always
6 | ports:
7 | - "4466:4466"
8 | environment:
9 | PRISMA_CONFIG: |
10 | port: 4466
11 | # uncomment the next line and provide the env var PRISMA_MANAGEMENT_API_SECRET=my-secret to activate cluster security
12 | # managementApiSecret: my-secret
13 | databases:
14 | default:
15 | connector: mysql
16 | host: mysql-handson-course
17 | user: root
18 | password: prisma
19 | rawAccess: true
20 | port: 3306
21 | migrations: true
22 | mysql-handson-course:
23 | image: mysql:5.7
24 | restart: always
25 | # Uncomment the next two lines to connect to your your database from outside the Docker environment, e.g. using a database GUI like Workbench
26 | # ports:
27 | # - "3306:3306"
28 | environment:
29 | MYSQL_ROOT_PASSWORD: prisma
30 | volumes:
31 | - mysql:/var/lib/mysql
32 | volumes:
33 | mysql:
34 |
--------------------------------------------------------------------------------
/Section3/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "local-prisma-db",
3 | "version": "0.1.0",
4 | "description": "The config for a prisma server with the specific datamodel for our kanban board",
5 | "author": "Robert Hostlowsky",
6 | "private": true,
7 | "devDependencies": {},
8 | "dependencies": {
9 | "prisma": "1.31.1"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Section3/server/prisma.yml:
--------------------------------------------------------------------------------
1 | endpoint: http://localhost:4466
2 |
3 | datamodel: datamodel.prisma
4 |
--------------------------------------------------------------------------------
/Section3/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
--------------------------------------------------------------------------------
/Section3/src/App.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 | import React, { Component } from 'react';
3 |
4 | import { ApolloClient } from 'apollo-client';
5 | import { InMemoryCache } from 'apollo-cache-inmemory';
6 |
7 | import { SchemaLink } from 'apollo-link-schema';
8 | import { createHttpLink } from 'apollo-link-http';
9 |
10 | import gql from 'graphql-tag';
11 | import { graphql } from 'react-apollo';
12 | import { ApolloProvider } from 'react-apollo';
13 |
14 | import { schema } from './schema';
15 |
16 | import './App.css';
17 |
18 | import { BoardContainer } from './components';
19 | import { CardList } from './components/CardList';
20 |
21 | const Board = ({ board }) => {
22 | const { name, lists = [] } = board;
23 | return (
24 |
25 | {lists.map(list => (
26 |
27 | ))}
28 |
29 | );
30 | };
31 |
32 | const BoardAdapter = ({ data }) => {
33 | const { loading, error, board } = data;
34 |
35 | if (loading) {
36 | return Loading Board
;
37 | }
38 | if (error) {
39 | return (
40 |
41 | sorry, some error... {error}
42 |
43 | );
44 | }
45 | if (board) {
46 | return ;
47 | }
48 |
49 | return Board does not exist.
;
50 | };
51 |
52 | const BoardQuery = gql`
53 | query board($boardId: ID) {
54 | board: Board(id: $boardId) {
55 | name
56 | lists {
57 | id
58 | ...CardList_list
59 | }
60 | }
61 | }
62 | ${CardList.fragments.list}
63 | `;
64 |
65 | const config = {
66 | options: props => ({
67 | variables: {
68 | boardId: props.boardId,
69 | },
70 | }),
71 | };
72 | const CoolBoard = graphql(BoardQuery, config)(BoardAdapter);
73 |
74 | function createClient() {
75 | return new ApolloClient({
76 | link: createHttpLink({
77 | uri: 'https://api.graph.cool/simple/v1/cjc10hp730o6u01143mnnalq4',
78 | }),
79 | cache: new InMemoryCache(),
80 | });
81 | }
82 |
83 | // eslint-disable-next-line no-unused-vars
84 | function createClientMock() {
85 | return new ApolloClient({
86 | link: new SchemaLink({ schema }),
87 | cache: new InMemoryCache(),
88 | });
89 | }
90 |
91 | class App extends Component {
92 | render() {
93 | return (
94 |
103 | );
104 | }
105 | }
106 |
107 | export default App;
108 |
--------------------------------------------------------------------------------
/Section3/src/components.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 | import React from 'react';
3 |
4 | export const BoardContainer = ({
5 | boardName = 'BOARD TITLE',
6 | children,
7 | }) => (
8 |
14 |
{boardName}
15 |
24 | {children}
25 |
26 |
27 | );
28 |
--------------------------------------------------------------------------------
/Section3/src/components/Card.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styled from 'styled-components';
4 | import gql from 'graphql-tag';
5 |
6 | export const CardComponent = styled.div`
7 | border-radius: 3px;
8 | margin: 0.1em 0 0 0;
9 | border-bottom: 1px solid #ccc;
10 | background-color: #fff;
11 | padding: 10px;
12 | `;
13 |
14 |
15 | export const Card = ({ name }) => {name} ;
16 |
17 | Card.propTypes = {
18 | name: PropTypes.string.isRequired,
19 | };
20 |
21 | Card.fragments = {
22 | card: gql`
23 | fragment Card_card on Card {
24 | name
25 | }
26 | `,
27 | };
28 |
--------------------------------------------------------------------------------
/Section3/src/components/CardList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import gql from 'graphql-tag';
4 |
5 | import { Card } from './Card';
6 |
7 | export class CardList extends React.Component {
8 | render() {
9 | const { cards, name = 'unknown' } = this.props;
10 | return (
11 |
12 |
17 | {name}
18 |
19 |
20 | {{renderCards(cards)} }
21 |
22 |
23 | );
24 | }
25 | }
26 |
27 | const CardsContainer = ({ children }) => (
28 |
35 | {children}
36 |
37 | );
38 |
39 | let renderCards = (cards = []) => cards.map(c => );
40 |
41 | const ListContainer = ({ children }) => (
42 |
52 | {children}
53 |
54 | );
55 |
56 | CardList.propTypes = {
57 | name: PropTypes.string.isRequired,
58 | cards: PropTypes.array,
59 | };
60 |
61 | CardList.fragments = {
62 | list: gql`
63 | fragment CardList_list on List {
64 | name
65 | cards {
66 | id
67 | ...Card_card
68 | }
69 | }
70 | ${Card.fragments.card}
71 | `,
72 | };
73 |
--------------------------------------------------------------------------------
/Section3/src/dummyData.js:
--------------------------------------------------------------------------------
1 | let boardData = {
2 | name: 'Course',
3 | lists: [
4 | {
5 | name: 'First Section',
6 | cards: [
7 | {
8 | name: 'Intro',
9 | },
10 | ],
11 | },
12 | {
13 | name: 'Second Section',
14 | cards: [
15 | {
16 | name: 'Video 1',
17 | },
18 | {
19 | name: 'Video 2',
20 | },
21 | {
22 | name: 'Video 3',
23 | },
24 | {
25 | name: 'Video 4',
26 | },
27 | {
28 | name: 'Video 5',
29 | },
30 | ],
31 | },
32 | ],
33 | };
34 |
35 | let numbers = Array.from(Array(20).keys());
36 | let cards = numbers.map(i => ({ name: `Video ${i}` }));
37 | boardData.lists[0].cards.push(...cards);
38 |
39 | export default boardData;
40 |
--------------------------------------------------------------------------------
/Section3/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
--------------------------------------------------------------------------------
/Section3/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import registerServiceWorker from './registerServiceWorker';
6 |
7 | ReactDOM.render( , document.getElementById('root'));
8 | registerServiceWorker();
9 |
--------------------------------------------------------------------------------
/Section3/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | /* global process */
3 | // In production, we register a service worker to serve assets from local cache.
4 |
5 | // This lets the app load faster on subsequent visits in production, and gives
6 | // it offline capabilities. However, it also means that developers (and users)
7 | // will only see deployed updates on the "N+1" visit to a page, since previously
8 | // cached resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
11 | // This link also includes instructions on opting out of this behavior.
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export default function register() {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Lets check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://goo.gl/SC7cgQ'
47 | );
48 | });
49 | } else {
50 | // Is not local host. Just register service worker
51 | registerValidSW(swUrl);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | installingWorker.onstatechange = () => {
64 | if (installingWorker.state === 'installed') {
65 | if (navigator.serviceWorker.controller) {
66 | // At this point, the old content will have been purged and
67 | // the fresh content will have been added to the cache.
68 | // It's the perfect time to display a "New content is
69 | // available; please refresh." message in your web app.
70 | console.log('New content is available; please refresh.');
71 | } else {
72 | // At this point, everything has been precached.
73 | // It's the perfect time to display a
74 | // "Content is cached for offline use." message.
75 | console.log('Content is cached for offline use.');
76 | }
77 | }
78 | };
79 | };
80 | })
81 | .catch(error => {
82 | console.error('Error during service worker registration:', error);
83 | });
84 | }
85 |
86 | function checkValidServiceWorker(swUrl) {
87 | // Check if the service worker can be found. If it can't reload the page.
88 | fetch(swUrl)
89 | .then(response => {
90 | // Ensure service worker exists, and that we really are getting a JS file.
91 | if (
92 | response.status === 404 ||
93 | response.headers.get('content-type').indexOf('javascript') === -1
94 | ) {
95 | // No service worker found. Probably a different app. Reload the page.
96 | navigator.serviceWorker.ready.then(registration => {
97 | registration.unregister().then(() => {
98 | window.location.reload();
99 | });
100 | });
101 | } else {
102 | // Service worker found. Proceed as normal.
103 | registerValidSW(swUrl);
104 | }
105 | })
106 | .catch(() => {
107 | console.log(
108 | 'No internet connection found. App is running in offline mode.'
109 | );
110 | });
111 | }
112 |
113 | export function unregister() {
114 | if ('serviceWorker' in navigator) {
115 | navigator.serviceWorker.ready.then(registration => {
116 | registration.unregister();
117 | });
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/Section3/src/schema.js:
--------------------------------------------------------------------------------
1 | import {
2 | addMockFunctionsToSchema,
3 | makeExecutableSchema,
4 | } from 'graphql-tools';
5 |
6 | // eslint-disable-next-line no-unused-vars
7 | import { MockList } from 'graphql-tools';
8 |
9 | // a schema
10 | const typeDefs = `
11 | type Board {
12 | id: ID!
13 | lists: [List!]!
14 | name: String!
15 | }
16 | type List {
17 | cards: [Card!]!
18 | id: ID!
19 | name: String!
20 | }
21 | type Card {
22 | id: ID!
23 | name: String!
24 | }
25 | type Query {
26 | hello: String
27 | Board(id: String): Board
28 | }`;
29 | const resolvers = {
30 | Board: () => ({
31 | name: 'old resolvers',
32 | }),
33 | };
34 | // Export the GraphQL.js schema object as "schema"
35 | export const schema = makeExecutableSchema({
36 | typeDefs,
37 | resolvers,
38 | });
39 |
40 | // Add mocking
41 | const mocks = {
42 | //Board: (parent, args) => ({
43 | //name: () => 'heeh',
44 | // id: args.id || uuid.v4(),
45 | // lists: () => new MockList(1)
46 | //}),
47 | };
48 |
49 | addMockFunctionsToSchema({ schema, mocks });
50 |
--------------------------------------------------------------------------------
/Section4/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: http://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | # Unix-style newlines with a newline ending every file
7 | [*]
8 | end_of_line = lf
9 | insert_final_newline = true
10 | indent_style = space
11 | indent_size = 2
12 | trim_trailing_whitespace = true
13 | charset = utf-8
14 |
15 | # Matches the exact files either package.json or .travis.yml
16 | [package.json]
17 | indent_size = 2
18 |
19 | [*.md]
20 | trim_trailing_whitespace = false
21 |
--------------------------------------------------------------------------------
/Section4/.eslintignore:
--------------------------------------------------------------------------------
1 | src/registerServiceWorker.js
2 |
--------------------------------------------------------------------------------
/Section4/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | },
5 | parser: 'babel-eslint',
6 | plugins: ['react', 'prettier'],
7 | extends: [
8 | 'eslint:recommended',
9 | 'plugin:react/recommended',
10 | 'plugin:prettier/recommended',
11 | ],
12 | rules: {
13 | // already defined with prettier:
14 | 'prettier/prettier': ['warn'],
15 | 'no-unused-vars': [1],
16 | 'no-console': ['off'],
17 | indent: ['off', 2],
18 | quotes: ['off', 'single'],
19 | semi: ['off', 'always'],
20 | },
21 | };
22 |
--------------------------------------------------------------------------------
/Section4/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/Section4/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "singleQuote": true,
4 | "jsxBracketSameLine": true,
5 | "printWidth": 55
6 | }
7 |
--------------------------------------------------------------------------------
/Section4/README.md:
--------------------------------------------------------------------------------
1 | # Hands-on Application Building with GraphQL (and React)
2 |
3 | ## Section 4 : Working with Client-Driven Mutations
4 |
5 | 1. Exploring the UI for Adding New Cards and New Lists - 00:08:15
6 | 1. Connecting to Server, Calling the Mutations for Adding Cards - 00:15:32
7 | 1. How the UI Gets Updated: Handle Mutations on the Client - 00:20:51
8 | 1. Implementing a UI for Editing Cards and Connecting to the Server - 00:14:55
9 | 1. Implementing a UI for Moving Cards and Connecting to the Server - 00:17:47
10 |
--------------------------------------------------------------------------------
/Section4/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "coolboard",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "apollo-cache-inmemory": "1.6.0",
7 | "apollo-client": "2.6.0",
8 | "apollo-link": "1.2.11",
9 | "apollo-link-http": "1.5.14",
10 | "graphql": "14.3.1",
11 | "graphql-tag": "2.10.1",
12 | "graphql-tools": "4.0.4",
13 | "prop-types": "15.7.2",
14 | "react": "16.8.6",
15 | "react-apollo": "2.5.5",
16 | "react-dnd": "2.5.4",
17 | "react-dnd-html5-backend": "2.5.4",
18 | "react-dom": "16.8.6",
19 | "react-scripts": "3.0.1",
20 | "semantic-ui-css": "2.2.12",
21 | "semantic-ui-react": "0.78.2",
22 | "styled-components": "4.2.0"
23 | },
24 | "scripts": {
25 | "start": "react-scripts start",
26 | "build": "react-scripts build",
27 | "test": "react-scripts test --env=jsdom",
28 | "eject": "react-scripts eject"
29 | },
30 | "devDependencies": {
31 | "babel-eslint": "10.0.1",
32 | "eslint": "^5.16.0",
33 | "eslint-config-prettier": "4.3.0",
34 | "eslint-config-react-app": "4.0.1",
35 | "eslint-plugin-prettier": "3.1.0",
36 | "eslint-plugin-react": "7.13.0",
37 | "prettier": "1.17.1"
38 | },
39 | "browserslist": [
40 | ">0.2%",
41 | "not dead",
42 | "not ie < 11",
43 | "not op_mini all"
44 | ]
45 | }
46 |
--------------------------------------------------------------------------------
/Section4/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 |
23 | Hands-on Application Building with GraphQL - Cool Board
24 |
25 |
26 |
27 | You need to enable JavaScript to run this app.
28 |
29 |
30 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/Section4/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/Section4/server/.graphqlconfig.yml:
--------------------------------------------------------------------------------
1 | projects:
2 | database:
3 | extensions:
4 | prisma: prisma.yml
5 |
--------------------------------------------------------------------------------
/Section4/server/README.md:
--------------------------------------------------------------------------------
1 | # Local GraphQL server based on Prisma
2 |
3 | To run you local server, you will have to run these commands in a
4 | terminal in this sub-folder (after a `cd server`):
5 |
6 | * After having docker running on you local machine,
7 | you can **start up** the prisma server with `docker-compose up -d`.
8 |
9 | * By running `npm install` or `yarn`, the prisma tooling will be installed and available in the next step:
10 |
11 | * You then run `npx prisma deploy` to **deploy the schema**.
12 |
13 | * Finally, **check** it by opening this page: [http://localhost:4466/](http://localhost:4466/) which shows the graphiql or graphql playground.
14 | ---
15 | * Later, you can **stop** the prisma server via `docker-compose stop` ,
16 | `docker-compose kill` ,
17 |
18 | * For **completely removing** these docker containers you will need to run `docker-compose kill` and `docker-compose rm` which will destroy all its stored data!
19 |
--------------------------------------------------------------------------------
/Section4/server/datamodel.prisma:
--------------------------------------------------------------------------------
1 | type Board {
2 | id: ID! @id
3 | lists: [List!]!
4 | name: String!
5 | }
6 | type List {
7 | cards: [Card!]!
8 | id: ID! @id
9 | name: String!
10 | }
11 | type Card {
12 | id: ID! @id
13 | name: String!
14 | description: String @default(value: "")
15 | }
16 | type User {
17 | id: ID! @id
18 | email: String! @unique
19 | password: String!
20 | name: String!
21 | avatarUrl: String @default(value:"")
22 | }
23 |
--------------------------------------------------------------------------------
/Section4/server/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | prisma-handson-course:
4 | image: prismagraphql/prisma:1.31
5 | restart: always
6 | ports:
7 | - "4466:4466"
8 | environment:
9 | PRISMA_CONFIG: |
10 | port: 4466
11 | # uncomment the next line and provide the env var PRISMA_MANAGEMENT_API_SECRET=my-secret to activate cluster security
12 | # managementApiSecret: my-secret
13 | databases:
14 | default:
15 | connector: mysql
16 | host: mysql-handson-course
17 | user: root
18 | password: prisma
19 | rawAccess: true
20 | port: 3306
21 | migrations: true
22 | mysql-handson-course:
23 | image: mysql:5.7
24 | restart: always
25 | # Uncomment the next two lines to connect to your your database from outside the Docker environment, e.g. using a database GUI like Workbench
26 | # ports:
27 | # - "3306:3306"
28 | environment:
29 | MYSQL_ROOT_PASSWORD: prisma
30 | volumes:
31 | - mysql:/var/lib/mysql
32 | volumes:
33 | mysql:
34 |
--------------------------------------------------------------------------------
/Section4/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "local-prisma-db",
3 | "version": "0.1.0",
4 | "description": "The config for a prisma server with the specific datamodel for our kanban board",
5 | "author": "Robert Hostlowsky",
6 | "private": true,
7 | "devDependencies": {},
8 | "dependencies": {
9 | "prisma": "1.31.1"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Section4/server/prisma.yml:
--------------------------------------------------------------------------------
1 | endpoint: http://localhost:4466
2 |
3 | datamodel: datamodel.prisma
4 |
--------------------------------------------------------------------------------
/Section4/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | display: flex;
4 | flex-direction: column;
5 | height: 100%;
6 | }
7 | #root {
8 | height: 100%;
9 | }
10 |
--------------------------------------------------------------------------------
/Section4/src/App.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 | import React, { Component } from 'react';
3 |
4 | import { ApolloClient } from 'apollo-client';
5 | import { InMemoryCache } from 'apollo-cache-inmemory';
6 |
7 | import { createHttpLink } from 'apollo-link-http';
8 |
9 | import { ApolloProvider } from 'react-apollo';
10 |
11 | import './App.css';
12 |
13 | import { CoolBoard } from './components/CoolBoard';
14 |
15 | function createClient() {
16 | return new ApolloClient({
17 | link: createHttpLink({
18 | uri: 'http://localhost:4466',
19 | }),
20 | cache: new InMemoryCache(),
21 | });
22 | }
23 |
24 | class App extends Component {
25 | render() {
26 | return (
27 |
32 | );
33 | }
34 | }
35 |
36 | export default App;
37 |
--------------------------------------------------------------------------------
/Section4/src/components/BoardContainer.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 | import React from 'react';
3 | import PropTypes from 'prop-types';
4 | import {
5 | Button,
6 | Container,
7 | Header,
8 | Icon,
9 | } from 'semantic-ui-react';
10 |
11 | import { DragDropContext } from 'react-dnd';
12 | import HTML5Backend from 'react-dnd-html5-backend';
13 |
14 | class BoardContainerInner extends React.Component {
15 | render() {
16 | const { boardName, children } = this.props;
17 |
18 | return (
19 |
26 |
27 | Board: {boardName}
28 |
29 |
38 | {children}
39 |
40 |
41 | );
42 | }
43 | }
44 |
45 | export const BoardContainer = DragDropContext(
46 | HTML5Backend
47 | )(BoardContainerInner);
48 |
49 | BoardContainer.propTypes = {
50 | boardName: PropTypes.string.isRequired,
51 | children: PropTypes.array.isRequired,
52 | };
53 |
54 | export const AddListButton = ({ onAddNewList }) => (
55 |
62 |
63 | Add a list
64 |
65 | );
66 | export const DelListButton = ({ action, children }) => (
67 | action()}
69 | style={{
70 | flexShrink: 0,
71 | flexGrow: 0,
72 | alignSelf: 'flex-start',
73 | }}>
74 |
75 | {children && children}
76 | {!children && 'Delete'}
77 |
78 | );
79 |
80 | DelListButton.propTypes = {
81 | onAddNewList: PropTypes.func,
82 | children: PropTypes.oneOfType([PropTypes.array, PropTypes.string]).isRequired,
83 | };
84 | AddListButton.propTypes = {
85 | onAddNewList: PropTypes.func,
86 | };
87 |
--------------------------------------------------------------------------------
/Section4/src/components/CardList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { DropTarget } from 'react-dnd';
3 | import PropTypes from 'prop-types';
4 | import gql from 'graphql-tag';
5 | import {
6 | Button,
7 | Header,
8 | Icon,
9 | } from 'semantic-ui-react';
10 |
11 | import Card from './Card';
12 | import { ItemTypes } from './Constants';
13 |
14 | class CardListWithoutDnd extends React.Component {
15 | render() {
16 | const {
17 | connectDropTarget,
18 | isOver,
19 | cards,
20 | name,
21 | id,
22 | addCardWithName = () => {},
23 | } = this.props;
24 |
25 | return (
26 |
27 | {connectDropTarget(
28 |
29 |
35 |
36 |
37 |
38 |
39 | {cards.map(c => (
40 |
45 | ))}
46 |
47 |
48 | addCardWithName(id)}
50 | />
51 |
52 |
53 | )}
54 |
55 | );
56 | }
57 | }
58 |
59 | const dropTarget = {
60 | drop(props, monitor, component) {
61 | console.log(
62 | 'dropped: ',
63 | props,
64 | monitor,
65 | component
66 | );
67 | let cardItem = monitor.getItem();
68 | const cardId = cardItem.id;
69 | const cardListId = props.id;
70 | const oldCardListId = cardItem.cardListId;
71 | props.moveCardToList(
72 | cardId,
73 | oldCardListId,
74 | cardListId
75 | );
76 | },
77 | hover(props, monitor) {},
78 | canDrop(props, monitor) {
79 | let item = monitor.getItem();
80 | let can = !(props.id === item.cardListId);
81 | return can;
82 | },
83 | };
84 |
85 | const collect = (connect, monitor) => ({
86 | connectDropTarget: connect.dropTarget(),
87 | isOver: monitor.isOver(),
88 | });
89 |
90 | export const CardList = DropTarget(
91 | ItemTypes.CARD,
92 | dropTarget,
93 | collect
94 | )(CardListWithoutDnd);
95 |
96 | const CardListHeader = ({ name }) => (
97 |
106 | );
107 |
108 | const InnerScrollContainer = ({ children }) => {
109 | return (
110 |
117 | {children}
118 |
119 | );
120 | };
121 |
122 | const CardsContainer = ({ children }) => (
123 |
129 | {children}
130 |
131 | );
132 |
133 | const ListContainer = ({ children, style }) => (
134 |
147 | {children}
148 |
149 | );
150 |
151 | const AddCardButton = ({ onAddCard }) => (
152 | onAddCard()}
155 | style={{
156 | margin: '0.1em 0 0 0',
157 | borderBottom: '1px solid #ccc',
158 | backgroundColor: '#grey',
159 | }}>
160 |
161 | Add a card
162 |
163 | );
164 |
165 | CardList.propTypes = {
166 | name: PropTypes.string.isRequired,
167 | id: PropTypes.string,
168 | addCardWithName: PropTypes.func,
169 | moveCardToList: PropTypes.func,
170 | cards: PropTypes.array,
171 | };
172 |
173 | CardList.fragments = {
174 | list: gql`
175 | fragment CardList_list on List {
176 | name
177 | id
178 | cards {
179 | id
180 | ...Card_card
181 | }
182 | }
183 | ${Card.fragments.card}
184 | `,
185 | };
186 |
--------------------------------------------------------------------------------
/Section4/src/components/Constants.js:
--------------------------------------------------------------------------------
1 | export const ItemTypes = {
2 | CARD: 'card',
3 | };
4 |
--------------------------------------------------------------------------------
/Section4/src/dummyData.js:
--------------------------------------------------------------------------------
1 | let boardData = {
2 | name: 'Course',
3 | lists: [
4 | {
5 | name: 'First Section',
6 | cards: [
7 | {
8 | name: 'Intro',
9 | },
10 | ],
11 | },
12 | {
13 | name: 'Second Section',
14 | cards: [
15 | {
16 | name: 'Video 1',
17 | },
18 | {
19 | name: 'Video 2',
20 | },
21 | {
22 | name: 'Video 3',
23 | },
24 | {
25 | name: 'Video 4',
26 | },
27 | {
28 | name: 'Video 5',
29 | },
30 | ],
31 | },
32 | ],
33 | };
34 |
35 | let numbers = Array.from(Array(20).keys());
36 | let cards = numbers.map(i => ({ name: `Video ${i}` }));
37 | boardData.lists[0].cards.push(...cards);
38 |
39 | export default boardData;
40 |
--------------------------------------------------------------------------------
/Section4/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
--------------------------------------------------------------------------------
/Section4/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import registerServiceWorker from './registerServiceWorker';
6 |
7 | // Instead of integrating the
8 | // css here, with running webpack bundling every time while developing,
9 | // I put this into the index page:
10 | //
11 | //
12 | // import 'semantic-ui-css/semantic.min.css';
13 |
14 | ReactDOM.render( , document.getElementById('root'));
15 | registerServiceWorker();
16 |
--------------------------------------------------------------------------------
/Section4/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | /* global process */
3 | // In production, we register a service worker to serve assets from local cache.
4 |
5 | // This lets the app load faster on subsequent visits in production, and gives
6 | // it offline capabilities. However, it also means that developers (and users)
7 | // will only see deployed updates on the "N+1" visit to a page, since previously
8 | // cached resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
11 | // This link also includes instructions on opting out of this behavior.
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export default function register() {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Lets check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://goo.gl/SC7cgQ'
47 | );
48 | });
49 | } else {
50 | // Is not local host. Just register service worker
51 | registerValidSW(swUrl);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | installingWorker.onstatechange = () => {
64 | if (installingWorker.state === 'installed') {
65 | if (navigator.serviceWorker.controller) {
66 | // At this point, the old content will have been purged and
67 | // the fresh content will have been added to the cache.
68 | // It's the perfect time to display a "New content is
69 | // available; please refresh." message in your web app.
70 | console.log('New content is available; please refresh.');
71 | } else {
72 | // At this point, everything has been precached.
73 | // It's the perfect time to display a
74 | // "Content is cached for offline use." message.
75 | console.log('Content is cached for offline use.');
76 | }
77 | }
78 | };
79 | };
80 | })
81 | .catch(error => {
82 | console.error('Error during service worker registration:', error);
83 | });
84 | }
85 |
86 | function checkValidServiceWorker(swUrl) {
87 | // Check if the service worker can be found. If it can't reload the page.
88 | fetch(swUrl)
89 | .then(response => {
90 | // Ensure service worker exists, and that we really are getting a JS file.
91 | if (
92 | response.status === 404 ||
93 | response.headers.get('content-type').indexOf('javascript') === -1
94 | ) {
95 | // No service worker found. Probably a different app. Reload the page.
96 | navigator.serviceWorker.ready.then(registration => {
97 | registration.unregister().then(() => {
98 | window.location.reload();
99 | });
100 | });
101 | } else {
102 | // Service worker found. Proceed as normal.
103 | registerValidSW(swUrl);
104 | }
105 | })
106 | .catch(() => {
107 | console.log(
108 | 'No internet connection found. App is running in offline mode.'
109 | );
110 | });
111 | }
112 |
113 | export function unregister() {
114 | if ('serviceWorker' in navigator) {
115 | navigator.serviceWorker.ready.then(registration => {
116 | registration.unregister();
117 | });
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/Section4/src/schema.js:
--------------------------------------------------------------------------------
1 | import { addMockFunctionsToSchema } from 'graphql-tools';
2 | //import { MockList } from 'graphql-tools';
3 |
4 | import { makeExecutableSchema } from 'graphql-tools';
5 |
6 | // a schema
7 | const typeDefs = `
8 | type Board {
9 | id: ID!
10 | lists: [List!]!
11 | name: String!
12 | }
13 | type List {
14 | cards: [Card!]!
15 | id: ID!
16 | name: String!
17 | }
18 | type Card {
19 | id: ID!
20 | name: String!
21 | }
22 | type Query {
23 | hello: String
24 | Board(id: String): Board
25 | }`;
26 | const resolvers = {
27 | Board: () => ({
28 | name: 'old resolvers',
29 | }),
30 | };
31 | // Export the GraphQL.js schema object as "schema"
32 | export const schema = makeExecutableSchema({
33 | typeDefs,
34 | resolvers,
35 | });
36 |
37 | // Add mocking
38 | const mocks = {
39 | //Board: (parent, args) => ({
40 | //name: () => 'heeh',
41 | // id: args.id || uuid.v4(),
42 | // lists: () => new MockList(1)
43 | //}),
44 | };
45 |
46 | addMockFunctionsToSchema({ schema, mocks });
47 |
--------------------------------------------------------------------------------
/Section5/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: http://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | # Unix-style newlines with a newline ending every file
7 | [*]
8 | end_of_line = lf
9 | insert_final_newline = true
10 | indent_style = space
11 | indent_size = 2
12 | trim_trailing_whitespace = true
13 | charset = utf-8
14 |
15 | # Matches the exact files either package.json or .travis.yml
16 | [package.json]
17 | indent_size = 2
18 |
19 | [*.md]
20 | trim_trailing_whitespace = false
21 |
--------------------------------------------------------------------------------
/Section5/.eslintignore:
--------------------------------------------------------------------------------
1 | src/registerServiceWorker.js
2 |
--------------------------------------------------------------------------------
/Section5/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | },
5 | parser: 'babel-eslint',
6 | plugins: ['react', 'prettier'],
7 | extends: [
8 | 'eslint:recommended',
9 | 'plugin:react/recommended',
10 | 'plugin:prettier/recommended',
11 | ],
12 | rules: {
13 | // already defined with prettier:
14 | 'prettier/prettier': ['warn'],
15 | 'no-unused-vars': [1],
16 | 'no-console': ['off'],
17 | indent: ['off', 2],
18 | quotes: ['off', 'single'],
19 | semi: ['off', 'always'],
20 | },
21 | };
22 |
--------------------------------------------------------------------------------
/Section5/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/Section5/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "singleQuote": true,
4 | "jsxBracketSameLine": true,
5 | "printWidth": 55
6 | }
7 |
--------------------------------------------------------------------------------
/Section5/README.md:
--------------------------------------------------------------------------------
1 | # Hands-on Application Building with GraphQL (and React)
2 |
3 | ## Section 5 : Subscriptions: Updating the Board on Changes
4 |
5 | 1. Subscriptions: Setting Up and Using in Playground - 00:08:55
6 | 1. Client-Side Connection via Web-Sockets - 00:06:54
7 | 1. Updating an Existing Card - 00:08:21
8 | 1. Advanced Subscription - 00:22:03
9 | 1. Updating the Mechanism and Strategy for Concurrent Changes - 00:14:34
10 |
--------------------------------------------------------------------------------
/Section5/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "coolboard",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "apollo-cache-inmemory": "1.6.0",
7 | "apollo-client": "2.6.0",
8 | "apollo-link": "1.2.11",
9 | "apollo-link-http": "1.5.14",
10 | "apollo-link-ws": "^1.0.17",
11 | "graphql": "14.3.1",
12 | "graphql-tag": "2.10.1",
13 | "graphql-tools": "4.0.4",
14 | "prop-types": "15.7.2",
15 | "react": "16.8.6",
16 | "react-apollo": "2.5.5",
17 | "react-dnd": "7.4.5",
18 | "react-dnd-html5-backend": "7.4.4",
19 | "react-dom": "16.8.6",
20 | "react-scripts": "3.0.1",
21 | "semantic-ui-css": "2.2.12",
22 | "semantic-ui-react": "0.78.2",
23 | "styled-components": "4.2.0",
24 | "subscriptions-transport-ws": "0.9.16"
25 | },
26 | "scripts": {
27 | "start": "react-scripts start",
28 | "build": "react-scripts build",
29 | "test": "react-scripts test --env=jsdom",
30 | "eject": "react-scripts eject"
31 | },
32 | "devDependencies": {
33 | "babel-eslint": "10.0.1",
34 | "eslint": "^5.16.0",
35 | "eslint-config-prettier": "4.3.0",
36 | "eslint-config-react-app": "4.0.1",
37 | "eslint-plugin-prettier": "3.1.0",
38 | "eslint-plugin-react": "7.13.0",
39 | "prettier": "1.17.1"
40 | },
41 | "browserslist": [
42 | ">0.2%",
43 | "not dead",
44 | "not ie < 11",
45 | "not op_mini all"
46 | ]
47 | }
48 |
--------------------------------------------------------------------------------
/Section5/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 |
23 | Hands-on Application Building with GraphQL - Cool Board
24 |
25 |
26 |
27 | You need to enable JavaScript to run this app.
28 |
29 |
30 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/Section5/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/Section5/server/.graphqlconfig.yml:
--------------------------------------------------------------------------------
1 | projects:
2 | database:
3 | extensions:
4 | prisma: prisma.yml
5 |
--------------------------------------------------------------------------------
/Section5/server/README.md:
--------------------------------------------------------------------------------
1 | # Local GraphQL server based on Prisma
2 |
3 | To run you local server, you will have to run these commands in a
4 | terminal in this sub-folder (after a `cd server`):
5 |
6 | * After having docker running on you local machine,
7 | you can **start up** the prisma server with `docker-compose up -d`.
8 |
9 | * By running `npm install` or `yarn`, the prisma tooling will be installed and available in the next step:
10 |
11 | * You then run `npx prisma deploy` to **deploy the schema**.
12 |
13 | * Finally, **check** it by opening this page: [http://localhost:4466/](http://localhost:4466/) which shows the graphiql or graphql playground.
14 | ---
15 | * Later, you can **stop** the prisma server via `docker-compose stop` ,
16 | `docker-compose kill` ,
17 |
18 | * For **completely removing** these docker containers you will need to run `docker-compose kill` and `docker-compose rm` which will destroy all its stored data!
19 |
--------------------------------------------------------------------------------
/Section5/server/datamodel.prisma:
--------------------------------------------------------------------------------
1 | type Board {
2 | id: ID! @id
3 | lists: [List!]!
4 | name: String!
5 | }
6 | type List {
7 | cards: [Card!]!
8 | id: ID! @id
9 | name: String!
10 | }
11 | type Card {
12 | id: ID! @id
13 | name: String!
14 | description: String @default(value: "")
15 | }
16 | type User {
17 | id: ID! @id
18 | email: String! @unique
19 | password: String!
20 | name: String!
21 | avatarUrl: String @default(value:"")
22 | }
23 |
--------------------------------------------------------------------------------
/Section5/server/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | prisma-handson-course:
4 | image: prismagraphql/prisma:1.31
5 | restart: always
6 | ports:
7 | - "4466:4466"
8 | environment:
9 | PRISMA_CONFIG: |
10 | port: 4466
11 | # uncomment the next line and provide the env var PRISMA_MANAGEMENT_API_SECRET=my-secret to activate cluster security
12 | # managementApiSecret: my-secret
13 | databases:
14 | default:
15 | connector: mysql
16 | host: mysql-handson-course
17 | user: root
18 | password: prisma
19 | rawAccess: true
20 | port: 3306
21 | migrations: true
22 | mysql-handson-course:
23 | image: mysql:5.7
24 | restart: always
25 | # Uncomment the next two lines to connect to your your database from outside the Docker environment, e.g. using a database GUI like Workbench
26 | # ports:
27 | # - "3306:3306"
28 | environment:
29 | MYSQL_ROOT_PASSWORD: prisma
30 | volumes:
31 | - mysql:/var/lib/mysql
32 | volumes:
33 | mysql:
34 |
--------------------------------------------------------------------------------
/Section5/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "local-prisma-db",
3 | "version": "0.1.0",
4 | "description": "The config for a prisma server with the specific datamodel for our kanban board",
5 | "author": "Robert Hostlowsky",
6 | "private": true,
7 | "devDependencies": {},
8 | "dependencies": {
9 | "prisma": "1.31.1"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Section5/server/prisma.yml:
--------------------------------------------------------------------------------
1 | endpoint: http://localhost:4466
2 |
3 | datamodel: datamodel.prisma
4 |
--------------------------------------------------------------------------------
/Section5/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | display: flex;
4 | flex-direction: column;
5 | height: 100%;
6 | }
7 | #root {
8 | height: 100%;
9 | }
10 |
--------------------------------------------------------------------------------
/Section5/src/App.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 | import React, { Component } from 'react';
3 |
4 | import { ApolloClient } from 'apollo-client';
5 | import { ApolloProvider } from 'react-apollo';
6 | import { InMemoryCache } from 'apollo-cache-inmemory';
7 | /* http link */
8 | import { createHttpLink } from 'apollo-link-http';
9 | /* ws link */
10 | import { WebSocketLink } from 'apollo-link-ws';
11 |
12 | import { getMainDefinition } from 'apollo-utilities';
13 | import { split } from 'apollo-link';
14 | /**/
15 |
16 | import './App.css';
17 | import { CoolBoard } from './components/CoolBoard';
18 |
19 | // Create a Http link
20 | let httpLink = createHttpLink({
21 | uri: 'http://localhost:4466/',
22 | });
23 |
24 | // Create a WebSocket link:
25 | const wsLink = new WebSocketLink({
26 | uri: `ws://localhost:4466/`,
27 | options: {
28 | reconnect: true,
29 | },
30 | });
31 |
32 | // using the ability to split links, you can send data to each link
33 | // depending on what kind of operation is being sent
34 | const returnTrueIfSubscription = ({ query }) => {
35 | const { kind, operation } = getMainDefinition(query);
36 | return (
37 | kind === 'OperationDefinition' &&
38 | operation === 'subscription'
39 | );
40 | };
41 |
42 | // split based on operation type
43 | const link = split(
44 | returnTrueIfSubscription,
45 | wsLink,
46 | httpLink
47 | );
48 |
49 | const client = new ApolloClient({
50 | link,
51 | cache: new InMemoryCache(),
52 | });
53 |
54 | class App extends Component {
55 | render() {
56 | return (
57 |
62 | );
63 | }
64 | }
65 |
66 | export default App;
67 |
--------------------------------------------------------------------------------
/Section5/src/components/BoardContainer.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 | import React from 'react';
3 | import PropTypes from 'prop-types';
4 | import {
5 | Button,
6 | Container,
7 | Header,
8 | Icon,
9 | } from 'semantic-ui-react';
10 |
11 | import { DragDropContext } from 'react-dnd';
12 | import HTML5Backend from 'react-dnd-html5-backend';
13 |
14 | class BoardContainerInner extends React.Component {
15 | render() {
16 | const { boardName, children } = this.props;
17 |
18 | return (
19 |
26 |
27 | Board: {boardName}
28 |
29 |
38 | {children}
39 |
40 |
41 | );
42 | }
43 | }
44 |
45 | export const BoardContainer = DragDropContext(
46 | HTML5Backend
47 | )(BoardContainerInner);
48 |
49 | BoardContainer.propTypes = {
50 | boardName: PropTypes.string.isRequired,
51 | children: PropTypes.array.isRequired,
52 | };
53 |
54 | export const AddListButton = ({ onAddNewList }) => (
55 |
62 |
63 | Add a list
64 |
65 | );
66 | export const DelListButton = ({ action, children }) => (
67 | action()}
69 | style={{
70 | flexShrink: 0,
71 | flexGrow: 0,
72 | alignSelf: 'flex-start',
73 | }}>
74 |
75 | {children && children}
76 | {!children && 'Delete'}
77 |
78 | );
79 |
80 | DelListButton.propTypes = {
81 | onAddNewList: PropTypes.func,
82 | children: PropTypes.oneOfType([PropTypes.array, PropTypes.string]).isRequired,
83 | };
84 | AddListButton.propTypes = {
85 | onAddNewList: PropTypes.func,
86 | };
87 |
--------------------------------------------------------------------------------
/Section5/src/components/CardList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { DropTarget } from 'react-dnd';
3 | import PropTypes from 'prop-types';
4 | import gql from 'graphql-tag';
5 | import {
6 | Button,
7 | Header,
8 | Icon,
9 | Popup,
10 | } from 'semantic-ui-react';
11 |
12 | import Card from './Card';
13 | import { ItemTypes } from './Constants';
14 |
15 | class CardListWithoutDnd extends React.Component {
16 | render() {
17 | const {
18 | connectDropTarget,
19 | isOver,
20 | cards,
21 | name,
22 | id,
23 | addCardWithName = () => {},
24 | deleteListWithId = () => {},
25 | } = this.props;
26 |
27 | return (
28 |
29 | {connectDropTarget(
30 |
31 |
37 |
38 | deleteListWithId(id)}>
39 |
40 |
41 |
42 |
43 |
44 |
45 | {cards.map(c => (
46 |
51 | ))}
52 |
53 |
54 | addCardWithName(id)}>
55 |
56 | Add a card
57 |
58 |
59 |
60 | )}
61 |
62 | );
63 | }
64 | }
65 |
66 | const dropTarget = {
67 | drop(props, monitor, component) {
68 | console.log(
69 | 'dropped: ',
70 | props,
71 | monitor,
72 | component
73 | );
74 | let cardItem = monitor.getItem();
75 | const cardId = cardItem.id;
76 | const cardListId = props.id;
77 | const oldCardListId = cardItem.cardListId;
78 | props.moveCardToList(
79 | cardId,
80 | oldCardListId,
81 | cardListId
82 | );
83 | },
84 | hover(props, monitor) {},
85 | canDrop(props, monitor) {
86 | let item = monitor.getItem();
87 | let can = !(props.id === item.cardListId);
88 | return can;
89 | },
90 | };
91 |
92 | const collect = (connect, monitor) => ({
93 | connectDropTarget: connect.dropTarget(),
94 | isOver: monitor.isOver(),
95 | });
96 |
97 | export const CardList = DropTarget(
98 | ItemTypes.CARD,
99 | dropTarget,
100 | collect
101 | )(CardListWithoutDnd);
102 |
103 | const CardListHeader = ({ name, children }) => (
104 |
111 |
119 |
126 | }
127 | on="click"
128 | basic>
129 | {children}
130 |
131 |
132 | );
133 |
134 | const InnerScrollContainer = ({ children }) => {
135 | return (
136 |
142 | {children}
143 |
144 | );
145 | };
146 |
147 | const CardsContainer = ({ children }) => (
148 |
154 | {children}
155 |
156 | );
157 |
158 | const ListContainer = ({ children, style }) => (
159 |
172 | {children}
173 |
174 | );
175 |
176 | const CardListButton = ({ onButtonClick, children }) => (
177 | onButtonClick()}
180 | style={{
181 | margin: '0.1em 0 0 0',
182 | borderBottom: '1px solid #ccc',
183 | backgroundColor: '#grey',
184 | }}>
185 | {children}
186 |
187 | );
188 |
189 | CardList.propTypes = {
190 | name: PropTypes.string.isRequired,
191 | id: PropTypes.string,
192 | addCardWithName: PropTypes.func,
193 | deleteListWithId: PropTypes.func,
194 | moveCardToList: PropTypes.func,
195 | cards: PropTypes.array,
196 | };
197 |
198 | CardList.fragments = {
199 | list: gql`
200 | fragment CardList_list on List {
201 | name
202 | id
203 | cards {
204 | ...Card_card
205 | }
206 | }
207 | ${Card.fragments.card}
208 | `,
209 | };
210 |
--------------------------------------------------------------------------------
/Section5/src/components/Constants.js:
--------------------------------------------------------------------------------
1 | export const ItemTypes = {
2 | CARD: 'card',
3 | };
4 |
--------------------------------------------------------------------------------
/Section5/src/dummyData.js:
--------------------------------------------------------------------------------
1 | let boardData = {
2 | name: 'Course',
3 | lists: [
4 | {
5 | name: 'First Section',
6 | cards: [
7 | {
8 | name: 'Intro',
9 | },
10 | ],
11 | },
12 | {
13 | name: 'Second Section',
14 | cards: [
15 | {
16 | name: 'Video 1',
17 | },
18 | {
19 | name: 'Video 2',
20 | },
21 | {
22 | name: 'Video 3',
23 | },
24 | {
25 | name: 'Video 4',
26 | },
27 | {
28 | name: 'Video 5',
29 | },
30 | ],
31 | },
32 | ],
33 | };
34 |
35 | let numbers = Array.from(Array(20).keys());
36 | let cards = numbers.map(i => ({ name: `Video ${i}` }));
37 | boardData.lists[0].cards.push(...cards);
38 |
39 | export default boardData;
40 |
--------------------------------------------------------------------------------
/Section5/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
--------------------------------------------------------------------------------
/Section5/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import registerServiceWorker from './registerServiceWorker';
6 |
7 | // Instead of integrating the
8 | // css here, with running webpack bundling every time while developing,
9 | // I put this into the index page:
10 | //
11 | //
12 | // import 'semantic-ui-css/semantic.min.css';
13 |
14 | ReactDOM.render( , document.getElementById('root'));
15 | registerServiceWorker();
16 |
--------------------------------------------------------------------------------
/Section5/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | /* global process */
3 | // In production, we register a service worker to serve assets from local cache.
4 |
5 | // This lets the app load faster on subsequent visits in production, and gives
6 | // it offline capabilities. However, it also means that developers (and users)
7 | // will only see deployed updates on the "N+1" visit to a page, since previously
8 | // cached resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
11 | // This link also includes instructions on opting out of this behavior.
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export default function register() {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Lets check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://goo.gl/SC7cgQ'
47 | );
48 | });
49 | } else {
50 | // Is not local host. Just register service worker
51 | registerValidSW(swUrl);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | installingWorker.onstatechange = () => {
64 | if (installingWorker.state === 'installed') {
65 | if (navigator.serviceWorker.controller) {
66 | // At this point, the old content will have been purged and
67 | // the fresh content will have been added to the cache.
68 | // It's the perfect time to display a "New content is
69 | // available; please refresh." message in your web app.
70 | console.log('New content is available; please refresh.');
71 | } else {
72 | // At this point, everything has been precached.
73 | // It's the perfect time to display a
74 | // "Content is cached for offline use." message.
75 | console.log('Content is cached for offline use.');
76 | }
77 | }
78 | };
79 | };
80 | })
81 | .catch(error => {
82 | console.error('Error during service worker registration:', error);
83 | });
84 | }
85 |
86 | function checkValidServiceWorker(swUrl) {
87 | // Check if the service worker can be found. If it can't reload the page.
88 | fetch(swUrl)
89 | .then(response => {
90 | // Ensure service worker exists, and that we really are getting a JS file.
91 | if (
92 | response.status === 404 ||
93 | response.headers.get('content-type').indexOf('javascript') === -1
94 | ) {
95 | // No service worker found. Probably a different app. Reload the page.
96 | navigator.serviceWorker.ready.then(registration => {
97 | registration.unregister().then(() => {
98 | window.location.reload();
99 | });
100 | });
101 | } else {
102 | // Service worker found. Proceed as normal.
103 | registerValidSW(swUrl);
104 | }
105 | })
106 | .catch(() => {
107 | console.log(
108 | 'No internet connection found. App is running in offline mode.'
109 | );
110 | });
111 | }
112 |
113 | export function unregister() {
114 | if ('serviceWorker' in navigator) {
115 | navigator.serviceWorker.ready.then(registration => {
116 | registration.unregister();
117 | });
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/Section5/src/schema.js:
--------------------------------------------------------------------------------
1 | import { addMockFunctionsToSchema } from 'graphql-tools';
2 | //import { MockList } from 'graphql-tools';
3 |
4 | import { makeExecutableSchema } from 'graphql-tools';
5 |
6 | // a schema
7 | const typeDefs = `
8 | type Board {
9 | id: ID!
10 | lists: [List!]!
11 | name: String!
12 | }
13 | type List {
14 | cards: [Card!]!
15 | id: ID!
16 | name: String!
17 | }
18 | type Card {
19 | id: ID!
20 | name: String!
21 | }
22 | type Query {
23 | hello: String
24 | Board(id: String): Board
25 | }`;
26 | const resolvers = {
27 | Board: () => ({
28 | name: 'old resolvers',
29 | }),
30 | };
31 | // Export the GraphQL.js schema object as "schema"
32 | export const schema = makeExecutableSchema({
33 | typeDefs,
34 | resolvers,
35 | });
36 |
37 | // Add mocking
38 | const mocks = {
39 | //Board: (parent, args) => ({
40 | //name: () => 'heeh',
41 | // id: args.id || uuid.v4(),
42 | // lists: () => new MockList(1)
43 | //}),
44 | };
45 |
46 | addMockFunctionsToSchema({ schema, mocks });
47 |
--------------------------------------------------------------------------------
/Section6/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: http://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | # Unix-style newlines with a newline ending every file
7 | [*]
8 | end_of_line = lf
9 | insert_final_newline = true
10 | indent_style = space
11 | indent_size = 2
12 | trim_trailing_whitespace = true
13 | charset = utf-8
14 |
15 | # Matches the exact files either package.json or .travis.yml
16 | [package.json]
17 | indent_size = 2
18 |
19 | [*.md]
20 | trim_trailing_whitespace = false
21 |
--------------------------------------------------------------------------------
/Section6/.eslintignore:
--------------------------------------------------------------------------------
1 | src/registerServiceWorker.js
2 |
--------------------------------------------------------------------------------
/Section6/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | },
5 | parser: 'babel-eslint',
6 | plugins: ['react', 'prettier'],
7 | extends: [
8 | 'eslint:recommended',
9 | 'plugin:react/recommended',
10 | 'plugin:prettier/recommended',
11 | ],
12 | rules: {
13 | // already defined with prettier:
14 | 'prettier/prettier': ['warn'],
15 | 'no-unused-vars': [1],
16 | 'no-console': ['off'],
17 | indent: ['off', 2],
18 | quotes: ['off', 'single'],
19 | semi: ['off', 'always'],
20 | },
21 | };
22 |
--------------------------------------------------------------------------------
/Section6/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/Section6/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "singleQuote": true,
4 | "jsxBracketSameLine": true,
5 | "printWidth": 55
6 | }
7 |
--------------------------------------------------------------------------------
/Section6/README.md:
--------------------------------------------------------------------------------
1 | # Hands-on Application Building with GraphQL (and React)
2 |
3 | ## Section 6 : Adding User Authentication
4 |
5 | 1. Extending the Server to Enable Authentication and User Management - 00:19:57
6 | 1. Add Sign-in, Log In/Out - 00:17:51
7 | 1. User’s Boards and More Authorisation - 00:25:45
8 | 1. Track and Show Author - 00:13:54
9 |
--------------------------------------------------------------------------------
/Section6/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "coolboard",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "apollo-cache-inmemory": "1.6.0",
7 | "apollo-client": "2.6.0",
8 | "apollo-link": "1.2.11",
9 | "apollo-link-http": "1.5.14",
10 | "apollo-link-ws": "^1.0.17",
11 | "graphql": "14.3.1",
12 | "graphql-tag": "2.10.1",
13 | "graphql-tools": "4.0.4",
14 | "prop-types": "15.7.2",
15 | "react": "16.8.6",
16 | "react-apollo": "2.5.5",
17 | "react-apollo-network-status": "1.1.1",
18 | "react-dnd": "7.4.5",
19 | "react-dnd-html5-backend": "7.4.4",
20 | "react-dom": "16.8.6",
21 | "react-router-dom": "5.0.0",
22 | "react-scripts": "3.0.1",
23 | "react-timeago": "4.4.0",
24 | "semantic-ui-css": "2.2.12",
25 | "semantic-ui-react": "0.78.2",
26 | "styled-components": "4.2.0",
27 | "subscriptions-transport-ws": "0.9.16"
28 | },
29 | "scripts": {
30 | "start": "react-scripts start",
31 | "build": "react-scripts build",
32 | "test": "react-scripts test --env=jsdom",
33 | "eject": "react-scripts eject"
34 | },
35 | "devDependencies": {
36 | "babel-eslint": "10.0.1",
37 | "eslint": "^5.16.0",
38 | "eslint-config-prettier": "4.3.0",
39 | "eslint-config-react-app": "4.0.1",
40 | "eslint-plugin-prettier": "3.1.0",
41 | "eslint-plugin-react": "7.13.0",
42 | "prettier": "1.17.1"
43 | },
44 | "browserslist": [
45 | ">0.2%",
46 | "not dead",
47 | "not ie < 11",
48 | "not op_mini all"
49 | ]
50 | }
51 |
--------------------------------------------------------------------------------
/Section6/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 |
23 | Hands-on Application Building with GraphQL - Cool Board
24 |
25 |
26 |
27 | You need to enable JavaScript to run this app.
28 |
29 |
30 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/Section6/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/Section6/server/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: http://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | # Unix-style newlines with a newline ending every file
7 | [*]
8 | end_of_line = lf
9 | insert_final_newline = true
10 | indent_style = space
11 | indent_size = 2
12 | trim_trailing_whitespace = true
13 | charset = utf-8
14 |
15 | # Matches the exact files either package.json or .travis.yml
16 | [package.json]
17 | indent_size = 2
18 |
19 | [*.md]
20 | trim_trailing_whitespace = false
21 |
--------------------------------------------------------------------------------
/Section6/server/.eslintignore:
--------------------------------------------------------------------------------
1 | /src/generated/*
2 |
--------------------------------------------------------------------------------
/Section6/server/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: false,
4 | node: true,
5 | },
6 | parser: 'babel-eslint',
7 | plugins: ['prettier'],
8 | extends: [
9 | 'eslint:recommended',
10 | 'plugin:prettier/recommended',
11 | ],
12 | rules: {
13 | // already defined with prettier:
14 | 'prettier/prettier': ['warn'],
15 | 'no-unused-vars': [1],
16 | 'no-console': ['off'],
17 | indent: ['off', 2],
18 | quotes: ['off', 'single'],
19 | semi: ['off', 'always'],
20 | },
21 | };
22 |
--------------------------------------------------------------------------------
/Section6/server/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | package-lock.json
3 | node_modules
4 | .idea
5 | .vscode
6 | *.log
7 | .env*
8 |
--------------------------------------------------------------------------------
/Section6/server/.graphqlconfig.yml:
--------------------------------------------------------------------------------
1 | projects:
2 | app:
3 | schemaPath: "src/schema.graphql"
4 | extensions:
5 | endpoints:
6 | default: "http://localhost:4000"
7 | database:
8 | schemaPath: "src/generated/prisma.graphql"
9 | extensions:
10 | prisma: database/prisma.yml
--------------------------------------------------------------------------------
/Section6/server/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "singleQuote": true,
4 | "jsxBracketSameLine": true,
5 | "printWidth": 55
6 | }
7 |
--------------------------------------------------------------------------------
/Section6/server/README.md:
--------------------------------------------------------------------------------
1 | # Local GraphQL server based on Prisma
2 |
3 | To run you local server, you will have to run these commands in a
4 | terminal in this sub-folder (after a `cd server`).
5 |
6 | Based on Boilerplate for an Advanced GraphQL Server
7 |
8 | ## Features
9 |
10 | - **Scalable GraphQL server:** The server uses [`graphql-yoga`](https://github.com/prisma/graphql-yoga) which is based on Apollo Server & Express
11 | - **GraphQL database:** Includes GraphQL database binding to [Prisma](https://www.prismagraphql.com) (running on MySQL)
12 | - **Authentication**: Signup and login workflows are ready to use for your users
13 | - **Tooling**: Out-of-the-box support for [GraphQL Playground](https://github.com/prisma/graphql-playground) & [query performance tracing](https://github.com/apollographql/apollo-tracing)
14 | - **Extensible**: Simple and flexible [data model](database/datamodel.prisma) – easy to adjust and extend
15 | - **No configuration overhead**: Preconfigured [`graphql-config`](https://github.com/prisma/graphql-config) setup
16 | - **Realtime updates**: Support for GraphQL subscriptions (_coming soon_)
17 |
18 | For a fully-fledged **GraphQL & Node.js tutorial**, visit [How to GraphQL](https://www.howtographql.com/graphql-js/0-introduction/). You can more learn about the idea behind GraphQL boilerplates [here](https://blog.graph.cool/graphql-boilerplates-graphql-create-how-to-setup-a-graphql-project-6428be2f3a5).
19 |
20 | ### Commands
21 |
22 |
23 | After having docker started on you local machine you can deploy _locally_.
24 |
25 | * `yarn start` starts GraphQL server on `http://localhost:4000`
26 | * `yarn dev` starts GraphQL server on `http://localhost:4000` _and_ opens GraphQL Playground
27 | * `yarn playground` opens the GraphQL Playground for the `projects` from [`.graphqlconfig.yml`](./.graphqlconfig.yml)
28 | * `yarn prisma ` gives access to local version of Prisma CLI (e.g. `yarn prisma deploy`)
29 |
30 | > **Note**: We recommend that you're using `yarn dev` during development as it will give you access to the GraphQL API or your server (defined by the [application schema](./src/schema.graphql)) as well as to the Prisma API directly (defined by the [Prisma database schema](./generated/prisma.graphql)). If you're starting the server with `yarn start`, you'll only be able to access the API of the application schema.
31 |
--------------------------------------------------------------------------------
/Section6/server/database/datamodel.prisma:
--------------------------------------------------------------------------------
1 | type Board {
2 | id: ID! @id
3 | lists: [List!]!
4 | name: String!
5 |
6 | #createdBy: User @createdAt
7 | updatedBy: User
8 |
9 | # Optional system fields, used for tracking changes
10 | createdAt: DateTime! @createdAt # read-only (managed by prisma)
11 | updatedAt: DateTime! @updatedAt # read-only (managed by prisma)
12 | }
13 |
14 | type List {
15 | cards: [Card!]!
16 | id: ID! @id
17 | name: String!
18 | #createdBy: User @createdAt
19 | updatedBy: User
20 |
21 | # Optional system fields, used for tracking changes
22 | createdAt: DateTime! @createdAt # read-only (managed by prisma)
23 | updatedAt: DateTime! @updatedAt # read-only (managed by prisma)
24 | }
25 |
26 | type Card {
27 | id: ID! @id
28 | name: String!
29 | description: String @default(value: "")
30 |
31 | #createdBy: User @createdAt
32 | updatedBy: User
33 |
34 | # Optional system fields, used for tracking changes
35 | createdAt: DateTime! @createdAt # read-only (managed by prisma)
36 | updatedAt: DateTime! @updatedAt # read-only (managed by prisma)
37 | }
38 |
39 | type User {
40 | id: ID! @id
41 | email: String! @unique
42 | password: String!
43 | name: String!
44 | avatarUrl: String @default(value:"")
45 | boards: [Board!]!
46 | }
47 |
--------------------------------------------------------------------------------
/Section6/server/database/prisma.yml:
--------------------------------------------------------------------------------
1 | # A new property called endpoint has been added. The new endpoint effectively encodes the information of the three removed properties.
2 | #endpoint: ${env:PRISMA_ENDPOINT}
3 | endpoint: http://localhost:4466/
4 |
5 | # to disable authentication:
6 | # disableAuth: true
7 | #secret: ${env:PRISMA_MANAGEMENT_API_SECRET}
8 |
9 | # the file path pointing to your data model
10 | datamodel: datamodel.prisma
11 |
12 | # seed your service with initial data based on seed.graphql
13 | seed:
14 | import: seed.graphql
15 |
16 | # automatically run by prisma deploy:
17 | generate:
18 | - generator: graphql-schema
19 | output: "../src/generated/prisma.graphql"
20 |
21 | hooks:
22 | post-deploy:
23 | - echo "Deployment finished."
24 | - echo run prisma generate
25 | - prisma generate
26 |
--------------------------------------------------------------------------------
/Section6/server/database/seed.graphql:
--------------------------------------------------------------------------------
1 | mutation {
2 | createUser(data: {
3 | email: "me@work"
4 | password: "xxx"
5 | name: "Robert"
6 | }) {
7 | id
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Section6/server/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | prisma-handson-course:
4 | image: prismagraphql/prisma:1.31
5 | restart: always
6 | ports:
7 | - "4466:4466"
8 | environment:
9 | PRISMA_CONFIG: |
10 | port: 4466
11 | # uncomment the next line and provide the env var PRISMA_MANAGEMENT_API_SECRET=my-secret to activate cluster security
12 | # managementApiSecret: my-secret
13 | databases:
14 | default:
15 | connector: mysql
16 | host: mysql-handson-course
17 | user: root
18 | password: prisma
19 | rawAccess: true
20 | port: 3306
21 | migrations: true
22 | mysql-handson-course:
23 | image: mysql:5.7.24
24 | restart: always
25 | # Uncomment the next two lines to connect to your your database from outside the Docker environment, e.g. using a database GUI like Workbench
26 | # ports:
27 | # - "3306:3306"
28 | environment:
29 | MYSQL_ROOT_PASSWORD: prisma
30 | volumes:
31 | - mysql:/var/lib/mysql
32 | volumes:
33 | mysql:
34 |
--------------------------------------------------------------------------------
/Section6/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "coolboardsecure",
3 | "version": "0.1.0",
4 | "description": "A GraphQL server based on Prisma",
5 | "scripts": {
6 | "start": "nodemon -e js,graphql -x node -r dotenv/config src/index.js",
7 | "debug": "nodemon -e js,graphql -x node --inspect -r dotenv/config src/index.js",
8 | "playground": "graphql playground",
9 | "dev": "npm-run-all --parallel start playground"
10 | },
11 | "author": "Robert Hostlowsky",
12 | "private": true,
13 | "dependencies": {
14 | "bcryptjs": "2.4.3",
15 | "graphql": "14.3.1",
16 | "graphql-yoga": "1.17.4",
17 | "jsonwebtoken": "8.2.1",
18 | "prisma-binding": "2.3.10"
19 | },
20 | "devDependencies": {
21 | "dotenv": "7.0.0",
22 | "nodemon": "1.18.11",
23 | "npm-run-all": "4.1.5",
24 | "prisma": "1.31.1"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Section6/server/src/index.js:
--------------------------------------------------------------------------------
1 | const { GraphQLServer } = require('graphql-yoga');
2 | const { Prisma } = require('prisma-binding');
3 | const resolvers = require('./resolvers');
4 |
5 |
6 | const server = new GraphQLServer({
7 | typeDefs: 'src/schema.graphql',
8 | resolvers,
9 | resolverValidationOptions: {
10 | requireResolversForResolveType: false,
11 | },
12 | context: req => ({
13 | ...req,
14 | db: new Prisma({
15 | // the Prisma DB schema
16 | typeDefs: 'src/generated/prisma.graphql',
17 | // the endpoint of the Prisma DB service (value is set in .env)
18 | endpoint: process.env.PRISMA_ENDPOINT,
19 | // taken from database/prisma.yml (value is set in .env)
20 | secret: process.env.PRISMA_SECRET,
21 | // log all GraphQL queries & mutations
22 | debug: true,
23 | }),
24 | }),
25 | });
26 | server.start(() =>
27 | console.log(
28 | 'Server is running on http://localhost:4000'
29 | )
30 | );
31 |
--------------------------------------------------------------------------------
/Section6/server/src/resolvers/AuthPayload.js:
--------------------------------------------------------------------------------
1 | const AuthPayload = {
2 | user: async ({ user: { id } }, args, ctx, info) => {
3 | return ctx.db.query.user({ where: { id } }, info);
4 | },
5 | };
6 |
7 | module.exports = { AuthPayload };
8 |
--------------------------------------------------------------------------------
/Section6/server/src/resolvers/Mutation/auth.js:
--------------------------------------------------------------------------------
1 | const bcrypt = require('bcryptjs');
2 | const jwt = require('jsonwebtoken');
3 |
4 | const auth = {
5 | async signup(parent, args, ctx, info) {
6 | const password = await bcrypt.hash(
7 | args.password,
8 | 10
9 | );
10 | const user = await ctx.db.mutation.createUser({
11 | data: { ...args, password },
12 | });
13 |
14 | const token = jwt.sign(
15 | { userId: user.id },
16 | process.env.APP_SECRET
17 | );
18 | return {
19 | token,
20 | user,
21 | };
22 | },
23 |
24 | async login(parent, { email, password }, ctx, info) {
25 | const user = await ctx.db.query.user({
26 | where: { email },
27 | });
28 | if (!user) {
29 | throw new Error(
30 | `No such user found for email: ${email}`
31 | );
32 | }
33 |
34 | const valid = await bcrypt.compare(
35 | password,
36 | user.password
37 | );
38 | if (!valid) {
39 | throw new Error('Invalid password');
40 | }
41 |
42 | const token = jwt.sign(
43 | { userId: user.id },
44 | process.env.APP_SECRET
45 | );
46 |
47 | return {
48 | token,
49 | user,
50 | };
51 | },
52 | };
53 |
54 | module.exports = { auth };
55 |
--------------------------------------------------------------------------------
/Section6/server/src/resolvers/Mutation/board.js:
--------------------------------------------------------------------------------
1 | const { getUserId } = require('../../utils');
2 |
3 | const board = {
4 | async updateBoard(parent, args, ctx, info) {
5 | const userId = getUserId(ctx);
6 | const board = await ctx.db.mutation.updateBoard(
7 | {
8 | where: args.where,
9 | data: {
10 | ...args.data,
11 | updatedBy: {
12 | connect: {
13 | id: userId,
14 | },
15 | },
16 | },
17 | },
18 | info
19 | );
20 | return board;
21 | },
22 | async createBoard(parent, args, ctx, info) {
23 | const { name } = args;
24 |
25 | const id = getUserId(ctx);
26 |
27 | console.log('user-id', id);
28 |
29 | const user = await ctx.db.mutation.updateUser(
30 | {
31 | data: {
32 | boards: {
33 | create: {
34 | name,
35 | },
36 | },
37 | },
38 | where: { id },
39 | },
40 | info
41 | );
42 |
43 | const onDatabase = `
44 | mutation {
45 | updateUser(
46 | data: {
47 | boards: {
48 | create: {
49 | name: "name"
50 | }
51 | }
52 | }
53 | where: {
54 | id: "234"
55 | }
56 | ) {
57 | id
58 | boards {
59 | name
60 | id
61 | }
62 | }
63 | }`;
64 |
65 | return user;
66 | },
67 | async deleteBoard(parent, args, ctx, info) {
68 | const { id } = args;
69 |
70 | getUserId(ctx);
71 |
72 | console.log('board-id', id);
73 |
74 | const board = await ctx.db.mutation.deleteBoard(
75 | {
76 | where: { id },
77 | },
78 | info
79 | );
80 |
81 | const onDatabase = `
82 | mutation {
83 | deleteBoard(where: {id: "xcjd90t1gw0019018143trudyk"}) {
84 | id
85 | name
86 | }
87 | }
88 | }`;
89 |
90 | return board;
91 | },
92 | };
93 |
94 | module.exports = { board };
95 |
--------------------------------------------------------------------------------
/Section6/server/src/resolvers/Mutation/card.js:
--------------------------------------------------------------------------------
1 | const { getUserId } = require('../../utils');
2 |
3 | const card = {
4 | /*
5 | mutation updateCard(data: CardUpdateInput!, where: CardWhereUniqueInput!): Card
6 |
7 | input CardUpdateInput {
8 | name: String
9 | description: String
10 | updatedBy: UserUpdateOneInput
11 | }
12 | input UserUpdateOneInput {
13 | connect: UserWhereUniqueInput
14 | # ...
15 | }
16 | */
17 | async updateCard(parent, args, ctx, info) {
18 | const userId = getUserId(ctx);
19 |
20 | const argsWithUpdatedByUser = {
21 | where: args.where,
22 | data: {
23 | ...args.data,
24 | updatedBy: {
25 | connect: {
26 | id: userId,
27 | },
28 | },
29 | },
30 | };
31 |
32 | const updatedCard = await ctx.db.mutation.updateCard(
33 | argsWithUpdatedByUser
34 | );
35 |
36 | const result = await ctx.db.query.card(
37 | { where: { id: updatedCard.id } },
38 | info
39 | );
40 | return result;
41 |
42 | /* Example:
43 | mutation {
44 | updateCard(
45 | data: {
46 | name: "Video 5.1",
47 | updatedBy: {
48 | connect: {
49 | id: "cjfbofu49003q0938r41q67vb"
50 | }
51 | }
52 | }
53 | where: {
54 | id: "cjfejkdzn001d09459bzzsyml"
55 | }
56 | )
57 | {
58 | name
59 | updatedBy {
60 | avatarUrl
61 | name
62 | }
63 | }
64 | }
65 | */
66 | },
67 | };
68 |
69 | module.exports = { card };
70 |
--------------------------------------------------------------------------------
/Section6/server/src/resolvers/Mutation/list.js:
--------------------------------------------------------------------------------
1 | const { getUserId } = require('../../utils');
2 |
3 | const list = {
4 | async updateList(parent, args, ctx, info) {
5 | const userId = getUserId(ctx);
6 |
7 | const list = await ctx.db.mutation.updateList(
8 | {
9 | where: args.where,
10 | data: {
11 | ...args.data,
12 | updatedBy: {
13 | connect: {
14 | id: userId,
15 | },
16 | },
17 | },
18 | },
19 | info
20 | );
21 | return list;
22 | },
23 | async deleteList(parent, args, ctx, info) {
24 | getUserId(ctx);
25 | return await ctx.db.mutation.deleteList(
26 | args,
27 | info
28 | );
29 | },
30 | };
31 |
32 | module.exports = { list };
33 |
--------------------------------------------------------------------------------
/Section6/server/src/resolvers/Query.js:
--------------------------------------------------------------------------------
1 | const { getUserId } = require('../utils');
2 |
3 | const Query = {
4 | board(parent, { where }, ctx, info) {
5 | getUserId(ctx);
6 | return ctx.db.query.board({ where }, info);
7 | },
8 |
9 | me(parent, { where }, ctx, info) {
10 | const id = getUserId(ctx);
11 | return ctx.db.query.user({ where: { id } }, info);
12 | },
13 | };
14 |
15 | module.exports = { Query };
16 |
--------------------------------------------------------------------------------
/Section6/server/src/resolvers/Subscription.js:
--------------------------------------------------------------------------------
1 | const { getUserId } = require('../../src/utils');
2 |
3 | const Subscription = {
4 | board: {
5 | subscribe: async (parent, args, ctx, info) => {
6 | // check User Auth Token
7 | getUserId(ctx);
8 | return ctx.db.subscription.board(args, info);
9 | },
10 | },
11 | list: {
12 | subscribe: async (parent, args, ctx, info) => {
13 | // check User Auth Token
14 | getUserId(ctx);
15 | return ctx.db.subscription.list(args, info);
16 | },
17 | },
18 | card: {
19 | subscribe: async (parent, args, ctx, info) => {
20 | // check User Auth Token
21 | getUserId(ctx);
22 | return ctx.db.subscription.card(args, info);
23 | },
24 | },
25 | user: {
26 | subscribe: (parent, args, ctx, info) => {
27 | // check User Auth Token
28 | getUserId(ctx);
29 | return ctx.db.subscription.user(args, info);
30 | },
31 | },
32 | };
33 |
34 | module.exports = { Subscription };
35 |
--------------------------------------------------------------------------------
/Section6/server/src/resolvers/index.js:
--------------------------------------------------------------------------------
1 | const { Query } = require('./Query');
2 | const { Subscription } = require('./Subscription');
3 | const { auth } = require('./Mutation/auth');
4 | const { board } = require('./Mutation/board');
5 | const { list } = require('./Mutation/list');
6 | const { card } = require('./Mutation/card');
7 | const { AuthPayload } = require('./AuthPayload');
8 |
9 | module.exports = {
10 | Query,
11 | Mutation: {
12 | ...auth,
13 | ...board,
14 | ...list,
15 | ...card,
16 | },
17 | Subscription,
18 | AuthPayload,
19 | };
20 |
--------------------------------------------------------------------------------
/Section6/server/src/schema.graphql:
--------------------------------------------------------------------------------
1 | # import Board from "./generated/prisma.graphql"
2 |
3 | type Query {
4 | me: User
5 | board(where: BoardWhereUniqueInput!): Board
6 | }
7 |
8 | type Mutation {
9 | createBoard(name: String!): User!
10 | deleteBoard(id: ID!): Board!
11 | signup(email: String!, password: String!, name: String!, avatarUrl: String): AuthPayload!
12 | login(email: String!, password: String!): AuthPayload!
13 |
14 | updateBoard(data: BoardUpdateInput!, where: BoardWhereUniqueInput!): Board
15 | #used in:
16 | #updateBoard(data: {lists: {create: {name: $name}}}, where: {id: $boardId})
17 | #mutation deletelistsOfBoard($boardId: ID!, $listIds: [ListWhereUniqueInput!]!) {
18 |
19 | updateList(data: ListUpdateInput!, where: ListWhereUniqueInput!): List
20 | #used in:
21 | #mutation AddCardMutation( $cardListId: ID! $name: String!
22 | #mutation moveCard( $cardId: ID! $oldCardListId: ID! $cardListId: ID!
23 |
24 | updateCard(data: CardUpdateInput!, where: CardWhereUniqueInput!): Card!
25 | #used in:
26 | #updateCard(data: CardUpdateInput!, where: CardWhereUniqueInput!): Card
27 |
28 | deleteList(where: ListWhereUniqueInput!): List
29 | }
30 |
31 | type Subscription {
32 | board(where: BoardSubscriptionWhereInput): BoardSubscriptionPayload
33 | list(where: ListSubscriptionWhereInput): ListSubscriptionPayload
34 | card(where: CardSubscriptionWhereInput): CardSubscriptionPayload
35 | user(where: UserSubscriptionWhereInput): UserSubscriptionPayload
36 | }
37 |
38 | type AuthPayload {
39 | token: String!
40 | user: User!
41 | }
42 |
43 | type User {
44 | id: ID!
45 | email: String!
46 | name: String!
47 | avatarUrl: String
48 | boards: [Board]
49 | }
50 |
--------------------------------------------------------------------------------
/Section6/server/src/utils.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken');
2 |
3 | function getUserId(ctx) {
4 | const Authorization = ctx.request
5 | ? ctx.request.get('Authorization')
6 | : ctx.connection.context.Authorization;
7 |
8 | if (Authorization) {
9 | const token = Authorization.replace('Bearer ', '');
10 | const { userId } = jwt.verify(
11 | token,
12 | process.env.APP_SECRET
13 | );
14 | return userId;
15 | }
16 |
17 | throw new AuthError();
18 | }
19 |
20 | class AuthError extends Error {
21 | constructor() {
22 | super('Not authorized');
23 | }
24 | }
25 |
26 | module.exports = {
27 | getUserId,
28 | AuthError,
29 | };
30 |
--------------------------------------------------------------------------------
/Section6/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | display: flex;
4 | flex-direction: column;
5 | height: 100%;
6 | }
7 | #root {
8 | height: 100%;
9 | }
10 |
--------------------------------------------------------------------------------
/Section6/src/App.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 | import React, { Component } from 'react';
3 |
4 | import { ApolloClient } from 'apollo-client';
5 | import { ApolloProvider } from 'react-apollo';
6 | import { InMemoryCache } from 'apollo-cache-inmemory';
7 | /* http link */
8 | import { createHttpLink } from 'apollo-link-http';
9 | /* ws link */
10 | import { WebSocketLink } from 'apollo-link-ws';
11 |
12 | import { getMainDefinition } from 'apollo-utilities';
13 | import { ApolloLink, split } from 'apollo-link';
14 | /**/
15 |
16 | import {
17 | Switch,
18 | Route,
19 | BrowserRouter,
20 | } from 'react-router-dom';
21 |
22 | import './App.css';
23 |
24 | import { CoolBoard } from './components/CoolBoard';
25 | import Boards from './components/Boards';
26 | import LoginForm from './components/LoginForm';
27 | import SignupForm from './components/SignupForm';
28 | import { FullVerticalContainer } from './components/FullVerticalContainer';
29 | import { ProfileHeader } from './components/ProfileHeader';
30 |
31 | // Create a Http link
32 | let httpLink = createHttpLink({
33 | uri: 'http://localhost:4000',
34 | });
35 |
36 | const middlewareAuthLink = new ApolloLink(
37 | (operation, forward) => {
38 | const token = localStorage.getItem('token');
39 |
40 | operation.setContext({
41 | headers: {
42 | authorization: token ? `Bearer ${token}` : '',
43 | },
44 | });
45 | return forward(operation);
46 | }
47 | );
48 |
49 | // Create a WebSocket link:
50 | const wsLink = new WebSocketLink({
51 | uri: `ws://localhost:4000`,
52 | options: {
53 | reconnect: true,
54 | connectionParams: {
55 | Authorization: `Bearer ${localStorage.getItem(
56 | 'token'
57 | )}`,
58 | },
59 | },
60 | });
61 |
62 | // using the ability to split links, you can send data to each link
63 | // depending on what kind of operation is being sent
64 | const returnTrueIfSubscription = ({ query }) => {
65 | const { kind, operation } = getMainDefinition(query);
66 | return (
67 | kind === 'OperationDefinition' &&
68 | operation === 'subscription'
69 | );
70 | };
71 |
72 | // split based on operation type
73 | const link = split(
74 | returnTrueIfSubscription,
75 | wsLink,
76 | middlewareAuthLink.concat(httpLink)
77 | );
78 |
79 | const client = new ApolloClient({
80 | link,
81 | cache: new InMemoryCache(),
82 | });
83 |
84 | class App extends Component {
85 | render() {
86 | return (
87 |
88 |
89 |
90 |
91 | (
95 |
96 |
97 |
98 |
99 | )}
100 | />
101 |
102 | (
106 |
107 |
108 |
111 |
112 | )}
113 | />
114 |
115 | (
119 |
120 | {
122 | localStorage.setItem(
123 | 'token',
124 | token
125 | );
126 | client
127 | .resetStore()
128 | .then(() => {
129 | history.push(`/`);
130 | });
131 | }}
132 | />
133 |
134 | )}
135 | />
136 |
137 | (
141 |
142 | {
144 | history.push('/login');
145 | }}
146 | />
147 |
148 | )}
149 | />
150 |
151 | {
155 | localStorage.removeItem('token');
156 | client.resetStore().then(() => {
157 | history.push(`/`);
158 | });
159 | return (
160 | Please wait, logging out ...
161 | );
162 | }}
163 | />
164 |
165 |
166 |
167 |
168 | );
169 | }
170 | }
171 |
172 | export default App;
173 |
--------------------------------------------------------------------------------
/Section6/src/components/AuthForm.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import {
4 | Button,
5 | Form,
6 | Icon,
7 | Container,
8 | Message,
9 | Segment,
10 | } from 'semantic-ui-react';
11 |
12 | import { Link } from 'react-router-dom';
13 |
14 | class AuthForm extends Component {
15 | state = {
16 | name: '',
17 | email: '',
18 | password: '',
19 | avatarUrl: '',
20 | };
21 |
22 | onSubmit = async event => {
23 | if (event) {
24 | event.preventDefault();
25 | }
26 | const { onSubmit } = this.props;
27 |
28 | if (onSubmit) {
29 | onSubmit(this.state);
30 | }
31 | };
32 |
33 | render() {
34 | const { errors = [], signUp = false } = this.props;
35 | const {
36 | email,
37 | name,
38 | password,
39 | avatarUrl,
40 | } = this.state;
41 |
42 | return (
43 |
44 |
51 | this.setState({ email: e.target.value })
52 | }
53 | label="Email"
54 | value={email}
55 | name="email"
56 | autoFocus
57 | required
58 | />
59 | {signUp && (
60 |
66 | this.setState({
67 | name: e.target.value,
68 | })
69 | }
70 | label="Login id or Short name"
71 | value={name}
72 | name="name"
73 | />
74 | )}
75 |
76 |
86 | this.setState({
87 | password: e.target.value,
88 | })
89 | }
90 | />
91 | {signUp && (
92 |
102 | this.setState({
103 | avatarUrl: e.target.value,
104 | })
105 | }
106 | />
107 | )}
108 |
113 |
114 |
119 |
120 | {signUp ? 'Sign Up' : 'Log in'}
121 |
122 |
123 |
124 |
125 | {!signUp && (
126 |
127 |
128 | Sign up here, if you do not have already
129 | an account
130 |
131 |
132 | )}
133 |
134 | );
135 | }
136 | }
137 |
138 | export default AuthForm;
139 |
--------------------------------------------------------------------------------
/Section6/src/components/BoardContainer.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 | import React from 'react';
3 | import PropTypes from 'prop-types';
4 | import {
5 | Button,
6 | Container,
7 | Header,
8 | Icon,
9 | } from 'semantic-ui-react';
10 |
11 | import { DragDropContext } from 'react-dnd';
12 | import HTML5Backend from 'react-dnd-html5-backend';
13 |
14 | class BoardContainerInner extends React.Component {
15 | render() {
16 | const { boardName, children } = this.props;
17 |
18 | return (
19 |
26 |
27 | Board: {boardName}
28 |
29 |
38 | {children}
39 |
40 |
41 | );
42 | }
43 | }
44 |
45 | export const BoardContainer = DragDropContext(
46 | HTML5Backend
47 | )(BoardContainerInner);
48 |
49 | BoardContainer.propTypes = {
50 | boardName: PropTypes.string.isRequired,
51 | children: PropTypes.array.isRequired,
52 | };
53 |
54 | export const AddListButton = ({ onAddNewList }) => (
55 |
62 |
63 | Add a list
64 |
65 | );
66 | export const DelListButton = ({ action, children }) => (
67 | action()}
69 | style={{
70 | flexShrink: 0,
71 | flexGrow: 0,
72 | alignSelf: 'flex-start',
73 | }}>
74 |
75 | {children && children}
76 | {!children && 'Delete'}
77 |
78 | );
79 |
80 | DelListButton.propTypes = {
81 | onAddNewList: PropTypes.func,
82 | children: PropTypes.oneOfType([PropTypes.array, PropTypes.string]).isRequired,
83 | };
84 | AddListButton.propTypes = {
85 | onAddNewList: PropTypes.func,
86 | };
87 |
--------------------------------------------------------------------------------
/Section6/src/components/Boards.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Query, Mutation } from 'react-apollo';
3 | import gql from 'graphql-tag';
4 |
5 | import {
6 | Segment,
7 | Loader,
8 | Message,
9 | Button,
10 | } from 'semantic-ui-react';
11 |
12 | import { Link } from 'react-router-dom';
13 |
14 | import { FullVerticalContainer } from './FullVerticalContainer';
15 | import { CreateBoardModal } from './CreateBoardModal';
16 |
17 | const BoardListItem = ({ name, id, deleteBoard }) => {
18 | return (
19 |
20 | {name}
21 | deleteBoard(id)}
23 | size={'mini'}
24 | icon="trash"
25 | />
26 |
27 | );
28 | };
29 |
30 | const BoardList = ({ boards, deleteBoard }) =>
31 | boards.map(({ id, ...info }) => (
32 |
38 | ));
39 |
40 | export default class Boards extends Component {
41 | state = { showModal: false };
42 |
43 | showCreateBoardDialog = () => {
44 | this.setState({ showModal: true });
45 | };
46 |
47 | hideCreateBoardDialog = () => {
48 | this.setState({ showModal: false });
49 | };
50 |
51 | render() {
52 | const { showModal } = this.state;
53 |
54 | const createBoardMutation = gql`
55 | mutation createBoard($name: String!) {
56 | createBoard(name: $name) {
57 | name
58 | id
59 | boards {
60 | name
61 | id
62 | }
63 | }
64 | }
65 | `;
66 | const deleteBoardMutation = gql`
67 | mutation delBoard($id: ID!) {
68 | deleteBoard(id: $id) {
69 | id
70 | }
71 | }
72 | `;
73 |
74 | const userWithBoardsQuery = gql`
75 | {
76 | me {
77 | name
78 | id
79 | boards {
80 | name
81 | id
82 | }
83 | }
84 | }
85 | `;
86 |
87 | return (
88 |
89 | List of Boards
90 |
91 |
92 | {({ loading, error, data, refetch }) => {
93 | if (loading) return ;
94 | if (error)
95 | return (
96 |
97 | Error:
98 | {`${error}`}
99 |
100 | );
101 |
102 | return (
103 |
106 | {(deleteBoard, { error }) => {
107 | return (
108 |
109 | {error && (
110 |
111 | Error:
112 | {`${error}`}
113 |
114 | )}
115 |
116 | {
119 | return deleteBoard({
120 | variables: { id },
121 | });
122 | }}
123 | />
124 |
125 | );
126 | }}
127 |
128 | );
129 | }}
130 |
131 |
132 | {(createBoard, { loading, error }) => {
133 | const { message } = error || {};
134 | return (
135 |
136 | {
143 | return createBoard({
144 | variables: { name },
145 | });
146 | }}
147 | />
148 |
149 | );
150 | }}
151 |
152 |
153 | );
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/Section6/src/components/CardList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { DropTarget } from 'react-dnd';
3 | import PropTypes from 'prop-types';
4 | import gql from 'graphql-tag';
5 | import {
6 | Button,
7 | Header,
8 | Icon,
9 | Popup,
10 | } from 'semantic-ui-react';
11 |
12 | import Card from './Card';
13 | import { ItemTypes } from './Constants';
14 |
15 | class CardListWithoutDnd extends React.Component {
16 | render() {
17 | const {
18 | connectDropTarget,
19 | isOver,
20 | cards,
21 | name,
22 | id,
23 | addCardWithName = () => {},
24 | deleteListWithId = () => {},
25 | } = this.props;
26 |
27 | return (
28 |
29 | {connectDropTarget(
30 |
31 |
37 |
38 | deleteListWithId(id)}>
39 |
40 |
41 |
42 |
43 |
44 |
45 | {cards.map(c => (
46 |
51 | ))}
52 |
53 |
54 | addCardWithName(id)}>
55 |
56 | Add a card
57 |
58 |
59 |
60 | )}
61 |
62 | );
63 | }
64 | }
65 |
66 | const dropTarget = {
67 | drop(props, monitor, component) {
68 | console.log(
69 | 'dropped: ',
70 | props,
71 | monitor,
72 | component
73 | );
74 | let cardItem = monitor.getItem();
75 | const cardId = cardItem.id;
76 | const cardListId = props.id;
77 | const oldCardListId = cardItem.cardListId;
78 | props.moveCardToList(
79 | cardId,
80 | oldCardListId,
81 | cardListId
82 | );
83 | },
84 | hover(props, monitor) {},
85 | canDrop(props, monitor) {
86 | let item = monitor.getItem();
87 | let can = !(props.id === item.cardListId);
88 | return can;
89 | },
90 | };
91 |
92 | const collect = (connect, monitor) => ({
93 | connectDropTarget: connect.dropTarget(),
94 | isOver: monitor.isOver(),
95 | });
96 |
97 | export const CardList = DropTarget(
98 | ItemTypes.CARD,
99 | dropTarget,
100 | collect
101 | )(CardListWithoutDnd);
102 |
103 | const CardListHeader = ({ name, children }) => (
104 |
111 |
119 |
126 | }
127 | on="click"
128 | basic>
129 | {children}
130 |
131 |
132 | );
133 |
134 | const InnerScrollContainer = ({ children }) => {
135 | return (
136 |
142 | {children}
143 |
144 | );
145 | };
146 |
147 | const CardsContainer = ({ children }) => (
148 |
154 | {children}
155 |
156 | );
157 |
158 | const ListContainer = ({ children, style }) => (
159 |
172 | {children}
173 |
174 | );
175 |
176 | const CardListButton = ({ onButtonClick, children }) => (
177 | onButtonClick()}
180 | style={{
181 | margin: '0.1em 0 0 0',
182 | borderBottom: '1px solid #ccc',
183 | backgroundColor: '#grey',
184 | }}>
185 | {children}
186 |
187 | );
188 |
189 | CardList.propTypes = {
190 | name: PropTypes.string.isRequired,
191 | id: PropTypes.string,
192 | addCardWithName: PropTypes.func,
193 | deleteListWithId: PropTypes.func,
194 | moveCardToList: PropTypes.func,
195 | cards: PropTypes.array,
196 | };
197 |
198 | CardList.fragments = {
199 | list: gql`
200 | fragment CardList_list on List {
201 | name
202 | id
203 | cards {
204 | ...Card_card
205 | }
206 | }
207 | ${Card.fragments.card}
208 | `,
209 | };
210 |
--------------------------------------------------------------------------------
/Section6/src/components/Constants.js:
--------------------------------------------------------------------------------
1 | export const ItemTypes = {
2 | CARD: 'card',
3 | };
4 |
--------------------------------------------------------------------------------
/Section6/src/components/CreateBoardModal.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import {
4 | Button,
5 | Form,
6 | Message,
7 | Modal,
8 | Icon,
9 | } from 'semantic-ui-react';
10 |
11 | export class CreateBoardModal extends Component {
12 | state = {
13 | name: '',
14 | };
15 |
16 | handleChange = (e, { name, value }) =>
17 | this.setState({ [name]: value });
18 |
19 | render() {
20 | const { name } = this.state;
21 | const {
22 | open,
23 | onOpen,
24 | onHide,
25 | createBoard,
26 | loading,
27 | error = false,
28 | } = this.props;
29 |
30 | return (
31 | Create a new Board}>
36 | Create Board
37 |
38 |
49 | {`${error}`}
50 |
51 |
52 |
53 | {
56 | createBoard({
57 | name,
58 | }).then(() => onHide());
59 | }}
60 | inverted>
61 | Create
62 |
63 |
67 | cancel
68 |
69 |
70 |
71 | );
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/Section6/src/components/FullVerticalContainer.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const FullVerticalContainer = styled.div`
4 | display: flex;
5 | height: 100%;
6 | flex-direction: column;
7 | `;
8 |
--------------------------------------------------------------------------------
/Section6/src/components/LoginForm.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import { graphql } from 'react-apollo';
4 | import gql from 'graphql-tag';
5 |
6 | import AuthForm from './AuthForm';
7 |
8 | class LoginForm extends Component {
9 | state = { errors: [] };
10 |
11 | onSubmit(formData) {
12 | const { mutate, successfulLogin } = this.props;
13 |
14 | try {
15 | mutate({
16 | variables: formData,
17 | })
18 | .then(({ data }) => {
19 | const { login: { token } } = data;
20 |
21 | successfulLogin(token);
22 | })
23 |
24 | .catch(res => {
25 | const errors = res.graphQLErrors.map(
26 | error => error.message
27 | );
28 |
29 | this.setState({ errors });
30 | });
31 | } catch (ex) {
32 | const errors = [
33 | `Login unsuccessful! Details: ${ex.message}`,
34 | ];
35 |
36 | this.setState({ errors });
37 | }
38 | }
39 |
40 | render() {
41 | return (
42 |
43 |
Login
44 |
46 | this.onSubmit(formData)
47 | }
48 | errors={this.state.errors}
49 | />
50 |
51 | );
52 | }
53 | }
54 |
55 | const LOGIN_MUTATION = gql`
56 | mutation LoginMutation(
57 | $email: String!
58 | $password: String!
59 | ) {
60 | login(email: $email, password: $password) {
61 | token
62 | user {
63 | email
64 | name
65 | avatarUrl
66 | }
67 | }
68 | }
69 | `;
70 |
71 | export default graphql(LOGIN_MUTATION)(LoginForm);
72 |
--------------------------------------------------------------------------------
/Section6/src/components/ProfileHeader.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { graphql } from 'react-apollo/index';
4 | import gql from 'graphql-tag';
5 | import {
6 | Container,
7 | Icon,
8 | Image,
9 | } from 'semantic-ui-react';
10 |
11 | import { Link } from 'react-router-dom';
12 |
13 | const ProfileHeaderContainer = ({ children }) => (
14 |
22 |
27 |
28 |
29 | Home
30 |
31 |
32 |
33 | {children}
34 |
35 |
36 | );
37 |
38 | const ProfileHeaderComponent = ({ data }) => {
39 | const { loading, error, me = {} } = data;
40 |
41 | if (loading) {
42 | return (
43 |
44 |
45 | );
46 | }
47 |
48 | if (error) {
49 | return (
50 |
51 | Log in
52 |
53 | );
54 | }
55 |
56 | let { avatarUrl, name } = me;
57 |
58 | return (
59 |
60 | {name}
61 | {avatarUrl && (
62 |
67 | )}
68 |
69 |
70 | Logout
71 |
72 |
73 | );
74 | };
75 |
76 | export const ProfileHeader = graphql(
77 | gql`
78 | query Profile {
79 | me {
80 | email
81 | id
82 | name
83 | avatarUrl
84 | }
85 | }
86 | `,
87 | { options: { fetchPolicy: 'network-only' } }
88 | )(ProfileHeaderComponent);
89 |
--------------------------------------------------------------------------------
/Section6/src/components/SignupForm.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import { graphql } from 'react-apollo';
4 | import gql from 'graphql-tag';
5 |
6 | import AuthForm from './AuthForm';
7 |
8 | class SignUpForm extends Component {
9 | state = { errors: [] };
10 |
11 | onSubmit({ name, email, password, avatarUrl }) {
12 | const { mutate, successfulSignup } = this.props;
13 |
14 | mutate({
15 | variables: {
16 | name,
17 | email,
18 | password,
19 | avatarUrl,
20 | },
21 | })
22 | .then(() => {
23 | successfulSignup();
24 | })
25 | .catch(res => {
26 | const errors = res.graphQLErrors.map(
27 | error => error.message
28 | );
29 | this.setState({ errors });
30 | });
31 | }
32 |
33 | render() {
34 | return (
35 |
36 |
Sign Up
37 |
40 | this.onSubmit(formData)
41 | }
42 | errors={this.state.errors}
43 | />
44 |
45 | );
46 | }
47 | }
48 |
49 | const SIGNUP_MUTATION = gql`
50 | mutation SignupMutation(
51 | $email: String!
52 | $password: String!
53 | $name: String!
54 | $avatarUrl: String
55 | ) {
56 | signup(
57 | email: $email
58 | password: $password
59 | name: $name
60 | avatarUrl: $avatarUrl
61 | ) {
62 | token
63 | }
64 | }
65 | `;
66 |
67 | export default graphql(SIGNUP_MUTATION)(SignUpForm);
68 |
--------------------------------------------------------------------------------
/Section6/src/dummyData.js:
--------------------------------------------------------------------------------
1 | let boardData = {
2 | name: 'Course',
3 | lists: [
4 | {
5 | name: 'First Section',
6 | cards: [
7 | {
8 | name: 'Intro',
9 | },
10 | ],
11 | },
12 | {
13 | name: 'Second Section',
14 | cards: [
15 | {
16 | name: 'Video 1',
17 | },
18 | {
19 | name: 'Video 2',
20 | },
21 | {
22 | name: 'Video 3',
23 | },
24 | {
25 | name: 'Video 4',
26 | },
27 | {
28 | name: 'Video 5',
29 | },
30 | ],
31 | },
32 | ],
33 | };
34 |
35 | let numbers = Array.from(Array(20).keys());
36 | let cards = numbers.map(i => ({ name: `Video ${i}` }));
37 | boardData.lists[0].cards.push(...cards);
38 |
39 | export default boardData;
40 |
--------------------------------------------------------------------------------
/Section6/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
--------------------------------------------------------------------------------
/Section6/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import registerServiceWorker from './registerServiceWorker';
6 |
7 | // Instead of integrating the
8 | // css here, with running webpack bundling every time while developing,
9 | // I put this into the index page:
10 | //
11 | //
12 | // import 'semantic-ui-css/semantic.min.css';
13 |
14 | ReactDOM.render( , document.getElementById('root'));
15 | registerServiceWorker();
16 |
--------------------------------------------------------------------------------
/Section6/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | /* global process */
3 | // In production, we register a service worker to serve assets from local cache.
4 |
5 | // This lets the app load faster on subsequent visits in production, and gives
6 | // it offline capabilities. However, it also means that developers (and users)
7 | // will only see deployed updates on the "N+1" visit to a page, since previously
8 | // cached resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
11 | // This link also includes instructions on opting out of this behavior.
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export default function register() {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Lets check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://goo.gl/SC7cgQ'
47 | );
48 | });
49 | } else {
50 | // Is not local host. Just register service worker
51 | registerValidSW(swUrl);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | installingWorker.onstatechange = () => {
64 | if (installingWorker.state === 'installed') {
65 | if (navigator.serviceWorker.controller) {
66 | // At this point, the old content will have been purged and
67 | // the fresh content will have been added to the cache.
68 | // It's the perfect time to display a "New content is
69 | // available; please refresh." message in your web app.
70 | console.log('New content is available; please refresh.');
71 | } else {
72 | // At this point, everything has been precached.
73 | // It's the perfect time to display a
74 | // "Content is cached for offline use." message.
75 | console.log('Content is cached for offline use.');
76 | }
77 | }
78 | };
79 | };
80 | })
81 | .catch(error => {
82 | console.error('Error during service worker registration:', error);
83 | });
84 | }
85 |
86 | function checkValidServiceWorker(swUrl) {
87 | // Check if the service worker can be found. If it can't reload the page.
88 | fetch(swUrl)
89 | .then(response => {
90 | // Ensure service worker exists, and that we really are getting a JS file.
91 | if (
92 | response.status === 404 ||
93 | response.headers.get('content-type').indexOf('javascript') === -1
94 | ) {
95 | // No service worker found. Probably a different app. Reload the page.
96 | navigator.serviceWorker.ready.then(registration => {
97 | registration.unregister().then(() => {
98 | window.location.reload();
99 | });
100 | });
101 | } else {
102 | // Service worker found. Proceed as normal.
103 | registerValidSW(swUrl);
104 | }
105 | })
106 | .catch(() => {
107 | console.log(
108 | 'No internet connection found. App is running in offline mode.'
109 | );
110 | });
111 | }
112 |
113 | export function unregister() {
114 | if ('serviceWorker' in navigator) {
115 | navigator.serviceWorker.ready.then(registration => {
116 | registration.unregister();
117 | });
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/Section6/src/schema.js:
--------------------------------------------------------------------------------
1 | import { addMockFunctionsToSchema } from 'graphql-tools';
2 | //import { MockList } from 'graphql-tools';
3 |
4 | import { makeExecutableSchema } from 'graphql-tools';
5 |
6 | // a schema
7 | const typeDefs = `
8 | type Board {
9 | id: ID!
10 | lists: [List!]!
11 | name: String!
12 | }
13 | type List {
14 | cards: [Card!]!
15 | id: ID!
16 | name: String!
17 | }
18 | type Card {
19 | id: ID!
20 | name: String!
21 | }
22 | type Query {
23 | hello: String
24 | Board(id: String): Board
25 | }`;
26 | const resolvers = {
27 | Board: () => ({
28 | name: 'old resolvers',
29 | }),
30 | };
31 | // Export the GraphQL.js schema object as "schema"
32 | export const schema = makeExecutableSchema({
33 | typeDefs,
34 | resolvers,
35 | });
36 |
37 | // Add mocking
38 | const mocks = {
39 | //Board: (parent, args) => ({
40 | //name: () => 'heeh',
41 | // id: args.id || uuid.v4(),
42 | // lists: () => new MockList(1)
43 | //}),
44 | };
45 |
46 | addMockFunctionsToSchema({ schema, mocks });
47 |
--------------------------------------------------------------------------------
/Section7/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: http://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | # Unix-style newlines with a newline ending every file
7 | [*]
8 | end_of_line = lf
9 | insert_final_newline = true
10 | indent_style = space
11 | indent_size = 2
12 | trim_trailing_whitespace = true
13 | charset = utf-8
14 |
15 | # Matches the exact files either package.json or .travis.yml
16 | [package.json]
17 | indent_size = 2
18 |
19 | [*.md]
20 | trim_trailing_whitespace = false
21 |
--------------------------------------------------------------------------------
/Section7/.eslintignore:
--------------------------------------------------------------------------------
1 | src/registerServiceWorker.js
2 |
--------------------------------------------------------------------------------
/Section7/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | },
5 | parser: 'babel-eslint',
6 | plugins: ['react', 'prettier'],
7 | extends: [
8 | 'eslint:recommended',
9 | 'plugin:react/recommended',
10 | 'plugin:prettier/recommended',
11 | ],
12 | rules: {
13 | // already defined with prettier:
14 | 'prettier/prettier': ['warn'],
15 | 'no-unused-vars': [1],
16 | 'no-console': ['off'],
17 | indent: ['off', 2],
18 | quotes: ['off', 'single'],
19 | semi: ['off', 'always'],
20 | },
21 | };
22 |
--------------------------------------------------------------------------------
/Section7/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/Section7/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "singleQuote": true,
4 | "jsxBracketSameLine": true,
5 | "printWidth": 55
6 | }
7 |
--------------------------------------------------------------------------------
/Section7/README.md:
--------------------------------------------------------------------------------
1 | # Hands-on Application Building with GraphQL (and React)
2 |
3 | ## Section 7 : Troubleshooting, Error Handling, and Tuning
4 |
5 | 1. Troubleshooting and Error Handling - 00:24:58
6 | 1. Tuning - 00:11:19
7 |
--------------------------------------------------------------------------------
/Section7/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "coolboard",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "apollo-cache-inmemory": "1.6.0",
7 | "apollo-client": "2.6.0",
8 | "apollo-link": "1.2.11",
9 | "apollo-link-http": "1.5.14",
10 | "apollo-link-ws": "^1.0.17",
11 | "graphql": "14.3.1",
12 | "graphql-tag": "2.10.1",
13 | "graphql-tools": "4.0.4",
14 | "prop-types": "15.7.2",
15 | "react": "16.8.6",
16 | "react-apollo": "2.5.5",
17 | "react-apollo-network-status": "1.1.1",
18 | "react-dnd": "7.4.5",
19 | "react-dnd-html5-backend": "7.4.4",
20 | "react-dom": "16.8.6",
21 | "react-router-dom": "5.0.0",
22 | "react-scripts": "3.0.1",
23 | "react-timeago": "4.4.0",
24 | "semantic-ui-css": "2.2.12",
25 | "semantic-ui-react": "0.78.2",
26 | "styled-components": "4.2.0",
27 | "subscriptions-transport-ws": "0.9.16"
28 | },
29 | "scripts": {
30 | "start": "react-scripts start",
31 | "build": "react-scripts build",
32 | "test": "react-scripts test --env=jsdom",
33 | "eject": "react-scripts eject"
34 | },
35 | "devDependencies": {
36 | "babel-eslint": "10.0.1",
37 | "eslint": "^5.16.0",
38 | "eslint-config-prettier": "4.3.0",
39 | "eslint-config-react-app": "4.0.1",
40 | "eslint-plugin-prettier": "3.1.0",
41 | "eslint-plugin-react": "7.13.0",
42 | "prettier": "1.17.1"
43 | },
44 | "browserslist": [
45 | ">0.2%",
46 | "not dead",
47 | "not ie < 11",
48 | "not op_mini all"
49 | ]
50 | }
51 |
--------------------------------------------------------------------------------
/Section7/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 |
23 | Hands-on Application Building with GraphQL - Cool Board
24 |
25 |
26 |
27 | You need to enable JavaScript to run this app.
28 |
29 |
30 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/Section7/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/Section7/server/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: http://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | # Unix-style newlines with a newline ending every file
7 | [*]
8 | end_of_line = lf
9 | insert_final_newline = true
10 | indent_style = space
11 | indent_size = 2
12 | trim_trailing_whitespace = true
13 | charset = utf-8
14 |
15 | # Matches the exact files either package.json or .travis.yml
16 | [package.json]
17 | indent_size = 2
18 |
19 | [*.md]
20 | trim_trailing_whitespace = false
21 |
--------------------------------------------------------------------------------
/Section7/server/.eslintignore:
--------------------------------------------------------------------------------
1 | /src/generated/*
2 |
--------------------------------------------------------------------------------
/Section7/server/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: false,
4 | node: true,
5 | },
6 | parser: 'babel-eslint',
7 | plugins: ['prettier'],
8 | extends: [
9 | 'eslint:recommended',
10 | 'plugin:prettier/recommended',
11 | ],
12 | rules: {
13 | // already defined with prettier:
14 | 'prettier/prettier': ['warn'],
15 | 'no-unused-vars': [1],
16 | 'no-console': ['off'],
17 | indent: ['off', 2],
18 | quotes: ['off', 'single'],
19 | semi: ['off', 'always'],
20 | },
21 | };
22 |
--------------------------------------------------------------------------------
/Section7/server/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | package-lock.json
3 | node_modules
4 | .idea
5 | .vscode
6 | *.log
7 | .env*
8 |
--------------------------------------------------------------------------------
/Section7/server/.graphqlconfig.yml:
--------------------------------------------------------------------------------
1 | projects:
2 | app:
3 | schemaPath: "src/schema.graphql"
4 | extensions:
5 | endpoints:
6 | default: "http://localhost:4000"
7 | database:
8 | schemaPath: "src/generated/prisma.graphql"
9 | extensions:
10 | prisma: database/prisma.yml
--------------------------------------------------------------------------------
/Section7/server/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "singleQuote": true,
4 | "jsxBracketSameLine": true,
5 | "printWidth": 55
6 | }
7 |
--------------------------------------------------------------------------------
/Section7/server/README.md:
--------------------------------------------------------------------------------
1 | # Local GraphQL server based on Prisma
2 |
3 | To run you local server, you will have to run these commands in a
4 | terminal in this sub-folder (after a `cd server`).
5 |
6 | Based on Boilerplate for an Advanced GraphQL Server
7 |
8 | ## Features
9 |
10 | - **Scalable GraphQL server:** The server uses [`graphql-yoga`](https://github.com/prisma/graphql-yoga) which is based on Apollo Server & Express
11 | - **GraphQL database:** Includes GraphQL database binding to [Prisma](https://www.prismagraphql.com) (running on MySQL)
12 | - **Authentication**: Signup and login workflows are ready to use for your users
13 | - **Tooling**: Out-of-the-box support for [GraphQL Playground](https://github.com/prisma/graphql-playground) & [query performance tracing](https://github.com/apollographql/apollo-tracing)
14 | - **Extensible**: Simple and flexible [data model](database/datamodel.prisma) – easy to adjust and extend
15 | - **No configuration overhead**: Preconfigured [`graphql-config`](https://github.com/prisma/graphql-config) setup
16 | - **Realtime updates**: Support for GraphQL subscriptions (_coming soon_)
17 |
18 | For a fully-fledged **GraphQL & Node.js tutorial**, visit [How to GraphQL](https://www.howtographql.com/graphql-js/0-introduction/). You can more learn about the idea behind GraphQL boilerplates [here](https://blog.graph.cool/graphql-boilerplates-graphql-create-how-to-setup-a-graphql-project-6428be2f3a5).
19 |
20 | ### Commands
21 |
22 |
23 | After having docker started on you local machine you can deploy _locally_.
24 |
25 | * `yarn start` starts GraphQL server on `http://localhost:4000`
26 | * `yarn dev` starts GraphQL server on `http://localhost:4000` _and_ opens GraphQL Playground
27 | * `yarn playground` opens the GraphQL Playground for the `projects` from [`.graphqlconfig.yml`](./.graphqlconfig.yml)
28 | * `yarn prisma ` gives access to local version of Prisma CLI (e.g. `yarn prisma deploy`)
29 |
30 | > **Note**: We recommend that you're using `yarn dev` during development as it will give you access to the GraphQL API or your server (defined by the [application schema](./src/schema.graphql)) as well as to the Prisma API directly (defined by the [Prisma database schema](./generated/prisma.graphql)). If you're starting the server with `yarn start`, you'll only be able to access the API of the application schema.
31 |
--------------------------------------------------------------------------------
/Section7/server/database/datamodel.graphql:
--------------------------------------------------------------------------------
1 | type Board {
2 | id: ID! @unique
3 | lists: [List!]!
4 | name: String!
5 |
6 | #createdBy: User
7 | updatedBy: User
8 |
9 | # Optional system fields, used for tracking changes
10 | createdAt: DateTime! # read-only (managed by Graphcool)
11 | updatedAt: DateTime! # read-only (managed by Graphcool)
12 | }
13 |
14 | type List {
15 | cards: [Card!]!
16 | id: ID! @unique
17 | name: String!
18 | #createdBy: User
19 | updatedBy: User
20 |
21 | # Optional system fields, used for tracking changes
22 | createdAt: DateTime! # read-only (managed by Graphcool)
23 | updatedAt: DateTime! # read-only (managed by Graphcool)
24 | }
25 |
26 | type Card {
27 | id: ID! @unique
28 | name: String!
29 | description: String @default(value: "")
30 |
31 | #createdBy: User
32 | updatedBy: User
33 |
34 | # Optional system fields, used for tracking changes
35 | createdAt: DateTime! # read-only (managed by Graphcool)
36 | updatedAt: DateTime! # read-only (managed by Graphcool)
37 | }
38 |
39 | type User {
40 | id: ID! @unique
41 | email: String! @unique
42 | password: String!
43 | name: String!
44 | avatarUrl: String @default(value:"")
45 | boards: [Board!]!
46 | }
47 |
--------------------------------------------------------------------------------
/Section7/server/database/datamodel.prisma:
--------------------------------------------------------------------------------
1 | type Board {
2 | id: ID! @id
3 | lists: [List!]!
4 | name: String!
5 |
6 | #createdBy: User @createdAt
7 | updatedBy: User
8 |
9 | # Optional system fields, used for tracking changes
10 | createdAt: DateTime! @createdAt # read-only (managed by prisma)
11 | updatedAt: DateTime! @updatedAt # read-only (managed by prisma)
12 | }
13 |
14 | type List {
15 | cards: [Card!]!
16 | id: ID! @id
17 | name: String!
18 | #createdBy: User @createdAt
19 | updatedBy: User
20 |
21 | # Optional system fields, used for tracking changes
22 | createdAt: DateTime! @createdAt # read-only (managed by prisma)
23 | updatedAt: DateTime! @updatedAt # read-only (managed by prisma)
24 | }
25 |
26 | type Card {
27 | id: ID! @id
28 | name: String!
29 | description: String @default(value: "")
30 |
31 | #createdBy: User @createdAt
32 | updatedBy: User
33 |
34 | # Optional system fields, used for tracking changes
35 | createdAt: DateTime! @createdAt # read-only (managed by prisma)
36 | updatedAt: DateTime! @updatedAt # read-only (managed by prisma)
37 | }
38 |
39 | type User {
40 | id: ID! @id
41 | email: String! @unique
42 | password: String!
43 | name: String!
44 | avatarUrl: String @default(value:"")
45 | boards: [Board!]!
46 | }
47 |
--------------------------------------------------------------------------------
/Section7/server/database/prisma.yml:
--------------------------------------------------------------------------------
1 | # A new property called endpoint has been added. The new endpoint effectively encodes the information of the three removed properties.
2 | #endpoint: ${env:PRISMA_ENDPOINT}
3 | endpoint: http://localhost:4466/
4 |
5 | # to disable authentication:
6 | # disableAuth: true
7 | #secret: ${env:PRISMA_MANAGEMENT_API_SECRET}
8 |
9 | # the file path pointing to your data model
10 | datamodel: datamodel.prisma
11 |
12 | # seed your service with initial data based on seed.graphql
13 | seed:
14 | import: seed.graphql
15 |
16 | # automatically run by prisma deploy:
17 | generate:
18 | - generator: graphql-schema
19 | output: "../src/generated/prisma.graphql"
20 |
21 | hooks:
22 | post-deploy:
23 | - echo "Deployment finished."
24 | - echo run prisma generate
25 | - prisma generate
26 |
--------------------------------------------------------------------------------
/Section7/server/database/seed.graphql:
--------------------------------------------------------------------------------
1 | mutation {
2 | createUser(data: {
3 | email: "me@work"
4 | password: "xxx"
5 | name: "Robert"
6 | }) {
7 | id
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Section7/server/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | prisma-handson-course:
4 | image: prismagraphql/prisma:1.31
5 | restart: always
6 | ports:
7 | - "4466:4466"
8 | environment:
9 | PRISMA_CONFIG: |
10 | port: 4466
11 | # uncomment the next line and provide the env var PRISMA_MANAGEMENT_API_SECRET=my-secret to activate cluster security
12 | # managementApiSecret: my-secret
13 | databases:
14 | default:
15 | connector: mysql
16 | host: mysql-handson-course
17 | user: root
18 | password: prisma
19 | rawAccess: true
20 | port: 3306
21 | migrations: true
22 | mysql-handson-course:
23 | image: mysql:5.7.24
24 | restart: always
25 | # Uncomment the next two lines to connect to your your database from outside the Docker environment, e.g. using a database GUI like Workbench
26 | # ports:
27 | # - "3306:3306"
28 | environment:
29 | MYSQL_ROOT_PASSWORD: prisma
30 | volumes:
31 | - mysql:/var/lib/mysql
32 | volumes:
33 | mysql:
34 |
--------------------------------------------------------------------------------
/Section7/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "coolboardsecure",
3 | "version": "0.1.0",
4 | "description": "A GraphQL server based on Prisma",
5 | "scripts": {
6 | "start": "nodemon -e js,graphql -x node -r dotenv/config src/index.js",
7 | "debug": "nodemon -e js,graphql -x node --inspect -r dotenv/config src/index.js",
8 | "playground": "graphql playground",
9 | "dev": "npm-run-all --parallel start playground"
10 | },
11 | "author": "Robert Hostlowsky",
12 | "private": true,
13 | "dependencies": {
14 | "apollo-errors": "1.7.1",
15 | "bcryptjs": "2.4.3",
16 | "graphql": "14.3.1",
17 | "graphql-yoga": "1.17.4",
18 | "jsonwebtoken": "8.2.1",
19 | "prisma-binding": "2.3.10"
20 | },
21 | "devDependencies": {
22 | "dotenv": "7.0.0",
23 | "nodemon": "1.18.11",
24 | "npm-run-all": "4.1.5",
25 | "prisma": "1.31.1"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Section7/server/src/index.js:
--------------------------------------------------------------------------------
1 | const { GraphQLServer } = require('graphql-yoga');
2 | const { Prisma } = require('prisma-binding');
3 | const { formatError } = require('apollo-errors');
4 | const resolvers = require('./resolvers');
5 |
6 | const options = {
7 | formatError: (...args) => {
8 | return formatError(...args);
9 | },
10 | };
11 |
12 | const server = new GraphQLServer({
13 | typeDefs: 'src/schema.graphql',
14 | resolvers,
15 | resolverValidationOptions: {
16 | requireResolversForResolveType: false,
17 | },
18 | context: req => ({
19 | ...req,
20 | db: new Prisma({
21 | // the Prisma DB schema
22 | typeDefs: 'src/generated/prisma.graphql',
23 | // the endpoint of the Prisma DB service (value is set in .env)
24 | endpoint: process.env.PRISMA_ENDPOINT,
25 | // taken from database/prisma.yml (value is set in .env)
26 | secret: process.env.PRISMA_SECRET,
27 | // log all GraphQL queries & mutations
28 | debug: true,
29 | }),
30 | }),
31 | });
32 |
33 | server.start(options, () =>
34 | console.log(
35 | 'Server is running on http://localhost:4000'
36 | )
37 | );
38 |
--------------------------------------------------------------------------------
/Section7/server/src/resolvers/AuthPayload.js:
--------------------------------------------------------------------------------
1 | const AuthPayload = {
2 | user: async ({ user: { id } }, args, ctx, info) => {
3 | return ctx.db.query.user({ where: { id } }, info);
4 | },
5 | };
6 |
7 | module.exports = { AuthPayload };
8 |
--------------------------------------------------------------------------------
/Section7/server/src/resolvers/Mutation/auth.js:
--------------------------------------------------------------------------------
1 | const bcrypt = require('bcryptjs');
2 | const jwt = require('jsonwebtoken');
3 |
4 | const auth = {
5 | async signup(parent, args, ctx, info) {
6 | const password = await bcrypt.hash(
7 | args.password,
8 | 10
9 | );
10 | const user = await ctx.db.mutation.createUser({
11 | data: { ...args, password },
12 | });
13 |
14 | const token = jwt.sign(
15 | { userId: user.id },
16 | process.env.APP_SECRET
17 | );
18 | return {
19 | token,
20 | user,
21 | };
22 | },
23 |
24 | async login(parent, { email, password }, ctx, info) {
25 | const user = await ctx.db.query.user({
26 | where: { email },
27 | });
28 | if (!user) {
29 | throw new Error(
30 | `No such user found for email: ${email}`
31 | );
32 | }
33 |
34 | const valid = await bcrypt.compare(
35 | password,
36 | user.password
37 | );
38 | if (!valid) {
39 | throw new Error('Invalid password');
40 | }
41 |
42 | const token = jwt.sign(
43 | { userId: user.id },
44 | process.env.APP_SECRET
45 | );
46 |
47 | return {
48 | token,
49 | user,
50 | };
51 | },
52 | };
53 |
54 | module.exports = { auth };
55 |
--------------------------------------------------------------------------------
/Section7/server/src/resolvers/Mutation/board.js:
--------------------------------------------------------------------------------
1 | const { getUserId } = require('../../utils');
2 |
3 | const board = {
4 | async updateBoard(parent, args, ctx, info) {
5 | const userId = getUserId(ctx);
6 | const board = await ctx.db.mutation.updateBoard(
7 | {
8 | where: args.where,
9 | data: {
10 | ...args.data,
11 | updatedBy: {
12 | connect: {
13 | id: userId,
14 | },
15 | },
16 | },
17 | },
18 | info
19 | );
20 | return board;
21 | },
22 | async createBoard(parent, args, ctx, info) {
23 | const { name } = args;
24 |
25 | const id = getUserId(ctx);
26 |
27 | console.log('user-id', id);
28 |
29 | const user = await ctx.db.mutation.updateUser(
30 | {
31 | data: {
32 | boards: {
33 | create: {
34 | name,
35 | },
36 | },
37 | },
38 | where: { id },
39 | },
40 | info
41 | );
42 |
43 | const onDatabase = `
44 | mutation {
45 | updateUser(
46 | data: {
47 | boards: {
48 | create: {
49 | name: "name"
50 | }
51 | }
52 | }
53 | where: {
54 | id: "234"
55 | }
56 | ) {
57 | id
58 | boards {
59 | name
60 | id
61 | }
62 | }
63 | }`;
64 |
65 | return user;
66 | },
67 | async deleteBoard(parent, args, ctx, info) {
68 | const { id } = args;
69 |
70 | getUserId(ctx);
71 |
72 | console.log('board-id', id);
73 |
74 | const board = await ctx.db.mutation.deleteBoard(
75 | {
76 | where: { id },
77 | },
78 | info
79 | );
80 |
81 | const onDatabase = `
82 | mutation {
83 | deleteBoard(where: {id: "xcjd90t1gw0019018143trudyk"}) {
84 | id
85 | name
86 | }
87 | }
88 | }`;
89 |
90 | return board;
91 | },
92 | };
93 |
94 | module.exports = { board };
95 |
--------------------------------------------------------------------------------
/Section7/server/src/resolvers/Mutation/card.js:
--------------------------------------------------------------------------------
1 | const { getUserId } = require('../../utils');
2 |
3 | const card = {
4 | /*
5 | mutation updateCard(data: CardUpdateInput!, where: CardWhereUniqueInput!): Card
6 |
7 | input CardUpdateInput {
8 | name: String
9 | description: String
10 | updatedBy: UserUpdateOneInput
11 | }
12 | input UserUpdateOneInput {
13 | connect: UserWhereUniqueInput
14 | # ...
15 | }
16 | */
17 | async updateCard(parent, args, ctx, info) {
18 | const userId = getUserId(ctx);
19 |
20 | const argsWithUpdatedByUser = {
21 | where: args.where,
22 | data: {
23 | ...args.data,
24 | updatedBy: {
25 | connect: {
26 | id: userId,
27 | },
28 | },
29 | },
30 | };
31 |
32 | const updatedCard = await ctx.db.mutation.updateCard(
33 | argsWithUpdatedByUser
34 | );
35 |
36 | const result = await ctx.db.query.card(
37 | { where: { id: updatedCard.id } },
38 | info
39 | );
40 | return result;
41 |
42 | /* Example:
43 | mutation {
44 | updateCard(
45 | data: {
46 | name: "Video 5.1",
47 | updatedBy: {
48 | connect: {
49 | id: "cjfbofu49003q0938r41q67vb"
50 | }
51 | }
52 | }
53 | where: {
54 | id: "cjfejkdzn001d09459bzzsyml"
55 | }
56 | )
57 | {
58 | name
59 | updatedBy {
60 | avatarUrl
61 | name
62 | }
63 | }
64 | }
65 | */
66 | },
67 | };
68 |
69 | module.exports = { card };
70 |
--------------------------------------------------------------------------------
/Section7/server/src/resolvers/Mutation/list.js:
--------------------------------------------------------------------------------
1 | const { getUserId } = require('../../utils');
2 |
3 | const list = {
4 | async updateList(parent, args, ctx, info) {
5 | const userId = getUserId(ctx);
6 |
7 | const list = await ctx.db.mutation.updateList(
8 | {
9 | where: args.where,
10 | data: {
11 | ...args.data,
12 | updatedBy: {
13 | connect: {
14 | id: userId,
15 | },
16 | },
17 | },
18 | },
19 | info
20 | );
21 | return list;
22 | },
23 | async deleteList(parent, args, ctx, info) {
24 | getUserId(ctx);
25 | return await ctx.db.mutation.deleteList(
26 | args,
27 | info
28 | );
29 | },
30 | };
31 |
32 | module.exports = { list };
33 |
--------------------------------------------------------------------------------
/Section7/server/src/resolvers/Query.js:
--------------------------------------------------------------------------------
1 | const { getUserId } = require('../utils');
2 |
3 | const Query = {
4 | board(parent, { where }, ctx, info) {
5 | getUserId(ctx);
6 | return ctx.db.query.board({ where }, info);
7 | },
8 |
9 | list(parent, { where }, ctx, info) {
10 | getUserId(ctx);
11 | return ctx.db.query.list({ where }, info);
12 | },
13 |
14 | me(parent, { where }, ctx, info) {
15 | const id = getUserId(ctx);
16 | return ctx.db.query.user({ where: { id } }, info);
17 | },
18 | };
19 |
20 | module.exports = { Query };
21 |
--------------------------------------------------------------------------------
/Section7/server/src/resolvers/Subscription.js:
--------------------------------------------------------------------------------
1 | const { getUserId } = require('../../src/utils');
2 |
3 | const Subscription = {
4 | board: {
5 | subscribe: async (parent, args, ctx, info) => {
6 | // check User Auth Token
7 | getUserId(ctx);
8 | return ctx.db.subscription.board(args, info);
9 | },
10 | },
11 | list: {
12 | subscribe: async (parent, args, ctx, info) => {
13 | // check User Auth Token
14 | getUserId(ctx);
15 | return ctx.db.subscription.list(args, info);
16 | },
17 | },
18 | card: {
19 | subscribe: async (parent, args, ctx, info) => {
20 | // check User Auth Token
21 | getUserId(ctx);
22 | return ctx.db.subscription.card(args, info);
23 | },
24 | },
25 | user: {
26 | subscribe: (parent, args, ctx, info) => {
27 | // check User Auth Token
28 | getUserId(ctx);
29 | return ctx.db.subscription.user(args, info);
30 | },
31 | },
32 | };
33 |
34 | module.exports = { Subscription };
35 |
--------------------------------------------------------------------------------
/Section7/server/src/resolvers/index.js:
--------------------------------------------------------------------------------
1 | const { Query } = require('./Query');
2 | const { Subscription } = require('./Subscription');
3 | const { auth } = require('./Mutation/auth');
4 | const { board } = require('./Mutation/board');
5 | const { list } = require('./Mutation/list');
6 | const { card } = require('./Mutation/card');
7 | const { AuthPayload } = require('./AuthPayload');
8 |
9 | module.exports = {
10 | Query,
11 | Mutation: {
12 | ...auth,
13 | ...board,
14 | ...list,
15 | ...card,
16 | },
17 | Subscription,
18 | AuthPayload,
19 | };
20 |
--------------------------------------------------------------------------------
/Section7/server/src/schema.graphql:
--------------------------------------------------------------------------------
1 | # import Board from "./generated/prisma.graphql"
2 |
3 | type Query {
4 | me: User
5 | board(where: BoardWhereUniqueInput!): Board
6 | list(where: ListWhereUniqueInput!): List
7 | }
8 |
9 | type Mutation {
10 | createBoard(name: String!): User!
11 | deleteBoard(id: ID!): Board!
12 | signup(email: String!, password: String!, name: String!, avatarUrl: String): AuthPayload!
13 | login(email: String!, password: String!): AuthPayload!
14 |
15 | updateBoard(data: BoardUpdateInput!, where: BoardWhereUniqueInput!): Board
16 | #used in:
17 | #updateBoard(data: {lists: {create: {name: $name}}}, where: {id: $boardId})
18 | #mutation deletelistsOfBoard($boardId: ID!, $listIds: [ListWhereUniqueInput!]!) {
19 |
20 | updateList(data: ListUpdateInput!, where: ListWhereUniqueInput!): List
21 | #used in:
22 | #mutation AddCardMutation( $cardListId: ID! $name: String!
23 | #mutation moveCard( $cardId: ID! $oldCardListId: ID! $cardListId: ID!
24 |
25 | updateCard(data: CardUpdateInput!, where: CardWhereUniqueInput!): Card!
26 | #used in:
27 | #updateCard(data: CardUpdateInput!, where: CardWhereUniqueInput!): Card
28 |
29 | deleteList(where: ListWhereUniqueInput!): List
30 | }
31 |
32 | type Subscription {
33 | board(where: BoardSubscriptionWhereInput): BoardSubscriptionPayload
34 | list(where: ListSubscriptionWhereInput): ListSubscriptionPayload
35 | card(where: CardSubscriptionWhereInput): CardSubscriptionPayload
36 | user(where: UserSubscriptionWhereInput): UserSubscriptionPayload
37 | }
38 |
39 | type AuthPayload {
40 | token: String!
41 | user: User!
42 | }
43 |
44 | type User {
45 | id: ID!
46 | email: String!
47 | name: String!
48 | avatarUrl: String
49 | boards: [Board]
50 | }
51 |
--------------------------------------------------------------------------------
/Section7/server/src/utils.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken');
2 |
3 | const { createError } = require('apollo-errors');
4 |
5 | const NotAuthorizedError = 'NotAuthorizedError';
6 |
7 | const AuthError = createError(NotAuthorizedError, {
8 | message: NotAuthorizedError,
9 | });
10 |
11 | function getUserId(ctx) {
12 | const Authorization = ctx.request
13 | ? ctx.request.get('Authorization')
14 | : ctx.connection.context.Authorization;
15 |
16 | if (Authorization) {
17 | const token = Authorization.replace('Bearer ', '');
18 | const { userId } = jwt.verify(
19 | token,
20 | process.env.APP_SECRET
21 | );
22 | return userId;
23 | }
24 |
25 | throw new AuthError({
26 | message: 'Not authorized',
27 | });
28 | }
29 |
30 | module.exports = {
31 | getUserId,
32 | AuthError,
33 | NotAuthorizedError,
34 | };
35 |
--------------------------------------------------------------------------------
/Section7/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | display: flex;
4 | flex-direction: column;
5 | height: 100%;
6 | }
7 | #root {
8 | height: 100%;
9 | }
10 |
--------------------------------------------------------------------------------
/Section7/src/authentication/AuthForm.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import {
4 | Button,
5 | Form,
6 | Icon,
7 | Container,
8 | Message,
9 | Segment,
10 | } from 'semantic-ui-react';
11 |
12 | import { Link } from 'react-router-dom';
13 |
14 | class AuthForm extends Component {
15 | state = {
16 | name: '',
17 | email: '',
18 | password: '',
19 | avatarUrl: '',
20 | };
21 |
22 | onSubmit = async event => {
23 | if (event) {
24 | event.preventDefault();
25 | }
26 | const { onSubmit } = this.props;
27 |
28 | if (onSubmit) {
29 | onSubmit(this.state);
30 | }
31 | };
32 |
33 | render() {
34 | const { errors = [], signUp = false } = this.props;
35 | const {
36 | email,
37 | name,
38 | password,
39 | avatarUrl,
40 | } = this.state;
41 |
42 | return (
43 |
44 |
51 | this.setState({ email: e.target.value })
52 | }
53 | label="Email"
54 | value={email}
55 | name="email"
56 | autoFocus
57 | required
58 | />
59 | {signUp && (
60 |
66 | this.setState({
67 | name: e.target.value,
68 | })
69 | }
70 | label="Login id or Short name"
71 | value={name}
72 | name="name"
73 | />
74 | )}
75 |
76 |
86 | this.setState({
87 | password: e.target.value,
88 | })
89 | }
90 | />
91 | {signUp && (
92 |
102 | this.setState({
103 | avatarUrl: e.target.value,
104 | })
105 | }
106 | />
107 | )}
108 |
113 |
114 |
119 |
120 | {signUp ? 'Sign Up' : 'Log in'}
121 |
122 |
123 |
124 |
125 | {!signUp && (
126 |
127 |
128 | Sign up here, if you do not have already
129 | an account
130 |
131 |
132 | )}
133 |
134 | );
135 | }
136 | }
137 |
138 | export default AuthForm;
139 |
--------------------------------------------------------------------------------
/Section7/src/authentication/LoginForm.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import { graphql } from 'react-apollo';
4 | import gql from 'graphql-tag';
5 |
6 | import AuthForm from './AuthForm';
7 |
8 | class LoginForm extends Component {
9 | state = { errors: [] };
10 |
11 | onSubmit(formData) {
12 | const { mutate, successfulLogin } = this.props;
13 |
14 | try {
15 | mutate({
16 | variables: formData,
17 | })
18 | .then(({ data }) => {
19 | const { login: { token } } = data;
20 |
21 | successfulLogin(token);
22 | })
23 |
24 | .catch(res => {
25 | const errors = res.graphQLErrors.map(
26 | error => error.message
27 | );
28 |
29 | this.setState({ errors });
30 | });
31 | } catch (ex) {
32 | const errors = [
33 | `Login unsuccessful! Details: ${ex.message}`,
34 | ];
35 |
36 | this.setState({ errors });
37 | }
38 | }
39 |
40 | render() {
41 | return (
42 |
43 |
Login
44 |
46 | this.onSubmit(formData)
47 | }
48 | errors={this.state.errors}
49 | />
50 |
51 | );
52 | }
53 | }
54 |
55 | const LOGIN_MUTATION = gql`
56 | mutation LoginMutation(
57 | $email: String!
58 | $password: String!
59 | ) {
60 | login(email: $email, password: $password) {
61 | token
62 | user {
63 | email
64 | name
65 | avatarUrl
66 | }
67 | }
68 | }
69 | `;
70 |
71 | export default graphql(LOGIN_MUTATION)(LoginForm);
72 |
--------------------------------------------------------------------------------
/Section7/src/authentication/SignupForm.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import { graphql } from 'react-apollo';
4 | import gql from 'graphql-tag';
5 |
6 | import AuthForm from './AuthForm';
7 |
8 | class SignUpForm extends Component {
9 | state = { errors: [] };
10 |
11 | onSubmit({ name, email, password, avatarUrl }) {
12 | const { mutate, successfulSignup } = this.props;
13 |
14 | mutate({
15 | variables: {
16 | name,
17 | email,
18 | password,
19 | avatarUrl,
20 | },
21 | })
22 | .then(() => {
23 | successfulSignup();
24 | })
25 | .catch(res => {
26 | const errors = res.graphQLErrors.map(
27 | error => error.message
28 | );
29 | this.setState({ errors });
30 | });
31 | }
32 |
33 | render() {
34 | return (
35 |
36 |
Sign Up
37 |
40 | this.onSubmit(formData)
41 | }
42 | errors={this.state.errors}
43 | />
44 |
45 | );
46 | }
47 | }
48 |
49 | const SIGNUP_MUTATION = gql`
50 | mutation SignupMutation(
51 | $email: String!
52 | $password: String!
53 | $name: String!
54 | $avatarUrl: String
55 | ) {
56 | signup(
57 | email: $email
58 | password: $password
59 | name: $name
60 | avatarUrl: $avatarUrl
61 | ) {
62 | token
63 | }
64 | }
65 | `;
66 |
67 | export default graphql(SIGNUP_MUTATION)(SignUpForm);
68 |
--------------------------------------------------------------------------------
/Section7/src/common/FullVerticalContainer.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const FullVerticalContainer = styled.div`
4 | display: flex;
5 | height: 100%;
6 | flex-direction: column;
7 | `;
8 |
--------------------------------------------------------------------------------
/Section7/src/common/GeneralErrorHandler.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Link } from 'react-router-dom';
4 |
5 | import { Message } from 'semantic-ui-react';
6 |
7 | // Error name, used on the server side, too
8 | const NotAuthorizedError = 'NotAuthorizedError';
9 |
10 | export const GeneralErrorHandler = ({
11 | NetworkStatusNotifier,
12 | }) => (
13 | {
15 | if (error) {
16 | const { graphQLErrors, networkError } = error;
17 | if (graphQLErrors) {
18 | const notAuthErr = graphQLErrors.find(
19 | err => err.name === NotAuthorizedError
20 | );
21 |
22 | if (notAuthErr) {
23 | return (
24 |
25 |
26 | You need to be authenticated to see
27 | or change items.
28 |
29 | Please click
30 |
31 | this link
32 | to log-in!
33 |
34 |
35 | );
36 | }
37 | return (
38 |
39 | {graphQLErrors
40 | .filter(error => error.message)
41 | .map(error => error.message)
42 | .map((message, idx) => (
43 | {message}
44 | ))}
45 |
46 | );
47 | } else if (networkError) {
48 | return (
49 |
50 |
51 | Network Error: {' '}
52 | {networkError.message}
53 |
54 |
55 | );
56 | } else {
57 | console.log('unknown error!', error);
58 | return (
59 |
60 | Unknown error!
61 |
62 | You could find more details in the
63 | browser console.
64 |
65 |
66 | );
67 | }
68 | }
69 | // do not render anything, when there is no error above
70 | return false;
71 | }}
72 | />
73 | );
74 |
--------------------------------------------------------------------------------
/Section7/src/common/ProfileHeader.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { graphql } from 'react-apollo';
4 | import gql from 'graphql-tag';
5 | import {
6 | Container,
7 | Icon,
8 | Image,
9 | } from 'semantic-ui-react';
10 |
11 | import { Link } from 'react-router-dom';
12 |
13 | const ProfileHeaderContainer = ({ children }) => (
14 |
22 |
27 |
28 |
29 | Home
30 |
31 |
32 |
33 | {children}
34 |
35 |
36 | );
37 |
38 | const ProfileHeaderComponent = ({ data }) => {
39 | const { loading, error, me = {} } = data;
40 |
41 | if (loading) {
42 | return (
43 |
44 |
45 | );
46 | }
47 |
48 | if (error) {
49 | return (
50 |
51 | Log in
52 |
53 | );
54 | }
55 |
56 | let { avatarUrl, name } = me;
57 |
58 | return (
59 |
60 | {name}
61 | {avatarUrl && (
62 |
67 | )}
68 |
69 |
70 | Logout
71 |
72 |
73 | );
74 | };
75 |
76 | export const ProfileHeader = graphql(
77 | gql`
78 | query Profile {
79 | me {
80 | email
81 | id
82 | name
83 | avatarUrl
84 | }
85 | }
86 | `,
87 | { options: { fetchPolicy: 'network-only' } }
88 | )(ProfileHeaderComponent);
89 |
--------------------------------------------------------------------------------
/Section7/src/components/BoardContainer.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 | import React from 'react';
3 | import PropTypes from 'prop-types';
4 | import {
5 | Button,
6 | Container,
7 | Header,
8 | Icon,
9 | } from 'semantic-ui-react';
10 |
11 | import { DragDropContext } from 'react-dnd';
12 | import HTML5Backend from 'react-dnd-html5-backend';
13 |
14 | class BoardContainerInner extends React.Component {
15 | render() {
16 | const { boardName, children } = this.props;
17 |
18 | return (
19 |
26 |
27 | Board: {boardName}
28 |
29 |
38 | {children}
39 |
40 |
41 | );
42 | }
43 | }
44 |
45 | export const BoardContainer = DragDropContext(
46 | HTML5Backend
47 | )(BoardContainerInner);
48 |
49 | BoardContainer.propTypes = {
50 | boardName: PropTypes.string.isRequired,
51 | children: PropTypes.array.isRequired,
52 | };
53 |
54 | export const AddListButton = ({ onAddNewList }) => (
55 |
62 |
63 | Add a list
64 |
65 | );
66 | export const DelListButton = ({ action, children }) => (
67 | action()}
69 | style={{
70 | flexShrink: 0,
71 | flexGrow: 0,
72 | alignSelf: 'flex-start',
73 | }}>
74 |
75 | {children && children}
76 | {!children && 'Delete'}
77 |
78 | );
79 |
80 | DelListButton.propTypes = {
81 | onAddNewList: PropTypes.func,
82 | children: PropTypes.oneOfType([PropTypes.array, PropTypes.string]).isRequired,
83 | };
84 | AddListButton.propTypes = {
85 | onAddNewList: PropTypes.func,
86 | };
87 |
--------------------------------------------------------------------------------
/Section7/src/components/Boards.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Query, Mutation } from 'react-apollo';
3 | import gql from 'graphql-tag';
4 |
5 | import {
6 | Segment,
7 | Loader,
8 | Button,
9 | } from 'semantic-ui-react';
10 |
11 | import { Link } from 'react-router-dom';
12 |
13 | import { CreateBoardModal } from './CreateBoardModal';
14 |
15 | const BoardListItem = ({ name, id, deleteBoard }) => {
16 | return (
17 |
18 | {name}
19 | deleteBoard(id)}
21 | size={'mini'}
22 | icon="trash"
23 | />
24 |
25 | );
26 | };
27 |
28 | const BoardList = ({ boards, deleteBoard }) =>
29 | boards.map(({ id, ...info }) => (
30 |
36 | ));
37 |
38 | export default class Boards extends Component {
39 | state = { showModal: false };
40 |
41 | showCreateBoardDialog = () => {
42 | this.setState({ showModal: true });
43 | };
44 |
45 | hideCreateBoardDialog = () => {
46 | this.setState({ showModal: false });
47 | };
48 |
49 | render() {
50 | const { showModal } = this.state;
51 |
52 | const createBoardMutation = gql`
53 | mutation createBoard($name: String!) {
54 | createBoard(name: $name) {
55 | name
56 | id
57 | boards {
58 | name
59 | id
60 | }
61 | }
62 | }
63 | `;
64 | const deleteBoardMutation = gql`
65 | mutation delBoard($id: ID!) {
66 | deleteBoard(id: $id) {
67 | id
68 | }
69 | }
70 | `;
71 |
72 | const userWithBoardsQuery = gql`
73 | {
74 | me {
75 | name
76 | id
77 | boards {
78 | name
79 | id
80 | }
81 | }
82 | }
83 | `;
84 |
85 | return (
86 | <>
87 | List of Boards
88 |
89 |
90 | {({ loading, error, data, refetch }) => {
91 | if (loading) return ;
92 | if (error) return false;
93 |
94 | return (
95 |
98 | {deleteBoard => (
99 | {
102 | return deleteBoard({
103 | variables: { id },
104 | });
105 | }}
106 | />
107 | )}
108 |
109 | );
110 | }}
111 |
112 |
113 | {(createBoard, { loading, error }) => {
114 | const { message } = error || {};
115 | return (
116 |
117 | {
124 | return createBoard({
125 | variables: { name },
126 | });
127 | }}
128 | />
129 |
130 | );
131 | }}
132 |
133 | >
134 | );
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/Section7/src/components/CreateBoardModal.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import {
4 | Button,
5 | Form,
6 | Message,
7 | Modal,
8 | Icon,
9 | } from 'semantic-ui-react';
10 |
11 | export class CreateBoardModal extends Component {
12 | state = {
13 | name: '',
14 | };
15 |
16 | handleChange = (e, { name, value }) =>
17 | this.setState({ [name]: value });
18 |
19 | render() {
20 | const { name } = this.state;
21 | const {
22 | open,
23 | onOpen,
24 | onHide,
25 | createBoard,
26 | loading,
27 | error = false,
28 | } = this.props;
29 |
30 | return (
31 | Create a new Board}>
36 | Create Board
37 |
38 |
49 | {`${error}`}
50 |
51 |
52 |
53 | {
56 | createBoard({
57 | name,
58 | }).then(() => onHide());
59 | }}
60 | inverted>
61 | Create
62 |
63 |
67 | cancel
68 |
69 |
70 |
71 | );
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/Section7/src/dummyData.js:
--------------------------------------------------------------------------------
1 | let boardData = {
2 | name: 'Course',
3 | lists: [
4 | {
5 | name: 'First Section',
6 | cards: [
7 | {
8 | name: 'Intro',
9 | },
10 | ],
11 | },
12 | {
13 | name: 'Second Section',
14 | cards: [
15 | {
16 | name: 'Video 1',
17 | },
18 | {
19 | name: 'Video 2',
20 | },
21 | {
22 | name: 'Video 3',
23 | },
24 | {
25 | name: 'Video 4',
26 | },
27 | {
28 | name: 'Video 5',
29 | },
30 | ],
31 | },
32 | ],
33 | };
34 |
35 | let numbers = Array.from(Array(20).keys());
36 | let cards = numbers.map(i => ({ name: `Video ${i}` }));
37 | boardData.lists[0].cards.push(...cards);
38 |
39 | export default boardData;
40 |
--------------------------------------------------------------------------------
/Section7/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
--------------------------------------------------------------------------------
/Section7/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import registerServiceWorker from './registerServiceWorker';
6 |
7 | // Instead of integrating the
8 | // css here, with running webpack bundling every time while developing,
9 | // I put this into the index page:
10 | //
11 | //
12 | // import 'semantic-ui-css/semantic.min.css';
13 |
14 | ReactDOM.render( , document.getElementById('root'));
15 | registerServiceWorker();
16 |
--------------------------------------------------------------------------------
/Section7/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | /* global process */
3 | // In production, we register a service worker to serve assets from local cache.
4 |
5 | // This lets the app load faster on subsequent visits in production, and gives
6 | // it offline capabilities. However, it also means that developers (and users)
7 | // will only see deployed updates on the "N+1" visit to a page, since previously
8 | // cached resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
11 | // This link also includes instructions on opting out of this behavior.
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export default function register() {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Lets check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://goo.gl/SC7cgQ'
47 | );
48 | });
49 | } else {
50 | // Is not local host. Just register service worker
51 | registerValidSW(swUrl);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | installingWorker.onstatechange = () => {
64 | if (installingWorker.state === 'installed') {
65 | if (navigator.serviceWorker.controller) {
66 | // At this point, the old content will have been purged and
67 | // the fresh content will have been added to the cache.
68 | // It's the perfect time to display a "New content is
69 | // available; please refresh." message in your web app.
70 | console.log('New content is available; please refresh.');
71 | } else {
72 | // At this point, everything has been precached.
73 | // It's the perfect time to display a
74 | // "Content is cached for offline use." message.
75 | console.log('Content is cached for offline use.');
76 | }
77 | }
78 | };
79 | };
80 | })
81 | .catch(error => {
82 | console.error('Error during service worker registration:', error);
83 | });
84 | }
85 |
86 | function checkValidServiceWorker(swUrl) {
87 | // Check if the service worker can be found. If it can't reload the page.
88 | fetch(swUrl)
89 | .then(response => {
90 | // Ensure service worker exists, and that we really are getting a JS file.
91 | if (
92 | response.status === 404 ||
93 | response.headers.get('content-type').indexOf('javascript') === -1
94 | ) {
95 | // No service worker found. Probably a different app. Reload the page.
96 | navigator.serviceWorker.ready.then(registration => {
97 | registration.unregister().then(() => {
98 | window.location.reload();
99 | });
100 | });
101 | } else {
102 | // Service worker found. Proceed as normal.
103 | registerValidSW(swUrl);
104 | }
105 | })
106 | .catch(() => {
107 | console.log(
108 | 'No internet connection found. App is running in offline mode.'
109 | );
110 | });
111 | }
112 |
113 | export function unregister() {
114 | if ('serviceWorker' in navigator) {
115 | navigator.serviceWorker.ready.then(registration => {
116 | registration.unregister();
117 | });
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/Section7/src/schema.js:
--------------------------------------------------------------------------------
1 | import { addMockFunctionsToSchema } from 'graphql-tools';
2 | //import { MockList } from 'graphql-tools';
3 |
4 | import { makeExecutableSchema } from 'graphql-tools';
5 |
6 | // a schema
7 | const typeDefs = `
8 | type Board {
9 | id: ID!
10 | lists: [List!]!
11 | name: String!
12 | }
13 | type List {
14 | cards: [Card!]!
15 | id: ID!
16 | name: String!
17 | }
18 | type Card {
19 | id: ID!
20 | name: String!
21 | }
22 | type Query {
23 | hello: String
24 | Board(id: String): Board
25 | }`;
26 | const resolvers = {
27 | Board: () => ({
28 | name: 'old resolvers',
29 | }),
30 | };
31 | // Export the GraphQL.js schema object as "schema"
32 | export const schema = makeExecutableSchema({
33 | typeDefs,
34 | resolvers,
35 | });
36 |
37 | // Add mocking
38 | const mocks = {
39 | //Board: (parent, args) => ({
40 | //name: () => 'heeh',
41 | // id: args.id || uuid.v4(),
42 | // lists: () => new MockList(1)
43 | //}),
44 | };
45 |
46 | addMockFunctionsToSchema({ schema, mocks });
47 |
--------------------------------------------------------------------------------