├── 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 |
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 | 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 | ![https://www.coolboard.fun/screenshot.png](https://www.coolboard.fun/screenshot.png) 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 | 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 |
95 | 100 | 101 | 102 |
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 | 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 |
28 | 29 | 30 | 31 |
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 | 65 | ); 66 | export const DelListButton = ({ action, children }) => ( 67 | 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 |
104 | {name} 105 |
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 | 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 | 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 |
58 | 59 | 60 | 61 |
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 | 65 | ); 66 | export const DelListButton = ({ action, children }) => ( 67 | 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 |
117 | {name} 118 |
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 | 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 | 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 |
45 | 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 | 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 | 65 | ); 66 | export const DelListButton = ({ action, children }) => ( 67 | 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 |
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 |
117 | {name} 118 |
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 | 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 |
39 | 49 | {`${error}`} 50 | 51 |
52 | 53 | 63 | 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 | 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 |
45 | 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 | 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 | 65 | ); 66 | export const DelListButton = ({ action, children }) => ( 67 | 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 |
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 |
39 | 49 | {`${error}`} 50 | 51 |
52 | 53 | 63 | 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 | --------------------------------------------------------------------------------