├── client ├── src │ ├── App.css │ ├── pages │ │ ├── Auth.css │ │ ├── Events.css │ │ ├── Auth.js │ │ ├── Bookings.js │ │ └── Events.js │ ├── components │ │ ├── Events │ │ │ └── EventList │ │ │ │ ├── EventList.css │ │ │ │ ├── EventItem │ │ │ │ ├── EventItem.css │ │ │ │ └── EventItem.js │ │ │ │ └── EventList.js │ │ ├── Backdrop │ │ │ ├── Backdrop.css │ │ │ └── Backdrop.js │ │ ├── Spinner │ │ │ ├── Spinner.js │ │ │ └── Spinner.css │ │ ├── Bookings │ │ │ ├── BookingList │ │ │ │ ├── BookingList.css │ │ │ │ └── BookingList.js │ │ │ ├── BookingsControl │ │ │ │ ├── BookingsControl.css │ │ │ │ └── BookingsControl.js │ │ │ └── BookingsChart │ │ │ │ └── BookingsChart.js │ │ ├── Modal │ │ │ ├── Modal.css │ │ │ └── Modal.js │ │ └── Navigation │ │ │ ├── MainNavigation.css │ │ │ └── MainNavigation.js │ ├── context │ │ └── auth-context.js │ ├── index.js │ ├── index.css │ └── App.js ├── public │ ├── favicon.ico │ ├── manifest.json │ └── index.html ├── .gitignore ├── package.json └── README.md ├── helpers └── date.js ├── graphql ├── resolvers │ ├── index.js │ ├── booking.js │ ├── auth.js │ ├── events.js │ └── merge.js └── schema │ └── index.js ├── .gitignore ├── models ├── booking.js ├── event.js └── user.js ├── package.json ├── middleware └── is-auth.js ├── README.md └── app.js /client/src/App.css: -------------------------------------------------------------------------------- 1 | .main-content { 2 | margin: 4rem 2.5rem; 3 | } 4 | -------------------------------------------------------------------------------- /helpers/date.js: -------------------------------------------------------------------------------- 1 | exports.dateToString = date => new Date(date).toDateString(); 2 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbertoletti/Event-Booking/HEAD/client/public/favicon.ico -------------------------------------------------------------------------------- /client/src/pages/Auth.css: -------------------------------------------------------------------------------- 1 | .auth-form { 2 | width: 25rem; 3 | max-width: 80%; 4 | margin: 5rem auto; 5 | } 6 | 7 | -------------------------------------------------------------------------------- /client/src/components/Events/EventList/EventList.css: -------------------------------------------------------------------------------- 1 | .events-list { 2 | width: 40rem; 3 | max-width: 90%; 4 | margin: 2rem auto; 5 | list-style: none; 6 | padding: 0; 7 | } -------------------------------------------------------------------------------- /client/src/components/Backdrop/Backdrop.css: -------------------------------------------------------------------------------- 1 | .backdrop { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | height: 100vh; 6 | width: 100%; 7 | background: rgba(0, 0, 0, 0.75); 8 | } 9 | -------------------------------------------------------------------------------- /client/src/context/auth-context.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default React.createContext({ 4 | userId: null, 5 | token: null, 6 | login: () => {}, 7 | logout: () => {} 8 | }); 9 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | 8 | -------------------------------------------------------------------------------- /client/src/components/Backdrop/Backdrop.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./Backdrop.css"; 3 | 4 | const Backdrop = props => { 5 | return
; 6 | }; 7 | 8 | export default Backdrop; 9 | -------------------------------------------------------------------------------- /client/src/pages/Events.css: -------------------------------------------------------------------------------- 1 | .events-control { 2 | text-align: center; 3 | border: 1px solid black; 4 | padding: 1rem; 5 | margin: 2rem auto; 6 | width: 30rem; 7 | max-width: 80%; 8 | } 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /graphql/resolvers/index.js: -------------------------------------------------------------------------------- 1 | const authResolver = require("./auth"); 2 | const eventsResolver = require("./events"); 3 | const bookingResolver = require("./booking"); 4 | 5 | const rootResolver = { 6 | ...authResolver, 7 | ...eventsResolver, 8 | ...bookingResolver 9 | }; 10 | 11 | module.exports = rootResolver; 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # editor 2 | 3 | .vs 4 | .vscode 5 | 6 | # node 7 | 8 | node_modules 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | 13 | # misc 14 | 15 | .DS_Store 16 | .DS_Store? 17 | 18 | # environment 19 | 20 | .env 21 | .env.local 22 | .env.development.local 23 | .env.test.local 24 | .env.production.local 25 | 26 | # Mongo info 27 | nodemon.json -------------------------------------------------------------------------------- /client/src/components/Spinner/Spinner.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./Spinner.css"; 3 | 4 | const Spinner = props => { 5 | return ( 6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | ); 14 | }; 15 | 16 | export default Spinner; 17 | -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /client/src/components/Bookings/BookingList/BookingList.css: -------------------------------------------------------------------------------- 1 | .bookings-list-wrapper { 2 | list-style: none; 3 | margin: 0 auto; 4 | padding: 0; 5 | width: 40rem; 6 | max-width: 90vw; 7 | 8 | } 9 | 10 | .bookings-list-item { 11 | display: flex; 12 | justify-content: space-between; 13 | margin: 0.5rem 0; 14 | padding: 0.5rem; 15 | border: 1px solid pink; 16 | align-items: center; 17 | } -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /client/src/components/Events/EventList/EventItem/EventItem.css: -------------------------------------------------------------------------------- 1 | .events-list-item { 2 | margin: 1rem 0; 3 | padding: 1rem; 4 | border: 1px solid pink; 5 | display: flex; 6 | justify-content: space-between; 7 | align-items: center; 8 | } 9 | 10 | .event-item-title { 11 | margin: 0; 12 | font-size: 1.5rem; 13 | text-transform: capitalize; 14 | } 15 | 16 | .event-item-price { 17 | margin-top: 0.5rem; 18 | font-size: 1rem; 19 | } 20 | 21 | .event-owner { 22 | margin: 0; 23 | } -------------------------------------------------------------------------------- /models/booking.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const Schema = mongoose.Schema; 4 | 5 | const bookingSchema = new Schema( 6 | { 7 | event: { 8 | type: Schema.Types.ObjectId, 9 | ref: "Event" 10 | }, 11 | user: { 12 | type: Schema.Types.ObjectId, 13 | ref: "User" 14 | } 15 | }, 16 | //so mongoose will provide a created/updated timestamp 17 | { timestamps: true } 18 | ); 19 | 20 | module.exports = mongoose.model("Booking", bookingSchema); 21 | -------------------------------------------------------------------------------- /client/src/components/Bookings/BookingsControl/BookingsControl.css: -------------------------------------------------------------------------------- 1 | .bookings-control { 2 | text-align: center; 3 | padding: 0.5rem; 4 | } 5 | 6 | .bookings-control button { 7 | font: inherit; 8 | border: none; 9 | background: transparent; 10 | color: black; 11 | padding: 0.2rem 3rem; 12 | border-bottom: 2px solid transparent; 13 | cursor: pointer; 14 | } 15 | 16 | .bookings-control button.active { 17 | border-bottom-color: #de997f; 18 | color: #de997f; 19 | } 20 | 21 | .bookings-control button:focus { 22 | outline: none; 23 | } 24 | -------------------------------------------------------------------------------- /client/src/components/Events/EventList/EventList.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import EventItem from "./EventItem/EventItem"; 3 | import "./EventList.css"; 4 | 5 | const EventList = props => { 6 | const events = props.events.map(event => { 7 | return ( 8 | 14 | ); 15 | }); 16 | 17 | return
    {events}
