Build a Complete App with GraphQL, Node.js, MongoDB and React.js
Github Repositories
In the Build a Complete App with GraphQL, Node.js, MongoDB and React.js Academind course we can see how we can create a React app using GraphQL, Node.js and MongoDD.
Table of contents
- Description
- 1 What is GraphQL 22:16
- 2 API Design & Project Setup 8:32
- 3 Schemas & Resolvers 24:57
- 4 Types & Data 19:21
- 5 GraphQL + MongoDB 32:22
- Setup MongoDB Atlas
- Install mongoose, the elegant mongodb object modeling fir node.js
- Configure nodemon by using the nodemon.json configuration file
- Create the models folder and the event.js file.
- Modify the app.js file to use MongoDB to create an event
- Modify the app.js file to use MongoDB to get the events from the database
- Fix the error returning the _id object
- 6 Adding Relations 26.46
- Create the Users model and modify the event model to include relatation to the user
- Install bcryptjs that will be used to hash the password before storing it.
- Modify app.js to include the User model
- Hide password when returning the data
- Avoid duplication of users
- Connect the created events with users (hard-coded at the moment)
- 7 Dynamic Relations - 33:12
- Modify app.js to adjust the data returned by the events query using the populate() mongoose method.
- Create the user function to simplify the way we obtain data from an user.
- Create the event function to simplify the way we obtain data from an event.
- Refactor the code putting each model in a different file
- Fix the way the date is returned
- Change to use async / await
- 8 Adding Bookings 20:57
- 9 Refactoring our code 27:32
- 10 Adding User Authentication 32:47
- 11 The React Frontend 18:51
- Create the new frontend folder where the React application is going to be deployed
- Get rid of the not needed files and modify some other ones
- Start developing the content
- Install the react-router-dom library
- Create the route and one component to test it
- Add the Bookings.js and Events.js files to ensure the router is working
- 12 Adding a Navbar 21:48
- 13 Hitting the API 32:11
- 14. Using the token 16:07
- 15. Adding a Modal 24:11
- 16. Adding Events 27:13
- 17. Adding Event Features 41:20
- 18. Creating and Displaying Bookings 14:48
- 19. Cancelling Bookings 20:24
- 20. Using Dataloader 17:44
- 21. Improving Queries and Bugfixing 16:13
- 22. Creating Charts 38:05
- 23. Finishing the App 7:53
Description
What is GraphQL and why would you use it?
GraphQL is a query language invented by Facebook. It’s a query language that is NOT used to query data from a database though. Instead, it’s exposed via an API to frontend applications (SPAs, mobile apps).
It’s more flexible than REST APIs because it allows frontend clients to request exactly the data format the client needs. REST APIs on the other hand typically send back a fixed data format that often contains redundant data which is not required by the client.
Follow the course to learn more about the advantages of GraphQL APIs and how such an API is implemented in practice.
1 What is GraphQL 22:16
What We'll build
What is GraphQL?
How does GraphQL Work?
- A GraphQL Query example
{
query {
use {
name
age
}
}
}
Example of Node.js GraphQL API endpoint
2 API Design & Project Setup 8:32
Designing the GraphQL API
Create the folder for the solution
C:\Users\juan.pablo.perez\OneDrive\Training\GraphQL>mkdir graphql-react-event-booking
C:\Users\juan.pablo.perez\OneDrive\Training\GraphQL>cd graphql-react-event-booking
C:\Users\juan.pablo.perez\OneDrive\Training\GraphQL\graphql-react-event-booking>code .
Initialize npm
C:\Users\juan.pablo.perez\OneDrive\Training\GraphQL\graphql-react-event-booking>npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.
See `npm help json` for definitive documentation on these fields
and exactly what they do.
Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.
Press ^C at any time to quit.
package name: (graphql-react-event-booking)
version: (1.0.0)
description:
entry point: (index.js)
test command:
git repository:
keywords:
author:
license: (ISC)
About to write to C:\Users\juan.pablo.perez\OneDrive\Training\GraphQL\graphql-react-event-booking\package.json:
{
"name": "graphql-react-event-booking",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
Is this OK? (yes)
Install the initial dependencies
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 ~/OneDrive/Training/GraphQL/graphql-react-event-booking
$ npm install --save express body-parser
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN graphql-react-event-booking@1.0.0 No description
npm WARN graphql-react-event-booking@1.0.0 No repository field.
+ body-parser@1.18.3
+ express@4.16.4
added 51 packages from 36 contributors and audited 151 packages in 7.723s
found 0 vulnerabilities
- Install
nodemon
used to automatically restart the nodeJs server
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 ~/OneDrive/Training/GraphQL/graphql-react-event-booking
$ npm i --save-dev nodemon
> nodemon@1.18.9 postinstall C:\Users\juan.pablo.perez\OneDrive\Training\GraphQL\graphql-react-event-booking\node_modules\nodemon
> node bin/postinstall || exit 0
Love nodemon? You can now support the project via the open collective:
> https://opencollective.com/nodemon/donate
npm WARN graphql-react-event-booking@1.0.0 No description
npm WARN graphql-react-event-booking@1.0.0 No repository field.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.4 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.4: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
+ nodemon@1.18.9
added 217 packages from 130 contributors and audited 2389 packages in 28.582s
found 0 vulnerabilities
app.js
app
Create the initial nodeJs app.js
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json());
app.listen(3000);
- Test if it works.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 ~/OneDrive/Training/GraphQL/graphql-react-event-booking
$ node app.js
package.json
to set up how to start the express server
Modify the
package.json
{
"name": "graphql-react-event-booking",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon app.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"body-parser": "^1.18.3",
"express": "^4.16.4"
},
"devDependencies": {
"nodemon": "^1.18.9"
}
}
- Test if it works.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 ~/OneDrive/Training/GraphQL/graphql-react-event-booking
$ npm start
> graphql-react-event-booking@1.0.0 start C:\Users\juan.pablo.perez\OneDrive\Training\GraphQL\graphql-react-event-booking
> nodemon app.js
[nodemon] 1.18.9
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: *.*
[nodemon] starting `node app.js`
Set up a get request as an example
app.js
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json());
app.get('/', (req,res,next) => {
res.send('Hello World!');
})
app.listen(3000);
- Open another terminal and check if it works
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 ~/OneDrive/Training/GraphQL/graphql-react-event-booking
$ curl localhost:3000
Hello World!
3 Schemas & Resolvers 24:57
ChromeiQL extension
Install theexpress-graphql
and graphql
packages
Install the Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/GraphQL/graphql-react-event-booking$ npm i express-graphql graphql
npm WARN graphql-react-event-booking@1.0.0 No descriptionnpm WARN graphql-react-event-booking@1.0.0 No repository field.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.4 (node_modules\fsevents):npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.4: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
+ express-graphql@0.7.1+ graphql@14.0.2
added 5 packages from 5 contributors and audited 2413 packages in 36.934sfound 0 vulnerabilities
app.js
to set up the GraphQL
environment
Modify app.js
const express = require('express');
const bodyParser = require('body-parser');
const graphqlHttp = require('express-graphql');
const { buildSchema } = require('graphql');
const app = express();
app.use(bodyParser.json());
app.use('/graphql', graphqlHttp({
schema: buildSchema(`
type RootQuery {
events: [String!]!
}
type RootMutation {
createEvent(name: String): String
}
schema {
query: RootQuery
mutation: RootMutation
}
`),
rootValue: {
events: () => {
return ['Romantic Cooking', 'Sailing', 'All-night coding']
},
createEvent: (args) => {
const eventName = args.name;
return eventName;
},
graphiql: true
}
}));
app.listen(3000);
- We can put the following query:
query {
events
}
- And we obtain the following result:
{
"data": {
"events": [
"Romantic Cooking",
"Sailing",
"All-night coding"
]
}
}
Query using Postman
- Browse to Get Postman for Windows
- The query is a bit different:
{"query": "{ events }"}
POST /graphql HTTP/1.1
Host: localhost:3000
Content-Type: application/json
cache-control: no-cache
Postman-Token: d1d80e9c-1397-4578-9568-d152ffd9a5f5
{"query": "{ events }"}------WebKitFormBoundary7MA4YWxkTrZu0gW--
- The response is the same:
{
"data": {
"events": [
"Romantic Cooking",
"Sailing",
"All-night coding"
]
}
}
- To execute the mutation:
mutation {
createEvent(name: "Sports")
}
- Response:
{
"data": {
"createEvent": "Sports"
}
}
- Using Postman
{"query": "mutation {createEvent(name: \"Sports\")}" }
POST /graphql HTTP/1.1
Host: localhost:3000
Content-Type: application/json
cache-control: no-cache
Postman-Token: 3a3dff28-ff85-48ec-a38b-b7cbb3abeb8e
{"query": "mutation {createEvent(name: \"Sports\")}" }------WebKitFormBoundary7MA4YWxkTrZu0gW--
- The response is the same:
{
"data": {
"createEvent": "Sports"
}
}
4 Types & Data 19:21
app.js
const express = require('express');
const bodyParser = require('body-parser');
const graphqlHttp = require('express-graphql');
const { buildSchema } = require('graphql');
const app = express();
app.use(bodyParser.json());
const events = [];
app.use('/graphql', graphqlHttp({
schema: buildSchema(`
type Event {
_id: ID!
title: String!
description: String!
price: Float!
date: String!
}
input EventInput {
title: String!
description: String!
price: Float!
date: String!
}
type RootQuery {
events: [Event!]!
}
type RootMutation {
createEvent(eventInput: EventInput): Event
}
schema {
query: RootQuery
mutation: RootMutation
}
`),
rootValue: {
events: () => {
return events;
},
createEvent: (args) => {
const event = {
_id: Math.random().toString(),
title: args.eventInput.title,
description: args.eventInput.description,
price: +args.eventInput.price,
date: args.eventInput.date
}
events.push(event);
return event;
},
graphiql: true
}
}));
app.listen(3000);
query
query {
events {
title
price
}
}
{
"data": {
"events": []
}
}
mutation createEvent
mutation {
createEvent(eventInput: {
title: "A Test",
description: "Does this work?",
price: 9.99,
date: "2019-01-13T08:55:49.045Z"
})
{
title
description
}
}
{
"data": {
"createEvent": {
"title": "A Test",
"description": "Does this work?"
}
}
}
query
query {
events {
title
price
}
}
{
"data": {
"events": [
{
"title": "A Test",
"price": 9.99
}
]
}
}
query
query {
events {
_id
date
}
}
{
"data": {
"events": [
{
"_id": "0.1299606023284463",
"date": "2019-01-13T08:55:49.045Z"
}
]
}
}
5 GraphQL + MongoDB 32:22
MongoDB Atlas
Setupmongoose, the elegant mongodb
object modeling fir node.js
Install Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/GraphQL/graphql-react-event-booking
$ npm i mongoose
npm WARN graphql-react-event-booking@1.0.0 No description
npm WARN graphql-react-event-booking@1.0.0 No repository field.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.4 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.4: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
+ mongoose@5.4.3
added 19 packages from 16 contributors and audited 2451 packages in 15.669s
found 0 vulnerabilities
nodemon
by using the nodemon.json
configuration file
Configure - Nodemon is going to inject the environment variables that with put in the configuration file.
nodemon.json
{
"env": {
"MONGO_USER": "juan",
"MONGO_PASSWORD": "xxxxxx",
"MONGO_DB": "events-react-dev"
}
}
app.js
const express = require('express');
const bodyParser = require('body-parser');
const graphqlHttp = require('express-graphql');
const { buildSchema } = require('graphql');
const mongoose = require('mongoose');
const app = express();
app.use(bodyParser.json());
const events = [];
app.use('/graphql', graphqlHttp({
schema: buildSchema(`
type Event {
_id: ID!
title: String!
description: String!
price: Float!
date: String!
}
input EventInput {
title: String!
description: String!
price: Float!
date: String!
}
type RootQuery {
events: [Event!]!
}
type RootMutation {
createEvent(eventInput: EventInput): Event
}
schema {
query: RootQuery
mutation: RootMutation
}
`),
rootValue: {
events: () => {
return events;
},
createEvent: (args) => {
const event = {
_id: Math.random().toString(),
title: args.eventInput.title,
description: args.eventInput.description,
price: +args.eventInput.price,
date: args.eventInput.date
}
events.push(event);
return event;
},
graphiql: true
}
}));
mongoose
.connect(`mongodb+srv://${process.env.MONGO_USER}:${process.env.MONGO_PASSWORD}@cluster0-ycwj8.mongodb.net/${process.env.MONGO_DB}?retryWrites=true`)
.then(() => {
app.listen(3000);
})
.catch(err => {
console.log(err);
})
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/GraphQL/graphql-react-event-booking
$ npm start
> graphql-react-event-booking@1.0.0 start C:\Work\Training\Pre\GraphQL\graphql-react-event-booking
> nodemon app.js
[nodemon] 1.18.9
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: *.*
[nodemon] starting `node app.js`
(node:23000) DeprecationWarning: current URL string parser is deprecated, and will be removed in a future version. To use the new parser, pass option { useNewUrlParser: true } to MongoClient.connect.
models
folder and the event.js
file.
Create the event.js
const moongose = require('mongoose');
const Schema = moongose.Schema;
const eventSchema = new Schema({
title: {
type: String,
required: true
},
description: {
type: String,
required: true
},
price: {
type: Number,
required: true
},
date: {
type: Date,
required: true
},
});
module.exports = moongose.model('Event', eventSchema);
app.js
file to use MongoDB to create an event
Modify the app.js
const express = require('express');
const bodyParser = require('body-parser');
const graphqlHttp = require('express-graphql');
const { buildSchema } = require('graphql');
const mongoose = require('mongoose');
const Event = require('./models/event');
const app = express();
app.use(bodyParser.json());
app.use('/graphql', graphqlHttp({
schema: buildSchema(`
type Event {
_id: ID!
title: String!
description: String!
price: Float!
date: String!
}
input EventInput {
title: String!
description: String!
price: Float!
date: String!
}
type RootQuery {
events: [Event!]!
}
type RootMutation {
createEvent(eventInput: EventInput): Event
}
schema {
query: RootQuery
mutation: RootMutation
}
`),
rootValue: {
events: () => {
return events;
},
createEvent: (args) => {
const event = new Event({
title: args.eventInput.title,
description: args.eventInput.description,
price: +args.eventInput.price,
date: new Date(args.eventInput.date)
});
return event
.save()
.then(result => {
console.log(result);
return { ...result._doc};
})
.catch(err => {
console.log(err);
throw err;
})
},
graphiql: true
}
}));
mongoose
.connect(`mongodb+srv://${process.env.MONGO_USER}:${process.env.MONGO_PASSWORD}@cluster0-ycwj8.mongodb.net/${process.env.MONGO_DB}?retryWrites=true`)
.then(() => {
app.listen(3000);
})
.catch(err => {
console.log(err);
})
- createEvent mutation request
mutation {
createEvent(eventInput: {
title: "A Test",
description: "Does this work?",
price: 9.99,
date: "2019-01-13T08:55:49.045Z"
})
{
title
description
}
}
- createEvent mutation response
{
"data": {
"createEvent": {
"title": "A Test",
"description": "Does this work?"
}
}
}
app.js
file to use MongoDB to get the events from the database
Modify the app.js
const express = require('express');
const bodyParser = require('body-parser');
const graphqlHttp = require('express-graphql');
const { buildSchema } = require('graphql');
const mongoose = require('mongoose');
const Event = require('./models/event');
const app = express();
app.use(bodyParser.json());
app.use('/graphql', graphqlHttp({
schema: buildSchema(`
type Event {
_id: ID!
title: String!
description: String!
price: Float!
date: String!
}
input EventInput {
title: String!
description: String!
price: Float!
date: String!
}
type RootQuery {
events: [Event!]!
}
type RootMutation {
createEvent(eventInput: EventInput): Event
}
schema {
query: RootQuery
mutation: RootMutation
}
`),
rootValue: {
events: () => {
return Event
.find()
.then(events => {
return events.map(event => {
return {...event._doc};
})
})
.catch(err => {
console.log(err);
throw err;
})
},
createEvent: (args) => {
const event = new Event({
title: args.eventInput.title,
description: args.eventInput.description,
price: +args.eventInput.price,
date: new Date(args.eventInput.date)
});
return event
.save()
.then(result => {
console.log(result);
return { ...result._doc};
})
.catch(err => {
console.log(err);
throw err;
})
},
graphiql: true
}
}));
mongoose
.connect(`mongodb+srv://${process.env.MONGO_USER}:${process.env.MONGO_PASSWORD}@cluster0-ycwj8.mongodb.net/${process.env.MONGO_DB}?retryWrites=true`)
.then(() => {
app.listen(3000);
})
.catch(err => {
console.log(err);
})
- query request
query {
events {
date
title
}
}
- query response
{
"data": {
"events": [
{
"date": "1547369749045",
"title": "A Test"
}
]
}
}
Fix the error returning the _id object
- query request
query {
events {
_id
date
title
}
}
- query response
{
"errors": [
{
"message": "ID cannot represent value: { _bsontype: \"ObjectID\", id: <Buffer 5c 3b 7f 39 54 53 b7 43 e8 a6 d0 9f> }",
"locations": [
{
"line": 3,
"column": 4
}
],
"path": [
"events",
0,
"_id"
]
}
],
"data": null
}
app.js
const express = require('express');
const bodyParser = require('body-parser');
const graphqlHttp = require('express-graphql');
const { buildSchema } = require('graphql');
const mongoose = require('mongoose');
const Event = require('./models/event');
const app = express();
app.use(bodyParser.json());
app.use('/graphql', graphqlHttp({
schema: buildSchema(`
type Event {
_id: ID!
title: String!
description: String!
price: Float!
date: String!
}
input EventInput {
title: String!
description: String!
price: Float!
date: String!
}
type RootQuery {
events: [Event!]!
}
type RootMutation {
createEvent(eventInput: EventInput): Event
}
schema {
query: RootQuery
mutation: RootMutation
}
`),
rootValue: {
events: () => {
return Event
.find()
.then(events => {
return events.map(event => {
return {...event._doc, _id: event.id};
})
})
.catch(err => {
console.log(err);
throw err;
})
},
createEvent: (args) => {
const event = new Event({
title: args.eventInput.title,
description: args.eventInput.description,
price: +args.eventInput.price,
date: new Date(args.eventInput.date)
});
return event
.save()
.then(result => {
console.log(result);
return { ...result._doc, _id: result._doc._id.toString()};
})
.catch(err => {
console.log(err);
throw err;
})
},
graphiql: true
}
}));
mongoose
.connect(`mongodb+srv://${process.env.MONGO_USER}:${process.env.MONGO_PASSWORD}@cluster0-ycwj8.mongodb.net/${process.env.MONGO_DB}?retryWrites=true`)
.then(() => {
app.listen(3000);
})
.catch(err => {
console.log(err);
})
- query request
query {
events {
_id
date
title
}
}
- query response
{
"data": {
"events": [
{
"_id": "5c3b7f395453b743e8a6d09f",
"date": "1547369749045",
"title": "A Test"
}
]
}
}
6 Adding Relations 26.46
Create the Users model and modify the event model to include relatation to the user
user.js
const moongose = require('mongoose');
const Schema = moongose.Schema;
const userSchema = new Schema({
email: {
type: String,
required: true
},
password: {
type: String,
required: true
},
createdEvents: [
{
type: Schema.Types.ObjectId,
ref: 'Event'
}
]
});
module.exports = moongose.model('User', userSchema);
event.js
const moongose = require('mongoose');
const Schema = moongose.Schema;
const eventSchema = new Schema({
title: {
type: String,
required: true
},
description: {
type: String,
required: true
},
price: {
type: Number,
required: true
},
date: {
type: Date,
required: true
},
creator: {
type: Schema.Types.ObjectId,
ref: 'User'
}
});
module.exports = moongose.model('Event', eventSchema);
bcryptjs
that will be used to hash the password before storing it.
Install Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/GraphQL/graphql-react-event-booking$ npm i bcryptjs
npm WARN graphql-react-event-booking@1.0.0 No description
npm WARN graphql-react-event-booking@1.0.0 No repository field.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.4 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.4: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
+ bcryptjs@2.4.3
added 1 package from 6 contributors and audited 2452 packages in 13.672s
found 0 vulnerabilities
app.js
to include the User
model
Modify app.js
const express = require('express');
const bodyParser = require('body-parser');
const graphqlHttp = require('express-graphql');
const { buildSchema } = require('graphql');
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const Event = require('./models/event');
const User = require('./models/user');
const app = express();
app.use(bodyParser.json());
app.use('/graphql', graphqlHttp({
schema: buildSchema(`
type Event {
_id: ID!
title: String!
description: String!
price: Float!
date: String!
}
type User {
_id: ID!
email: String!
password: String
}
input EventInput {
title: String!
description: String!
price: Float!
date: String!
}
input UserInput {
email: String!
password: String!
}
type RootQuery {
events: [Event!]!
}
type RootMutation {
createEvent(eventInput: EventInput): Event
createUser(userInput: UserInput): User
}
schema {
query: RootQuery
mutation: RootMutation
}
`),
rootValue: {
events: () => {
return Event
.find()
.then(events => {
return events.map(event => {
return {...event._doc, _id: event.id};
})
})
.catch(err => {
console.log(err);
throw err;
})
},
createEvent: (args) => {
const event = new Event({
title: args.eventInput.title,
description: args.eventInput.description,
price: +args.eventInput.price,
date: new Date(args.eventInput.date)
});
return event
.save()
.then(result => {
console.log(result);
return { ...result._doc, _id: result._doc._id.toString()};
})
.catch(err => {
console.log(err);
throw err;
})
},
createUser: args => {
return bcrypt
.hash(args.userInput.password, 12)
.then(hashPassword => {
const user = new User({
email: args.userInput.email,
password: hashPassword
});
return user
.save()
.then(result => {
console.log(result);
return { ...result._doc, _id: result.id};
})
})
.catch(err => {
console.log(err);
throw err;
})
},
graphiql: true
}
}));
mongoose
.connect(`mongodb+srv://${process.env.MONGO_USER}:${process.env.MONGO_PASSWORD}@cluster0-ycwj8.mongodb.net/${process.env.MONGO_DB}?retryWrites=true`)
.then(() => {
app.listen(3000);
})
.catch(err => {
console.log(err);
})
createUser
mutation request
mutation {
createUser(userInput: {
email: "test@test.email",
password: "tester"
})
{
email
password
}
}
createUser
mutation response
{
"data": {
"createUser": {
"email": "test@test.email",
"password": "$2a$12$pkAsva5waO6OL/Og7zLH7OsUTBGBfwWo1hvfZiE/oTr7aLbsHYzG6"
}
}
}
Hide password when returning the data
app.js
const express = require('express');
const bodyParser = require('body-parser');
const graphqlHttp = require('express-graphql');
const { buildSchema } = require('graphql');
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const Event = require('./models/event');
const User = require('./models/user');
const app = express();
app.use(bodyParser.json());
app.use('/graphql', graphqlHttp({
schema: buildSchema(`
type Event {
_id: ID!
title: String!
description: String!
price: Float!
date: String!
}
type User {
_id: ID!
email: String!
password: String
}
input EventInput {
title: String!
description: String!
price: Float!
date: String!
}
input UserInput {
email: String!
password: String!
}
type RootQuery {
events: [Event!]!
}
type RootMutation {
createEvent(eventInput: EventInput): Event
createUser(userInput: UserInput): User
}
schema {
query: RootQuery
mutation: RootMutation
}
`),
rootValue: {
events: () => {
return Event
.find()
.then(events => {
return events.map(event => {
return {...event._doc, _id: event.id};
})
})
.catch(err => {
console.log(err);
throw err;
})
},
createEvent: (args) => {
const event = new Event({
title: args.eventInput.title,
description: args.eventInput.description,
price: +args.eventInput.price,
date: new Date(args.eventInput.date)
});
return event
.save()
.then(result => {
console.log(result);
return { ...result._doc, _id: result._doc._id.toString()};
})
.catch(err => {
console.log(err);
throw err;
})
},
createUser: args => {
return bcrypt
.hash(args.userInput.password, 12)
.then(hashPassword => {
const user = new User({
email: args.userInput.email,
password: hashPassword
});
return user
.save()
.then(result => {
console.log(result);
return { ...result._doc, password: null, _id: result.id};
})
})
.catch(err => {
console.log(err);
throw err;
})
},
graphiql: true
}
}));
mongoose
.connect(`mongodb+srv://${process.env.MONGO_USER}:${process.env.MONGO_PASSWORD}@cluster0-ycwj8.mongodb.net/${process.env.MONGO_DB}?retryWrites=true`)
.then(() => {
app.listen(3000);
})
.catch(err => {
console.log(err);
})
- `createUser` mutation request
```json
mutation {
createUser(userInput: {
email: "test2@test.email",
password: "tester"
})
{
email
password
}
}
createUser
mutation response
{
"data": {
"createUser": {
"email": "test2@test.email",
"password": null
}
}
}
Avoid duplication of users
app.js
const express = require('express');
const bodyParser = require('body-parser');
const graphqlHttp = require('express-graphql');
const { buildSchema } = require('graphql');
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const Event = require('./models/event');
const User = require('./models/user');
const app = express();
app.use(bodyParser.json());
app.use('/graphql', graphqlHttp({
schema: buildSchema(`
type Event {
_id: ID!
title: String!
description: String!
price: Float!
date: String!
}
type User {
_id: ID!
email: String!
password: String
}
input EventInput {
title: String!
description: String!
price: Float!
date: String!
}
input UserInput {
email: String!
password: String!
}
type RootQuery {
events: [Event!]!
}
type RootMutation {
createEvent(eventInput: EventInput): Event
createUser(userInput: UserInput): User
}
schema {
query: RootQuery
mutation: RootMutation
}
`),
rootValue: {
events: () => {
return Event
.find()
.then(events => {
return events.map(event => {
return {...event._doc, _id: event.id};
})
})
.catch(err => {
console.log(err);
throw err;
})
},
createEvent: (args) => {
const event = new Event({
title: args.eventInput.title,
description: args.eventInput.description,
price: +args.eventInput.price,
date: new Date(args.eventInput.date)
});
return event
.save()
.then(result => {
console.log(result);
return { ...result._doc, _id: result._doc._id.toString()};
})
.catch(err => {
console.log(err);
throw err;
})
},
createUser: args => {
return User
.findOne({email: args.userInput.email})
.then(user => {
if (user) {
throw new Error('User already exists.')
}
return bcrypt.hash(args.userInput.password, 12);
})
.then(hashPassword => {
const user = new User({
email: args.userInput.email,
password: hashPassword
});
return user
.save()
.then(result => {
console.log(result);
return { ...result._doc, password: null, _id: result.id};
})
})
.catch(err => {
console.log(err);
throw err;
})
},
graphiql: true
}
}));
mongoose
.connect(`mongodb+srv://${process.env.MONGO_USER}:${process.env.MONGO_PASSWORD}@cluster0-ycwj8.mongodb.net/${process.env.MONGO_DB}?retryWrites=true`)
.then(() => {
app.listen(3000);
})
.catch(err => {
console.log(err);
})
createUser
mutation request
mutation {
createUser(userInput: {
email: "test2@test.email",
password: "tester"
})
{
email
password
}
}
createUser
mutation response
{
"errors": [
{
"message": "User already exists.",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"createUser"
]
}
],
"data": {
"createUser": null
}
}
created events
with users
(hard-coded at the moment)
Connect the - Update
createEvent
app.js
.
.
.
createEvent: (args) => {
const event = new Event({
title: args.eventInput.title,
description: args.eventInput.description,
price: +args.eventInput.price,
date: new Date(args.eventInput.date),
creator: "5c3cc489a1f3273f984495ad"
});
let createdEvent;
return event
.save()
.then(result => {
console.log(result);
createdEvent = { ...result._doc, _id: result._doc._id.toString()};
return User.findById(result._doc.creator.toString())
})
.then(user => {
if (!user) {
throw new Error('User does not exists.')
}
user.createdEvents.push(event);
return user.save();
})
.then(result => {
console.log(result);
return createdEvent;
})
.catch(err => {
console.log(err);
throw err;
})
},
.
- createEvent mutation request
mutation {
createEvent(eventInput: {
title: "Testing",
description: "This is a test event",
price: 9.99,
date: "2019-01-14T17:36:49.045Z"
})
{
title
description
}
}
- createEvent mutation response
{
"data": {
"createEvent": {
"title": "Testing",
"description": "This is a test event"
}
}
}
7 Dynamic Relations - 33:12
app.js
to adjust the data returned by the events
query using the populate()
mongoose method.
Modify app.js
const express = require('express');
const bodyParser = require('body-parser');
const graphqlHttp = require('express-graphql');
const { buildSchema } = require('graphql');
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const Event = require('./models/event');
const User = require('./models/user');
const app = express();
app.use(bodyParser.json());
app.use('/graphql', graphqlHttp({
schema: buildSchema(`
type Event {
_id: ID!
title: String!
description: String!
price: Float!
date: String!
creator: User!
}
type User {
_id: ID!
email: String!
password: String
createdEvents: [Event!]
}
input EventInput {
title: String!
description: String!
price: Float!
date: String!
}
input UserInput {
email: String!
password: String!
}
type RootQuery {
events: [Event!]!
}
type RootMutation {
createEvent(eventInput: EventInput): Event
createUser(userInput: UserInput): User
}
schema {
query: RootQuery
mutation: RootMutation
}
`),
rootValue: {
events: () => {
return Event
.find()
.populate('creator')
.then(events => {
return events.map(event => {
return {
...event._doc,
_id: event.id,
creator: {
...event._doc.creator._doc,
_id: event._doc.creator.id
}
};
})
})
.catch(err => {
console.log(err);
throw err;
})
},
createEvent: (args) => {
const event = new Event({
title: args.eventInput.title,
description: args.eventInput.description,
price: +args.eventInput.price,
date: new Date(args.eventInput.date),
creator: "5c3cc489a1f3273f984495ad"
});
let createdEvent;
return event
.save()
.then(result => {
console.log(result);
createdEvent = { ...result._doc, _id: result._doc._id.toString()};
return User.findById(result._doc.creator.toString())
})
.then(user => {
if (!user) {
throw new Error('User does not exists.')
}
user.createdEvents.push(event);
return user.save();
})
.then(result => {
console.log(result);
return createdEvent;
})
.catch(err => {
console.log(err);
throw err;
})
},
createUser: args => {
return User
.findOne({email: args.userInput.email})
.then(user => {
if (user) {
throw new Error('User already exists.')
}
return bcrypt.hash(args.userInput.password, 12);
})
.then(hashPassword => {
const user = new User({
email: args.userInput.email,
password: hashPassword
});
return user
.save()
.then(result => {
console.log(result);
return { ...result._doc, password: null, _id: result.id};
})
})
.catch(err => {
console.log(err);
throw err;
})
},
graphiql: true
}
}));
mongoose
.connect(`mongodb+srv://${process.env.MONGO_USER}:${process.env.MONGO_PASSWORD}@cluster0-ycwj8.mongodb.net/${process.env.MONGO_DB}?retryWrites=true`)
.then(() => {
app.listen(3000);
})
.catch(err => {
console.log(err);
})
events
query request
query {
events {
_id
date
title
creator {
_id
email
}
}
}
events
query response
{
"data": {
"events": [
{
"_id": "5c3cc8bd7fabad484caf94b9",
"date": "1547487409045",
"title": "Testing",
"creator": {
"_id": "5c3cc489a1f3273f984495ad",
"email": "test@test.email"
}
}
]
}
}
user
function to simplify the way we obtain data from an user.
Create the app.js
const express = require('express');
const bodyParser = require('body-parser');
const graphqlHttp = require('express-graphql');
const { buildSchema } = require('graphql');
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const Event = require('./models/event');
const User = require('./models/user');
const app = express();
app.use(bodyParser.json());
const user = userId => {
return User.findById(userId)
.then(user => {
return {...user._doc, _id: user.id};
})
.catch(err => {
console.log(err);
throw err;
})
};
app.use('/graphql', graphqlHttp({
schema: buildSchema(`
type Event {
_id: ID!
title: String!
description: String!
price: Float!
date: String!
creator: User!
}
type User {
_id: ID!
email: String!
password: String
createdEvents: [Event!]
}
input EventInput {
title: String!
description: String!
price: Float!
date: String!
}
input UserInput {
email: String!
password: String!
}
type RootQuery {
events: [Event!]!
}
type RootMutation {
createEvent(eventInput: EventInput): Event
createUser(userInput: UserInput): User
}
schema {
query: RootQuery
mutation: RootMutation
}
`),
rootValue: {
events: () => {
return Event
.find()
.then(events => {
return events.map(event => {
return {
...event._doc,
_id: event.id,
creator: user.bind(this, event._doc.creator)
};
})
})
.catch(err => {
console.log(err);
throw err;
})
},
createEvent: (args) => {
const event = new Event({
title: args.eventInput.title,
description: args.eventInput.description,
price: +args.eventInput.price,
date: new Date(args.eventInput.date),
creator: "5c3cc489a1f3273f984495ad"
});
let createdEvent;
return event
.save()
.then(result => {
console.log(result);
createdEvent = { ...result._doc, _id: result._doc._id.toString()};
return User.findById(result._doc.creator.toString())
})
.then(user => {
if (!user) {
throw new Error('User does not exists.')
}
user.createdEvents.push(event);
return user.save();
})
.then(result => {
console.log(result);
return createdEvent;
})
.catch(err => {
console.log(err);
throw err;
})
},
createUser: args => {
return User
.findOne({email: args.userInput.email})
.then(user => {
if (user) {
throw new Error('User already exists.')
}
return bcrypt.hash(args.userInput.password, 12);
})
.then(hashPassword => {
const user = new User({
email: args.userInput.email,
password: hashPassword
});
return user
.save()
.then(result => {
console.log(result);
return { ...result._doc, password: null, _id: result.id};
})
})
.catch(err => {
console.log(err);
throw err;
})
},
graphiql: true
}
}));
mongoose
.connect(`mongodb+srv://${process.env.MONGO_USER}:${process.env.MONGO_PASSWORD}@cluster0-ycwj8.mongodb.net/${process.env.MONGO_DB}?retryWrites=true`)
.then(() => {
app.listen(3000);
})
.catch(err => {
console.log(err);
})
events
query request
query {
events {
_id
date
title
creator {
_id
email
}
}
}
events
query response
{
"data": {
"events": [
{
"_id": "5c3cc8bd7fabad484caf94b9",
"date": "1547487409045",
"title": "Testing",
"creator": {
"_id": "5c3cc489a1f3273f984495ad",
"email": "test@test.email"
}
}
]
}
}
event
function to simplify the way we obtain data from an event.
Create the app.js
const express = require('express');
const bodyParser = require('body-parser');
const graphqlHttp = require('express-graphql');
const { buildSchema } = require('graphql');
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const Event = require('./models/event');
const User = require('./models/user');
const app = express();
app.use(bodyParser.json());
const events = eventIds => {
return Event.find({_id: {$in: eventIds}})
.then(events => {
return events.map(event => {
return {
...event._doc,
_id: event.id,
creator: user.bind(this, event._doc.creator)};
});
})
.catch(err => {
console.log(err);
throw err;
});
}
const user = userId => {
return User.findById(userId)
.then(user => {
return {...user._doc, _id: user.id, createdEvents: events.bind(this, user._doc.createdEvents)};
})
.catch(err => {
console.log(err);
throw err;
})
};
app.use('/graphql', graphqlHttp({
schema: buildSchema(`
type Event {
_id: ID!
title: String!
description: String!
price: Float!
date: String!
creator: User!
}
type User {
_id: ID!
email: String!
password: String
createdEvents: [Event!]
}
input EventInput {
title: String!
description: String!
price: Float!
date: String!
}
input UserInput {
email: String!
password: String!
}
type RootQuery {
events: [Event!]!
}
type RootMutation {
createEvent(eventInput: EventInput): Event
createUser(userInput: UserInput): User
}
schema {
query: RootQuery
mutation: RootMutation
}
`),
rootValue: {
events: () => {
return Event
.find()
.then(events => {
return events.map(event => {
return {
...event._doc,
_id: event.id,
creator: user.bind(this, event._doc.creator)
};
})
})
.catch(err => {
console.log(err);
throw err;
})
},
createEvent: (args) => {
const event = new Event({
title: args.eventInput.title,
description: args.eventInput.description,
price: +args.eventInput.price,
date: new Date(args.eventInput.date),
creator: "5c3cc489a1f3273f984495ad"
});
let createdEvent;
return event
.save()
.then(result => {
console.log(result);
createdEvent = {
...result._doc,
_id: result._doc._id.toString(),
creator: user.bind(this, result._doc.creator)
};
return User.findById(result._doc.creator.toString())
})
.then(user => {
if (!user) {
throw new Error('User does not exists.')
}
user.createdEvents.push(event);
return user.save();
})
.then(result => {
console.log(result);
return createdEvent;
})
.catch(err => {
console.log(err);
throw err;
})
},
createUser: args => {
return User
.findOne({email: args.userInput.email})
.then(user => {
if (user) {
throw new Error('User already exists.')
}
return bcrypt.hash(args.userInput.password, 12);
})
.then(hashPassword => {
const user = new User({
email: args.userInput.email,
password: hashPassword
});
return user
.save()
.then(result => {
console.log(result);
return { ...result._doc, password: null, _id: result.id};
})
})
.catch(err => {
console.log(err);
throw err;
})
},
graphiql: true
}
}));
mongoose
.connect(`mongodb+srv://${process.env.MONGO_USER}:${process.env.MONGO_PASSWORD}@cluster0-ycwj8.mongodb.net/${process.env.MONGO_DB}?retryWrites=true`)
.then(() => {
app.listen(3000);
})
.catch(err => {
console.log(err);
})
events
query request
query {
events {
_id
date
title
creator {
_id
email
createdEvents {
_id
title
creator {
email
}
}
}
}
}
events
query response
{
"data": {
"events": [
{
"_id": "5c3cc8bd7fabad484caf94b9",
"date": "1547487409045",
"title": "Testing",
"creator": {
"_id": "5c3cc489a1f3273f984495ad",
"email": "test@test.email",
"createdEvents": [
{
"_id": "5c3cc8bd7fabad484caf94b9",
"title": "Testing",
"creator": {
"email": "test@test.email"
}
}
]
}
}
]
}
}
- createEvent mutation request
mutation {
createEvent(eventInput: {
title: "Another Event",
description: "This is another test event",
price: 999.99,
date: "2019-01-14T18:30:49.045Z"
})
{
title
description
}
}
- createEvent mutation response
{
"data": {
"createEvent": {
"title": "Another Event",
"description": "This is another test event"
}
}
}
events
query request
query {
events {
_id
date
title
creator {
_id
email
createdEvents {
_id
title
creator {
email
}
}
}
}
}
events
query response
{
"data": {
"events": [
{
"_id": "5c3cc8bd7fabad484caf94b9",
"date": "1547487409045",
"title": "Testing",
"creator": {
"_id": "5c3cc489a1f3273f984495ad",
"email": "test@test.email",
"createdEvents": [
{
"_id": "5c3cc8bd7fabad484caf94b9",
"title": "Testing",
"creator": {
"email": "test@test.email"
}
},
{
"_id": "5c3cd53f984bef4744ac6f9f",
"title": "Another Event",
"creator": {
"email": "test@test.email"
}
}
]
}
},
{
"_id": "5c3cd53f984bef4744ac6f9f",
"date": "1547490649045",
"title": "Another Event",
"creator": {
"_id": "5c3cc489a1f3273f984495ad",
"email": "test@test.email",
"createdEvents": [
{
"_id": "5c3cc8bd7fabad484caf94b9",
"title": "Testing",
"creator": {
"email": "test@test.email"
}
},
{
"_id": "5c3cd53f984bef4744ac6f9f",
"title": "Another Event",
"creator": {
"email": "test@test.email"
}
}
]
}
}
]
}
}
- createEvent mutation request
mutation {
createEvent(eventInput: {
title: "Third Event",
description: "This is the third test event",
price: 129.99,
date: "2019-01-14T18:36:49.045Z"
})
{
title
description
creator {
email
}
}
}
- createEvent mutation response
{
"data": {
"createEvent": {
"title": "Third Event",
"description": "This is the third test event",
"creator": {
"email": "test@test.email"
}
}
}
}
Refactor the code putting each model in a different file
The
graphql
folder must be created with theschema
andresolvers
subfolders.In the
schema
subfolder the following files must be created
index.js
const { buildSchema } = require('graphql');
module.exports = buildSchema(`
type Event {
_id: ID!
title: String!
description: String!
price: Float!
date: String!
creator: User!
}
type User {
_id: ID!
email: String!
password: String
createdEvents: [Event!]
}
input EventInput {
title: String!
description: String!
price: Float!
date: String!
}
input UserInput {
email: String!
password: String!
}
type RootQuery {
events: [Event!]!
}
type RootMutation {
createEvent(eventInput: EventInput): Event
createUser(userInput: UserInput): User
}
schema {
query: RootQuery
mutation: RootMutation
}
`);
- In the
resolvers
subfolder the following files must be created
index.js
const bcrypt = require('bcryptjs');
const Event = require('../../models/event');
const User = require('../../models/user');
const events = eventIds => {
return Event.find({_id: {$in: eventIds}})
.then(events => {
return events.map(event => {
return {
...event._doc,
_id: event.id,
creator: user.bind(this, event._doc.creator)};
});
})
.catch(err => {
console.log(err);
throw err;
});
}
const user = userId => {
return User.findById(userId)
.then(user => {
return {...user._doc, _id: user.id, createdEvents: events.bind(this, user._doc.createdEvents)};
})
.catch(err => {
console.log(err);
throw err;
})
};
module.exports = {
events: () => {
return Event
.find()
.then(events => {
return events.map(event => {
return {
...event._doc,
_id: event.id,
creator: user.bind(this, event._doc.creator)
};
})
})
.catch(err => {
console.log(err);
throw err;
})
},
createEvent: (args) => {
const event = new Event({
title: args.eventInput.title,
description: args.eventInput.description,
price: +args.eventInput.price,
date: new Date(args.eventInput.date),
creator: "5c3cc489a1f3273f984495ad"
});
let createdEvent;
return event
.save()
.then(result => {
console.log(result);
createdEvent = {
...result._doc,
_id: result._doc._id.toString(),
creator: user.bind(this, result._doc.creator)
};
return User.findById(result._doc.creator.toString())
})
.then(user => {
if (!user) {
throw new Error('User does not exists.')
}
user.createdEvents.push(event);
return user.save();
})
.then(result => {
console.log(result);
return createdEvent;
})
.catch(err => {
console.log(err);
throw err;
})
},
createUser: args => {
return User
.findOne({email: args.userInput.email})
.then(user => {
if (user) {
throw new Error('User already exists.')
}
return bcrypt.hash(args.userInput.password, 12);
})
.then(hashPassword => {
const user = new User({
email: args.userInput.email,
password: hashPassword
});
return user
.save()
.then(result => {
console.log(result);
return { ...result._doc, password: null, _id: result.id};
})
})
.catch(err => {
console.log(err);
throw err;
})
}
}
- the
app.js
contains just
app.js
const express = require('express');
const bodyParser = require('body-parser');
const graphqlHttp = require('express-graphql');
const mongoose = require('mongoose');
const graphQlSchema = require('./graphql/schema/index')
const graphQlResolvers = require('./graphql/resolvers/index')
const app = express();
app.use(bodyParser.json());
app.use('/graphql', graphqlHttp({
schema: graphQlSchema,
rootValue: graphQlResolvers,
graphiql: true
}));
mongoose
.connect(`mongodb+srv://${process.env.MONGO_USER}:${process.env.MONGO_PASSWORD}@cluster0-ycwj8.mongodb.net/${process.env.MONGO_DB}?retryWrites=true`)
.then(() => {
app.listen(3000);
})
.catch(err => {
console.log(err);
})
- createEvent mutation request
mutation {
createEvent(eventInput: {
title: "Fourth Event",
description: "This is the Fourth test event",
price: 129.99,
date: "2019-01-14T18:57:49.045Z"
})
{
title
description
creator {
email
}
}
}
- createEvent mutation response
{
"data": {
"createEvent": {
"title": "Fourth Event",
"description": "This is the Fourth test event",
"creator": {
"email": "test@test.email"
}
}
}
}
events
query request
query {
events {
_id
date
title
creator {
email
createdEvents {
title
creator {
email
}
}
}
}
}
events
query response
{
"data": {
"events": [
{
"_id": "5c3cc8bd7fabad484caf94b9",
"date": "1547487409045",
"title": "Testing",
"creator": {
"email": "test@test.email",
"createdEvents": [
{
"title": "Testing",
"creator": {
"email": "test@test.email"
}
},
{
"title": "Another Event",
"creator": {
"email": "test@test.email"
}
},
{
"title": "Third Event",
"creator": {
"email": "test@test.email"
}
},
{
"title": "Fourth Event",
"creator": {
"email": "test@test.email"
}
}
]
}
},
{
"_id": "5c3cd53f984bef4744ac6f9f",
"date": "1547490649045",
"title": "Another Event",
"creator": {
"email": "test@test.email",
"createdEvents": [
{
"title": "Testing",
"creator": {
"email": "test@test.email"
}
},
{
"title": "Another Event",
"creator": {
"email": "test@test.email"
}
},
{
"title": "Third Event",
"creator": {
"email": "test@test.email"
}
},
{
"title": "Fourth Event",
"creator": {
"email": "test@test.email"
}
}
]
}
},
{
"_id": "5c3cd6e0764e1362c4483e85",
"date": "1547491009045",
"title": "Third Event",
"creator": {
"email": "test@test.email",
"createdEvents": [
{
"title": "Testing",
"creator": {
"email": "test@test.email"
}
},
{
"title": "Another Event",
"creator": {
"email": "test@test.email"
}
},
{
"title": "Third Event",
"creator": {
"email": "test@test.email"
}
},
{
"title": "Fourth Event",
"creator": {
"email": "test@test.email"
}
}
]
}
},
{
"_id": "5c3cdb968e001e26a8bbe48e",
"date": "1547492269045",
"title": "Fourth Event",
"creator": {
"email": "test@test.email",
"createdEvents": [
{
"title": "Testing",
"creator": {
"email": "test@test.email"
}
},
{
"title": "Another Event",
"creator": {
"email": "test@test.email"
}
},
{
"title": "Third Event",
"creator": {
"email": "test@test.email"
}
},
{
"title": "Fourth Event",
"creator": {
"email": "test@test.email"
}
}
]
}
}
]
}
}
Fix the way the date is returned
resolvers - index.js
const bcrypt = require('bcryptjs');
const Event = require('../../models/event');
const User = require('../../models/user');
const events = eventIds => {
return Event.find({_id: {$in: eventIds}})
.then(events => {
return events.map(event => {
return {
...event._doc,
_id: event.id,
date: new Date(event._doc.date).toISOString(),
creator: user.bind(this, event._doc.creator)};
});
})
.catch(err => {
console.log(err);
throw err;
});
}
const user = userId => {
return User.findById(userId)
.then(user => {
return {
...user._doc,
_id: user.id,
createdEvents: events.bind(this, user._doc.createdEvents)
};
})
.catch(err => {
console.log(err);
throw err;
})
};
module.exports = {
events: () => {
return Event
.find()
.then(events => {
return events.map(event => {
return {
...event._doc,
_id: event.id,
date: new Date(event._doc.date).toISOString(),
creator: user.bind(this, event._doc.creator)
};
})
})
.catch(err => {
console.log(err);
throw err;
})
},
createEvent: (args) => {
const event = new Event({
title: args.eventInput.title,
description: args.eventInput.description,
price: +args.eventInput.price,
date: new Date(args.eventInput.date),
creator: "5c3cc489a1f3273f984495ad"
});
let createdEvent;
return event
.save()
.then(result => {
console.log(result);
createdEvent = {
...result._doc,
_id: result._doc._id.toString(),
date: new Date(event._doc.date).toISOString(),
creator: user.bind(this, result._doc.creator)
};
return User.findById(result._doc.creator.toString())
})
.then(user => {
if (!user) {
throw new Error('User does not exists.')
}
user.createdEvents.push(event);
return user.save();
})
.then(result => {
console.log(result);
return createdEvent;
})
.catch(err => {
console.log(err);
throw err;
})
},
createUser: args => {
return User
.findOne({email: args.userInput.email})
.then(user => {
if (user) {
throw new Error('User already exists.')
}
return bcrypt.hash(args.userInput.password, 12);
})
.then(hashPassword => {
const user = new User({
email: args.userInput.email,
password: hashPassword
});
return user
.save()
.then(result => {
console.log(result);
return { ...result._doc, password: null, _id: result.id};
})
})
.catch(err => {
console.log(err);
throw err;
})
}
}
events
query request
query {
events {
_id
date
title
creator {
email
}
}
}
events
query response
{
"data": {
"events": [
{
"_id": "5c3cc8bd7fabad484caf94b9",
"date": "2019-01-14T17:36:49.045Z",
"title": "Testing",
"creator": {
"email": "test@test.email"
}
},
{
"_id": "5c3cd53f984bef4744ac6f9f",
"date": "2019-01-14T18:30:49.045Z",
"title": "Another Event",
"creator": {
"email": "test@test.email"
}
},
{
"_id": "5c3cd6e0764e1362c4483e85",
"date": "2019-01-14T18:36:49.045Z",
"title": "Third Event",
"creator": {
"email": "test@test.email"
}
},
{
"_id": "5c3cdb968e001e26a8bbe48e",
"date": "2019-01-14T18:57:49.045Z",
"title": "Fourth Event",
"creator": {
"email": "test@test.email"
}
}
]
}
}
Change to use async / await
resolvers - index.js
const bcrypt = require('bcryptjs');
const Event = require('../../models/event');
const User = require('../../models/user');
const events = async eventIds => {
try {
const events = await Event.find({ _id: { $in: eventIds } });
return events.map(event => {
return {
...event._doc,
_id: event.id,
date: new Date(event._doc.date).toISOString(),
creator: user.bind(this, event._doc.creator)
};
});
}
catch (err) {
console.log(err);
throw err;
}
}
const user = async userId => {
try {
const user = await User.findById(userId);
return {
...user._doc,
_id: user.id,
createdEvents: events.bind(this, user._doc.createdEvents)
};
}
catch (err) {
console.log(err);
throw err;
}
};
module.exports = {
events: async () => {
try {
const events = await Event.find();
return events.map(event => {
return {
...event._doc,
_id: event.id,
date: new Date(event._doc.date).toISOString(),
creator: user.bind(this, event._doc.creator)
};
});
}
catch (err) {
console.log(err);
throw err;
}
},
createEvent: async args => {
try {
const event = new Event({
title: args.eventInput.title,
description: args.eventInput.description,
price: +args.eventInput.price,
date: new Date(args.eventInput.date),
creator: "5c3cc489a1f3273f984495ad"
});
const result = await event.save();
const creator = await User.findById(result._doc.creator.toString())
if (!creator) {
throw new Error('User does not exists.')
}
creator.createdEvents.push(event);
await creator.save();
return {
...result._doc,
_id: result._doc._id.toString(),
date: new Date(event._doc.date).toISOString(),
creator: user.bind(this, result._doc.creator)
};
}
catch (err) {
console.log(err);
throw err;
}
},
createUser: async args => {
try {
const user = await User.findOne({ email: args.userInput.email });
if (user) {
throw new Error('User already exists.');
}
const hashPassword = await bcrypt.hash(args.userInput.password, 12);
const newUser = new User({
email: args.userInput.email,
password: hashPassword
});
const result = await newUser.save();
return {
...result._doc,
password: null,
_id: result.id
}
}
catch (err) {
console.log(err);
throw err;
}
}
}
- createuser mutation request
mutation {
createUser(userInput: {
email: "test2@test.email",
password: "tester"
})
{
email
password
}
}
- createuser mutation response
{
"data": {
"createUser": {
"email": "test2@test.email",
"password": null
}
}
}
- createEvent mutation request
mutation {
createEvent(eventInput: {
title: "Fifth Event",
description: "This is the fifth test event",
price: 129.99,
date: "2019-01-14T19:31:49.045Z"
})
{
title
description
creator {
email
}
}
}
- createEvent mutation response
{
"data": {
"createEvent": {
"title": "Fifth Event",
"description": "This is the fifth test event",
"creator": {
"email": "test@test.email"
}
}
}
}
events
query request
query {
events {
_id
date
title
creator {
email
}
}
}
events
query response
{
"data": {
"events": [
{
"_id": "5c3cc8bd7fabad484caf94b9",
"date": "2019-01-14T17:36:49.045Z",
"title": "Testing",
"creator": {
"email": "test@test.email"
}
},
{
"_id": "5c3cd53f984bef4744ac6f9f",
"date": "2019-01-14T18:30:49.045Z",
"title": "Another Event",
"creator": {
"email": "test@test.email"
}
},
{
"_id": "5c3cd6e0764e1362c4483e85",
"date": "2019-01-14T18:36:49.045Z",
"title": "Third Event",
"creator": {
"email": "test@test.email"
}
},
{
"_id": "5c3cdb968e001e26a8bbe48e",
"date": "2019-01-14T18:57:49.045Z",
"title": "Fourth Event",
"creator": {
"email": "test@test.email"
}
},
{
"_id": "5c3ce3da28127360043d9cd0",
"date": "2019-01-14T19:31:49.045Z",
"title": "Fifth Event",
"creator": {
"email": "test@test.email"
}
},
{
"_id": "5c3ce4d7a0ae7c38c8d7218c",
"date": "2019-01-14T19:31:49.045Z",
"title": "Fifth Event",
"creator": {
"email": "test@test.email"
}
},
{
"_id": "5c3ce51ca7e8602c9c0d35fd",
"date": "2019-01-14T19:31:49.045Z",
"title": "Fifth Event",
"creator": {
"email": "test@test.email"
}
}
]
}
}
8 Adding Bookings 20:57
booking
model
Create the new models booking.js
const moongose = require("mongoose");
const Schema = moongose.Schema;
const bookingSchema = new Schema(
{
event: {
type: Schema.Types.ObjectId,
ref: "Event"
},
user: {
type: Schema.Types.ObjectId,
ref: "User"
}
},
{ timestamps: true }
);
module.exports = moongose.model("Booking", bookingSchema);
schema index.js
const { buildSchema } = require('graphql');
module.exports = buildSchema(`
type Booking {
_id: ID!
event: Event!
user: User!
createdAt: String
updatedAt: String
}
type Event {
_id: ID!
title: String!
description: String!
price: Float!
date: String!
creator: User!
}
type User {
_id: ID!
email: String!
password: String
createdEvents: [Event!]
}
input EventInput {
title: String!
description: String!
price: Float!
date: String!
}
input UserInput {
email: String!
password: String!
}
type RootQuery {
events: [Event!]!
bookings: [Booking!]!
}
type RootMutation {
createEvent(eventInput: EventInput): Event
createUser(userInput: UserInput): User
bookEvent(eventId: ID!): Booking!
cancelBooking(bookingId: ID!): Event!
}
schema {
query: RootQuery
mutation: RootMutation
}
`);
resolvers index.js
const bcrypt = require("bcryptjs");
const Event = require("../../models/event");
const User = require("../../models/user");
const Booking = require("../../models/booking");
const events = async eventIds => {
try {
const events = await Event.find({ _id: { $in: eventIds } });
return events.map(event => {
return {
...event._doc,
_id: event.id,
date: new Date(event._doc.date).toISOString(),
creator: user.bind(this, event._doc.creator)
};
});
} catch (err) {
console.log(err);
throw err;
}
};
const user = async userId => {
try {
const user = await User.findById(userId);
return {
...user._doc,
_id: user.id,
createdEvents: events.bind(this, user._doc.createdEvents)
};
} catch (err) {
console.log(err);
throw err;
}
};
module.exports = {
events: async () => {
try {
const events = await Event.find();
return events.map(event => {
return {
...event._doc,
_id: event.id,
date: new Date(event._doc.date).toISOString(),
creator: user.bind(this, event._doc.creator)
};
});
} catch (err) {
console.log(err);
throw err;
}
},
bookings: async () => {
try {
const bookings = await Booking.find();
return bookings.map(booking => {
return {
...booking._doc,
_id: booking.id,
createdAt: new Date(booking._doc.createdAt).toISOString(),
updatedAt: new Date(booking._doc.updatedAt).toISOString()
};
});
} catch (err) {
console.log(err);
throw err;
}
},
createEvent: async args => {
try {
const event = new Event({
title: args.eventInput.title,
description: args.eventInput.description,
price: +args.eventInput.price,
date: new Date(args.eventInput.date),
creator: "5c3cc489a1f3273f984495ad"
});
const result = await event.save();
const creator = await User.findById(result._doc.creator.toString());
if (!creator) {
throw new Error("User does not exists.");
}
creator.createdEvents.push(event);
await creator.save();
return {
...result._doc,
_id: result._doc._id.toString(),
date: new Date(event._doc.date).toISOString(),
creator: user.bind(this, result._doc.creator)
};
} catch (err) {
console.log(err);
throw err;
}
},
createUser: async args => {
try {
const user = await User.findOne({ email: args.userInput.email });
if (user) {
throw new Error("User already exists.");
}
const hashPassword = await bcrypt.hash(args.userInput.password, 12);
const newUser = new User({
email: args.userInput.email,
password: hashPassword
});
const result = await newUser.save();
return {
...result._doc,
password: null,
_id: result.id
};
} catch (err) {
console.log(err);
throw err;
}
},
bookEvent: async args => {
try {
const fectchedEvent = await Event.findById(args.eventId);
const booking = new Booking({
user: "5c3cc489a1f3273f984495ad",
event: fectchedEvent
});
const result = await booking.save();
return {
...result._doc,
_id: result.id,
createdAt: new Date(result._doc.createdAt).toISOString(),
updatedAt: new Date(result._doc.updatedAt).toISOString()
};
} catch (err) {
console.log(err);
throw err;
}
}
};
- bookEvent mutation request
mutation {
bookEvent(eventId: "5c3cc8bd7fabad484caf94b9")
{
_id
createdAt
updatedAt
}
}
- bookEvent mutation response
{
"data": {
"bookEvent": {
"_id": "5c3e22506de3492b686879b5",
"createdAt": "2019-01-15T18:11:28.966Z",
"updatedAt": "2019-01-15T18:11:28.966Z"
}
}
}
singleEvent
function
Create the resolvers index.js
const bcrypt = require("bcryptjs");
const Event = require("../../models/event");
const User = require("../../models/user");
const Booking = require("../../models/booking");
const events = async eventIds => {
try {
const events = await Event.find({ _id: { $in: eventIds } });
return events.map(event => {
return {
...event._doc,
_id: event.id,
date: new Date(event._doc.date).toISOString(),
creator: user.bind(this, event._doc.creator)
};
});
} catch (err) {
console.log(err);
throw err;
}
};
const singleEvent = async eventId => {
try {
const event = await Event.findById(eventId);
return {
...event._doc,
_id: event.id,
date: new Date(event._doc.date).toISOString(),
creator: user.bind(this, event._doc.creator)
};
} catch (err) {
console.log(err);
throw err;
}
};
const user = async userId => {
try {
const user = await User.findById(userId);
return {
...user._doc,
_id: user.id,
createdEvents: events.bind(this, user._doc.createdEvents)
};
} catch (err) {
console.log(err);
throw err;
}
};
module.exports = {
events: async () => {
try {
const events = await Event.find();
return events.map(event => {
return {
...event._doc,
_id: event.id,
date: new Date(event._doc.date).toISOString(),
creator: user.bind(this, event._doc.creator)
};
});
} catch (err) {
console.log(err);
throw err;
}
},
bookings: async () => {
try {
const bookings = await Booking.find();
return bookings.map(booking => {
return {
...booking._doc,
_id: booking.id,
user: user.bind(this, booking._doc.user),
event: singleEvent.bind(this, booking._doc.event),
createdAt: new Date(booking._doc.createdAt).toISOString(),
updatedAt: new Date(booking._doc.updatedAt).toISOString()
};
});
} catch (err) {
console.log(err);
throw err;
}
},
createEvent: async args => {
try {
const event = new Event({
title: args.eventInput.title,
description: args.eventInput.description,
price: +args.eventInput.price,
date: new Date(args.eventInput.date),
creator: "5c3cc489a1f3273f984495ad"
});
const result = await event.save();
const creator = await User.findById(result._doc.creator.toString());
if (!creator) {
throw new Error("User does not exists.");
}
creator.createdEvents.push(event);
await creator.save();
return {
...result._doc,
_id: result._doc._id.toString(),
date: new Date(event._doc.date).toISOString(),
creator: user.bind(this, result._doc.creator)
};
} catch (err) {
console.log(err);
throw err;
}
},
createUser: async args => {
try {
const user = await User.findOne({ email: args.userInput.email });
if (user) {
throw new Error("User already exists.");
}
const hashPassword = await bcrypt.hash(args.userInput.password, 12);
const newUser = new User({
email: args.userInput.email,
password: hashPassword
});
const result = await newUser.save();
return {
...result._doc,
password: null,
_id: result.id
};
} catch (err) {
console.log(err);
throw err;
}
},
bookEvent: async args => {
try {
const fectchedEvent = await Event.findById(args.eventId);
const booking = new Booking({
user: "5c3cc489a1f3273f984495ad",
event: fectchedEvent
});
const result = await booking.save();
return {
...result._doc,
_id: result.id,
user: user.bind(this, result._doc.user),
event: singleEvent.bind(this, result._doc.event),
createdAt: new Date(result._doc.createdAt).toISOString(),
updatedAt: new Date(result._doc.updatedAt).toISOString()
};
} catch (err) {
console.log(err);
throw err;
}
}
};
- bookEvent mutation request
mutation {
bookEvent(eventId: "5c3cc8bd7fabad484caf94b9")
{
_id
event {
_id
description
}
user {
_id
email
}
createdAt
updatedAt
}
}
- bookEvent mutation response
{
"data": {
"bookEvent": {
"_id": "5c3e24b8905d223930e24b13",
"event": {
"_id": "5c3cc8bd7fabad484caf94b9",
"description": "This is a test event"
},
"user": {
"_id": "5c3cc489a1f3273f984495ad",
"email": "test@test.email"
},
"createdAt": "2019-01-15T18:21:44.820Z",
"updatedAt": "2019-01-15T18:21:44.820Z"
}
}
}
- bookings query request
query {
bookings {
_id
createdAt
event {
title
creator {
email
}
}
user {
email
}
}
}
- bookings query response
{
"data": {
"bookings": [
{
"_id": "5c3e22506de3492b686879b5",
"createdAt": "2019-01-15T18:11:28.966Z",
"event": {
"title": "Testing",
"creator": {
"email": "test@test.email"
}
},
"user": {
"email": "test@test.email"
}
},
{
"_id": "5c3e24b8905d223930e24b13",
"createdAt": "2019-01-15T18:21:44.820Z",
"event": {
"title": "Testing",
"creator": {
"email": "test@test.email"
}
},
"user": {
"email": "test@test.email"
}
}
]
}
}
cancelBooking
mutation
Create the resolvers index.js
const bcrypt = require("bcryptjs");
const Event = require("../../models/event");
const User = require("../../models/user");
const Booking = require("../../models/booking");
const events = async eventIds => {
try {
const events = await Event.find({ _id: { $in: eventIds } });
return events.map(event => {
return {
...event._doc,
_id: event.id,
date: new Date(event._doc.date).toISOString(),
creator: user.bind(this, event._doc.creator)
};
});
} catch (err) {
console.log(err);
throw err;
}
};
const singleEvent = async eventId => {
try {
const event = await Event.findById(eventId);
return {
...event._doc,
_id: event.id,
date: new Date(event._doc.date).toISOString(),
creator: user.bind(this, event._doc.creator)
};
} catch (err) {
console.log(err);
throw err;
}
};
const user = async userId => {
try {
const user = await User.findById(userId);
return {
...user._doc,
_id: user.id,
createdEvents: events.bind(this, user._doc.createdEvents)
};
} catch (err) {
console.log(err);
throw err;
}
};
module.exports = {
events: async () => {
try {
const events = await Event.find();
return events.map(event => {
return {
...event._doc,
_id: event.id,
date: new Date(event._doc.date).toISOString(),
creator: user.bind(this, event._doc.creator)
};
});
} catch (err) {
console.log(err);
throw err;
}
},
bookings: async () => {
try {
const bookings = await Booking.find();
return bookings.map(booking => {
return {
...booking._doc,
_id: booking.id,
user: user.bind(this, booking._doc.user),
event: singleEvent.bind(this, booking._doc.event),
createdAt: new Date(booking._doc.createdAt).toISOString(),
updatedAt: new Date(booking._doc.updatedAt).toISOString()
};
});
} catch (err) {
console.log(err);
throw err;
}
},
createEvent: async args => {
try {
const event = new Event({
title: args.eventInput.title,
description: args.eventInput.description,
price: +args.eventInput.price,
date: new Date(args.eventInput.date),
creator: "5c3cc489a1f3273f984495ad"
});
const result = await event.save();
const creator = await User.findById(result._doc.creator.toString());
if (!creator) {
throw new Error("User does not exists.");
}
creator.createdEvents.push(event);
await creator.save();
return {
...result._doc,
_id: result._doc._id.toString(),
date: new Date(event._doc.date).toISOString(),
creator: user.bind(this, result._doc.creator)
};
} catch (err) {
console.log(err);
throw err;
}
},
createUser: async args => {
try {
const user = await User.findOne({ email: args.userInput.email });
if (user) {
throw new Error("User already exists.");
}
const hashPassword = await bcrypt.hash(args.userInput.password, 12);
const newUser = new User({
email: args.userInput.email,
password: hashPassword
});
const result = await newUser.save();
return {
...result._doc,
password: null,
_id: result.id
};
} catch (err) {
console.log(err);
throw err;
}
},
bookEvent: async args => {
try {
const fectchedEvent = await Event.findById(args.eventId);
const booking = new Booking({
user: "5c3cc489a1f3273f984495ad",
event: fectchedEvent
});
const result = await booking.save();
return {
...result._doc,
_id: result.id,
user: user.bind(this, result._doc.user),
event: singleEvent.bind(this, result._doc.event),
createdAt: new Date(result._doc.createdAt).toISOString(),
updatedAt: new Date(result._doc.updatedAt).toISOString()
};
} catch (err) {
console.log(err);
throw err;
}
},
cancelBooking: async args => {
try {
const booking = await Booking.findById(args.bookingId).populate("event");
const event = {
...booking.event._doc,
_id: booking.event.id,
creator: user.bind(this, booking.event._doc.creator)
};
await Booking.deleteOne({ _id: args.bookingId });
return event;
} catch (err) {
console.log(err);
throw err;
}
}
};
- cancelBooking mutation request
mutation {
cancelBooking(bookingId: "5c3e2d6d5a9cf134b4de0202")
{
_id
title
creator {
email
}
}
}
- cancelBooking mutation response
{
"data": {
"cancelBooking": {
"_id": "5c3cc8bd7fabad484caf94b9",
"title": "Testing",
"creator": {
"email": "test@test.email"
}
}
}
}
9 Refactoring our code 27:32
Change some code to make it cleaner
- Create the
helpers
folder and thedate.js
file
helpers -> date.js
exports.dateToString = date => new Date(date).toISOString();
- Create the
transformEvent
andtransformBooking
functions and use the newdateToString
function.
resolvers -> index.js
const bcrypt = require("bcryptjs");
const Event = require("../../models/event");
const User = require("../../models/user");
const Booking = require("../../models/booking");
const { dateToString } = require('../../helpers/date');
const transformEvent = event => {
if (event) {
return {
...event._doc,
_id: event.id,
date: dateToString(event._doc.date),
creator: user.bind(this, event._doc.creator)
};
}
return null;
};
const transformBooking = booking => {
if (booking) {
return {
...booking._doc,
_id: booking.id,
user: user.bind(this, booking._doc.user),
event: singleEvent.bind(this, booking._doc.event),
createdAt: dateToString(booking._doc.createdAt),
updatedAt: dateToString(booking._doc.updatedAt)
};
}
return null;
}
const events = async eventIds => {
try {
const events = await Event.find({ _id: { $in: eventIds } });
return events.map(event => {
return transformEvent(event);
});
} catch (err) {
console.log(err);
throw err;
}
};
const singleEvent = async eventId => {
try {
const event = await Event.findById(eventId);
return transformEvent(event);
} catch (err) {
console.log(err);
throw err;
}
};
const user = async userId => {
try {
const user = await User.findById(userId);
return {
...user._doc,
_id: user.id,
createdEvents: events.bind(this, user._doc.createdEvents)
};
} catch (err) {
console.log(err);
throw err;
}
};
module.exports = {
events: async () => {
try {
const events = await Event.find();
return events.map(event => {
return transformEvent(event);
});
} catch (err) {
console.log(err);
throw err;
}
},
bookings: async () => {
try {
const bookings = await Booking.find();
return bookings.map(booking => {
return transformBooking(booking);
});
} catch (err) {
console.log(err);
throw err;
}
},
createEvent: async args => {
try {
const event = new Event({
title: args.eventInput.title,
description: args.eventInput.description,
price: +args.eventInput.price,
date: new Date(args.eventInput.date),
creator: "5c3cc489a1f3273f984495ad"
});
const result = await event.save();
const creator = await User.findById(result._doc.creator.toString());
if (!creator) {
throw new Error("User does not exists.");
}
creator.createdEvents.push(event);
await creator.save();
return transformEvent(result);
} catch (err) {
console.log(err);
throw err;
}
},
createUser: async args => {
try {
const user = await User.findOne({ email: args.userInput.email });
if (user) {
throw new Error("User already exists.");
}
const hashPassword = await bcrypt.hash(args.userInput.password, 12);
const newUser = new User({
email: args.userInput.email,
password: hashPassword
});
const result = await newUser.save();
return {
...result._doc,
password: null,
_id: result.id
};
} catch (err) {
console.log(err);
throw err;
}
},
bookEvent: async args => {
try {
const fectchedEvent = await Event.findById(args.eventId);
if (!fectchedEvent) {
throw new Error("Event Id does not exist");
};
const booking = new Booking({
user: "5c3cc489a1f3273f984495ad",
event: fectchedEvent
});
const result = await booking.save();
return transformBooking(result);
} catch (err) {
console.log(err);
throw err;
}
},
cancelBooking: async args => {
try {
const booking = await Booking.findById(args.bookingId).populate("event");
if (!booking) {
throw new Error("Booking Id does not exist");
};
await Booking.deleteOne({ _id: args.bookingId });
return transformEvent(booking.event);
} catch (err) {
console.log(err);
throw err;
}
}
};
Split the resolvers
resolvers => events.js
const Event = require("../../models/event");
const { transformEvent } = require("./merge");
module.exports = {
events: async () => {
try {
const events = await Event.find();
return events.map(event => {
return transformEvent(event);
});
} catch (err) {
console.log(err);
throw err;
}
},
createEvent: async args => {
try {
const event = new Event({
title: args.eventInput.title,
description: args.eventInput.description,
price: +args.eventInput.price,
date: new Date(args.eventInput.date),
creator: "5c3cc489a1f3273f984495ad"
});
const result = await event.save();
const creator = await User.findById(result._doc.creator.toString());
if (!creator) {
throw new Error("User does not exists.");
}
creator.createdEvents.push(event);
await creator.save();
return transformEvent(result);
} catch (err) {
console.log(err);
throw err;
}
}
};
resolvers => auth.js
const bcrypt = require("bcryptjs");
const User = require("../../models/user");
module.exports = {
createUser: async args => {
try {
const user = await User.findOne({ email: args.userInput.email });
if (user) {
throw new Error("User already exists.");
}
const hashPassword = await bcrypt.hash(args.userInput.password, 12);
const newUser = new User({
email: args.userInput.email,
password: hashPassword
});
const result = await newUser.save();
return {
...result._doc,
password: null,
_id: result.id
};
} catch (err) {
console.log(err);
throw err;
}
}
};
resolvers => bookings.js
const Booking = require("../../models/booking");
const Event = require("../../models/event");
const { transformBooking, transformEvent } = require("./merge");
module.exports = {
bookings: async () => {
try {
const bookings = await Booking.find();
return bookings.map(booking => {
return transformBooking(booking);
});
} catch (err) {
console.log(err);
throw err;
}
},
bookEvent: async args => {
try {
const fectchedEvent = await Event.findById(args.eventId);
if (!fectchedEvent) {
throw new Error("Event Id does not exist");
}
const booking = new Booking({
user: "5c3cc489a1f3273f984495ad",
event: fectchedEvent
});
const result = await booking.save();
return transformBooking(result);
} catch (err) {
console.log(err);
throw err;
}
},
cancelBooking: async args => {
try {
const booking = await Booking.findById(args.bookingId).populate("event");
if (!booking) {
throw new Error("Booking Id does not exist");
}
await Booking.deleteOne({ _id: args.bookingId });
return transformEvent(booking.event);
} catch (err) {
console.log(err);
throw err;
}
}
};
resolvers => merge.js
const Event = require("../../models/event");
const User = require("../../models/user");
const { dateToString } = require("../../helpers/date");
const transformEvent = event => {
if (event) {
return {
...event._doc,
_id: event.id,
date: dateToString(event._doc.date),
creator: user.bind(this, event._doc.creator)
};
}
return null;
};
const transformBooking = booking => {
if (booking) {
return {
...booking._doc,
_id: booking.id,
user: user.bind(this, booking._doc.user),
event: singleEvent.bind(this, booking._doc.event),
createdAt: dateToString(booking._doc.createdAt),
updatedAt: dateToString(booking._doc.updatedAt)
};
}
return null;
};
const events = async eventIds => {
try {
const events = await Event.find({ _id: { $in: eventIds } });
return events.map(event => {
return transformEvent(event);
});
} catch (err) {
console.log(err);
throw err;
}
};
const singleEvent = async eventId => {
try {
const event = await Event.findById(eventId);
return transformEvent(event);
} catch (err) {
console.log(err);
throw err;
}
};
const user = async userId => {
try {
const user = await User.findById(userId);
return {
...user._doc,
_id: user.id,
createdEvents: events.bind(this, user._doc.createdEvents)
};
} catch (err) {
console.log(err);
throw err;
}
};
exports.transformEvent = transformEvent;
exports.transformBooking = transformBooking;
resolvers => index.js
const authResolver = require("./auth");
const eventsResolver = require("./events");
const bookingsResolver = require("./bookings");
const rootResolver = {
...authResolver,
...eventsResolver,
...bookingsResolver
};
module.exports = rootResolver;
10 Adding User Authentication 32:47
Add an endpoint on the schema for the users to authenticate
- It is not a mutation but a query because it is not changing any data
schema index.js
const { buildSchema } = require('graphql');
module.exports = buildSchema(`
type Booking {
_id: ID!
event: Event!
user: User!
createdAt: String
updatedAt: String
}
type Event {
_id: ID!
title: String!
description: String!
price: Float!
date: String!
creator: User!
}
type User {
_id: ID!
email: String!
password: String
createdEvents: [Event!]
}
type AuthData {
userId: ID!
token: String!
tokenExpiration: Int!
}
input EventInput {
title: String!
description: String!
price: Float!
date: String!
}
input UserInput {
email: String!
password: String!
}
type RootQuery {
events: [Event!]!
bookings: [Booking!]!
login(email: String, password: String): AuthData!
}
type RootMutation {
createEvent(eventInput: EventInput): Event
createUser(userInput: UserInput): User
bookEvent(eventId: ID!): Booking!
cancelBooking(bookingId: ID!): Event!
}
schema {
query: RootQuery
mutation: RootMutation
}
`);
jsonwebtoken
library
install Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/GraphQL/graphql-react-event-booking
$ npm i jsonwebtoken
npm WARN graphql-react-event-booking@1.0.0 No description
npm WARN graphql-react-event-booking@1.0.0 No repository field.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.4 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.4: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
+ jsonwebtoken@8.4.0
added 13 packages from 9 contributors and audited 2468 packages in 16.888s
found 0 vulnerabilities
login
function
create the resolvers auth.js
const bcrypt = require("bcryptjs");
const jwt = require("jsonwebtoken");
const User = require("../../models/user");
module.exports = {
createUser: async args => {
try {
const user = await User.findOne({ email: args.userInput.email });
if (user) {
throw new Error("User already exists.");
}
const hashPassword = await bcrypt.hash(args.userInput.password, 12);
const newUser = new User({
email: args.userInput.email,
password: hashPassword
});
const result = await newUser.save();
return {
...result._doc,
password: null,
_id: result.id
};
} catch (err) {
console.log(err);
throw err;
}
},
login: async ({ email, password }) => {
try {
const user = await User.findOne({ email: email });
if (!user) {
throw new Error("Invalid credentials.");
}
const isEqual = await bcrypt.compare(password, user.password);
if (!isEqual) {
throw new Error("Invalid credentials.");
}
const token = jwt.sign(
{ userId: user.id, email: user.email },
"somesupersecretkey",
{ expiresIn: "1h" }
);
return {
userId: user.id,
token: token,
tokenExpiration: 1
};
} catch (err) {
console.log(err);
throw err;
}
}
};
- login query request
query {
login(email: "test@test.com", password: "aaa")
{
userId
token
tokenExpiration
}
}
- login query response
{
"errors": [
{
"message": "Invalid credentials.",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"login"
]
}
],
"data": null
}
- login query request
query {
login(email: "test3@test.com", password: "tester")
{
userId
token
tokenExpiration
}
}
- login query response
{
"errors": [
{
"message": "Invalid credentials.",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"login"
]
}
],
"data": null
}
- login query request
query {
login(email: "test@test.com", password: "tester")
{
userId
token
tokenExpiration
}
}
- login query response
{
"data": {
"login": {
"userId": "5c3cc489a1f3273f984495ad",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI1YzNjYzQ4OWExZjMyNzNmOTg0NDk1YWQiLCJlbWFpbCI6InRlc3RAdGVzdC5jb20iLCJpYXQiOjE1NDc3MDYwOTMsImV4cCI6MTU0NzcwOTY5M30.cSQjd823q-Tb54ogAT2SadNUfNzrxP12ufyuGjFjcao",
"tokenExpiration": 1
}
}
}
Add a middleware to authenticate the calls
- The new
middleware
folder and the newis-auth.js
file must be created.
middleware is-auth.js
const jwt = require('jsonwebtoken');
module.exports = (req, res, next) => {
const authHeader = req.get('Authorization');
if (!authHeader) {
req.isAuth = false;
return next();
}
const tokens = authHeader.split(' '); // Authorization: Bearer tokendsdaadadad
if (tokens.length !=2 || tokens[0]!=='Bearer' || !tokens[1] || tokens[1] === '') {
req.isAuth = false;
return next();
}
let decodedToken;
try {
decodedToken = jwt.verify(tokens[1], 'somesupersecretkey');
} catch (error) {
console.log(error);
req.isAuth = false;
return next();
}
if (!decodedToken) {
req.isAuth = false;
return next();
}
req.isAuth = true;
req.userId = decodedToken.userId;
next();
}
app.js
const express = require("express");
const bodyParser = require("body-parser");
const graphqlHttp = require("express-graphql");
const mongoose = require("mongoose");
const isAuth = require("./middleware/is-auth");
const graphQlSchema = require("./graphql/schema/index");
const graphQlResolvers = require("./graphql/resolvers/index");
const app = express();
app.use(bodyParser.json());
app.use(isAuth);
app.use(
"/graphql",
graphqlHttp({
schema: graphQlSchema,
rootValue: graphQlResolvers,
graphiql: true
})
);
mongoose
.connect(
`mongodb+srv://${process.env.MONGO_USER}:${
process.env.MONGO_PASSWORD
}@cluster0-ycwj8.mongodb.net/${process.env.MONGO_DB}?retryWrites=true`
)
.then(() => {
app.listen(3000);
})
.catch(err => {
console.log(err);
});
- Modify
events.js
to authenticaecreateEvent
resolvers events.js
const Event = require("../../models/event");
const User = require('../../models/user');
const { transformEvent } = require("./merge");
module.exports = {
events: async () => {
try {
const events = await Event.find();
return events.map(event => {
return transformEvent(event);
});
} catch (err) {
console.log(err);
throw err;
}
},
createEvent: async (args, req) => {
try {
if (!req.isAuth) {
throw new Error("Unauthenticated.");
}
const event = new Event({
title: args.eventInput.title,
description: args.eventInput.description,
price: +args.eventInput.price,
date: new Date(args.eventInput.date),
creator: req.userId
});
const result = await event.save();
const creator = await User.findById(result._doc.creator.toString());
if (!creator) {
throw new Error("User does not exists.");
}
creator.createdEvents.push(event);
await creator.save();
return transformEvent(result);
} catch (err) {
console.log(err);
throw err;
}
}
};
resolvers bookings.js
const Booking = require("../../models/booking");
const Event = require("../../models/event");
const { transformBooking, transformEvent } = require("./merge");
module.exports = {
bookings: async (args, req) => {
try {
if (!req.isAuth) {
throw new Error("Unauthenticated.");
}
const bookings = await Booking.find();
return bookings.map(booking => {
return transformBooking(booking);
});
} catch (err) {
console.log(err);
throw err;
}
},
bookEvent: async (args, req) => {
try {
if (!req.isAuth) {
throw new Error("Unauthenticated.");
}
const fectchedEvent = await Event.findById(args.eventId);
if (!fectchedEvent) {
throw new Error("Event Id does not exist");
}
const booking = new Booking({
user: req.userId,
event: fectchedEvent
});
const result = await booking.save();
return transformBooking(result);
} catch (err) {
console.log(err);
throw err;
}
},
cancelBooking: async (args, req) => {
try {
if (!req.isAuth) {
throw new Error("Unauthenticated.");
}
const booking = await Booking.findById(args.bookingId).populate("event");
if (!booking) {
throw new Error("Booking Id does not exist");
}
await Booking.deleteOne({ _id: args.bookingId });
return transformEvent(booking.event);
} catch (err) {
console.log(err);
throw err;
}
}
};
- createEvent mutation rquest
mutation {
createEvent(eventInput: {title: "Should not work", description: "Not much", price: 120.99, date: "2019-01-17T06:44:34.575Z"})
{
_id
description
}
}
- createEvent mutation response
{
"errors": [
{
"message": "Unauthenticated.",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"createEvent"
]
}
],
"data": {
"createEvent": null
}
}
use Postman to send the authentication token
- get the token we are going to use
query {
login(email: "test@test.com", password: "tester")
{
userId
token
tokenExpiration
}
}
- login query response
{
"data": {
"login": {
"userId": "5c3cc489a1f3273f984495ad",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI1YzNjYzQ4OWExZjMyNzNmOTg0NDk1YWQiLCJlbWFpbCI6InRlc3RAdGVzdC5jb20iLCJpYXQiOjE1NDc3MDgwMjcsImV4cCI6MTU0NzcxMTYyN30.weW2ckiLwci5341Ah1W3RoywIFgr4tT1e4Wo1g9Tnu0",
"tokenExpiration": 1
}
}
}
- createEvent mutation request
POST /graphql HTTP/1.1
Host: localhost:3000
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI1YzNjYzQ4OWExZjMyNzNmOTg0NDk1YWQiLCJlbWFpbCI6InRlc3RAdGVzdC5jb20iLCJpYXQiOjE1NDc3MDgwMjcsImV4cCI6MTU0NzcxMTYyN30.weW2ckiLwci5341Ah1W3RoywIFgr4tT1e4Wo1g9Tnu0
Cache-Control: no-cache
Postman-Token: fbbeef86-ad9a-715b-851f-c94678b053ed
{
"query": "mutation {createEvent(eventInput: {title: \"A new authenticated event\", description: \"It should work as well\", price: 120.99, date: \"2019-01-17T07:23:34.575Z\"}){ _id title description price date creator {_id email}}}"
}
- createEvent mutation response
{
"data": {
"createEvent": {
"_id": "5c402d75208d592820acf109",
"title": "A new authenticated event",
"description": "It should work as well",
"price": 120.99,
"date": "2019-01-17T07:23:34.575Z",
"creator": {
"_id": "5c3cc489a1f3273f984495ad",
"email": "test@test.com"
}
}
}
}
11 The React Frontend 18:51
frontend
folder where the React
application is going to be deployed
Create the new - create-react-app CLI will be used to create the app.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/GraphQL/graphql-react-event-booking$ cd frontend/
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/GraphQL/graphql-react-event-booking/frontend
$ npx create-react-app .
npx: installed 63 in 11.576s
Creating a new React app in C:\Work\Training\Pre\GraphQL\graphql-react-event-booking\frontend.
Installing packages. This might take a couple of minutes.
Installing react, react-dom, and react-scripts...
yarn add v1.13.0
[1/4] Resolving packages...
[2/4] Fetching packages...
info fsevents@1.2.4: The platform "win32" is incompatible with this module.
info "fsevents@1.2.4" is an optional dependency and failed compatibility check. Excluding it from installation.
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Saved lockfile.
success Saved 5 new dependencies.
info Direct dependencies
├─ react-dom@16.7.0
├─ react-scripts@2.1.3
└─ react@16.7.0
info All dependencies
├─ react-dev-utils@7.0.1
├─ react-dom@16.7.0
├─ react-error-overlay@5.1.2
├─ react-scripts@2.1.3
└─ react@16.7.0
Done in 134.37s.
Initialized a git repository.
Success! Created frontend at C:\Work\Training\Pre\GraphQL\graphql-react-event-booking\frontend
Inside that directory, you can run several commands:
yarn start
Starts the development server.
yarn build
Bundles the app into static files for production.
yarn test
Starts the test runner.
yarn eject
Removes this tool and copies build dependencies, configuration files
and scripts into the app directory. If you do this, you can’t go back!
We suggest that you begin by typing:
cd C:\Work\Training\Pre\GraphQL\graphql-react-event-booking\frontend
yarn start
Happy hacking!
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/GraphQL/graphql-react-event-booking/frontend (master)
#
Get rid of the not needed files and modify some other ones
- Remove the following files
App.test.js logo.svg serviceWorker.js
- Empty the following files
App.css
- Modify the following files
index.css
body {
margin: 0;
padding: 0;
}
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
App.js
import React, { Component } from 'react';
import './App.css';
class App extends Component {
render() {
return (
<div className="App">
</div>
);
}
}
export default App;
Start developing the content
index.css
body {
margin: 0;
padding: 0;
font-family: sans-serif;
}
App.js
import React, { Component } from 'react';
import './App.css';
class App extends Component {
render() {
return (
<div className="App">
<h1>It works!</h1>
</div>
);
}
}
export default App;
- Check if it works.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/GraphQL/graphql-react-event-booking/frontend (master)
$ npm start
> frontend@0.1.0 start C:\Work\Training\Pre\GraphQL\graphql-react-event-booking\frontend
> react-scripts start
Starting the development server...
Compiled successfully!
You can now view frontend in the browser.
Local: http://localhost:3000/
On Your Network: http://10.0.75.1:3000/
Note that the development build is not optimized.
To create a production build, use yarn build.
react-router-dom
library
Install the pacjage.json
{
"name": "frontend",
"version": "0.1.0",
"private": true,
"dependencies": {
"react": "^16.7.0",
"react-dom": "^16.7.0",
"react-scripts": "2.1.3",
"react-router-dom": "^4.3.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/GraphQL/graphql-react-event-booking/frontend (master)
$ npm i
npm WARN deprecated circular-json@0.3.3: CircularJSON is in maintenance only, flatted is its successor.
npm WARN deprecated kleur@2.0.2: Please upgrade to kleur@3 or migrate to 'ansi-colors' if you prefer the old syntax. Visit <https://github.com/lukeed/kleur/releases/tag/v3.0.0\> for migration path(s).
npm WARN rm not removing C:\Work\Training\Pre\GraphQL\graphql-react-event-booking\frontend\node_modules\.bin\jest.cmd as it wasn't installed by C:\Work\Training\Pre\GraphQL\graphql-react-event-booking\frontend\node_modules\jest
npm WARN rm not removing C:\Work\Training\Pre\GraphQL\graphql-react-event-booking\frontend\node_modules\.bin\jest as it wasn't installed by C:\Work\Training\Pre\GraphQL\graphql-react-event-booking\frontend\node_modules\jest
npm WARN rm not removing C:\Work\Training\Pre\GraphQL\graphql-react-event-booking\frontend\node_modules\.bin\regjsparser.cmd as it wasn't installed by C:\Work\Training\Pre\GraphQL\graphql-react-event-booking\frontend\node_modules\regjsparser
npm WARN rm not removing C:\Work\Training\Pre\GraphQL\graphql-react-event-booking\frontend\node_modules\.bin\regjsparser as it wasn't installed by C:\Work\Training\Pre\GraphQL\graphql-react-event-booking\frontend\node_modules\regjsparser
npm WARN rm not removing C:\Work\Training\Pre\GraphQL\graphql-react-event-booking\frontend\node_modules\.bin\json5.cmd as it wasn't installed by C:\Work\Training\Pre\GraphQL\graphql-react-event-booking\frontend\node_modules\json5
npm WARN rm not removing C:\Work\Training\Pre\GraphQL\graphql-react-event-booking\frontend\node_modules\.bin\json5 as it wasn't installed by C:\Work\Training\Pre\GraphQL\graphql-react-event-booking\frontend\node_modules\json5
npm WARN rm not removing C:\Work\Training\Pre\GraphQL\graphql-react-event-booking\frontend\node_modules\.bin\jsesc.cmd as it wasn't installed by C:\Work\Training\Pre\GraphQL\graphql-react-event-booking\frontend\node_modules\jsesc
npm WARN rm not removing C:\Work\Training\Pre\GraphQL\graphql-react-event-booking\frontend\node_modules\.bin\jsesc as it wasn't installed by C:\Work\Training\Pre\GraphQL\graphql-react-event-booking\frontend\node_modules\jsesc
npm WARN rm not removing C:\Work\Training\Pre\GraphQL\graphql-react-event-booking\frontend\node_modules\.bin\esparse.cmd as it wasn't installed by C:\Work\Training\Pre\GraphQL\graphql-react-event-booking\frontend\node_modules\esprima
npm WARN rm not removing C:\Work\Training\Pre\GraphQL\graphql-react-event-booking\frontend\node_modules\.bin\esvalidate.cmd as it wasn't installed by C:\Work\Training\Pre\GraphQL\graphql-react-event-booking\frontend\node_modules\esprima
npm WARN rm not removing C:\Work\Training\Pre\GraphQL\graphql-react-event-booking\frontend\node_modules\.bin\esparse as it wasn't installed by C:\Work\Training\Pre\GraphQL\graphql-react-event-booking\frontend\node_modules\esprima
npm WARN rm not removing C:\Work\Training\Pre\GraphQL\graphql-react-event-booking\frontend\node_modules\.bin\esvalidate as it wasn't installed by C:\Work\Training\Pre\GraphQL\graphql-react-event-booking\frontend\node_modules\esprima
npm WARN rm not removing C:\Work\Training\Pre\GraphQL\graphql-react-event-booking\frontend\node_modules\.bin\cssesc.cmd as it wasn't installed by C:\Work\Training\Pre\GraphQL\graphql-react-event-booking\frontend\node_modules\cssesc
npm WARN rm not removing C:\Work\Training\Pre\GraphQL\graphql-react-event-booking\frontend\node_modules\.bin\cssesc as it wasn't installed by C:\Work\Training\Pre\GraphQL\graphql-react-event-booking\frontend\node_modules\cssesc
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.4 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.4: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
added 701 packages from 176 contributors, removed 257 packages, updated 1232 packages and audited 35789 packages in 252.211s
found 0 vulnerabilities
Create the route and one component to test it
App.js
import React, { Component } from 'react';
import { BrowserRouter, Route } from 'react-router-dom';
import './App.css';
class App extends Component {
render() {
return (
<BrowserRouter>
<Route path="/" component={null} />
<Route path="/auth" component={null} />
<Route path="/events" component={null} />
<Route path="/bookings" component={null} />
</BrowserRouter>
);
}
}
export default App;
- Create the
pages
folder from thesrc
folder and theAuth.js
file inside of it.
pages > Auth.js
import React, { Component } from "react";
class AuthPage extends Component {
render() {
return <h1>The Auth Page</h1>;
}
}
export default AuthPage;
App.js
import React, { Component } from "react";
import { BrowserRouter, Route, Redirect, Switch } from "react-router-dom";
import "./App.css";
import AuthPage from "./pages/Auth";
class App extends Component {
render() {
return (
<BrowserRouter>
<Switch>
<Redirect from="/" to="/auth" exact />
<Route path="/auth" component={AuthPage} />
<Route path="/events" component={null} />
<Route path="/bookings" component={null} />
</Switch>
</BrowserRouter>
);
}
}
export default App;
> frontend@0.1.0 start C:\Work\Training\Pre\GraphQL\graphql-react-event-booking\frontend
> react-scripts start
Starting the development server...
Compiled successfully!
You can now view frontend in the browser.
Local: http://localhost:3000/
On Your Network: http://10.0.75.1:3000/
Note that the development build is not optimized.
To create a production build, use yarn build.
Bookings.js
and Events.js
files to ensure the router is working
Add the pages -> Bookings.js
import React, { Component } from "react";
class BookingsPage extends Component {
render() {
return <h1>The Bookings Page</h1>;
}
}
export default BookingsPage;
pages -> Events.js
import React, { Component } from "react";
class EventsPage extends Component {
render() {
return <h1>The Events Page</h1>;
}
}
export default EventsPage;
App.js
import React, { Component } from "react";
import { BrowserRouter, Route, Redirect, Switch } from "react-router-dom";
import "./App.css";
import AuthPage from "./pages/Auth";
import BookingsPage from "./pages/Bookings";
import EventsPage from "./pages/Events";
class App extends Component {
render() {
return (
<BrowserRouter>
<Switch>
<Redirect from="/" to="/auth" exact />
<Route path="/auth" component={AuthPage} />
<Route path="/events" component={EventsPage} />
<Route path="/bookings" component={BookingsPage} />
</Switch>
</BrowserRouter>
);
}
}
export default App;
12 Adding a Navbar 21:48
components
folder from the src
folder and the Navigation
folder inside it
Create the components -> Navigation -> MainNavigation.js
import React from "react";
import { NavLink } from "react-router-dom";
const mainNavigation = props => (
<header>
<div className="main_navigation__logo">
<h1>EasyEvent</h1>
</div>
<nav className="main_navigation__logo">
<ul>
<li>
<NavLink to="/auth">Authenticate</NavLink>
</li>
<li>
<NavLink to="/events">Events</NavLink>
</li>
<li>
<NavLink to="/bookings">Bookings</NavLink>
</li>
</ul>
</nav>
</header>
);
export default mainNavigation;
App.js
import React, { Component } from "react";
import { BrowserRouter, Route, Redirect, Switch } from "react-router-dom";
import "./App.css";
import AuthPage from "./pages/Auth";
import BookingsPage from "./pages/Bookings";
import EventsPage from "./pages/Events";
import MainNavigation from "./components/Navigation/MainNavigation";
class App extends Component {
render() {
return (
<BrowserRouter>
<React.Fragment>
<MainNavigation />
<main>
<Switch>
<Redirect from="/" to="/auth" exact />
<Route path="/auth" component={AuthPage} />
<Route path="/events" component={EventsPage} />
<Route path="/bookings" component={BookingsPage} />
</Switch>
</main>
</React.Fragment>
</BrowserRouter>
);
}
}
export default App;
Add some style to the Navigation Bar
components -> Navigation -> MainNavigation.css
.main_navigation {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 3.5rem;
background: #01d1d1;
padding: 0 1rem;
display: flex;
align-items: center;
}
.main_navigation__logo h1 {
margin: 0;
font-size: 1.5rem;
}
.main_navigation__items {
margin-left: 1.5rem;
}
.main_navigation__items ul {
display: flex;
list-style: none;
margin: 0;
padding: 0;
}
.main_navigation__items li {
margin: 0 1rem;
}
.main_navigation__items a {
text-decoration: none;
color: #000;
}
.main_navigation__items a:hover,
.main_navigation__items a:active,
.main_navigation__items a.active {
color: #f8e264;
}
components -> Navigation -> MainNavigation.js
import React from "react";
import { NavLink } from "react-router-dom";
import './MainNavigation.css'
const mainNavigation = props => (
<header className="main_navigation">
<div className="main_navigation__logo">
<h1>EasyEvent</h1>
</div>
<nav className="main_navigation__items">
<ul>
<li>
<NavLink to="/auth">Authenticate</NavLink>
</li>
<li>
<NavLink to="/events">Events</NavLink>
</li>
<li>
<NavLink to="/bookings">Bookings</NavLink>
</li>
</ul>
</nav>
</header>
);
export default mainNavigation;
export default mainNavigation;
App.js
import React, { Component } from "react";
import { BrowserRouter, Route, Redirect, Switch } from "react-router-dom";
import "./App.css";
import AuthPage from "./pages/Auth";
import BookingsPage from "./pages/Bookings";
import EventsPage from "./pages/Events";
import MainNavigation from "./components/Navigation/MainNavigation";
class App extends Component {
render() {
return (
<BrowserRouter>
<React.Fragment>
<MainNavigation />
<main className="main-content">
<Switch>
<Redirect from="/" to="/auth" exact />
<Route path="/auth" component={AuthPage} />
<Route path="/events" component={EventsPage} />
<Route path="/bookings" component={BookingsPage} />
</Switch>
</main>
</React.Fragment>
</BrowserRouter>
);
}
}
export default App;
App.css
.main-content {
margin: 4rem 2.5rem;
}
13 Hitting the API 32:11
Develop the AuthPage to be able to authenticate
pages - Auth.js
import React, { Component } from "react";
import "./Auth.css";
class AuthPage extends Component {
render() {
return (
<form className="auth-form">
<div className="form-control">
<label htmlFor="email">E-Mail</label>
<input type="email" id="email" />
</div>
<div className="form-control">
<label htmlFor="password">Password</label>
<input type="password" id="password" />
</div>
<div className="form-actions">
<button type="submit">Submit</button>
<button type="button">Sign to Signup</button>
</div>
</form>
);
}
}
export default AuthPage;
pages - Auth.css
.auth-form {
width: 25rem;
max-width: 80%;
margin: 5rem auto;
}
.form-control label,
.form-control input {
width: 100%;
display: block;
}
.form-control label {
margin-bottom: 0.5rem;
}
.form-control {
margin-bottom: 1rem;
}
.form-actions button {
background-color: #01d1d1;
font: inherit;
border: 1px solid #01d1d1;
border-radius: 3px;
padding: 0.25rem 1rem;
margin-right: 1rem;
box-shadow: 1px 1px 5px rgba(0,0,0,0.26);
color: white;
cursor: pointer;
}
.form-actions button:hover,
.form-actions button:active {
background: #01a7a7;
border-color: #01a7a7;
}
components -> Navigation -> MainNavigation.css
.main_navigation {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 3.5rem;
background: #01d1d1;
padding: 0 1rem;
display: flex;
align-items: center;
}
.main_navigation__logo h1 {
margin: 0;
font-size: 1.5rem;
}
.main_navigation__items {
margin-left: 1.5rem;
}
.main_navigation__items ul {
display: flex;
list-style: none;
margin: 0;
padding: 0;
}
.main_navigation__items li {
margin: 0 1rem;
}
.main_navigation__items a {
text-decoration: none;
color: #000;
}
.main_navigation__items a:hover,
.main_navigation__items a:active,
.main_navigation__items a.active {
color: #fdefa0;
}
9000
and to avoid CORS errors
Change the port to the server to app.js
const express = require("express");
const bodyParser = require("body-parser");
const graphqlHttp = require("express-graphql");
const mongoose = require("mongoose");
const isAuth = require("./middleware/is-auth");
const graphQlSchema = require("./graphql/schema/index");
const graphQlResolvers = require("./graphql/resolvers/index");
const app = express();
app.use(bodyParser.json());
app.use((req, res, next) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "POST,GET,OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
if (req.method === "OPTIONS") {
return res.sendStatus(200);
}
next();
});
app.use(isAuth);
app.use(
"/graphql",
graphqlHttp({
schema: graphQlSchema,
rootValue: graphQlResolvers,
graphiql: true
})
);
mongoose
.connect(
`mongodb+srv://${process.env.MONGO_USER}:${
process.env.MONGO_PASSWORD
}@cluster0-ycwj8.mongodb.net/${process.env.MONGO_DB}?retryWrites=true`
)
.then(() => {
app.listen(9000);
})
.catch(err => {
console.log(err);
});
- Start the server in another terminal window
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/GraphQL/graphql-react-event-booking
$ npm start
> graphql-react-event-booking@1.0.0 start C:\Work\Training\Pre\GraphQL\graphql-react-event-booking
> nodemon app.js
[nodemon] 1.18.9
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: *.*
[nodemon] starting `node app.js`
(node:16596) DeprecationWarning: current URL string parser is deprecated, and will be removed in a future version. To use the new parser, pass option { useNewUrlParser: true } to MongoClient.connect.
createUser
endpoint ti sing up.
Develop a call to the pages - Auth.js
import React, { Component } from "react";
import "./Auth.css";
class AuthPage extends Component {
constructor(props) {
super(props);
this.emailEl = React.createRef();
this.passwordEl = React.createRef();
}
submitHandler = event => {
event.preventDefault();
const email = this.emailEl.current.value;
const password = this.passwordEl.current.value;
if (email.trim().length === 0 || password.trim().length === 0) {
return;
}
const requestBody = {
query: `
mutation {
createUser(userInput: {email: "${email}", password: "${password}"})
{
_id
email
}
}
`
};
fetch("http://localhost:9000/graphql", {
method: "POST",
body: JSON.stringify(requestBody),
headers: {
"Content-Type": "application/json"
}
})
.then(res => {
if (res.status !== 200 && res.status !== 201) {
throw new Error("Failed!");
}
return res.json();
})
.then(resData => {
console.log(resData);
})
.catch(err => {
console.log(err);
});
};
render() {
return (
<form className="auth-form" onSubmit={this.submitHandler}>
<div className="form-control">
<label htmlFor="email">E-Mail</label>
<input type="email" id="email" ref={this.emailEl} />
</div>
<div className="form-control">
<label htmlFor="password">Password</label>
<input type="password" id="password" ref={this.passwordEl} />
</div>
<div className="form-actions">
<button type="submit">Submit</button>
<button type="button">Sign to Login</button>
</div>
</form>
);
}
}
export default AuthPage;
Modify the Auth.js program to allow to login
pages - Auth.js
import React, { Component } from "react";
import "./Auth.css";
class AuthPage extends Component {
state = {
isLogin: true
};
constructor(props) {
super(props);
this.emailEl = React.createRef();
this.passwordEl = React.createRef();
}
switchModeHandler = () => {
this.setState(prevSate => {
return { isLogin: !prevSate.isLogin };
});
};
submitHandler = event => {
event.preventDefault();
const email = this.emailEl.current.value;
const password = this.passwordEl.current.value;
if (email.trim().length === 0 || password.trim().length === 0) {
return;
}
let requestBody = {
query: `
query {
login(email: "${email}", password: "${password}")
{
userId
token
tokenExpiration
}
}
`
};
if (!this.state.isLogin) {
requestBody = {
query: `
mutation {
createUser(userInput: {email: "${email}", password: "${password}"})
{
_id
email
}
}
`
};
}
fetch("http://localhost:9000/graphql", {
method: "POST",
body: JSON.stringify(requestBody),
headers: {
"Content-Type": "application/json"
}
})
.then(res => {
if (res.status !== 200 && res.status !== 201) {
throw new Error("Failed!");
}
return res.json();
})
.then(resData => {
console.log(resData);
})
.catch(err => {
console.log(err);
});
};
render() {
return (
<form className="auth-form" onSubmit={this.submitHandler}>
<div className="form-control">
<label htmlFor="email">E-Mail</label>
<input type="email" id="email" ref={this.emailEl} />
</div>
<div className="form-control">
<label htmlFor="password">Password</label>
<input type="password" id="password" ref={this.passwordEl} />
</div>
<div className="form-actions">
<button type="submit">Submit</button>
<button type="button" onClick={this.switchModeHandler}>
Sign to {this.state.isLogin ? "Signup" : "Login"}
</button>
</div>
</form>
);
}
}
export default AuthPage;
14. Using the token 16:07
Using data from Auth.js in App.js using the context API
- Create the new
context
folder and the newauth-context.js
file
context -> auth-context.js
import React from 'react';
export default React.createContext({
token: null,
userId: null,
login: (token, userId, tokenExpiration) => {},
logout: () => {}
})
- Modify the
App.js
program to import and use thecontext
App.js
import React, { Component } from "react";
import { BrowserRouter, Route, Redirect, Switch } from "react-router-dom";
import "./App.css";
import AuthPage from "./pages/Auth";
import BookingsPage from "./pages/Bookings";
import EventsPage from "./pages/Events";
import MainNavigation from "./components/Navigation/MainNavigation";
import AuthContext from "./context/auth-context";
class App extends Component {
state = {
token: null,
userId: null
}
login = (token, userId, tokenExpiration) => {
this.setState({token: token, userId: userId});
};
logout = () => {
this.setState({token: null, userId: null});
};
render() {
return (
<BrowserRouter>
<React.Fragment>
<AuthContext.Provider
value={{
token: this.state.token,
userId: this.state.userId,
login: this.login,
logout: this.logout
}}
>
<MainNavigation />
<main className="main-content">
<Switch>
<Redirect from="/" to="/auth" exact />
<Route path="/auth" component={AuthPage} />
<Route path="/events" component={EventsPage} />
<Route path="/bookings" component={BookingsPage} />
</Switch>
</main>
</AuthContext.Provider>
</React.Fragment>
</BrowserRouter>
);
}
}
export default App;
- Modify the
Auth.js
program to import and use thecontext
pages - Auth.js
import React, { Component } from "react";
import "./Auth.css";
import AuthContext from "../context/auth-context";
class AuthPage extends Component {
state = {
isLogin: true
};
static contextType = AuthContext;
constructor(props) {
super(props);
this.emailEl = React.createRef();
this.passwordEl = React.createRef();
}
switchModeHandler = () => {
this.setState(prevSate => {
return { isLogin: !prevSate.isLogin };
});
};
submitHandler = event => {
event.preventDefault();
const email = this.emailEl.current.value;
const password = this.passwordEl.current.value;
if (email.trim().length === 0 || password.trim().length === 0) {
return;
}
let requestBody = {
query: `
query {
login(email: "${email}", password: "${password}")
{
userId
token
tokenExpiration
}
}
`
};
if (!this.state.isLogin) {
requestBody = {
query: `
mutation {
createUser(userInput: {email: "${email}", password: "${password}"})
{
_id
email
}
}
`
};
}
fetch("http://localhost:9000/graphql", {
method: "POST",
body: JSON.stringify(requestBody),
headers: {
"Content-Type": "application/json"
}
})
.then(res => {
if (res.status !== 200 && res.status !== 201) {
throw new Error("Failed!");
}
return res.json();
})
.then(resData => {
if (resData.data.login.token) {
this.context.login(
resData.data.login.token,
resData.data.login.userId,
resData.data.login.tokenExpiration
);
}
console.log(resData);
})
.catch(err => {
console.log(err);
});
};
render() {
return (
<form className="auth-form" onSubmit={this.submitHandler}>
<div className="form-control">
<label htmlFor="email">E-Mail</label>
<input type="email" id="email" ref={this.emailEl} />
</div>
<div className="form-control">
<label htmlFor="password">Password</label>
<input type="password" id="password" ref={this.passwordEl} />
</div>
<div className="form-actions">
<button type="submit">Submit</button>
<button type="button" onClick={this.switchModeHandler}>
Sign to {this.state.isLogin ? "Signup" : "Login"}
</button>
</div>
</form>
);
}
}
export default AuthPage;
- Modify the
MainNavigation.js
program to import and use thecontext
components -> Navigation -> MainNavigation.js
import React from "react";
import { NavLink } from "react-router-dom";
import AuthContext from "../../context/auth-context";
import "./MainNavigation.css";
const mainNavigation = props => (
<AuthContext.Consumer>
{context => {
return (
<header className="main_navigation">
<div className="main_navigation__logo">
<h1>EasyEvent</h1>
</div>
<nav className="main_navigation__items">
<ul>
{!context.token && (
<li>
<NavLink to="/auth">Authenticate</NavLink>
</li>
)}
<li>
<NavLink to="/events">Events</NavLink>
</li>
{context.token && (
<li>
<NavLink to="/bookings">Bookings</NavLink>
</li>
)}
</ul>
</nav>
</header>
);
}}
</AuthContext.Consumer>
);
export default mainNavigation;
- Before being authenticated
- After authenticating
Redirect when authenticated and protect routes when not authenticated
pages - Auth.js
import React, { Component } from "react";
import { BrowserRouter, Route, Redirect, Switch } from "react-router-dom";
import "./App.css";
import AuthPage from "./pages/Auth";
import BookingsPage from "./pages/Bookings";
import EventsPage from "./pages/Events";
import MainNavigation from "./components/Navigation/MainNavigation";
import AuthContext from "./context/auth-context";
class App extends Component {
state = {
token: null,
userId: null
}
login = (token, userId, tokenExpiration) => {
this.setState({token: token, userId: userId});
};
logout = () => {
this.setState({token: null, userId: null});
};
render() {
return (
<BrowserRouter>
<React.Fragment>
<AuthContext.Provider
value={{
token: this.state.token,
userId: this.state.userId,
login: this.login,
logout: this.logout
}}
>
<MainNavigation />
<main className="main-content">
<Switch>
{!this.state.token && <Redirect from="/" to="/auth" exact />}
{this.state.token && <Redirect from="/" to="/events" exact />}
{this.state.token && <Redirect from="/auth" to="/events" exact />}
{!this.state.token && <Route path="/auth" component={AuthPage} />}
<Route path="/events" component={EventsPage} />
{this.state.token && <Route path="/bookings" component={BookingsPage} />}
</Switch>
</main>
</AuthContext.Provider>
</React.Fragment>
</BrowserRouter>
);
}
}
export default App;
- Before being authenticated
- After being authenticated
15. Adding a Modal 24:11
Changing the style
pages -> Auth.css
.auth-form {
width: 25rem;
max-width: 80%;
margin: 5rem auto;
}
.form-control label,
.form-control input {
width: 100%;
display: block;
}
.form-control label {
margin-bottom: 0.5rem;
}
.form-control {
margin-bottom: 1rem;
}
.form-actions button {
background-color: #5101d1;
font: inherit;
border: 1px solid #5101d1;
border-radius: 3px;
padding: 0.25rem 1rem;
margin-right: 1rem;
box-shadow: 1px 1px 5px rgba(0,0,0,0.26);
color: white;
cursor: pointer;
}
.form-actions button:hover,
.form-actions button:active {
background: #6219d6;
border-color: #6219d6;
}
components -> Navigation -> MainNavigation.css
.main_navigation {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 3.5rem;
background: #5101d1;
padding: 0 2rem;
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
}
.main_navigation__logo h1 {
margin: 0;
font-size: 1.5rem;
color: #dfcefc;
}
.main_navigation__items {
margin-left: 1.5rem;
}
.main_navigation__items ul {
display: flex;
list-style: none;
margin: 0;
padding: 0;
}
.main_navigation__items li {
margin: 0 1rem;
}
.main_navigation__items a {
text-decoration: none;
color: white;
padding: 0.25rem 0.5rem;
}
.main_navigation__items a:hover,
.main_navigation__items a:active,
.main_navigation__items a.active {
background: #ffffff;
color: #5101d1;
border-radius: 5px;
}
Include the logout button
App.js
import React, { Component } from "react";
import { BrowserRouter, Route, Redirect, Switch } from "react-router-dom";
import "./App.css";
import AuthPage from "./pages/Auth";
import BookingsPage from "./pages/Bookings";
import EventsPage from "./pages/Events";
import MainNavigation from "./components/Navigation/MainNavigation";
import AuthContext from "./context/auth-context";
class App extends Component {
state = {
token: null,
userId: null
}
login = (token, userId, tokenExpiration) => {
this.setState({token: token, userId: userId});
};
logout = () => {
this.setState({token: null, userId: null});
};
render() {
return (
<BrowserRouter>
<React.Fragment>
<AuthContext.Provider
value={{
token: this.state.token,
userId: this.state.userId,
login: this.login,
logout: this.logout
}}
>
<MainNavigation />
<main className="main-content">
<Switch>
{this.state.token && <Redirect from="/" to="/events" exact />}
{this.state.token && <Redirect from="/auth" to="/events" exact />}
{!this.state.token && <Route path="/auth" component={AuthPage} />}
<Route path="/events" component={EventsPage} />
{this.state.token && <Route path="/bookings" component={BookingsPage} />}
{!this.state.token && <Redirect to="/auth" exact />}
</Switch>
</main>
</AuthContext.Provider>
</React.Fragment>
</BrowserRouter>
);
}
}
export default App;
components -> Navigation -> MainNavigation.css
.main_navigation {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 3.5rem;
background: #5101d1;
padding: 0 2rem;
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
}
.main_navigation__logo h1 {
margin: 0;
font-size: 1.5rem;
color: #dfcefc;
}
.main_navigation__items {
margin-left: 1.5rem;
}
.main_navigation__items ul {
display: flex;
list-style: none;
margin: 0;
padding: 0;
align-items: center;
}
.main_navigation__items li {
margin: 0 1rem;
}
.main_navigation__items a,
.main_navigation__items button {
text-decoration: none;
color: white;
padding: 0.25rem 0.5rem;
border: none;
font: inherit;
background: transparent;
cursor: pointer;
margin: 0;
}
.main_navigation__items a:hover,
.main_navigation__items a:active,
.main_navigation__items a.active,
.main_navigation__items button:hover,
.main_navigation__items button:active {
background: #ffffff;
color: #5101d1;
border-radius: 5px;
}
components -> Navigation -> MainNavigation.js
import React from "react";
import { NavLink } from "react-router-dom";
import AuthContext from "../../context/auth-context";
import "./MainNavigation.css";
const mainNavigation = props => (
<AuthContext.Consumer>
{context => {
return (
<header className="main_navigation">
<div className="main_navigation__logo">
<h1>EasyEvent</h1>
</div>
<nav className="main_navigation__items">
<ul>
{!context.token && (
<li>
<NavLink to="/auth">Authenticate</NavLink>
</li>
)}
<li>
<NavLink to="/events">Events</NavLink>
</li>
{context.token && (
<React.Fragment>
<li>
<NavLink to="/bookings">Bookings</NavLink>
</li>
<li>
<button onClick={context.logout}>Logout</button>
</li>
</React.Fragment>
)}
</ul>
</nav>
</header>
);
}}
</AuthContext.Consumer>
);
export default mainNavigation;
Events.js
programs to add a Create Event
button
Modify the pages -> Events.js
import React, { Component } from "react";
class EventsPage extends Component {
render() {
return <div>
<button className="btn">Create Event</button>
</div>;
}
}
export default EventsPage;
pages -> Auth.css
.auth-form {
width: 25rem;
max-width: 80%;
margin: 5rem auto;
}
index.css
body {
margin: 0;
padding: 0;
font-family: sans-serif;
}
.form-control label,
.form-control input {
width: 100%;
display: block;
}
.form-control label {
margin-bottom: 0.5rem;
}
.form-control {
margin-bottom: 1rem;
}
.form-actions button,
.btn {
background-color: #5101d1;
font: inherit;
border: 1px solid #5101d1;
border-radius: 3px;
padding: 0.25rem 1rem;
margin-right: 1rem;
box-shadow: 1px 1px 5px rgba(0,0,0,0.26);
color: white;
cursor: pointer;
}
.form-actions button:hover,
.form-actions button:active,
.btn:hover,
.btn:active {
background: #6219d6;
border-color: #6219d6;
}
Events.js
document to allow to add a new Event
Modify the pages -> Events.js
import React from "react";
import "./Modal.css";
const modal = props => (
<div className="modal">
<header className="modal__header">
<h1>{props.title}</h1>
</header>
<section className="modal__content">{props.children}</section>
<section className="modal__actions">
{props.canCancel && <button className="btn">Cancel</button>}
{props.canConfirm && <button className="btn">Confirm</button>}
</section>
</div>
);
export default modal;
pages -> Events.css
.events-control {
text-align: center;
border: 1px solid #5101d1;
padding: 1rem;
margin: 2rem auto;
width: 30rem;
max-width: 80%;
}
- Create the new
components -> Modal
folder and theModal.js
andModal.css
documents inside it.
components -> Modal -> Modal.js
import React from "react";
import "./Modal.css";
const modal = props => (
<div className="modal">
<header className="modal__header">
<h1>{props.title}</h1>
</header>
<section className="modal__content">{props.children}</section>
<section className="modal__actions">
{props.canCancel && <button className="btn">Cancel</button>}
{props.canConfirm && <button className="btn">Confirm</button>}
</section>
</div>
);
export default modal;
components -> Modal -> Modal.css
.modal {
width: 90%;
background: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.26);
position: fixed;
top: 20vh;
left: 5%;
}
.modal__header {
padding: 1rem;
background: #5101d1;
color: white;
}
.modal__header h1 {
margin: 0;
font-size: 1.25rem;
}
.modal__content {
padding: 1rem;
}
.modal__actions {
display: flex;
justify-content: flex-end;
padding: 1rem;
}
@media (min-width: 768px) {
.modal {
width: 30rem;
left: calc((100% - 30rem) / 2);
}
}
Create the Backdrop component
- Create the
components -> Backdrop
folder and theBackdrop.js
andBackdrop.css
documents inside it.
components -> Backdrop -> Backdrop.js
import React from "react";
import "./Backdrop.css";
const backdrop = props => <div className="backdrop" />;
export default backdrop;
components -> Backdrop -> Backdrop.css
.backdrop {
position: fixed;
top: 0;
left: 0;
height: 100vh;
width: 100%;
background: rgba(0, 0, 0, 0.75);
}
pages -> Events.js
import React, { Component } from "react";
import Modal from "../components/Modal/Modal";
import Backdrop from "../components/Backdrop/Backdrop";
import "./Events.css";
class EventsPage extends Component {
render() {
return (
<React.Fragment>
<Backdrop />
<Modal title="Add Event" canCancel canConfirm>
<p>Modal Conntent</p>
</Modal>
<div className="events-control">
<p>Share your own Events!</p>
<button className="btn">Create Event</button>
</div>
</React.Fragment>
);
}
}
export default EventsPage;
Event.js
to use the Modal component when clicking the button.
Modify the pages -> Events.js
import React, { Component } from "react";
import Modal from "../components/Modal/Modal";
import Backdrop from "../components/Backdrop/Backdrop";
import "./Events.css";
class EventsPage extends Component {
state = {
creating: false
};
startCreateEventHandler = () => {
this.setState({ creating: true });
};
modalConfirmHandler = () => {
this.setState({ creating: false });
};
modalCancelHandler = () => {
this.setState({ creating: false });
};
render() {
return (
<React.Fragment>
{this.state.creating && (
<React.Fragment>
<Backdrop />
<Modal
title="Add Event"
canCancel
canConfirm
onCancel={this.modalCancelHandler}
onConfirm={this.modalConfirmHandler}
>
<p>Modal Content</p>
</Modal>
</React.Fragment>
)}
<div className="events-control">
<p>Share your own Events!</p>
<button className="btn" onClick={this.startCreateEventHandler}>
Create Event
</button>
</div>
</React.Fragment>
);
}
}
export default EventsPage;
components -> Modal -> Modal.js
import React from "react";
import "./Modal.css";
const modal = props => (
<div className="modal">
<header className="modal__header">
<h1>{props.title}</h1>
</header>
<section className="modal__content">{props.children}</section>
<section className="modal__actions">
{props.canCancel && (
<button className="btn" onClick={props.onCancel}>
Cancel
</button>
)}
{props.canConfirm && (
<button className="btn" onClick={props.onConfirm}>
Confirm
</button>
)}
</section>
</div>
);
export default modal;
16. Adding Events 27:13
Modify the Events page to allow to create new Events
- Modify the
index.css
document to include style for thetextarea
index.css
body {
margin: 0;
padding: 0;
font-family: sans-serif;
}
.form-control label,
.form-control input,
.form-control textarea {
width: 100%;
display: block;
}
.form-control label {
margin-bottom: 0.5rem;
}
.form-control {
margin-bottom: 1rem;
}
.form-actions button,
.btn {
background-color: #5101d1;
font: inherit;
border: 1px solid #5101d1;
border-radius: 3px;
padding: 0.25rem 1rem;
margin-right: 1rem;
box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.26);
color: white;
cursor: pointer;
}
.form-actions button:hover,
.form-actions button:active,
.btn:hover,
.btn:active {
background: #6219d6;
border-color: #6219d6;
}
- Modify the
Events.js
document to add a form to create an event using references to simplify the code.
pages -> Event.js
import React, { Component } from "react";
import Modal from "../components/Modal/Modal";
import Backdrop from "../components/Backdrop/Backdrop";
import "./Events.css";
class EventsPage extends Component {
state = {
creating: false
};
constructor(props) {
super(props);
this.titleElRef = React.createRef();
this.priceElRef = React.createRef();
this.dateElRef = React.createRef();
this.descriptionElRef = React.createRef();
}
startCreateEventHandler = () => {
this.setState({ creating: true });
};
modalConfirmHandler = () => {
this.setState({ creating: false });
const title = this.titleElRef.current.value;
const price = +this.priceElRef.current.value;
const date = this.dateElRef.current.value;
const description = this.descriptionElRef.current.value;
if (
title.trim().length === 0 ||
price <= 0 ||
date.trim().length === 0 ||
description.trim().length === 0
) {
return;
}
const event = { title, price, date, description };
console.log(event);
};
modalCancelHandler = () => {
this.setState({ creating: false });
};
render() {
return (
<React.Fragment>
{this.state.creating && (
<React.Fragment>
<Backdrop />
<Modal
title="Add Event"
canCancel
canConfirm
onCancel={this.modalCancelHandler}
onConfirm={this.modalConfirmHandler}
>
<form>
<div className="form-control">
<label htmlFor="title">Title</label>
<input type="text" id="title" ref={this.titleElRef} />
</div>
<div className="form-control">
<label htmlFor="price">Price</label>
<input type="number" id="price" ref={this.priceElRef} />
</div>
<div className="form-control">
<label htmlFor="date">Date</label>
<input type="datetime-local" id="date" ref={this.dateElRef} />
</div>
<div className="form-control">
<label htmlFor="description">Description</label>
<textarea
id="description"
rows="4"
ref={this.descriptionElRef}
/>
</div>
</form>
</Modal>
</React.Fragment>
)}
<div className="events-control">
<p>Share your own Events!</p>
<button className="btn" onClick={this.startCreateEventHandler}>
Create Event
</button>
</div>
</React.Fragment>
);
}
}
export default EventsPage;
Send the request with the Event to the Backend
pages -> Event.js
import React, { Component } from "react";
import Modal from "../components/Modal/Modal";
import Backdrop from "../components/Backdrop/Backdrop";
import AuthContext from "../context/auth-context";
import "./Events.css";
class EventsPage extends Component {
state = {
creating: false
};
static contextType = AuthContext;
constructor(props) {
super(props);
this.titleElRef = React.createRef();
this.priceElRef = React.createRef();
this.dateElRef = React.createRef();
this.descriptionElRef = React.createRef();
}
startCreateEventHandler = () => {
this.setState({ creating: true });
};
modalConfirmHandler = () => {
this.setState({ creating: false });
const title = this.titleElRef.current.value;
const price = +this.priceElRef.current.value;
const date = this.dateElRef.current.value;
const description = this.descriptionElRef.current.value;
if (
title.trim().length === 0 ||
price <= 0 ||
date.trim().length === 0 ||
description.trim().length === 0
) {
return;
}
const event = { title, price, date, description };
console.log(event);
const requestBody = {
query: `
mutation {
createEvent(eventInput: {title: "${title}", description: "${description}", price: ${price}, date: "${date}"}) {
_id
title
description
date
price
creator {
_id
email
}
}
}
`
};
const token = this.context.token;
fetch("http://localhost:9000/graphql", {
method: "POST",
body: JSON.stringify(requestBody),
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token
}
})
.then(res => {
if (res.status !== 200 && res.status !== 201) {
throw new Error("Failed!");
}
return res.json();
})
.then(resData => {
console.log(resData);
// this.fetchEvents();
})
.catch(err => {
console.log(err);
});
};
modalCancelHandler = () => {
this.setState({ creating: false });
};
render() {
return (
<React.Fragment>
{this.state.creating && (
<React.Fragment>
<Backdrop />
<Modal
title="Add Event"
canCancel
canConfirm
onCancel={this.modalCancelHandler}
onConfirm={this.modalConfirmHandler}
>
<form>
<div className="form-control">
<label htmlFor="title">Title</label>
<input type="text" id="title" ref={this.titleElRef} />
</div>
<div className="form-control">
<label htmlFor="price">Price</label>
<input type="number" id="price" ref={this.priceElRef} />
</div>
<div className="form-control">
<label htmlFor="date">Date</label>
<input type="datetime-local" id="date" ref={this.dateElRef} />
</div>
<div className="form-control">
<label htmlFor="description">Description</label>
<textarea
id="description"
rows="4"
ref={this.descriptionElRef}
/>
</div>
</form>
</Modal>
</React.Fragment>
)}
<div className="events-control">
<p>Share your own Events!</p>
<button className="btn" onClick={this.startCreateEventHandler}>
Create Event
</button>
</div>
</React.Fragment>
);
}
}
export default EventsPage;
Fetch the events and show them
- Modify the
Event.css
document to include the style for the list and events.
pages > Event.css
.events-control {
text-align: center;
border: 1px solid #5101d1;
padding: 1rem;
margin: 2rem auto;
width: 30rem;
max-width: 80%;
}
.events__list {
width: 40rem;
max-width: 90%;
margin: 2rem auto;
list-style: none;
padding: 0;
}
.events__list-item {
margin: 1rem 0;
padding: 1rem;
border: 1px solid #5101d1;
}
pages -> Event.js
import React, { Component } from "react";
import Modal from "../components/Modal/Modal";
import Backdrop from "../components/Backdrop/Backdrop";
import AuthContext from "../context/auth-context";
import "./Events.css";
class EventsPage extends Component {
state = {
creating: false,
events: []
};
static contextType = AuthContext;
constructor(props) {
super(props);
this.titleElRef = React.createRef();
this.priceElRef = React.createRef();
this.dateElRef = React.createRef();
this.descriptionElRef = React.createRef();
}
componentDidMount() {
this.fetchEvents();
}
startCreateEventHandler = () => {
this.setState({ creating: true });
};
modalConfirmHandler = () => {
this.setState({ creating: false });
const title = this.titleElRef.current.value;
const price = +this.priceElRef.current.value;
const date = this.dateElRef.current.value;
const description = this.descriptionElRef.current.value;
if (
title.trim().length === 0 ||
price <= 0 ||
date.trim().length === 0 ||
description.trim().length === 0
) {
return;
}
const event = { title, price, date, description };
console.log(event);
const requestBody = {
query: `
mutation {
createEvent(eventInput: {title: "${title}", description: "${description}", price: ${price}, date: "${date}"}) {
_id
title
description
date
price
creator {
_id
email
}
}
}
`
};
const token = this.context.token;
fetch("http://localhost:9000/graphql", {
method: "POST",
body: JSON.stringify(requestBody),
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token
}
})
.then(res => {
if (res.status !== 200 && res.status !== 201) {
throw new Error("Failed!");
}
return res.json();
})
.then(resData => {
this.fetchEvents();
})
.catch(err => {
console.log(err);
});
};
modalCancelHandler = () => {
this.setState({ creating: false });
};
fetchEvents() {
const requestBody = {
query: `
query {
events {
_id
title
description
date
price
creator {
_id
email
}
}
}
`
};
fetch("http://localhost:9000/graphql", {
method: "POST",
body: JSON.stringify(requestBody),
headers: {
"Content-Type": "application/json"
}
})
.then(res => {
if (res.status !== 200 && res.status !== 201) {
throw new Error("Failed!");
}
return res.json();
})
.then(resData => {
const events = resData.data.events;
this.setState({ events: events });
})
.catch(err => {
console.log(err);
});
}
render() {
const eventList = this.state.events.map(event => {
return (
<li key={event._id} className="events__list-item">
{event.title}
</li>
);
});
return (
<React.Fragment>
{this.state.creating && (
<React.Fragment>
<Backdrop />
<Modal
title="Add Event"
canCancel
canConfirm
onCancel={this.modalCancelHandler}
onConfirm={this.modalConfirmHandler}
>
<form>
<div className="form-control">
<label htmlFor="title">Title</label>
<input type="text" id="title" ref={this.titleElRef} />
</div>
<div className="form-control">
<label htmlFor="price">Price</label>
<input type="number" id="price" ref={this.priceElRef} />
</div>
<div className="form-control">
<label htmlFor="date">Date</label>
<input type="datetime-local" id="date" ref={this.dateElRef} />
</div>
<div className="form-control">
<label htmlFor="description">Description</label>
<textarea
id="description"
rows="4"
ref={this.descriptionElRef}
/>
</div>
</form>
</Modal>
</React.Fragment>
)}
{this.context.token && (
<div className="events-control">
<p>Share your own Events!</p>
<button className="btn" onClick={this.startCreateEventHandler}>
Create Event
</button>
</div>
)}
<ul className="events__list">{eventList}</ul>
</React.Fragment>
);
}
}
export default EventsPage;
AddingEvents7
AddingEvents8
17. Adding Event Features 41:20
Create the EventItem components
- Create the new
Events
folder from thecomponents
folder and theEventsList
andEventItem
folders inside it and the following documents inside them.
components -> Events -> EventList -> EventItem -> EventItem.js
import React from "react";
import "./EventItem.css";
const eventItem = props => (
<li key={props.eventId} className="events__list-item">
<h1>{props.title}</h1>
</li>
);
export default eventItem;
components -> Events -> EventList -> EventItem -> EventItem.css
.events__list-item {
margin: 1rem 0;
padding: 1rem;
border: 1px solid #5101d1;
display: flex;
justify-content: space-between;
align-items: center;
}
components -> Events -> EventList -> EventList.js
import React from "react";
import EventItem from "./EventItem/EventItem";
import "./EventList.css";
const eventList = props => {
const events = props.events.map(event => {
return (
<EventItem
key={event._id}
eventId={event._id}
title={event.title}
price={event.price}
date={event.date}
userId={props.authUserId}
creatorId={event.creator._id}
onDetail={props.onViewDetail}
/>
);
});
return <ul className="event__list">{events}</ul>;
};
export default eventList;
components -> Events -> EventList -> EventList.css
.event__list {
width: 40rem;
max-width: 90%;
margin: 2rem auto;
list-style: none;
padding: 0;
}
- Modify the
Event.css
documment to remove the styles put in the othercss
documents
pages > Event.css
.events-control {
text-align: center;
border: 1px solid #5101d1;
padding: 1rem;
margin: 2rem auto;
width: 30rem;
max-width: 80%;
}
- Modify the
Event.js
documment to use the new Event components.
pages -> Event.js
import React, { Component } from "react";
import Modal from "../components/Modal/Modal";
import Backdrop from "../components/Backdrop/Backdrop";
import EventList from "../components/Events/EventList/EventList";
import AuthContext from "../context/auth-context";
import "./Events.css";
class EventsPage extends Component {
state = {
creating: false,
events: []
};
static contextType = AuthContext;
constructor(props) {
super(props);
this.titleElRef = React.createRef();
this.priceElRef = React.createRef();
this.dateElRef = React.createRef();
this.descriptionElRef = React.createRef();
}
componentDidMount() {
this.fetchEvents();
}
startCreateEventHandler = () => {
this.setState({ creating: true });
};
modalConfirmHandler = () => {
this.setState({ creating: false });
const title = this.titleElRef.current.value;
const price = +this.priceElRef.current.value;
const date = this.dateElRef.current.value;
const description = this.descriptionElRef.current.value;
if (
title.trim().length === 0 ||
price <= 0 ||
date.trim().length === 0 ||
description.trim().length === 0
) {
return;
}
const event = { title, price, date, description };
console.log(event);
const requestBody = {
query: `
mutation {
createEvent(eventInput: {title: "${title}", description: "${description}", price: ${price}, date: "${date}"}) {
_id
title
description
date
price
creator {
_id
email
}
}
}
`
};
const token = this.context.token;
fetch("http://localhost:9000/graphql", {
method: "POST",
body: JSON.stringify(requestBody),
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token
}
})
.then(res => {
if (res.status !== 200 && res.status !== 201) {
throw new Error("Failed!");
}
return res.json();
})
.then(resData => {
this.fetchEvents();
})
.catch(err => {
console.log(err);
});
};
modalCancelHandler = () => {
this.setState({ creating: false });
};
fetchEvents() {
const requestBody = {
query: `
query {
events {
_id
title
description
date
price
creator {
_id
email
}
}
}
`
};
fetch("http://localhost:9000/graphql", {
method: "POST",
body: JSON.stringify(requestBody),
headers: {
"Content-Type": "application/json"
}
})
.then(res => {
if (res.status !== 200 && res.status !== 201) {
throw new Error("Failed!");
}
return res.json();
})
.then(resData => {
const events = resData.data.events;
this.setState({ events: events });
})
.catch(err => {
console.log(err);
});
}
render() {
return (
<React.Fragment>
{this.state.creating && (
<React.Fragment>
<Backdrop />
<Modal
title="Add Event"
canCancel
canConfirm
onCancel={this.modalCancelHandler}
onConfirm={this.modalConfirmHandler}
>
<form>
<div className="form-control">
<label htmlFor="title">Title</label>
<input type="text" id="title" ref={this.titleElRef} />
</div>
<div className="form-control">
<label htmlFor="price">Price</label>
<input type="number" id="price" ref={this.priceElRef} />
</div>
<div className="form-control">
<label htmlFor="date">Date</label>
<input type="datetime-local" id="date" ref={this.dateElRef} />
</div>
<div className="form-control">
<label htmlFor="description">Description</label>
<textarea
id="description"
rows="4"
ref={this.descriptionElRef}
/>
</div>
</form>
</Modal>
</React.Fragment>
)}
{this.context.token && (
<div className="events-control">
<p>Share your own Events!</p>
<button className="btn" onClick={this.startCreateEventHandler}>
Create Event
</button>
</div>
)}
<EventList
events={this.state.events}
/>
</React.Fragment>
);
}
}
export default EventsPage;
- Improve the new Event components.
components -> Events -> EventList -> EventItem -> EventItem.css
.events__list-item {
margin: 1rem 0;
padding: 1rem;
border: 1px solid #5101d1;
display: flex;
justify-content: space-between;
align-items: center;
}
.events__list-item h1 {
margin: 0;
font-size: 1.5rem;
color: #5101d1;
}
.events__list-item h2 {
margin: 0;
font-size: 1rem;
color: #7c7c7c;
}
.events__list-item p {
margin: 0;
}
components -> Events -> EventList -> EventItem -> EventItem.js
import React from "react";
import "./EventItem.css";
const eventItem = props => (
<li key={props.eventId} className="events__list-item">
<div>
<h1>{props.title}</h1>
<h2>
${props.price} - {new Date(props.date).toLocaleDateString()}
</h2>
</div>
<div>
{props.userId === props.creatorId ? (
<p>You are the owner of this event.</p>
) : (
<button
className="btn"
// onClick={props.onDetail.bind(this, props.eventId)}
>
View Details
</button>
)}
</div>
</li>
);
export default eventItem;
components -> Events -> EventList -> EventList.js
import React from "react";
import EventItem from "./EventItem/EventItem";
import "./EventList.css";
const eventList = props => {
const events = props.events.map(event => {
return (
<EventItem
key={event._id}
eventId={event._id}
title={event.title}
price={event.price}
date={event.date}
userId={props.authUserId}
creatorId={event.creator._id}
onDetail={props.onViewDetail}
/>
);
});
return <ul className="event__list">{events}</ul>;
};
export default eventList;
pages -> Event.js
import React, { Component } from "react";
import Modal from "../components/Modal/Modal";
import Backdrop from "../components/Backdrop/Backdrop";
import EventList from "../components/Events/EventList/EventList";
import AuthContext from "../context/auth-context";
import "./Events.css";
class EventsPage extends Component {
state = {
creating: false,
events: []
};
static contextType = AuthContext;
constructor(props) {
super(props);
this.titleElRef = React.createRef();
this.priceElRef = React.createRef();
this.dateElRef = React.createRef();
this.descriptionElRef = React.createRef();
}
componentDidMount() {
this.fetchEvents();
}
startCreateEventHandler = () => {
this.setState({ creating: true });
};
modalConfirmHandler = () => {
this.setState({ creating: false });
const title = this.titleElRef.current.value;
const price = +this.priceElRef.current.value;
const date = this.dateElRef.current.value;
const description = this.descriptionElRef.current.value;
if (
title.trim().length === 0 ||
price <= 0 ||
date.trim().length === 0 ||
description.trim().length === 0
) {
return;
}
const event = { title, price, date, description };
console.log(event);
const requestBody = {
query: `
mutation {
createEvent(eventInput: {title: "${title}", description: "${description}", price: ${price}, date: "${date}"}) {
_id
title
description
date
price
creator {
_id
email
}
}
}
`
};
const token = this.context.token;
fetch("http://localhost:9000/graphql", {
method: "POST",
body: JSON.stringify(requestBody),
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token
}
})
.then(res => {
if (res.status !== 200 && res.status !== 201) {
throw new Error("Failed!");
}
return res.json();
})
.then(resData => {
this.fetchEvents();
})
.catch(err => {
console.log(err);
});
};
modalCancelHandler = () => {
this.setState({ creating: false });
};
fetchEvents() {
const requestBody = {
query: `
query {
events {
_id
title
description
date
price
creator {
_id
email
}
}
}
`
};
fetch("http://localhost:9000/graphql", {
method: "POST",
body: JSON.stringify(requestBody),
headers: {
"Content-Type": "application/json"
}
})
.then(res => {
if (res.status !== 200 && res.status !== 201) {
throw new Error("Failed!");
}
return res.json();
})
.then(resData => {
const events = resData.data.events;
this.setState({ events: events });
})
.catch(err => {
console.log(err);
});
}
render() {
return (
<React.Fragment>
{this.state.creating && (
<React.Fragment>
<Backdrop />
<Modal
title="Add Event"
canCancel
canConfirm
onCancel={this.modalCancelHandler}
onConfirm={this.modalConfirmHandler}
>
<form>
<div className="form-control">
<label htmlFor="title">Title</label>
<input type="text" id="title" ref={this.titleElRef} />
</div>
<div className="form-control">
<label htmlFor="price">Price</label>
<input type="number" id="price" ref={this.priceElRef} />
</div>
<div className="form-control">
<label htmlFor="date">Date</label>
<input type="datetime-local" id="date" ref={this.dateElRef} />
</div>
<div className="form-control">
<label htmlFor="description">Description</label>
<textarea
id="description"
rows="4"
ref={this.descriptionElRef}
/>
</div>
</form>
</Modal>
</React.Fragment>
)}
{this.context.token && (
<div className="events-control">
<p>Share your own Events!</p>
<button className="btn" onClick={this.startCreateEventHandler}>
Create Event
</button>
</div>
)}
<EventList
events={this.state.events}
authUserId={this.context.userId}
/>
</React.Fragment>
);
}
}
export default EventsPage;
Add a spinner to be shown when data is loading
- Search for more information about Spinners on loading.io
- Create the
Spinner
component
components -> Spinner -> Spinner.css
.spinner {
display: flex;
justify-content: center;
align-items: center;
}
.lds-dual-ring {
display: inline-block;
width: 64px;
height: 64px;
}
.lds-dual-ring:after {
content: ' ';
display: block;
width: 46px;
height: 46px;
margin: 1px;
border-radius: 50%;
border: 5px solid #5101d1;
border-color: #5101d1 transparent #5101d1 transparent;
animation: lds-dual-ring 1.2s linear infinite;
}
@keyframes lds-dual-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
components -> Spinner -> Spinner.js
import React from 'react';
import './Spinner.css';
const spinner = () => (
<div className="spinner">
<div className="lds-dual-ring" />
</div>
);
export default spinner;
pages -> Event.js
import React, { Component } from "react";
import Modal from "../components/Modal/Modal";
import Backdrop from "../components/Backdrop/Backdrop";
import EventList from "../components/Events/EventList/EventList";
import Spinner from "../components/Spinner/Spinner";
import AuthContext from "../context/auth-context";
import "./Events.css";
class EventsPage extends Component {
state = {
creating: false,
events: [],
isLoading: false
};
static contextType = AuthContext;
constructor(props) {
super(props);
this.titleElRef = React.createRef();
this.priceElRef = React.createRef();
this.dateElRef = React.createRef();
this.descriptionElRef = React.createRef();
}
componentDidMount() {
this.fetchEvents();
}
startCreateEventHandler = () => {
this.setState({ creating: true });
};
modalConfirmHandler = () => {
this.setState({ creating: false });
const title = this.titleElRef.current.value;
const price = +this.priceElRef.current.value;
const date = this.dateElRef.current.value;
const description = this.descriptionElRef.current.value;
if (
title.trim().length === 0 ||
price <= 0 ||
date.trim().length === 0 ||
description.trim().length === 0
) {
return;
}
const event = { title, price, date, description };
console.log(event);
const requestBody = {
query: `
mutation {
createEvent(eventInput: {title: "${title}", description: "${description}", price: ${price}, date: "${date}"}) {
_id
title
description
date
price
}
}
`
};
const token = this.context.token;
fetch("http://localhost:9000/graphql", {
method: "POST",
body: JSON.stringify(requestBody),
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token
}
})
.then(res => {
if (res.status !== 200 && res.status !== 201) {
throw new Error("Failed!");
}
return res.json();
})
.then(resData => {
this.setState(prevState => {
const updatedEvents = [...prevState.events];
updatedEvents.push({
_id: resData.data.createEvent._id,
title: resData.data.createEvent.title,
description: resData.data.createEvent.description,
date: resData.data.createEvent.date,
price: resData.data.createEvent.price,
creator: {
_id: this.context.userId
}
});
return { events: updatedEvents };
});
})
.catch(err => {
console.log(err);
});
};
modalCancelHandler = () => {
this.setState({ creating: false });
};
fetchEvents() {
this.setState({ isLoading: true });
const requestBody = {
query: `
query {
events {
_id
title
description
date
price
creator {
_id
email
}
}
}
`
};
fetch("http://localhost:9000/graphql", {
method: "POST",
body: JSON.stringify(requestBody),
headers: {
"Content-Type": "application/json"
}
})
.then(res => {
if (res.status !== 200 && res.status !== 201) {
throw new Error("Failed!");
}
return res.json();
})
.then(resData => {
const events = resData.data.events;
this.setState({ events: events, isLoading: false });
})
.catch(err => {
console.log(err);
this.setState({ isLoading: false });
});
}
render() {
return (
<React.Fragment>
{this.state.creating && (
<React.Fragment>
<Backdrop />
<Modal
title="Add Event"
canCancel
canConfirm
onCancel={this.modalCancelHandler}
onConfirm={this.modalConfirmHandler}
>
<form>
<div className="form-control">
<label htmlFor="title">Title</label>
<input type="text" id="title" ref={this.titleElRef} />
</div>
<div className="form-control">
<label htmlFor="price">Price</label>
<input type="number" id="price" ref={this.priceElRef} />
</div>
<div className="form-control">
<label htmlFor="date">Date</label>
<input type="datetime-local" id="date" ref={this.dateElRef} />
</div>
<div className="form-control">
<label htmlFor="description">Description</label>
<textarea
id="description"
rows="4"
ref={this.descriptionElRef}
/>
</div>
</form>
</Modal>
</React.Fragment>
)}
{this.context.token && (
<div className="events-control">
<p>Share your own Events!</p>
<button className="btn" onClick={this.startCreateEventHandler}>
Create Event
</button>
</div>
)}
{this.state.isLoading ? (
<Spinner />
) : (
<EventList
events={this.state.events}
authUserId={this.context.userId}
/>
)}
</React.Fragment>
);
}
}
export default EventsPage;
Improve the Event detail component
components -> Events -> EventList -> EventList.js
import React from "react";
import EventItem from "./EventItem/EventItem";
import "./EventList.css";
const eventList = props => {
const events = props.events.map(event => {
return (
<EventItem
key={event._id}
eventId={event._id}
title={event.title}
price={event.price}
date={event.date}
userId={props.authUserId}
creatorId={event.creator._id}
onDetail={props.onViewDetail}
/>
);
});
return <ul className="event__list">{events}</ul>;
};
export default eventList;
components -> Events -> EventList -> EventItem -> EventItem.js
import React from "react";
import "./EventItem.css";
const eventItem = props => (
<li key={props.eventId} className="events__list-item">
<div>
<h1>{props.title}</h1>
<h2>
${props.price} - {new Date(props.date).toLocaleDateString()}
</h2>
</div>
<div>
{props.userId === props.creatorId ? (
<p>Your the owner of this event.</p>
) : (
<button
className="btn"
onClick={props.onDetail.bind(this, props.eventId)}
>
View Details
</button>
)}
</div>
</li>
);
export default eventItem;
components -> Events -> Modal -> Modal.js
import React from "react";
import "./Modal.css";
const modal = props => (
<div className="modal">
<header className="modal__header">
<h1>{props.title}</h1>
</header>
<section className="modal__content">{props.children}</section>
<section className="modal__actions">
{props.canCancel && (
<button className="btn" onClick={props.onCancel}>
Cancel
</button>
)}
{props.canConfirm && (
<button className="btn" onClick={props.onConfirm}>
{props.confirmText}
</button>
)}
</section>
</div>
);
export default modal;
pages -> Event.js
import React, { Component } from "react";
import Modal from "../components/Modal/Modal";
import Backdrop from "../components/Backdrop/Backdrop";
import EventList from "../components/Events/EventList/EventList";
import Spinner from "../components/Spinner/Spinner";
import AuthContext from "../context/auth-context";
import "./Events.css";
class EventsPage extends Component {
state = {
creating: false,
events: [],
isLoading: false,
selectedEvent: null
};
static contextType = AuthContext;
constructor(props) {
super(props);
this.titleElRef = React.createRef();
this.priceElRef = React.createRef();
this.dateElRef = React.createRef();
this.descriptionElRef = React.createRef();
}
componentDidMount() {
this.fetchEvents();
}
startCreateEventHandler = () => {
this.setState({ creating: true });
};
modalConfirmHandler = () => {
this.setState({ creating: false });
const title = this.titleElRef.current.value;
const price = +this.priceElRef.current.value;
const date = this.dateElRef.current.value;
const description = this.descriptionElRef.current.value;
if (
title.trim().length === 0 ||
price <= 0 ||
date.trim().length === 0 ||
description.trim().length === 0
) {
return;
}
const event = { title, price, date, description };
console.log(event);
const requestBody = {
query: `
mutation {
createEvent(eventInput: {title: "${title}", description: "${description}", price: ${price}, date: "${date}"}) {
_id
title
description
date
price
}
}
`
};
const token = this.context.token;
fetch("http://localhost:9000/graphql", {
method: "POST",
body: JSON.stringify(requestBody),
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token
}
})
.then(res => {
if (res.status !== 200 && res.status !== 201) {
throw new Error("Failed!");
}
return res.json();
})
.then(resData => {
this.setState(prevState => {
const updatedEvents = [...prevState.events];
updatedEvents.push({
_id: resData.data.createEvent._id,
title: resData.data.createEvent.title,
description: resData.data.createEvent.description,
date: resData.data.createEvent.date,
price: resData.data.createEvent.price,
creator: {
_id: this.context.userId
}
});
return { events: updatedEvents };
});
})
.catch(err => {
console.log(err);
});
};
modalCancelHandler = () => {
this.setState({ creating: false, selectedEvent: null });
};
fetchEvents() {
this.setState({ isLoading: true });
const requestBody = {
query: `
query {
events {
_id
title
description
date
price
creator {
_id
email
}
}
}
`
};
fetch("http://localhost:9000/graphql", {
method: "POST",
body: JSON.stringify(requestBody),
headers: {
"Content-Type": "application/json"
}
})
.then(res => {
if (res.status !== 200 && res.status !== 201) {
throw new Error("Failed!");
}
return res.json();
})
.then(resData => {
const events = resData.data.events;
this.setState({ events: events, isLoading: false });
})
.catch(err => {
console.log(err);
this.setState({ isLoading: false });
});
}
showDetailHandler = eventId => {
this.setState(prevState => {
const selectedEvent = prevState.events.find(e => e._id === eventId);
return { selectedEvent: selectedEvent };
});
};
bookEventHandler = () => {};
render() {
return (
<React.Fragment>
{(this.state.creating || this.state.selectedEvent) && <Backdrop />}
{this.state.creating && (
<Modal
title="Add Event"
canCancel
canConfirm
onCancel={this.modalCancelHandler}
onConfirm={this.modalConfirmHandler}
confirmText="Confirm"
>
<form>
<div className="form-control">
<label htmlFor="title">Title</label>
<input type="text" id="title" ref={this.titleElRef} />
</div>
<div className="form-control">
<label htmlFor="price">Price</label>
<input type="number" id="price" ref={this.priceElRef} />
</div>
<div className="form-control">
<label htmlFor="date">Date</label>
<input type="datetime-local" id="date" ref={this.dateElRef} />
</div>
<div className="form-control">
<label htmlFor="description">Description</label>
<textarea
id="description"
rows="4"
ref={this.descriptionElRef}
/>
</div>
</form>
</Modal>
)}
{this.state.selectedEvent && (
<Modal
title={this.state.selectedEvent.title}
canCancel
canConfirm
onCancel={this.modalCancelHandler}
onConfirm={this.bookEventHandler}
confirmText="Book"
>
<h1>{this.state.selectedEvent.title}</h1>
<h2>
${this.state.selectedEvent.price} -{" "}
{new Date(this.state.selectedEvent.date).toLocaleDateString()}
</h2>
<p>{this.state.selectedEvent.description}</p>
</Modal>
)}
{this.context.token && (
<div className="events-control">
<p>Share your own Events!</p>
<button className="btn" onClick={this.startCreateEventHandler}>
Create Event
</button>
</div>
)}
{this.state.isLoading ? (
<Spinner />
) : (
<EventList
events={this.state.events}
authUserId={this.context.userId}
onViewDetail={this.showDetailHandler}
/>
)}
</React.Fragment>
);
}
}
export default EventsPage;
18. Creating and Displaying Bookings 14:48
Make a request to make a booking
- Develop the
bookEventHandler
function.
pages -> Event.js
import React, { Component } from "react";
import Modal from "../components/Modal/Modal";
import Backdrop from "../components/Backdrop/Backdrop";
import EventList from "../components/Events/EventList/EventList";
import Spinner from "../components/Spinner/Spinner";
import AuthContext from "../context/auth-context";
import "./Events.css";
class EventsPage extends Component {
state = {
creating: false,
events: [],
isLoading: false,
selectedEvent: null
};
static contextType = AuthContext;
constructor(props) {
super(props);
this.titleElRef = React.createRef();
this.priceElRef = React.createRef();
this.dateElRef = React.createRef();
this.descriptionElRef = React.createRef();
}
componentDidMount() {
this.fetchEvents();
}
startCreateEventHandler = () => {
this.setState({ creating: true });
};
modalConfirmHandler = () => {
this.setState({ creating: false });
const title = this.titleElRef.current.value;
const price = +this.priceElRef.current.value;
const date = this.dateElRef.current.value;
const description = this.descriptionElRef.current.value;
if (
title.trim().length === 0 ||
price <= 0 ||
date.trim().length === 0 ||
description.trim().length === 0
) {
return;
}
const event = { title, price, date, description };
console.log(event);
const requestBody = {
query: `
mutation {
createEvent(eventInput: {title: "${title}", description: "${description}", price: ${price}, date: "${date}"}) {
_id
title
description
date
price
}
}
`
};
const token = this.context.token;
fetch("http://localhost:9000/graphql", {
method: "POST",
body: JSON.stringify(requestBody),
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token
}
})
.then(res => {
if (res.status !== 200 && res.status !== 201) {
throw new Error("Failed!");
}
return res.json();
})
.then(resData => {
this.setState(prevState => {
const updatedEvents = [...prevState.events];
updatedEvents.push({
_id: resData.data.createEvent._id,
title: resData.data.createEvent.title,
description: resData.data.createEvent.description,
date: resData.data.createEvent.date,
price: resData.data.createEvent.price,
creator: {
_id: this.context.userId
}
});
return { events: updatedEvents };
});
})
.catch(err => {
console.log(err);
});
};
modalCancelHandler = () => {
this.setState({ creating: false, selectedEvent: null });
};
fetchEvents() {
this.setState({ isLoading: true });
const requestBody = {
query: `
query {
events {
_id
title
description
date
price
creator {
_id
email
}
}
}
`
};
fetch("http://localhost:9000/graphql", {
method: "POST",
body: JSON.stringify(requestBody),
headers: {
"Content-Type": "application/json"
}
})
.then(res => {
if (res.status !== 200 && res.status !== 201) {
throw new Error("Failed!");
}
return res.json();
})
.then(resData => {
const events = resData.data.events;
this.setState({ events: events, isLoading: false });
})
.catch(err => {
console.log(err);
this.setState({ isLoading: false });
});
}
showDetailHandler = eventId => {
this.setState(prevState => {
const selectedEvent = prevState.events.find(e => e._id === eventId);
return { selectedEvent: selectedEvent };
});
};
bookEventHandler = () => {
if (!this.context.token) {
this.setState({ selectedEvent: null });
return;
}
const requestBody = {
query: `
mutation {
bookEvent(eventId: "${this.state.selectedEvent._id}") {
_id
createdAt
updatedAt
}
}
`
};
fetch("http://localhost:9000/graphql", {
method: "POST",
body: JSON.stringify(requestBody),
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + this.context.token
}
})
.then(res => {
if (res.status !== 200 && res.status !== 201) {
throw new Error("Failed!");
}
return res.json();
})
.then(resData => {
console.log(resData);
this.setState({ selectedEvent: null });
})
.catch(err => {
console.log(err);
});
};
render() {
return (
<React.Fragment>
{(this.state.creating || this.state.selectedEvent) && <Backdrop />}
{this.state.creating && (
<Modal
title="Add Event"
canCancel
canConfirm
onCancel={this.modalCancelHandler}
onConfirm={this.modalConfirmHandler}
confirmText="Confirm"
>
<form>
<div className="form-control">
<label htmlFor="title">Title</label>
<input type="text" id="title" ref={this.titleElRef} />
</div>
<div className="form-control">
<label htmlFor="price">Price</label>
<input type="number" id="price" ref={this.priceElRef} />
</div>
<div className="form-control">
<label htmlFor="date">Date</label>
<input type="datetime-local" id="date" ref={this.dateElRef} />
</div>
<div className="form-control">
<label htmlFor="description">Description</label>
<textarea
id="description"
rows="4"
ref={this.descriptionElRef}
/>
</div>
</form>
</Modal>
)}
{this.state.selectedEvent && (
<Modal
title={this.state.selectedEvent.title}
canCancel
canConfirm
onCancel={this.modalCancelHandler}
onConfirm={this.bookEventHandler}
confirmText={this.context.token ? "Book" : "Confirm"}
>
<h1>{this.state.selectedEvent.title}</h1>
<h2>
${this.state.selectedEvent.price} -{" "}
{new Date(this.state.selectedEvent.date).toLocaleDateString()}
</h2>
<p>{this.state.selectedEvent.description}</p>
</Modal>
)}
{this.context.token && (
<div className="events-control">
<p>Share your own Events!</p>
<button className="btn" onClick={this.startCreateEventHandler}>
Create Event
</button>
</div>
)}
{this.state.isLoading ? (
<Spinner />
) : (
<EventList
events={this.state.events}
authUserId={this.context.userId}
onViewDetail={this.showDetailHandler}
/>
)}
</React.Fragment>
);
}
}
export default EventsPage;
bookings.js
document to show the bookings for an user
Modify the pages -> Bookings.js
import React, { Component } from "react";
import Spinner from "../components/Spinner/Spinner";
import AuthContext from "../context/auth-context";
class BookingsPage extends Component {
state = {
isLoading: false,
bookings: []
};
static contextType = AuthContext;
componentDidMount() {
this.fetchBookings();
}
fetchBookings = () => {
this.setState({ isLoading: true });
const requestBody = {
query: `
query {
bookings {
_id
createdAt
event {
_id
title
date
}
}
}
`
};
fetch("http://localhost:9000/graphql", {
method: "POST",
body: JSON.stringify(requestBody),
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + this.context.token
}
})
.then(res => {
if (res.status !== 200 && res.status !== 201) {
throw new Error("Failed!");
}
return res.json();
})
.then(resData => {
const bookings = resData.data.bookings;
this.setState({ bookings: bookings, isLoading: false });
})
.catch(err => {
console.log(err);
this.setState({ isLoading: false });
});
};
render() {
return (
<React.Fragment>
{this.state.isLoading ? (
<Spinner />
) : (
<ul>
{this.state.bookings.map(booking => (
<li key={booking._id}>
{booking.event.title} -{" "}
{new Date(booking.createdAt).toLocaleDateString()}
</li>
))}
</ul>
)}
</React.Fragment>
);
}
}
export default BookingsPage;
- Include the
isActive
variable onEvents.js
pages -> Events.js
import React, { Component } from "react";
import Modal from "../components/Modal/Modal";
import Backdrop from "../components/Backdrop/Backdrop";
import EventList from "../components/Events/EventList/EventList";
import Spinner from "../components/Spinner/Spinner";
import AuthContext from "../context/auth-context";
import "./Events.css";
class EventsPage extends Component {
state = {
creating: false,
events: [],
isLoading: false,
selectedEvent: null
};
isActive = true;
static contextType = AuthContext;
constructor(props) {
super(props);
this.titleElRef = React.createRef();
this.priceElRef = React.createRef();
this.dateElRef = React.createRef();
this.descriptionElRef = React.createRef();
}
componentDidMount() {
this.fetchEvents();
}
startCreateEventHandler = () => {
this.setState({ creating: true });
};
modalConfirmHandler = () => {
this.setState({ creating: false });
const title = this.titleElRef.current.value;
const price = +this.priceElRef.current.value;
const date = this.dateElRef.current.value;
const description = this.descriptionElRef.current.value;
if (
title.trim().length === 0 ||
price <= 0 ||
date.trim().length === 0 ||
description.trim().length === 0
) {
return;
}
const event = { title, price, date, description };
console.log(event);
const requestBody = {
query: `
mutation {
createEvent(eventInput: {title: "${title}", description: "${description}", price: ${price}, date: "${date}"}) {
_id
title
description
date
price
}
}
`
};
const token = this.context.token;
fetch("http://localhost:9000/graphql", {
method: "POST",
body: JSON.stringify(requestBody),
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token
}
})
.then(res => {
if (res.status !== 200 && res.status !== 201) {
throw new Error("Failed!");
}
return res.json();
})
.then(resData => {
this.setState(prevState => {
const updatedEvents = [...prevState.events];
updatedEvents.push({
_id: resData.data.createEvent._id,
title: resData.data.createEvent.title,
description: resData.data.createEvent.description,
date: resData.data.createEvent.date,
price: resData.data.createEvent.price,
creator: {
_id: this.context.userId
}
});
return { events: updatedEvents };
});
})
.catch(err => {
console.log(err);
});
};
modalCancelHandler = () => {
this.setState({ creating: false, selectedEvent: null });
};
fetchEvents() {
this.setState({ isLoading: true });
const requestBody = {
query: `
query {
events {
_id
title
description
date
price
creator {
_id
email
}
}
}
`
};
fetch("http://localhost:9000/graphql", {
method: "POST",
body: JSON.stringify(requestBody),
headers: {
"Content-Type": "application/json"
}
})
.then(res => {
if (res.status !== 200 && res.status !== 201) {
throw new Error("Failed!");
}
return res.json();
})
.then(resData => {
const events = resData.data.events;
if (this.isActive) {
this.setState({ events: events, isLoading: false });
}
})
.catch(err => {
console.log(err);
if (this.isActive) {
this.setState({ isLoading: false });
}
});
}
showDetailHandler = eventId => {
this.setState(prevState => {
const selectedEvent = prevState.events.find(e => e._id === eventId);
return { selectedEvent: selectedEvent };
});
};
bookEventHandler = () => {
if (!this.context.token) {
this.setState({ selectedEvent: null });
return;
}
const requestBody = {
query: `
mutation {
bookEvent(eventId: "${this.state.selectedEvent._id}") {
_id
createdAt
updatedAt
}
}
`
};
fetch("http://localhost:9000/graphql", {
method: "POST",
body: JSON.stringify(requestBody),
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + this.context.token
}
})
.then(res => {
if (res.status !== 200 && res.status !== 201) {
throw new Error("Failed!");
}
return res.json();
})
.then(resData => {
console.log(resData);
this.setState({ selectedEvent: null });
})
.catch(err => {
console.log(err);
});
};
componentWillUnmount() {
this.isActive = false;
}
render() {
return (
<React.Fragment>
{(this.state.creating || this.state.selectedEvent) && <Backdrop />}
{this.state.creating && (
<Modal
title="Add Event"
canCancel
canConfirm
onCancel={this.modalCancelHandler}
onConfirm={this.modalConfirmHandler}
confirmText="Confirm"
>
<form>
<div className="form-control">
<label htmlFor="title">Title</label>
<input type="text" id="title" ref={this.titleElRef} />
</div>
<div className="form-control">
<label htmlFor="price">Price</label>
<input type="number" id="price" ref={this.priceElRef} />
</div>
<div className="form-control">
<label htmlFor="date">Date</label>
<input type="datetime-local" id="date" ref={this.dateElRef} />
</div>
<div className="form-control">
<label htmlFor="description">Description</label>
<textarea
id="description"
rows="4"
ref={this.descriptionElRef}
/>
</div>
</form>
</Modal>
)}
{this.state.selectedEvent && (
<Modal
title={this.state.selectedEvent.title}
canCancel
canConfirm
onCancel={this.modalCancelHandler}
onConfirm={this.bookEventHandler}
confirmText={this.context.token ? "Book" : "Confirm"}
>
<h1>{this.state.selectedEvent.title}</h1>
<h2>
${this.state.selectedEvent.price} -{" "}
{new Date(this.state.selectedEvent.date).toLocaleDateString()}
</h2>
<p>{this.state.selectedEvent.description}</p>
</Modal>
)}
{this.context.token && (
<div className="events-control">
<p>Share your own Events!</p>
<button className="btn" onClick={this.startCreateEventHandler}>
Create Event
</button>
</div>
)}
{this.state.isLoading ? (
<Spinner />
) : (
<EventList
events={this.state.events}
authUserId={this.context.userId}
onViewDetail={this.showDetailHandler}
/>
)}
</React.Fragment>
);
}
}
export default EventsPage;
19. Cancelling Bookings 20:24
Make booking list look nicer.
- Ceate the
Bookings
folder inside thecomponents
folder and then theBookingList
component
components -> Bookings -> BookingList -> BookingList.js
import React from "react";
import "./BookingList.css";
const bookingList = props => (
<ul className="bookings__list">
{props.bookings.map(booking => {
return (
<li key={booking._id} className="bookings__item">
<div className="bookings__item-data">
{booking.event.title} -{" "}
{new Date(booking.createdAt).toLocaleDateString()}
</div>
<div className="bookings__item-actions">
<button className="btn">Cancel</button>
</div>
</li>
);
})}
</ul>
);
export default bookingList;
components -> Bookings -> BookingList -> BookingList.css
.bookings__list {
list-style: none;
margin: 0 auto;
padding: 0;
width: 40rem;
max-width: 90%;
}
.bookings__item {
margin: 0.5rem 0;
padding: 0.5rem;
border: 1px solid #5101d1;
display: flex;
justify-content: space-between;
align-items: center;
}
- Modify the
Booking.js
document to use the newBookingList
component.
pages -> Booking.js
import React, { Component } from "react";
import Spinner from "../components/Spinner/Spinner";
import AuthContext from "../context/auth-context";
import BookingList from "../components/Bookings/BookingList/BookingList";
class BookingsPage extends Component {
state = {
isLoading: false,
bookings: []
};
static contextType = AuthContext;
componentDidMount() {
this.fetchBookings();
}
fetchBookings = () => {
this.setState({ isLoading: true });
const requestBody = {
query: `
query {
bookings {
_id
createdAt
event {
_id
title
date
}
}
}
`
};
fetch("http://localhost:9000/graphql", {
method: "POST",
body: JSON.stringify(requestBody),
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + this.context.token
}
})
.then(res => {
if (res.status !== 200 && res.status !== 201) {
throw new Error("Failed!");
}
return res.json();
})
.then(resData => {
const bookings = resData.data.bookings;
this.setState({ bookings: bookings, isLoading: false });
})
.catch(err => {
console.log(err);
this.setState({ isLoading: false });
});
};
render() {
return (
<React.Fragment>
{this.state.isLoading ? (
<Spinner />
) : (
<BookingList
bookings={this.state.bookings}
/>
)}
</React.Fragment>
);
}
}
export default BookingsPage;
- Develop the feature to cancel a booking
components -> Bookings -> BookingList -> BookingList.js
import React from "react";
import "./BookingList.css";
const bookingList = props => (
<ul className="bookings__list">
{props.bookings.map(booking => {
return (
<li key={booking._id} className="bookings__item">
<div className="bookings__item-data">
{booking.event.title} -{" "}
{new Date(booking.createdAt).toLocaleDateString()}
</div>
<div className="bookings__item-actions">
<button
className="btn"
onClick={props.onDelete.bind(this, booking._id)}
>
Cancel
</button>
</div>
</li>
);
})}
</ul>
);
export default bookingList;
pages -> Booking.js
import React, { Component } from "react";
import Spinner from "../components/Spinner/Spinner";
import AuthContext from "../context/auth-context";
import BookingList from "../components/Bookings/BookingList/BookingList";
class BookingsPage extends Component {
state = {
isLoading: false,
bookings: []
};
static contextType = AuthContext;
componentDidMount() {
this.fetchBookings();
}
fetchBookings = () => {
this.setState({ isLoading: true });
const requestBody = {
query: `
query {
bookings {
_id
createdAt
event {
_id
title
date
}
}
}
`
};
fetch("http://localhost:9000/graphql", {
method: "POST",
body: JSON.stringify(requestBody),
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + this.context.token
}
})
.then(res => {
if (res.status !== 200 && res.status !== 201) {
throw new Error("Failed!");
}
return res.json();
})
.then(resData => {
const bookings = resData.data.bookings;
this.setState({ bookings: bookings, isLoading: false });
})
.catch(err => {
console.log(err);
this.setState({ isLoading: false });
});
};
deleteBookingHandler = bookingId => {
this.setState({ isLoading: true });
const requestBody = {
query: `
mutation {
cancelBooking(bookingId: "${bookingId}") {
_id
title
}
}
`
};
fetch("http://localhost:9000/graphql", {
method: "POST",
body: JSON.stringify(requestBody),
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + this.context.token
}
})
.then(res => {
if (res.status !== 200 && res.status !== 201) {
throw new Error("Failed!");
}
return res.json();
})
.then(resData => {
this.setState(prevState => {
const updatedBookings = prevState.bookings.filter(booking => {
return booking._id !== bookingId;
});
return { bookings: updatedBookings, isLoading: false };
});
})
.catch(err => {
console.log(err);
this.setState({ isLoading: false });
});
};
render() {
return (
<React.Fragment>
{this.state.isLoading ? (
<Spinner />
) : (
<BookingList
bookings={this.state.bookings}
onDelete={this.deleteBookingHandler}
/>
)}
</React.Fragment>
);
}
}
export default BookingsPage;
- Modify the backend to avoid a user removing bookings that does not belong to them
graphql -> resolvers -> bookings.js
const Booking = require("../../models/booking");
const Event = require("../../models/event");
const { transformBooking, transformEvent } = require("./merge");
module.exports = {
bookings: async (args, req) => {
try {
if (!req.isAuth) {
throw new Error("Unauthenticated.");
}
const bookings = await Booking.find({ user: req.userId });
return bookings.map(booking => {
return transformBooking(booking);
});
} catch (err) {
console.log(err);
throw err;
}
},
bookEvent: async (args, req) => {
try {
if (!req.isAuth) {
throw new Error("Unauthenticated.");
}
const fectchedEvent = await Event.findById(args.eventId);
if (!fectchedEvent) {
throw new Error("Event Id does not exist");
}
const booking = new Booking({
user: req.userId,
event: fectchedEvent
});
const result = await booking.save();
return transformBooking(result);
} catch (err) {
console.log(err);
throw err;
}
},
cancelBooking: async (args, req) => {
try {
if (!req.isAuth) {
throw new Error("Unauthenticated.");
}
const booking = await Booking.findById(args.bookingId).populate("event");
if (!booking) {
throw new Error("Booking Id does not exist");
}
await Booking.deleteOne({ _id: args.bookingId });
return transformEvent(booking.event);
} catch (err) {
console.log(err);
throw err;
}
}
};
20. Using Dataloader 17:44
Facebook dataloader to get the events data properly
Use the- Install dataloader
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/GraphQL/graphql-react-event-booking (master)$ npm i dataloader
npm WARN graphql-react-event-booking@1.0.0 No repository field.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.4 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.4: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
+ dataloader@1.4.0
added 1 package from 3 contributors and audited 2469 packages in 9.178s
found 0 vulnerabilities
- Modify the
merge.js
document to implementdataloader
graphql -> resolvers -> merge.js
const DataLoader = require("dataloader");
const Event = require("../../models/event");
const User = require("../../models/user");
const { dateToString } = require("../../helpers/date");
const eventLoader = new DataLoader(eventIds => {
return events(eventIds);
});
const userLoader = new DataLoader(userIds => {
return User.find({ _id: { $in: userIds } });
});
const events = async eventIds => {
try {
const events = await Event.find({ _id: { $in: eventIds } });
return events.map(event => {
return transformEvent(event);
});
} catch (err) {
console.log(err);
throw err;
}
};
const singleEvent = async eventId => {
try {
const event = await eventLoader.load(eventId.toString());
return event;
} catch (err) {
console.log(err);
throw err;
}
};
const user = async userId => {
try {
const user = await userLoader.load(userId.toString());
return {
...user._doc,
_id: user.id,
createdEvents: eventLoader.loadMany.bind(this, user._doc.createdEvents)
};
} catch (err) {
console.log(err);
throw err;
}
};
const transformEvent = event => {
if (event) {
return {
...event._doc,
_id: event.id,
date: dateToString(event._doc.date),
creator: user.bind(this, event._doc.creator)
};
}
return null;
};
const transformBooking = booking => {
if (booking) {
return {
...booking._doc,
_id: booking.id,
user: user.bind(this, booking._doc.user),
event: singleEvent.bind(this, booking._doc.event),
createdAt: dateToString(booking._doc.createdAt),
updatedAt: dateToString(booking._doc.updatedAt)
};
}
return null;
};
exports.transformEvent = transformEvent;
exports.transformBooking = transformBooking;
21. Improving Queries and Bugfixing 16:13
- There is a bug that must be fixed in the
merge.js
document.
- The
user
function must be amended.
graphql -> resolvers -> merge.js
const DataLoader = require("dataloader");
const Event = require("../../models/event");
const User = require("../../models/user");
const { dateToString } = require("../../helpers/date");
const eventLoader = new DataLoader(eventIds => {
return events(eventIds);
});
const userLoader = new DataLoader(userIds => {
return User.find({ _id: { $in: userIds } });
});
const events = async eventIds => {
try {
const events = await Event.find({ _id: { $in: eventIds } });
return events.map(event => {
return transformEvent(event);
});
} catch (err) {
console.log(err);
throw err;
}
};
const singleEvent = async eventId => {
try {
const event = await eventLoader.load(eventId.toString());
return event;
} catch (err) {
console.log(err);
throw err;
}
};
const user = async userId => {
try {
const user = await userLoader.load(userId.toString());
return {
...user._doc,
_id: user.id,
createdEvents: () => eventLoader.loadMany(user._doc.createdEvents)
};
} catch (err) {
console.log(err);
throw err;
}
};
const transformEvent = event => {
if (event) {
return {
...event._doc,
_id: event.id,
date: dateToString(event._doc.date),
creator: user.bind(this, event._doc.creator)
};
}
return null;
};
const transformBooking = booking => {
if (booking) {
return {
...booking._doc,
_id: booking.id,
user: user.bind(this, booking._doc.user),
event: singleEvent.bind(this, booking._doc.event),
createdAt: dateToString(booking._doc.createdAt),
updatedAt: dateToString(booking._doc.updatedAt)
};
}
return null;
};
exports.transformEvent = transformEvent;
exports.transformBooking = transformBooking;
There is another improvement in the
Bookings.js
document with the way we inject the parameters to the queries.The
deleteBookingHandler
function is changed to add thevariables
object to the request.
pages -> Bookings.js
import React, { Component } from "react";
import Spinner from "../components/Spinner/Spinner";
import AuthContext from "../context/auth-context";
import BookingList from "../components/Bookings/BookingList/BookingList";
class BookingsPage extends Component {
state = {
isLoading: false,
bookings: []
};
static contextType = AuthContext;
componentDidMount() {
this.fetchBookings();
}
fetchBookings = () => {
this.setState({ isLoading: true });
const requestBody = {
query: `
query {
bookings {
_id
createdAt
event {
_id
title
date
}
}
}
`
};
fetch("http://localhost:9000/graphql", {
method: "POST",
body: JSON.stringify(requestBody),
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + this.context.token
}
})
.then(res => {
if (res.status !== 200 && res.status !== 201) {
throw new Error("Failed!");
}
return res.json();
})
.then(resData => {
const bookings = resData.data.bookings;
this.setState({ bookings: bookings, isLoading: false });
})
.catch(err => {
console.log(err);
this.setState({ isLoading: false });
});
};
deleteBookingHandler = bookingId => {
this.setState({ isLoading: true });
const requestBody = {
query: `
mutation CancelBooking($id: ID!) {
cancelBooking(bookingId: $id) {
_id
title
}
}
`,
variables: {
id: bookingId
}
};
fetch("http://localhost:9000/graphql", {
method: "POST",
body: JSON.stringify(requestBody),
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + this.context.token
}
})
.then(res => {
if (res.status !== 200 && res.status !== 201) {
throw new Error("Failed!");
}
return res.json();
})
.then(resData => {
this.setState(prevState => {
const updatedBookings = prevState.bookings.filter(booking => {
return booking._id !== bookingId;
});
return { bookings: updatedBookings, isLoading: false };
});
})
.catch(err => {
console.log(err);
this.setState({ isLoading: false });
});
};
render() {
return (
<React.Fragment>
{this.state.isLoading ? (
<Spinner />
) : (
<BookingList
bookings={this.state.bookings}
onDelete={this.deleteBookingHandler}
/>
)}
</React.Fragment>
);
}
}
export default BookingsPage;
- We can do the same for the
submitHandler
function on theAuth.js
document
pages -> Auth.js
import React, { Component } from "react";
import "./Auth.css";
import AuthContext from "../context/auth-context";
class AuthPage extends Component {
state = {
isLogin: true
};
static contextType = AuthContext;
constructor(props) {
super(props);
this.emailEl = React.createRef();
this.passwordEl = React.createRef();
}
switchModeHandler = () => {
this.setState(prevSate => {
return { isLogin: !prevSate.isLogin };
});
};
submitHandler = event => {
event.preventDefault();
const email = this.emailEl.current.value;
const password = this.passwordEl.current.value;
if (email.trim().length === 0 || password.trim().length === 0) {
return;
}
let requestBody = {
query: `
query Login($email: String!, $password: String!) {
login(email: $email, password: $password) {
userId
token
tokenExpiration
}
}
`,
variables: {
email: email,
password: password
}
};
if (!this.state.isLogin) {
requestBody = {
query: `
mutation CreateUser($email: String!, $password: String!) {
createUser(userInput: {email: $email, password: $password}) {
_id
email
}
}
`,
variables: {
email: email,
password: password
}
};
}
fetch("http://localhost:9000/graphql", {
method: "POST",
body: JSON.stringify(requestBody),
headers: {
"Content-Type": "application/json"
}
})
.then(res => {
if (res.status !== 200 && res.status !== 201) {
throw new Error("Failed!");
}
return res.json();
})
.then(resData => {
if (resData.data.login && resData.data.login.token) {
this.context.login(
resData.data.login.token,
resData.data.login.userId,
resData.data.login.tokenExpiration
);
}
})
.catch(err => {
console.log(err);
});
};
render() {
return (
<form className="auth-form" onSubmit={this.submitHandler}>
<div className="form-control">
<label htmlFor="email">E-Mail</label>
<input type="email" id="email" ref={this.emailEl} />
</div>
<div className="form-control">
<label htmlFor="password">Password</label>
<input type="password" id="password" ref={this.passwordEl} />
</div>
<div className="form-actions">
<button type="submit">Submit</button>
<button type="button" onClick={this.switchModeHandler}>
Sign to {this.state.isLogin ? "Signup" : "Login"}
</button>
</div>
</form>
);
}
}
export default AuthPage;
- We can do the same for the main
requestBody
constant and thebookEventHandler
on theMutation.js
document
pages -> Events.js
import React, { Component } from "react";
import Modal from "../components/Modal/Modal";
import Backdrop from "../components/Backdrop/Backdrop";
import EventList from "../components/Events/EventList/EventList";
import Spinner from "../components/Spinner/Spinner";
import AuthContext from "../context/auth-context";
import "./Events.css";
class EventsPage extends Component {
state = {
creating: false,
events: [],
isLoading: false,
selectedEvent: null
};
isActive = true;
static contextType = AuthContext;
constructor(props) {
super(props);
this.titleElRef = React.createRef();
this.priceElRef = React.createRef();
this.dateElRef = React.createRef();
this.descriptionElRef = React.createRef();
}
componentDidMount() {
this.fetchEvents();
}
startCreateEventHandler = () => {
this.setState({ creating: true });
};
modalConfirmHandler = () => {
this.setState({ creating: false });
const title = this.titleElRef.current.value;
const price = +this.priceElRef.current.value;
const date = this.dateElRef.current.value;
const description = this.descriptionElRef.current.value;
if (
title.trim().length === 0 ||
price <= 0 ||
date.trim().length === 0 ||
description.trim().length === 0
) {
return;
}
const event = { title, price, date, description };
console.log(event);
const requestBody = {
query: `
mutation CreateEvent($title: String!, $desc: String!, $price: Float!, $date: String!) {
createEvent(eventInput: {title: $title, description: $desc, price: $price, date: $date}) {
_id
title
description
date
price
}
}
`,
variables: {
title: title,
desc: description,
price: price,
date: date
}
};
const token = this.context.token;
fetch("http://localhost:9000/graphql", {
method: "POST",
body: JSON.stringify(requestBody),
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token
}
})
.then(res => {
if (res.status !== 200 && res.status !== 201) {
throw new Error("Failed!");
}
return res.json();
})
.then(resData => {
this.setState(prevState => {
const updatedEvents = [...prevState.events];
updatedEvents.push({
_id: resData.data.createEvent._id,
title: resData.data.createEvent.title,
description: resData.data.createEvent.description,
date: resData.data.createEvent.date,
price: resData.data.createEvent.price,
creator: {
_id: this.context.userId
}
});
return { events: updatedEvents };
});
})
.catch(err => {
console.log(err);
});
};
modalCancelHandler = () => {
this.setState({ creating: false, selectedEvent: null });
};
fetchEvents() {
this.setState({ isLoading: true });
const requestBody = {
query: `
query {
events {
_id
title
description
date
price
creator {
_id
email
}
}
}
`
};
fetch("http://localhost:9000/graphql", {
method: "POST",
body: JSON.stringify(requestBody),
headers: {
"Content-Type": "application/json"
}
})
.then(res => {
if (res.status !== 200 && res.status !== 201) {
throw new Error("Failed!");
}
return res.json();
})
.then(resData => {
const events = resData.data.events;
if (this.isActive) {
this.setState({ events: events, isLoading: false });
}
})
.catch(err => {
console.log(err);
if (this.isActive) {
this.setState({ isLoading: false });
}
});
}
showDetailHandler = eventId => {
this.setState(prevState => {
const selectedEvent = prevState.events.find(e => e._id === eventId);
return { selectedEvent: selectedEvent };
});
};
bookEventHandler = () => {
if (!this.context.token) {
this.setState({ selectedEvent: null });
return;
}
const requestBody = {
query: `
mutation BookEvent($id: ID!) {
bookEvent(eventId: $id) {
_id
createdAt
updatedAt
}
}
`,
variables: {
id: this.state.selectedEvent._id
}
};
fetch("http://localhost:9000/graphql", {
method: "POST",
body: JSON.stringify(requestBody),
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + this.context.token
}
})
.then(res => {
if (res.status !== 200 && res.status !== 201) {
throw new Error("Failed!");
}
return res.json();
})
.then(resData => {
console.log(resData);
this.setState({ selectedEvent: null });
})
.catch(err => {
console.log(err);
});
};
componentWillUnmount() {
this.isActive = false;
}
render() {
return (
<React.Fragment>
{(this.state.creating || this.state.selectedEvent) && <Backdrop />}
{this.state.creating && (
<Modal
title="Add Event"
canCancel
canConfirm
onCancel={this.modalCancelHandler}
onConfirm={this.modalConfirmHandler}
confirmText="Confirm"
>
<form>
<div className="form-control">
<label htmlFor="title">Title</label>
<input type="text" id="title" ref={this.titleElRef} />
</div>
<div className="form-control">
<label htmlFor="price">Price</label>
<input type="number" id="price" ref={this.priceElRef} />
</div>
<div className="form-control">
<label htmlFor="date">Date</label>
<input type="datetime-local" id="date" ref={this.dateElRef} />
</div>
<div className="form-control">
<label htmlFor="description">Description</label>
<textarea
id="description"
rows="4"
ref={this.descriptionElRef}
/>
</div>
</form>
</Modal>
)}
{this.state.selectedEvent && (
<Modal
title={this.state.selectedEvent.title}
canCancel
canConfirm
onCancel={this.modalCancelHandler}
onConfirm={this.bookEventHandler}
confirmText={this.context.token ? "Book" : "Confirm"}
>
<h1>{this.state.selectedEvent.title}</h1>
<h2>
${this.state.selectedEvent.price} -{" "}
{new Date(this.state.selectedEvent.date).toLocaleDateString()}
</h2>
<p>{this.state.selectedEvent.description}</p>
</Modal>
)}
{this.context.token && (
<div className="events-control">
<p>Share your own Events!</p>
<button className="btn" onClick={this.startCreateEventHandler}>
Create Event
</button>
</div>
)}
{this.state.isLoading ? (
<Spinner />
) : (
<EventList
events={this.state.events}
authUserId={this.context.userId}
onViewDetail={this.showDetailHandler}
/>
)}
</React.Fragment>
);
}
}
export default EventsPage;
22. Creating Charts 38:05
Clear all the existing events and add 3 new ones
Create the new component to see the chart
- Create the new
BookingsChart.js
component in thecomponents\Bookins\BookingsChart
folder.
components\Bookins\BookingsChart\BookingsChart.js
import React from "react";
const bookingsChart = props => {
return <p>The Chart!</p>;
};
export default bookingsChart;
- Modify the
Bookings.js
page to put a message when there are no bookings.
pages\Bookings.js
import React, { Component } from "react";
import Spinner from "../components/Spinner/Spinner";
import AuthContext from "../context/auth-context";
import BookingList from "../components/Bookings/BookingList/BookingList";
import BookingsChart from "../components/Bookings/BookingsChart/BookingsChart";
class BookingsPage extends Component {
state = {
isLoading: false,
bookings: [],
outputType: "list"
};
static contextType = AuthContext;
componentDidMount() {
this.fetchBookings();
}
fetchBookings = () => {
this.setState({ isLoading: true });
const requestBody = {
query: `
query {
bookings {
_id
createdAt
event {
_id
title
date
}
}
}
`
};
fetch("http://localhost:9000/graphql", {
method: "POST",
body: JSON.stringify(requestBody),
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + this.context.token
}
})
.then(res => {
if (res.status !== 200 && res.status !== 201) {
throw new Error("Failed!");
}
return res.json();
})
.then(resData => {
const bookings = resData.data.bookings;
this.setState({ bookings: bookings, isLoading: false });
})
.catch(err => {
console.log(err);
this.setState({ isLoading: false });
});
};
deleteBookingHandler = bookingId => {
this.setState({ isLoading: true });
const requestBody = {
query: `
mutation CancelBooking($id: ID!) {
cancelBooking(bookingId: $id) {
_id
title
}
}
`,
variables: {
id: bookingId
}
};
fetch("http://localhost:9000/graphql", {
method: "POST",
body: JSON.stringify(requestBody),
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + this.context.token
}
})
.then(res => {
if (res.status !== 200 && res.status !== 201) {
throw new Error("Failed!");
}
return res.json();
})
.then(resData => {
this.setState(prevState => {
const updatedBookings = prevState.bookings.filter(booking => {
return booking._id !== bookingId;
});
return { bookings: updatedBookings, isLoading: false };
});
})
.catch(err => {
console.log(err);
this.setState({ isLoading: false });
});
};
changeOutputTypeHandler = outputType => {
if (outputType === "list") {
this.setState({ outputType: "list" });
} else {
this.setState({ outputType: "chart" });
}
};
render() {
let content = <Spinner />;
if (!this.state.isLoading) {
content = (
<React.Fragment>
<div>
<button onClick={this.changeOutputTypeHandler.bind(this, "list")}>
List
</button>
<button onClick={this.changeOutputTypeHandler.bind(this, "chart")}>
Chart
</button>
</div>
<div>
{this.state.outputType === "list" ? (
<BookingList
bookings={this.state.bookings}
onDelete={this.deleteBookingHandler}
/>
) : (
<BookingsChart bookings={this.state.bookings} />
)}
</div>
</React.Fragment>
);
}
return <React.Fragment>{content}</React.Fragment>;
}
}
export default BookingsPage;
Create the BookingControls component
- Create the
components\Bookings\BookingsControls
folder with the following documents
components\Bookings\BookingsControls\BookingsControls.css
.bookings-control {
text-align: center;
padding: 0.5rem;
}
.bookings-control button {
font: inherit;
border: none;
background: transparent;
color: black;
padding: 0.25rem 3rem;
border-bottom: 2px solid transparent;
cursor: pointer;
}
.bookings-control button.active {
border-bottom-color: #5101d1;
color: #5101d1;
}
.bookings-control button:focus {
outline: none;
}
components\Bookings\BookingsControls\BookingsControls.js
import React from 'react';
import './BookingsControls.css';
const bookingsControl = props => {
return (
<div className="bookings-control">
<button
className={props.activeOutputType === 'list' ? 'active' : ''}
onClick={props.onChange.bind(this, 'list')}
>
List
</button>
<button
className={props.activeOutputType === 'chart' ? 'active' : ''}
onClick={props.onChange.bind(this, 'chart')}
>
Chart
</button>
</div>
);
};
export default bookingsControl;
- Update the
Bookings.js
page to use the new component.
pages\Bookings.js
import React, { Component } from "react";
import Spinner from "../components/Spinner/Spinner";
import AuthContext from "../context/auth-context";
import BookingList from "../components/Bookings/BookingList/BookingList";
import BookingsChart from "../components/Bookings/BookingsChart/BookingsChart";
import BookingsControls from "../components/Bookings/BookingsControls/BookingsControls";
class BookingsPage extends Component {
state = {
isLoading: false,
bookings: [],
outputType: "list"
};
static contextType = AuthContext;
componentDidMount() {
this.fetchBookings();
}
fetchBookings = () => {
this.setState({ isLoading: true });
const requestBody = {
query: `
query {
bookings {
_id
createdAt
event {
_id
title
date
}
}
}
`
};
fetch("http://localhost:9000/graphql", {
method: "POST",
body: JSON.stringify(requestBody),
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + this.context.token
}
})
.then(res => {
if (res.status !== 200 && res.status !== 201) {
throw new Error("Failed!");
}
return res.json();
})
.then(resData => {
const bookings = resData.data.bookings;
this.setState({ bookings: bookings, isLoading: false });
})
.catch(err => {
console.log(err);
this.setState({ isLoading: false });
});
};
deleteBookingHandler = bookingId => {
this.setState({ isLoading: true });
const requestBody = {
query: `
mutation CancelBooking($id: ID!) {
cancelBooking(bookingId: $id) {
_id
title
}
}
`,
variables: {
id: bookingId
}
};
fetch("http://localhost:9000/graphql", {
method: "POST",
body: JSON.stringify(requestBody),
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + this.context.token
}
})
.then(res => {
if (res.status !== 200 && res.status !== 201) {
throw new Error("Failed!");
}
return res.json();
})
.then(resData => {
this.setState(prevState => {
const updatedBookings = prevState.bookings.filter(booking => {
return booking._id !== bookingId;
});
return { bookings: updatedBookings, isLoading: false };
});
})
.catch(err => {
console.log(err);
this.setState({ isLoading: false });
});
};
changeOutputTypeHandler = outputType => {
if (outputType === "list") {
this.setState({ outputType: "list" });
} else {
this.setState({ outputType: "chart" });
}
};
render() {
let content = <Spinner />;
if (!this.state.isLoading) {
content = (
<React.Fragment>
<BookingsControls
activeOutputType={this.state.outputType}
onChange={this.changeOutputTypeHandler}
/>
<div>
{this.state.outputType === "list" ? (
<BookingList
bookings={this.state.bookings}
onDelete={this.deleteBookingHandler}
/>
) : (
<BookingsChart bookings={this.state.bookings} />
)}
</div>
</React.Fragment>
);
}
return <React.Fragment>{content}</React.Fragment>;
}
}
export default BookingsPage;
Develop the BookingsCharts component
- We need to install the react-chartjs component.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/GraphQL/graphql-react-event-booking/frontend (master)
$ npm install --save react-chartjs
npm WARN react-chartjs@1.2.0 requires a peer of chart.js@^1.1.1 but none is installed. You must install peer dependencies yourself.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.4 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.4: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
+ react-chartjs@1.2.0
added 9 packages from 84 contributors and audited 35810 packages in 37.232s
found 0 vulnerabilities
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/GraphQL/graphql-react-event-booking/frontend (master)
$ npm install --save chart.js@^1.1.1
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.4 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.4: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
+ chart.js@1.1.1
added 1 package and audited 35811 packages in 40.69s
found 0 vulnerabilities
- Modify the
Bookings.js
page to include the price with the events.
pages\Bookings.js
const Booking = require("../../models/booking");
const Event = require("../../models/event");
const { transformBooking, transformEvent } = require("./merge");
module.exports = {
bookings: async (args, req) => {
try {
if (!req.isAuth) {
throw new Error("Unauthenticated.");
}
const bookings = await Booking.find({ user: req.userId });
return bookings.map(booking => {
return transformBooking(booking);
});
} catch (err) {
console.log(err);
throw err;
}
},
bookEvent: async (args, req) => {
try {
if (!req.isAuth) {
throw new Error("Unauthenticated.");
}
const fectchedEvent = await Event.findById(args.eventId);
if (!fectchedEvent) {
throw new Error("Event Id does not exist");
}
const booking = new Booking({
user: req.userId,
event: fectchedEvent
});
const result = await booking.save();
return transformBooking(result);
} catch (err) {
console.log(err);
throw err;
}
},
cancelBooking: async (args, req) => {
try {
if (!req.isAuth) {
throw new Error("Unauthenticated.");
}
const booking = await Booking.findById(args.bookingId).populate("event");
if (!booking) {
throw new Error("Booking Id does not exist");
}
await Booking.deleteOne({ _id: args.bookingId });
return transformEvent(booking.event);
} catch (err) {
console.log(err);
throw err;
}
}
};
components\Bookins\BookingsChart\BookingsChart.js
import React from 'react';
import { Bar as BarChart } from 'react-chartjs';
const BOOKINGS_BUCKETS = {
Cheap: {
min: 0,
max: 100
},
Normal: {
min: 100,
max: 200
},
Expensive: {
min: 200,
max: 10000000
}
};
const bookingsChart = props => {
const chartData = { labels: [], datasets: [] };
let values = [];
for (const bucket in BOOKINGS_BUCKETS) {
const filteredBookingsCount = props.bookings.reduce((prev, current) => {
if (
current.event.price > BOOKINGS_BUCKETS[bucket].min &&
current.event.price < BOOKINGS_BUCKETS[bucket].max
) {
return prev + 1;
} else {
return prev;
}
}, 0);
values.push(filteredBookingsCount);
chartData.labels.push(bucket);
chartData.datasets.push({
// label: "My First dataset",
fillColor: 'rgba(220,220,220,0.5)',
strokeColor: 'rgba(220,220,220,0.8)',
highlightFill: 'rgba(220,220,220,0.75)',
highlightStroke: 'rgba(220,220,220,1)',
data: values
});
values = [...values];
values[values.length - 1] = 0;
}
return (
<div style={{ textAlign: 'center' }}>
<BarChart data={chartData} />
</div>
);
};
export default bookingsChart;
23. Finishing the App 7:53
merge.js
document to fix the events
function.
Updating the graphql\resolvers\merge.js
const DataLoader = require("dataloader");
const Event = require("../../models/event");
const User = require("../../models/user");
const { dateToString } = require("../../helpers/date");
const eventLoader = new DataLoader(eventIds => {
return events(eventIds);
});
const userLoader = new DataLoader(userIds => {
return User.find({ _id: { $in: userIds } });
});
const events = async eventIds => {
try {
const events = await Event.find({ _id: { $in: eventIds } });
events.sort((a, b) => {
return (
eventIds.indexOf(a._id.toString()) - eventIds.indexOf(b._id.toString())
);
});
return events.map(event => {
return transformEvent(event);
});
} catch (err) {
console.log(err);
throw err;
}
};
const singleEvent = async eventId => {
try {
const event = await eventLoader.load(eventId.toString());
return event;
} catch (err) {
console.log(err);
throw err;
}
};
const user = async userId => {
try {
const user = await userLoader.load(userId.toString());
return {
...user._doc,
_id: user.id,
createdEvents: () => eventLoader.loadMany(user._doc.createdEvents)
};
} catch (err) {
console.log(err);
throw err;
}
};
const transformEvent = event => {
if (event) {
return {
...event._doc,
_id: event.id,
date: dateToString(event._doc.date),
creator: user.bind(this, event._doc.creator)
};
}
return null;
};
const transformBooking = booking => {
if (booking) {
return {
...booking._doc,
_id: booking.id,
user: user.bind(this, booking._doc.user),
event: singleEvent.bind(this, booking._doc.event),
createdAt: dateToString(booking._doc.createdAt),
updatedAt: dateToString(booking._doc.updatedAt)
};
}
return null;
};
exports.transformEvent = transformEvent;
exports.transformBooking = transformBooking;