├── 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 |
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 ;
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 |
10 |
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 |
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 | 
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 |
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 |
251 |
252 |
253 | )}
254 | {this.state.selectedEvent && (
255 |
256 |
257 |
265 | {this.state.selectedEvent.title}
266 |
267 | {this.state.selectedEvent.date} - $
268 | {this.state.selectedEvent.price}
269 |
270 | {this.state.selectedEvent.description}
271 |
272 |
273 | )}
274 | {this.context.token && (
275 |
276 |
Share your own events!
277 |
280 |
281 | )}
282 | {this.state.isLoading ? (
283 |
284 | ) : (
285 |
290 | )}
291 |
292 | );
293 | }
294 | }
295 |
296 | export default EventsPage;
297 |
--------------------------------------------------------------------------------