; 18 | }; 19 | 20 | export default EventList; 21 | -------------------------------------------------------------------------------- /models/event.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const Schema = mongoose.Schema; 4 | 5 | const eventSchema = new Schema({ 6 | title: { 7 | type: String, 8 | required: true 9 | }, 10 | description: { 11 | type: String, 12 | required: true 13 | }, 14 | price: { 15 | type: Number, 16 | required: true 17 | }, 18 | date: { 19 | type: Date, 20 | required: true 21 | }, 22 | creator: { 23 | type: Schema.Types.ObjectId, 24 | ref: 'User' 25 | } 26 | }); 27 | 28 | module.exports = mongoose.model("Event", eventSchema); 29 | -------------------------------------------------------------------------------- /models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const Schema = mongoose.Schema; 4 | 5 | const userSchema = new Schema({ 6 | email: { 7 | type: String, 8 | required: true 9 | }, 10 | password: { 11 | type: String, 12 | required: true 13 | }, 14 | createdEvents: [ 15 | { 16 | type: Schema.Types.ObjectId, 17 | //let mongoose know that two models are related, it'll merge data automatically. I'm passing in the name of my other model that I've defined in event.js 18 | ref: "Event" 19 | } 20 | ] 21 | }); 22 | 23 | module.exports = mongoose.model("User", userSchema); 24 | -------------------------------------------------------------------------------- /client/src/components/Bookings/BookingsControl/BookingsControl.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./BookingsControl.css"; 3 | 4 | const BookingsControl = props => { 5 | return ( 6 |
7 | 13 | 19 |
20 | ); 21 | }; 22 | 23 | export default BookingsControl; 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-react-event-booking", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "nodemon app.js" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "bcryptjs": "^2.4.3", 15 | "body-parser": "^1.18.3", 16 | "dataloader": "^1.4.0", 17 | "express": "^4.16.4", 18 | "express-graphql": "^0.7.1", 19 | "graphql": "^14.0.2", 20 | "jsonwebtoken": "^8.4.0", 21 | "mongoose": "^5.4.1" 22 | }, 23 | "devDependencies": { 24 | "nodemon": "^1.18.9" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /client/src/components/Modal/Modal.css: -------------------------------------------------------------------------------- 1 | .modal { 2 | width: 90%; 3 | background: white; 4 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.26); 5 | position: fixed; 6 | top: 20vh; 7 | left: 5%; 8 | z-index: 100; 9 | } 10 | 11 | .modal-header { 12 | padding: 1rem; 13 | background: pink; 14 | color: white; 15 | } 16 | 17 | .modal-header h1 { 18 | margin: 0; 19 | font-size: 1.25rem; 20 | } 21 | 22 | .modal-content { 23 | padding: 1rem; 24 | } 25 | 26 | .modal-actions { 27 | display: flex; 28 | justify-content: flex-end; 29 | padding: 1rem; 30 | } 31 | 32 | @media (min-width: 768px) { 33 | .modal { 34 | width: 30rem; 35 | left: calc((100% - 30rem) / 2); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "chart.js": "^1.1.1", 7 | "react": "^16.7.0", 8 | "react-chartjs": "^1.2.0", 9 | "react-dom": "^16.7.0", 10 | "react-router-dom": "^4.3.1", 11 | "react-scripts": "2.1.2" 12 | }, 13 | "scripts": { 14 | "start": "react-scripts start", 15 | "build": "react-scripts build", 16 | "test": "react-scripts test", 17 | "eject": "react-scripts eject" 18 | }, 19 | "eslintConfig": { 20 | "extends": "react-app" 21 | }, 22 | "browserslist": [ 23 | ">0.2%", 24 | "not dead", 25 | "not ie <= 11", 26 | "not op_mini all" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /middleware/is-auth.js: -------------------------------------------------------------------------------- 1 | const jwt = require("jsonwebtoken"); 2 | 3 | module.exports = (req, res, next) => { 4 | const authHeader = req.get("Authorization"); 5 | if (!authHeader) { 6 | req.isAuth = false; 7 | return next(); 8 | } 9 | const token = authHeader.split(" ")[1]; 10 | if (!token || token === "") { 11 | req.isAuth = false; 12 | return next(); 13 | } 14 | let decodedToken; 15 | try { 16 | decodedToken = jwt.verify(token, "somesupersecretkey"); 17 | } catch (err) { 18 | req.isAuth = false; 19 | return next(); 20 | } 21 | if (!decodedToken) { 22 | return next(); 23 | } 24 | req.isAuth = true; 25 | req.userId = decodedToken.userId; 26 | next(); 27 | }; 28 | -------------------------------------------------------------------------------- /client/src/components/Modal/Modal.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./Modal.css"; 3 | 4 | const Modal = props => { 5 | return ( 6 |
7 |
8 |

{props.title}

9 |
10 |
{props.children}
11 |
12 | {props.canCancel && ( 13 | 16 | )} 17 | {props.canConfirm && ( 18 | 21 | )} 22 |
23 |
24 | ); 25 | }; 26 | 27 | export default Modal; 28 | -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | 7 | .form-control label, 8 | .form-control input, 9 | .form-control textarea { 10 | width: 100%; 11 | display: block; 12 | } 13 | 14 | .form-control label { 15 | margin-bottom: 0.5rem; 16 | } 17 | 18 | .form-control { 19 | margin-bottom: 1rem; 20 | } 21 | 22 | .form-actions button, 23 | .btn { 24 | background-color: skyblue; 25 | font: inherit; 26 | border: 1px solid skyblue; 27 | border-radius: 3px; 28 | padding: 0.5rem 1rem; 29 | margin-right: 1rem; 30 | box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.65); 31 | color: white; 32 | cursor: pointer; 33 | } 34 | 35 | .form-actions button:hover, 36 | .form-actions button:active, 37 | .btn:hover, 38 | .btn:active { 39 | background: rgb(235, 180, 135); 40 | border-color: rgb(235, 180, 135); 41 | } 42 | -------------------------------------------------------------------------------- /client/src/components/Bookings/BookingList/BookingList.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./BookingList.css"; 3 | 4 | const BookingList = props => { 5 | return ( 6 |
    7 | {props.bookings.map(booking => ( 8 |
  • 9 |
    10 | {booking.event.title} - 11 | {new Date(booking.createdAt).toLocaleString()} 12 |
    13 |
    14 | 20 |
    21 |
  • 22 | ))} 23 |
24 | ); 25 | }; 26 | 27 | export default BookingList; 28 | -------------------------------------------------------------------------------- /client/src/components/Events/EventList/EventItem/EventItem.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./EventItem.css"; 3 | 4 | const EventItem = props => { 5 | return ( 6 |
  • 7 |
    8 |

    {props.event.title}

    9 |

    10 | ${props.event.price} - 11 | {new Date(props.event.date).toLocaleDateString()} 12 |

    13 |
    14 |
    15 | {props.authUserId !== props.event.creator._id ? ( 16 | 22 | ) : ( 23 |

    You are the owner of this event

    24 | )} 25 |
    26 |
  • 27 | ); 28 | }; 29 | 30 | export default EventItem; 31 | -------------------------------------------------------------------------------- /client/src/components/Spinner/Spinner.css: -------------------------------------------------------------------------------- 1 | .spinner { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | height: 100vh; 6 | width: 100vw; 7 | } 8 | 9 | .lds-facebook { 10 | display: inline-block; 11 | position: relative; 12 | width: 64px; 13 | height: 64px; 14 | } 15 | .lds-facebook div { 16 | display: inline-block; 17 | position: absolute; 18 | left: 6px; 19 | width: 13px; 20 | background: #cef; 21 | animation: lds-facebook 1.2s cubic-bezier(0, 0.5, 0.5, 1) infinite; 22 | } 23 | .lds-facebook div:nth-child(1) { 24 | left: 6px; 25 | animation-delay: -0.24s; 26 | } 27 | .lds-facebook div:nth-child(2) { 28 | left: 26px; 29 | animation-delay: -0.12s; 30 | } 31 | .lds-facebook div:nth-child(3) { 32 | left: 45px; 33 | animation-delay: 0; 34 | } 35 | @keyframes lds-facebook { 36 | 0% { 37 | top: 6px; 38 | height: 51px; 39 | } 40 | 50%, 41 | 100% { 42 | top: 19px; 43 | height: 26px; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Event Booking Web App 2 | 3 | A full stack web application practice, please note there're minimal styles applied. 4 | 5 | ## Perks 🧙‍ 6 | 7 | - A Event Booking system where users can signup, login, logout, create events, update and delete them. 8 | - Book any events they wish to attend. 9 | - Non logged in users can see all events but cannot interact with the system without signing up. 10 | - From the bookings page, the user can see a list of their booked events, or click on the chart tab to see how many cheap, moderate or expensive events they have booked. 11 | 12 | Quick tour: 13 | 14 | ![eventsbooking](https://user-images.githubusercontent.com/40447526/54247302-a22c4f80-44f5-11e9-8116-e7d2d546133a.gif) 15 | 16 | 17 | ## Technologies 18 | 19 | **Front End:** React.js 20 | **Back End:** GraphQL API, Node.js and Express Server and Mongo Database. 21 | 22 | ## Usage 23 | 24 | - Download this repo, 25 | - `npm install` in the root of the project, 26 | - `cd client` and `npm install`, 27 | - Make sure to `npm start` both in the root and in the client folder simultaneously to run this project. 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /client/src/components/Navigation/MainNavigation.css: -------------------------------------------------------------------------------- 1 | .main-nav { 2 | position: fixed; 3 | left: 0; 4 | top: 0; 5 | width: 100%; 6 | height: 3.5rem; 7 | background-color: darksalmon; 8 | padding: 0 1rem; 9 | display: flex; 10 | align-items: center; 11 | justify-content: space-between; 12 | } 13 | 14 | .main-nav-logo h1 { 15 | margin-left: 3rem; 16 | font-size: 1, 5rem; 17 | } 18 | 19 | .main-nav-items { 20 | margin-right: 3rem; 21 | } 22 | 23 | .main-nav-items ul { 24 | display: flex; 25 | list-style: none; 26 | text-decoration: none; 27 | 28 | } 29 | 30 | .main-nav-items li { 31 | margin: 0 1rem; 32 | } 33 | 34 | .main-nav-items a, 35 | .main-nav-items button { 36 | text-decoration: none; 37 | color: white; 38 | border: none; 39 | font: inherit; 40 | background: transparent; 41 | cursor: pointer; 42 | vertical-align: middle; 43 | margin: 0; 44 | padding: 0.25rem 0.5rem; 45 | } 46 | 47 | .main-nav-items a:hover, 48 | .main-nav-items a:active, 49 | .main-nav-items a.active, 50 | .main-nav-items button:hover, 51 | .main-nav-items button:active { 52 | color: black; 53 | background: rgba(236, 225, 225, 0.815); 54 | } 55 | -------------------------------------------------------------------------------- /graphql/schema/index.js: -------------------------------------------------------------------------------- 1 | const { buildSchema } = require("graphql"); 2 | 3 | module.exports = buildSchema(` 4 | type Booking { 5 | _id: ID! 6 | event: Event! 7 | user: User! 8 | createdAt: String! 9 | updatedAt: String! 10 | } 11 | type Event { 12 | _id: ID! 13 | title: String! 14 | description: String! 15 | price: Float! 16 | date: String! 17 | creator: User! 18 | } 19 | 20 | type User { 21 | _id: ID! 22 | email: String! 23 | password: String 24 | createdEvents: [Event!] 25 | } 26 | 27 | type AuthData { 28 | userId: ID! 29 | token: String! 30 | tokenExpiration: Int! 31 | } 32 | 33 | input EventInput { 34 | title: String! 35 | description: String! 36 | price: Float! 37 | date: String! 38 | 39 | } 40 | 41 | input UserInput { 42 | email: String! 43 | password: String! 44 | } 45 | 46 | type RootQuery { 47 | events: [Event!]! 48 | bookings: [Booking!]! 49 | login(email: String!, password: String!): AuthData! 50 | } 51 | 52 | type RootMutation { 53 | createEvent(eventInput: EventInput): Event 54 | createUser(userInput: UserInput): User 55 | bookEvent(eventId: ID!): Booking! 56 | cancelBooking(bookingId: ID!): Event! 57 | } 58 | schema { 59 | query: RootQuery 60 | mutation: RootMutation 61 | } 62 | `); 63 | -------------------------------------------------------------------------------- /client/src/components/Navigation/MainNavigation.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { NavLink } from "react-router-dom"; 3 | import "./MainNavigation.css"; 4 | import AuthContext from "../../context/auth-context"; 5 | 6 | const MainNavigation = props => ( 7 | 8 | {context => { 9 | return ( 10 |
    11 |
    12 |

    Events Land

    13 |
    14 | 36 |
    37 | ); 38 | }} 39 |
    40 | ); 41 | 42 | export default MainNavigation; 43 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const bodyParser = require("body-parser"); 3 | const graphqlHttp = require("express-graphql"); 4 | const mongoose = require("mongoose"); 5 | const graphQlSchema = require("./graphql/schema/index"); 6 | const graphQlResolvers = require("./graphql/resolvers/index"); 7 | const isAuth = require("./middleware/is-auth"); 8 | 9 | const app = express(); 10 | 11 | app.use(bodyParser.json()); 12 | 13 | app.use((req, res, next) => { 14 | //allows a web service to specify that it's OK for it to be invoked from any domain 15 | res.setHeader("Access-Control-Allow-Origin", "*"); 16 | //browser sends an OPTIONS request before sending POST 17 | res.setHeader("Access-Control-Allow-Methods", "POST,GET,OPTIONS"); 18 | res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); 19 | 20 | if (req.method === "OPTIONS") { 21 | return res.sendStatus(200); 22 | } 23 | next(); 24 | }); 25 | 26 | app.use(isAuth); 27 | 28 | app.use( 29 | "/graphql", 30 | graphqlHttp({ 31 | schema: graphQlSchema, 32 | rootValue: graphQlResolvers, 33 | //graphql playground 34 | graphiql: true 35 | }) 36 | ); 37 | 38 | mongoose 39 | .connect( 40 | `mongodb+srv://${process.env.MONGO_USER}:${ 41 | process.env.MONGO_PASSWORD 42 | }@cluster0-uagfb.mongodb.net/${process.env.MONGO_DB}?retryWrites=true` 43 | ) 44 | .then(() => { 45 | app.listen(8000); 46 | }) 47 | .catch(err => { 48 | console.log(err); 49 | }); 50 | -------------------------------------------------------------------------------- /client/src/components/Bookings/BookingsChart/BookingsChart.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Bar as BarChart } from "react-chartjs"; 3 | 4 | const BOOKINGS_BUCKETS = { 5 | Cheap: { 6 | min: 0, 7 | max: 50 8 | }, 9 | 10 | Moderate: { 11 | min: 50, 12 | max: 100 13 | }, 14 | 15 | Expensive: { 16 | min: 100, 17 | max: 10000000 18 | } 19 | }; 20 | 21 | const BookingsChart = props => { 22 | const chartData = { labels: [], datasets: [] }; 23 | let values = []; 24 | for (const bucket in BOOKINGS_BUCKETS) { 25 | const filteredBookingsCount = props.bookings.reduce((prev, current) => { 26 | if ( 27 | current.event.price > BOOKINGS_BUCKETS[bucket].min && 28 | current.event.price < BOOKINGS_BUCKETS[bucket].max 29 | ) { 30 | return prev + 1; 31 | } else { 32 | return prev; 33 | } 34 | }, 0); 35 | values.push(filteredBookingsCount); 36 | chartData.labels.push(bucket); 37 | chartData.datasets.push({ 38 | fillColor: 'rgba(220,220,220,0.5)', 39 | strokeColor: 'rgba(220,220,220,0.8)', 40 | highlightFill: 'rgba(220,220,220,0.75)', 41 | highlightStroke: 'rgba(220,220,220,1)', 42 | data: values 43 | }); 44 | values = [...values]; 45 | values[values.length - 1] = 0; 46 | } 47 | 48 | return ( 49 |
    50 | 51 |
    52 | ); 53 | }; 54 | 55 | export default BookingsChart; 56 | -------------------------------------------------------------------------------- /graphql/resolvers/booking.js: -------------------------------------------------------------------------------- 1 | const Booking = require("../../models/booking.js"); 2 | const { transformBooking, transformEvent } = require("./merge"); 3 | const Event = require("../../models/event"); 4 | 5 | module.exports = { 6 | bookings: async (args, req) => { 7 | if (!req.isAuth) { 8 | throw new Error("User is not authorized to create event"); 9 | } 10 | try { 11 | const bookings = await Booking.find({user: req.userId}); 12 | return bookings.map(booking => { 13 | return transformBooking(booking); 14 | }); 15 | } catch (err) { 16 | throw err; 17 | } 18 | }, 19 | 20 | bookEvent: async (args, req) => { 21 | if (!req.isAuth) { 22 | throw new Error("User is not authorized to create event"); 23 | } 24 | const fetchedEvent = await Event.findOne({ _id: args.eventId }); 25 | const booking = new Booking({ 26 | user: req.userId, 27 | event: fetchedEvent 28 | }); 29 | const result = await booking.save(); 30 | return transformBooking(result); 31 | }, 32 | 33 | cancelBooking: async (args, req) => { 34 | if (!req.isAuth) { 35 | throw new Error("User is not authorized to create event"); 36 | } 37 | try { 38 | const booking = await Booking.findById(args.bookingId).populate("event"); 39 | const event = transformEvent(booking.event); 40 | await Booking.deleteOne({ _id: args.bookingId }); 41 | return event; 42 | } catch (err) { 43 | throw err; 44 | } 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | React App 26 | 27 | 28 | 29 |
    30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /graphql/resolvers/auth.js: -------------------------------------------------------------------------------- 1 | const User = require("../../models/user.js"); 2 | const bcrypt = require("bcryptjs"); 3 | const jwt = require("jsonwebtoken"); 4 | 5 | module.exports = { 6 | //To create a user, first I need to check if the provided email address already exists. Have to 'return' in order for graphql to wait until promise is complete, async. 7 | createUser: async args => { 8 | try { 9 | const existingUser = await User.findOne({ email: args.userInput.email }); 10 | 11 | if (existingUser) { 12 | throw new Error("User already exists."); 13 | } 14 | //first arg is what I want to hash and second is how many rounds of salting. 15 | const hashedPassword = await bcrypt.hash(args.userInput.password, 12); 16 | 17 | const user = new User({ 18 | email: args.userInput.email, 19 | password: hashedPassword 20 | }); 21 | const result = await user.save(); 22 | return { ...result._doc, password: null, _id: result.id }; 23 | // setting password to null so it cannot be retrieved from graphql, even though it's hashed. 24 | } catch (err) { 25 | throw err; 26 | } 27 | }, 28 | 29 | login: async ({ email, password }) => { 30 | const user = await User.findOne({ email: email }); 31 | if (!user) { 32 | throw new Error("User does not exist"); 33 | } 34 | //compares the string password to the hashed DB password 35 | const isEqual = bcrypt.compare(password, user.password); 36 | if (!isEqual) { 37 | throw new Error("Password is incorrect"); 38 | } 39 | //1st arg is what I want to store in the token, 2nd is required, a string to hash the token like a private key, 3rd expiration time (optional) 40 | const token = jwt.sign( 41 | { userId: user.id, email: user.email }, 42 | "somesupersecretkey", 43 | { expiresIn: "1h" } 44 | ); 45 | return { userId: user.id, token: token, tokenExpiration: 1 }; 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /graphql/resolvers/events.js: -------------------------------------------------------------------------------- 1 | const { dateToString } = require("../../helpers/date.js"); 2 | const Event = require("../../models/event.js"); 3 | const User = require("../../models/user.js"); 4 | const { transformEvent } = require("./merge"); 5 | 6 | module.exports = { 7 | events: async () => { 8 | try { 9 | const events = await Event.find(); 10 | return events.map(event => { 11 | //I need to transform the _id into a string to solve graphql error. Mongoose allows me to use only event.id - this also works fine. 12 | return transformEvent(event); 13 | }); 14 | } catch (err) { 15 | throw err; 16 | } 17 | }, 18 | 19 | //this will take in the args passed when creating the schema. 20 | createEvent: async (args, req) => { 21 | if (!req.isAuth) { 22 | throw new Error("User is not authorized to create event"); 23 | } 24 | 25 | const event = new Event({ 26 | title: args.eventInput.title, 27 | description: args.eventInput.description, 28 | price: +args.eventInput.price, 29 | date: dateToString(args.eventInput.date), 30 | //mongoose will store automatically an objectId, only need to pass in a string: 31 | creator: req.userId 32 | }); 33 | 34 | let createdEvent; 35 | 36 | try { 37 | //save method is provided by mongoose 38 | const result = await event.save(); 39 | //_doc is provided by mongoose, it leaves out meta data 40 | createdEvent = transformEvent(result); 41 | const creator = await User.findById(req.userId); 42 | 43 | if (!creator) { 44 | throw new Error("User not found."); 45 | } 46 | //I need to pass in the id, however I can pass the entire event object and mongoose will handle the id. 47 | creator.createdEvents.push(event); 48 | //this will update the user in the DB: 49 | await creator.save(); 50 | 51 | return createdEvent; 52 | } catch (err) { 53 | throw err; 54 | } 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /client/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import "./App.css"; 3 | import { BrowserRouter, Route, Redirect, Switch } from "react-router-dom"; 4 | import AuthPage from "./pages/Auth"; 5 | import BookingsPage from "./pages/Bookings"; 6 | import EventsPage from "./pages/Events"; 7 | import MainNavigation from "./components/Navigation/MainNavigation"; 8 | import AuthContext from "./context/auth-context"; 9 | 10 | class App extends Component { 11 | state = { 12 | token: null, 13 | userId: null 14 | }; 15 | 16 | login = (token, userId, tokenExpiration) => { 17 | this.setState({ token: token, userId: userId }); 18 | }; 19 | 20 | logout = () => { 21 | this.setState({ token: null, userId: null }); 22 | }; 23 | 24 | render() { 25 | return ( 26 | 27 | 28 | 36 | 37 |
    38 | 39 | {this.state.token && } 40 | {this.state.token && ( 41 | 42 | )} 43 | {!this.state.token && ( 44 | 45 | )} 46 | 47 | {this.state.token && ( 48 | 49 | )} 50 | {!this.state.token && } 51 | 52 |
    53 |
    54 |
    55 |
    56 | ); 57 | } 58 | } 59 | 60 | export default App; 61 | -------------------------------------------------------------------------------- /graphql/resolvers/merge.js: -------------------------------------------------------------------------------- 1 | const Event = require("../../models/event"); 2 | const User = require("../../models/user"); 3 | const { dateToString } = require("../../helpers/date"); 4 | const DataLoader = require("dataloader"); 5 | 6 | const eventLoader = new DataLoader(eventIds => { 7 | return events(eventIds); 8 | }); 9 | 10 | const userLoader = new DataLoader(userIds => { 11 | return User.find({ _id: { $in: userIds } }); 12 | }); 13 | 14 | const events = async eventIds => { 15 | try { 16 | const events = await Event.find({ _id: { $in: eventIds } }); 17 | events.sort((a, b) => { 18 | return ( 19 | eventIds.indexOf(a._id.toString()) - eventIds.indexOf(b._id.toString()) 20 | ); 21 | }); 22 | return events.map(event => { 23 | return transformEvent(event); 24 | }); 25 | } catch (err) { 26 | throw err; 27 | } 28 | }; 29 | 30 | const singleEvent = async eventId => { 31 | try { 32 | const event = await eventLoader.load(eventId.toString()); 33 | return event; 34 | } catch (err) { 35 | throw err; 36 | } 37 | }; 38 | 39 | //.populate('creator') is a method provided by mongoose that populates any relations that it knows, so for example GraphQL returns the email from the user. However it can be replaced by the following function: 40 | const user = async userId => { 41 | try { 42 | const user = await userLoader.load(userId.toString()); 43 | return { 44 | ...user._doc, 45 | _id: user.id, 46 | createdEvents: eventLoader.load.bind(this, user._doc.createdEvents) 47 | }; 48 | } catch (err) { 49 | throw err; 50 | } 51 | }; 52 | 53 | const transformEvent = event => { 54 | return { 55 | ...event._doc, 56 | _id: event.id, 57 | date: dateToString(event._doc.date), 58 | creator: user.bind(this, event.creator) 59 | }; 60 | }; 61 | 62 | const transformBooking = booking => { 63 | return { 64 | ...booking._doc, 65 | _id: booking.id, 66 | createdAt: dateToString(booking._doc.createdAt), 67 | updatedAt: dateToString(booking._doc.updatedAt), 68 | user: user.bind(this, booking._doc.user), 69 | event: singleEvent.bind(this, booking._doc.event) 70 | }; 71 | }; 72 | 73 | // exports.user = user; 74 | // exports.events = events; 75 | // exports.singleEvent = singleEvent; 76 | 77 | exports.transformEvent = transformEvent; 78 | exports.transformBooking = transformBooking; 79 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
    10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
    13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
    18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
    23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
    26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analyzing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `npm run build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /client/src/pages/Auth.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import "./Auth.css"; 3 | import AuthContext from "../context/auth-context"; 4 | 5 | class AuthPage extends Component { 6 | state = { 7 | isLogin: true 8 | }; 9 | 10 | static contextType = AuthContext; 11 | 12 | constructor(props) { 13 | super(props); 14 | this.emailElement = React.createRef(); 15 | this.passwordElement = React.createRef(); 16 | } 17 | 18 | handleSubmit = event => { 19 | event.preventDefault(); 20 | 21 | const email = this.emailElement.current.value; 22 | const password = this.passwordElement.current.value; 23 | 24 | //trim() removes whitespace from both sides of a str 25 | if (email.trim().length === 0 || password.trim().length === 0) { 26 | return; 27 | } 28 | 29 | let requestBody = { 30 | query: ` 31 | query { 32 | login(email: "${email}", password: "${password}") { 33 | userId 34 | token 35 | tokenExpiration 36 | } 37 | } 38 | ` 39 | }; 40 | 41 | if (!this.state.isLogin) { 42 | requestBody = { 43 | query: `mutation { 44 | createUser(userInput: {email: "${email}", password: "${password}"}) { 45 | _id 46 | email 47 | } 48 | }` 49 | }; 50 | } 51 | 52 | fetch("http://localhost:8000/graphql", { 53 | method: "POST", 54 | body: JSON.stringify(requestBody), 55 | headers: { 56 | "Content-Type": "application/json" 57 | } 58 | }) 59 | .then(res => { 60 | if (res.status !== 200 && res.status !== 201) { 61 | throw new Error("Failed"); 62 | } 63 | //this will automatically extract and parse the res body 64 | return res.json(); 65 | }) 66 | .then(resData => { 67 | if (resData.data.login.token) { 68 | this.context.login( 69 | resData.data.login.token, 70 | resData.data.login.userId, 71 | resData.data.login.tokenExpiration 72 | ); 73 | } 74 | }) 75 | .catch(err => { 76 | console.log(err); 77 | }); 78 | }; 79 | 80 | handleSwitch = () => { 81 | this.setState(prevState => { 82 | return { isLogin: !prevState.isLogin }; 83 | }); 84 | }; 85 | 86 | render() { 87 | return ( 88 |
    89 |
    90 | 91 | 92 |
    93 | 94 |
    95 | 96 | 97 |
    98 |
    99 | 100 | 103 |
    104 |
    105 | ); 106 | } 107 | } 108 | 109 | export default AuthPage; 110 | -------------------------------------------------------------------------------- /client/src/pages/Bookings.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import AuthContext from "../context/auth-context"; 3 | import Spinner from "../components/Spinner/Spinner"; 4 | import BookingList from "../components/Bookings/BookingList/BookingList"; 5 | import BookingsChart from "../components/Bookings/BookingsChart/BookingsChart"; 6 | import BookingsControl from "../components/Bookings/BookingsControl/BookingsControl"; 7 | 8 | class BookingsPage extends Component { 9 | state = { 10 | isLoading: false, 11 | bookings: [], 12 | outputType: "list" 13 | }; 14 | 15 | static contextType = AuthContext; 16 | 17 | componentDidMount() { 18 | this.fetchBookings(); 19 | } 20 | 21 | fetchBookings = () => { 22 | this.setState({ isLoading: true }); 23 | const requestBody = { 24 | query: `query { 25 | bookings { 26 | _id 27 | createdAt 28 | event { 29 | _id 30 | title 31 | date 32 | price 33 | } 34 | } 35 | }` 36 | }; 37 | 38 | fetch("http://localhost:8000/graphql", { 39 | method: "POST", 40 | body: JSON.stringify(requestBody), 41 | headers: { 42 | "Content-Type": "application/json", 43 | Authorization: "Bearer " + this.context.token 44 | } 45 | }) 46 | .then(res => { 47 | if (res.status !== 200 && res.status !== 201) { 48 | throw new Error("Failed"); 49 | } 50 | //this will automatically extract and parse the res body 51 | return res.json(); 52 | }) 53 | .then(resData => { 54 | const bookings = resData.data.bookings; 55 | this.setState({ bookings: bookings, isLoading: false }); 56 | }) 57 | .catch(err => { 58 | console.log(err); 59 | this.setState({ isLoading: false }); 60 | }); 61 | }; 62 | 63 | deleteBookingHandler = bookingId => { 64 | this.setState({ isLoading: true }); 65 | const requestBody = { 66 | query: `mutation { 67 | cancelBooking(bookingId: "${bookingId}") { 68 | _id 69 | title 70 | } 71 | }` 72 | }; 73 | 74 | fetch("http://localhost:8000/graphql", { 75 | method: "POST", 76 | body: JSON.stringify(requestBody), 77 | headers: { 78 | "Content-Type": "application/json", 79 | Authorization: "Bearer " + this.context.token 80 | } 81 | }) 82 | .then(res => { 83 | if (res.status !== 200 && res.status !== 201) { 84 | throw new Error("Failed"); 85 | } 86 | //this will automatically extract and parse the res body 87 | return res.json(); 88 | }) 89 | .then(resData => { 90 | this.setState(prevState => { 91 | const updatedBooking = prevState.bookings.filter(booking => { 92 | return booking._id !== bookingId; 93 | }); 94 | return { bookings: updatedBooking, isLoading: false }; 95 | }); 96 | }) 97 | .catch(err => { 98 | console.log(err); 99 | this.setState({ isLoading: false }); 100 | }); 101 | }; 102 | 103 | ChangeOutputTypeHandler = outputType => { 104 | outputType === "list" 105 | ? this.setState({ outputType: "list" }) 106 | : this.setState({ outputType: "chart" }); 107 | }; 108 | render() { 109 | let content = ; 110 | if (!this.state.isLoading) { 111 | content = ( 112 | 113 | 117 |
    118 | {this.state.outputType === "list" ? ( 119 | 123 | ) : ( 124 | 125 | )} 126 |
    127 |
    128 | ); 129 | } 130 | return {content}; 131 | } 132 | } 133 | 134 | export default BookingsPage; 135 | -------------------------------------------------------------------------------- /client/src/pages/Events.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import "./Events.css"; 3 | import Modal from "../components/Modal/Modal"; 4 | import Backdrop from "../components/Backdrop/Backdrop"; 5 | import AuthContext from "../context/auth-context"; 6 | import Spinner from "../components/Spinner/Spinner"; 7 | import EventList from "../components/Events/EventList/EventList"; 8 | 9 | class EventsPage extends Component { 10 | state = { 11 | creating: false, 12 | events: [], 13 | isLoading: false, 14 | selectedEvent: null 15 | }; 16 | isActive = true; 17 | 18 | static contextType = AuthContext; 19 | 20 | constructor(props) { 21 | super(props); 22 | this.titleElRef = React.createRef(); 23 | this.priceElRef = React.createRef(); 24 | this.dateElRef = React.createRef(); 25 | this.descriptionElRef = React.createRef(); 26 | } 27 | 28 | componentDidMount() { 29 | this.fetchEvents(); 30 | } 31 | 32 | startCreateEventHandler = () => { 33 | this.setState({ creating: true }); 34 | }; 35 | 36 | modalConfirmHandler = () => { 37 | this.setState({ creating: false }); 38 | const title = this.titleElRef.current.value; 39 | //adding a + in price to convert this to a number 40 | const price = +this.priceElRef.current.value; 41 | const date = this.dateElRef.current.value; 42 | const description = this.descriptionElRef.current.value; 43 | 44 | if ( 45 | title.trim().length === 0 || 46 | price <= 0 || 47 | date.trim().length === 0 || 48 | description.trim().length === 0 49 | ) { 50 | return; 51 | } 52 | 53 | //title: title 54 | const event = { title, price, date, description }; 55 | 56 | const requestBody = { 57 | query: `mutation { 58 | createEvent(eventInput: {title: "${title}", description: "${description}", date: "${date}", price: ${price}}) { 59 | _id 60 | title 61 | description 62 | date 63 | price 64 | } 65 | }` 66 | }; 67 | 68 | const token = this.context.token; 69 | 70 | fetch("http://localhost:8000/graphql", { 71 | method: "POST", 72 | body: JSON.stringify(requestBody), 73 | headers: { 74 | "Content-Type": "application/json", 75 | Authorization: "Bearer " + token 76 | } 77 | }) 78 | .then(res => { 79 | if (res.status !== 200 && res.status !== 201) { 80 | throw new Error("Failed"); 81 | } 82 | //this will automatically extract and parse the res body 83 | return res.json(); 84 | }) 85 | .then(resData => { 86 | this.setState(prevState => { 87 | const updatedEvents = [...prevState.events]; 88 | updatedEvents.push({ 89 | _id: resData.data.createEvent._id, 90 | title: resData.data.createEvent.title, 91 | description: resData.data.createEvent.description, 92 | date: resData.data.createEvent.date, 93 | price: resData.data.createEvent.price, 94 | creator: { 95 | _id: this.context.userId 96 | } 97 | }); 98 | return { events: updatedEvents }; 99 | }); 100 | }) 101 | .catch(err => { 102 | console.log(err); 103 | }); 104 | }; 105 | 106 | modalCancelHandler = () => { 107 | this.setState({ creating: false, selectedEvent: null }); 108 | }; 109 | 110 | fetchEvents() { 111 | this.setState({ isLoading: true }); 112 | const requestBody = { 113 | query: `query { 114 | events { 115 | _id 116 | title 117 | description 118 | date 119 | price 120 | creator { 121 | _id 122 | email 123 | } 124 | } 125 | }` 126 | }; 127 | 128 | fetch("http://localhost:8000/graphql", { 129 | method: "POST", 130 | body: JSON.stringify(requestBody), 131 | headers: { 132 | "Content-Type": "application/json" 133 | } 134 | }) 135 | .then(res => { 136 | if (res.status !== 200 && res.status !== 201) { 137 | throw new Error("Failed"); 138 | } 139 | //this will automatically extract and parse the res body 140 | return res.json(); 141 | }) 142 | .then(resData => { 143 | const events = resData.data.events; 144 | if (this.isActive) { 145 | this.setState({ events: events, isLoading: false }); 146 | } 147 | }) 148 | .catch(err => { 149 | console.log(err); 150 | if (this.isActive) { 151 | this.setState({ isLoading: false }); 152 | } 153 | }); 154 | } 155 | 156 | showDetailHandler = eventId => { 157 | this.setState(prevState => { 158 | const selectedEvent = prevState.events.find(e => e._id === eventId); 159 | return { 160 | selectedEvent: selectedEvent 161 | }; 162 | }); 163 | }; 164 | 165 | bookEventHandler = () => { 166 | if (!this.context.token) { 167 | this.setState({ selectedEvent: null }); 168 | return; 169 | } 170 | 171 | const requestBody = { 172 | query: `mutation { 173 | bookEvent(eventId: "${this.state.selectedEvent._id}") { 174 | _id 175 | createdAt 176 | updatedAt 177 | 178 | } 179 | }` 180 | }; 181 | 182 | const token = this.context.token; 183 | 184 | fetch("http://localhost:8000/graphql", { 185 | method: "POST", 186 | body: JSON.stringify(requestBody), 187 | headers: { 188 | "Content-Type": "application/json", 189 | Authorization: "Bearer " + this.context.token 190 | } 191 | }) 192 | .then(res => { 193 | if (res.status !== 200 && res.status !== 201) { 194 | throw new Error("Failed"); 195 | } 196 | //this will automatically extract and parse the res body 197 | return res.json(); 198 | }) 199 | .then(resData => { 200 | console.log(resData); 201 | this.setState({ selectedEvent: null }); 202 | }) 203 | .catch(err => { 204 | console.log(err); 205 | }); 206 | }; 207 | 208 | componentWillUnmount() { 209 | this.isActive = false; 210 | } 211 | 212 | render() { 213 | return ( 214 | 215 | {this.state.creating && ( 216 | 217 | } 218 | 226 |
    227 |
    228 | 229 | 230 |
    231 | 232 |
    233 | 234 | 235 |
    236 | 237 |
    238 | 239 | 240 |
    241 | 242 |
    243 | 244 |