Skip to content
booker (GA WDI Project3)
Branch: master
Clone or download
Pull request Compare This branch is 2 commits ahead, 26 commits behind wesley-hall:master.
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
config
controllers
db
lib
models
src
test
.babelrc
.gitignore
README.md
index.js
nodemon.json
package.json
project-brief.md
webpack.config.js
yarn.lock

README.md

Booker : The Community Library Project

Project 3 : Web Development Immersive, General Assembly General Assembly

Live link: http://www.orjon.com/booker

(Please note that sections of this README are currently being worked on.)

Table of Contents

  1. Overview
  2. Team
  3. Technical Acceptance Criteria
  4. Project Proposal
  5. Technologies
  6. Team Organisation
  7. Wins
  8. Challenges
  9. Project Roadmap
  10. Project Deliverables

Overview

The third WDI project was to work in a team to deliver a fully-functional user-generated CMS (Content Management System) that includes multiple relationships between database models and consumes at least one public API (Application Programming Interface).

Team

In alphabetical order:

Name GitHub
Orjon https://github.com/orjon
Ru https://github.com/RuLette
Sumi https://github.com/SumiSastri
Wesley https://github.com/wesley-hall

Technical Acceptance Criteria

  1. The app must deliver something of value to the end-user with a visually impressive design, ideally should be mobile responsive.

  2. It should store user generated content (UGC) by authenticated users who login to upload content they have generated

  3. Users must have a fully functional CMS using the MERN stack (Mongo-Express-React-Node) to upload content

  4. The user flow from the front end (logged out and logged in) experience to the back end use of data must be simple, bug-free and fully functional

  5. The database should store 3-4 data-schemas

  6. A minimum of one external API should be integrated

  7. A complete CRUD (Create, Read, Update, Delete) cycle must be integrated into the user experience and be tested, demonstrating test-driven-development

  8. The project should be deployed (fully-working and bug-free) on Heroku

  9. A README file should outline approach and how the project meets technical requirements

Project Proposal:

App Name: Booker

Value Proposition: A book sharing community - where users share their book collections by loaning out and borrowing books from other users

Key Use Cases: "As a user, I would like to...."

  • register my details so that I can use the app and keep my details safe

  • login and create, update or delete my user profile as the details change so that accurate information is stored in the app

  • upload my books to the app and create a library of my books so that other users can borrow my books

  • see other peoples’ books and allow me to request to borrow books from their libraries

  • accept or reject a request to borrow the books in my library so that I retain control over who can borrow books from me

  • know how far the books that I want to borrow are from me so that I can make decisions on whether I want to travel that far to get the book

  • know the contact details of the owner of the books I want to borrow, so that I can arrange to pick the book up

  • know the contact details of the borrower of my books I want to borrow, so that I can arrange the book pick up

  • see the book title, author, reviews and ratings of books that can help me make decisions whether I would like to borrow the book or not

  • easily view the books I have out on loan and the books I am loaning in one place, so that I can manage the books I am reading and keep track that all books that have been borrowed are returned in a timely fashion

Technologies

Front End Back End Testing Other
React Node.js Mocha yarn
ReactDOM MongoDB (NoSQL) Chai Webpack
React Router DOM Express SuperTest Babel
Bulma Mongoose Axios
SCSS mongoose-autopopulate
Mapbox GL JS JSON Web Tokens (JWT)
bcrypt
dotenv  

View the full list of dependencies and dev dependencies in the package.json

Team Organisation - Loose Agile Framework

  • Team is self-organising
  • Decisions are made democratically
  • Trouble shoot early and often
  • Support quickly and solve problem
  • Seek to solve the problem with root cause analysis
  • The whole team is responsible for positive outcomes and good quality code
  • Interactions better than documentation

Wins

  • Key technologies used by everyone
  • User journeys well mapped out and data-flows discussed in detail
  • Good road-maps to map out back-log
  • Testing started early
  • Ongoing styling rather than leaving it to right at the end

Challenges

  • Creating the promise functions in the seeds file - figuring out the order of promises needed
  • Creating the test files - had to create a proxy user to test functionality
  • Nav bar bugs - challenges logging out users
  • Figuring out search and filter functions in React - pulling data into the render function
  • User profile - giving users the ability to set their own location using a map marker
  • Loans - scoping features and functions to fit time lines
  • Project management of roles and division of work - sprint rules difficult to follow for a one week project

Project Roadmap

Work in progress:

  • Front-end user journey (orjon)
  • Back-end configuration structure
  • Data schemas
  • Product backlog

User journey

Early in the development stage we broke down all the application's functions into groups that would become the 'pages' of the application. These were sketched out on pieces of paper and the arrangement of these helped us to map out a clear user journey, and separate concerns.

Libraries Logged In

This was an iterative, sometimes subjective, but ultimately very constructive process. Sketching out the user flows in this way greatly assisted in structuring the code and filing.

Before signing up, the app allows visitors access to view the contents of the site so that they have a clear understanding of what the site does and what books are available.

New user journey

After sign-up & login top navigation menu changes slightly. It is from here that the user can control their own journey through the site. This was intentionally reduced to a minimum number of options to aid clarity and ease-of-use.

Project Deliverables - Front End

Pages

Page Path Features
(Logged Out)
Additional Features
(Logged In)
Nav bar On all pages - Navigate to pages that do not require login
- Login or register
- Navigate to SecureRoute pages
- Logout
Home / View the app name/logo and tagline
About /about View the value proposition/brief explanation
Login /login Login as a returning (registered) users
Register /register Register as a new user
Books (All) /books View all books in the database View the distance between the logged in user's library and the libraries that the books are in
Book Show (Individual books) /books/:id View details of the chosen book:
- Book title
- Author
- Rating and reviews
- Owner information
- Loan request functionality
- All users can rate and review the book
- Existing reviews can be deleted by the user that created the review
- Users that own the book can remove/delete it
Book Add /books/add Login required to access this page Add a book by filling in a blank BookForm with the following:
- Text fields for title, author, image URL
- Select dropdown with options for genre
- Checkbox (styled as a toggle button) for non-/fiction
- Radio buttons for review
- Textarea for description and review
Book Update /books/:id/update Login required to access this page Users that own the book can change book information by filling in a pre-populated version of the BookForm
Book Loan /books/:id/loan Login required to access this page Users that don't own the book can create loan requests
Libraries /libraries View all libraries by location, including:
- A book count in the marker
- Library name, picture and description in a popup
- View the logged in user's own library location and details
- Link to the User Profile page to view and edit user information
Loans /loans Login required to access this page Loan management page for books loaned out and books borrowed
User Profile /users Login required to access this page Profile page of the user where they can view and delete their profile and library information
Edit Profile /userEdit Login required to access this page Page for users to update their profile and library information
404 /* Error 404 page for when users attempt to access a page that does not exist  

Home and About

Homepage About Page
Homepage About page

Login and Register

Login Register
Login Register

Navigation Bar

Logged Out
Navbar Logged Out
Logged In
Navbar Logged In

Libraries

Logged Out Logged In
Libraries Logged Out Libraries Logged In

Books

Viewing all books, filtering by library, searching for a specific book and then rating and reviewing it.
Books
Adding a new book from the main Books page
Adding a book

Book Loans - Borrowed

Borrowing a book and managing loan requests for books that the logged in user has borrow
Books Borrowed

Book Loans - Loaned Out

Managing loan requests for loans from the logged in user
Books Loaned Out

Forms

Form Description
Book Form Used for creating and updating book information:
- Text fields for title, author, image URL
- Select dropdown with options for genre
- Checkbox (styled as a toggle button) for non-/fiction
- Radio buttons for review
- Textarea for description and review
Loan Form Used for creating loan requests:
- Loan start date
- Loan end date
Loan updating is not handled by this form
User Form Used for creating and updating user information:
- Text fields for username, email, password, password confirmation and profile picture
and library information:
- Mapbox map for library location

Loan Management

Loan status changes were handled on the front end - there was no 'status' field stored in the database for each loan request.

To do this, functions were created to filter loans based on requirements that determined their status.

// Example: Some functions used to determine the status of a loan request

isPending(loan) {
  const { approved, declined, returned, end } = loan
  return !approved && !declined && !returned && new Date() < new Date(end)
}

isAwaitingCollection(loan) {
  const { approved, collected, returned } = loan
  return approved && !collected && !returned
}

isOnLoan(loan) {
  const { approved, collected, returned, end } = loan
  return approved && !!collected && !returned && new Date() < new Date(end)
}

isReturned(loan) {
  return !!loan.returned
}

isOverdue(loan) {
  const { end, approved, collected, returned } = loan
  return approved && !!collected && !returned && new Date() > new Date(end)
}

This then allowed a certain status to be displayed, as well as a corresponding user action, if one was required.

// Example: If the loan is pending, display the LoanedPending component

{isPending(loan) &&
  <LoanedPending
    className="loan-border-bottom"
    loan={loan}
    approveLoanRequest={approveLoanRequest}
    declineLoanRequest={declineLoanRequest}
  />
}

View the LoanedPending component here

Functions were also written to handle a PUT axios request to update the loan request in the database

// Example: Function to allow a user to approve a loan request:

approveLoanRequest(e) {
  axios({
    method: 'PUT',
    url: `/api/loans/${e.target.value}`,
    headers: {
      'Authorization': `Bearer ${Auth.getToken()}`
    },
    data: {
      // Here the request is to change 'approved' to true
      approved: true
    }
  })
    .then(() => this.getLoans())
    .catch(err => console.log(err))
}

Styling

Concept

Of primary styling concern was to keep the interface very simple and intuitive to use.

This begins with the about page which clearly states the purpose of the application.

Libraries Logged In

Bulma Framework

Styling was implemented using the Bulma CSS framework. Bulma has classes which are structured greatly speed up the process of creating grid layouts in particular, such as we used for the Books (All) page.

Libraries Logged In

There are several different sets of information that need to be displayed on the various pages of the site - the aim was to keep these as uniformed as possible. To help visually tie the pages together a colour-coded styling language was developed for the buttons.

Buttons
Large Libraries Logged In
Small Libraries Logged In

View the style SCSS file here

Project Deliverables - Back End

Models

Libraries Logged In

User

Login/authentication credentials, as well as profile and library information

const userSchema = new mongoose.Schema({
 username: { type: String, required: true, unique: true },
 profilePicture: { type: String},
 email: { type: String, required: true, unique: true },
 password: { type: String, required: true, unique: true },
 libraryName: { type: String, required: true, unique: true },
 location: {
   lat: { type: Number, required: true },
   lng: { type: Number, required: true }
 },
 libraryPicture: { type: String},
 libraryDescription: { type: String },
 userRating: [ userRatingSchema ]
}, {
 timestamps: true
})
  • Virtual fields were also included for books, loans and password confirmation

Genre

A simple Mongoose Schema containing one string for the genre name/title

const bookGenreSchema = new mongoose.Schema({
 genre: { type: String, required: true }
})
  • Genres were created separately from books so that the list could be scaled up as required

Book

Book information with references to the BookGenre and User schemas, as well as information for book ratings and reviews

const ratingSchema = new mongoose.Schema({
 rating: {type: Number, min: 1, max: 5},
 user: {type: mongoose.Schema.ObjectId, ref: 'User',  autopopulate: true }
})
const reviewSchema = new mongoose.Schema({
 review: {type: String},
 user: {type: mongoose.Schema.ObjectId, ref: 'User', autopopulate: true }
})
const bookSchema = new mongoose.Schema({
 title: {type: String, required: true},
 authors: {type: String},
 image: {type: String},
 fiction: {type: Boolean, required: true},
 genre: { type: mongoose.Schema.ObjectId, ref: 'BookGenre'},
 description: {type: String},
 rating: [ratingSchema],
 review: [reviewSchema],
 owner: { type: mongoose.Schema.ObjectId, ref: 'User', autopopulate: true }
})
  • Virtual fields were also used for book loans
  • Where autopopulate: true can be seen, mongoose-autopopulate has been used to autopopulate the local field (e.g. owner) with information from the referenced model (e.g. User)

Loan

Loan information with references to the Book and User schemas

const loanSchema = new mongoose.Schema({
 book: { type: mongoose.Schema.ObjectId, ref: 'Book'},
 borrower: { type: mongoose.Schema.ObjectId, ref: 'User'},
 start: { type: Date, required: true},
 end: { type: Date, required: true},
 message: { type: String },
 approved: { type: Boolean },
 declined: { type: Boolean },
 collected: { type: Date },
 returned: { type: Date}
}, {
 timestamps: true
})

Controllers

Authentication

User login and registration functionality with JSON Web Tokens (JWT)

// Example: User registration (CRUD - Create)

function register(req, res, next) {
User
  .create(req.body)
  .then(user => {
    const token = jwt.sign({ sub: user._id }, secret, { expiresIn: '6h' })
    res.json({
      message: `Thanks for registering, ${user.username}`,
      token,
      user
    })
  })
  .catch(next)
}

Users

Complete CRUD cycle for users:

CRUD API Route HTTP Method
Create /api/register POST
Read /api/users
/api/users/:id
GET
Update /api/users/:id PUT
Delete /api/users/:id DELETE
// Example: Show information on a specific user (CRUD - Read)

function userShow(req, res) {
User
  .findById(req.params.id)
  .then(user => res.status(200).json(user))
  .catch(err => res.json(err))
}

Genres

Read only (no Create, Update or Delete for genres):

CRUD API Route HTTP Method
Read /api/genres GET
// Example: Show all genres (CRUD - Read)

function genresAll(req, res) {
Genres
  .find()
  .then(genres => res.json(genres))
  .catch(e => console.log(e))
}

Books, including reviews and ratings

Complete CRUD cycle for books:

CRUD API Route HTTP Method
Create /api/books POST
Read /api/books
/api/books/:id
GET
Update /api/books/:id PUT
Delete /api/books/:id DELETE
// Example: Delete a book (CRUD - Delete)

function bookDelete(req, res) {
  Book
    .findByIdAndRemove(req.params.id)
    .then(() => res.sendStatus(204))
    .catch(err => res.status(500).json(err))
}

Create and Delete for reviews, Create only for ratings:

CRUD API Route HTTP Method
Create /api/books/:id/review
/api/books/:id/rating
POST
Delete /api/books/:id/review/:reviewId DELETE
  • No Read is required as this information is sent with the books
// Example: Add a book rating (CRUD - Create)

function ratingAdd(req, res) {
req.body.user = req.currentUser
Book
  .findById(req.params.id)
  .populate('rating')
  .then(book => {
    book.rating.push(req.body)
    return book.save()
  })
  .then(book => res.json(book))
  .catch(err => res.status(422).json(err))
}

Loans

Complete CRUD cycle for loans:

CRUD API Route HTTP Method
Create /api/books/:id/loan POST
Read /api/loans
/api/loans/:id
GET
Update /api/loans/:id PUT
Delete /api/loans/:id DELETE
// Example: Update loan information (CRUD - Update)

function loanUpdate(req, res) {
Loan
  .findByIdAndUpdate(req.params.id, req.body, {new: true, runValidators: true})
  .exec()
  .then(loan => res.status(200).json(loan))
  .catch(err => res.status(500).json(err))
}

Configuration

Environment

Set up for the environment, port, database URI and secret


Routes

Pathways to the controller functions for the CRUD cycle

// Example: Routes for /books and /books/:id
// Note that some routes are secure and some are not

router.route('/books')
.get(books.booksAll)
.post(secureRoute, books.bookCreate)

router.route('/books/:id')
.get(books.bookShow)
.put(secureRoute, books.bookUpdate)
.delete(secureRoute, books.bookDelete)

Library

Error handler

For custom error messages and response statuses

// Example: 401 Unauthorized

if (err.message === 'Unauthorized') {
  return res.status(401).json({ message: 'Unauthorized' })
}

Secure route

Functionality to restrict access by unregistered and not logged in users

function secureRoute(req, res, next) {
 // Check if the request has an Authorization header
 if (!req.headers.authorization) return res.status(401).json({ message: 'Unauthorized' })
 // Remove 'Bearer ' from the Authorization header to just be left with the token
 const token = req.headers.authorization.replace('Bearer ', '')
 // Use jwt verify to check if the token is a valid JSON Web Token
 new Promise((resolve, reject) => {
   jwt.verify(token, secret, (err, payload) => {
     if (err) reject(err)
     resolve(payload)
   })
 })
  // If the token is valid, the promise will be resolved and the payload sub (user id)
  // can be used to find the user associated to the token
   .then(payload => User.findById(payload.sub))
   .then(user => {
     if (!user) return res.status(401).json({ message: 'Unauthorized' })
     req.currentUser = user
     next()
   })
   // If the token is not valid, the promise will be rejected and the catch block will run
   .catch(next)
}

Database

Seeds

To drop the current database and populate it with:

  • 11 users
  • 11 genres
  • 91 books
  • 24 loan requests

Seeds promises

In the seeds file, JavaScript promises were used to ensure that the database is always seeded in the correct order. This is because certain data models require others to exist before they can be created:

  • Books can only be created once users (book owners) and genres have been created
  • Loans can only be created once users and books have been created
// Example: Promise in seeds.js

// Create users and genres inside a promise array
const promiseArray = [
  User.create([...]),
  BookGenre.create([...])
]

// Wait for all promises to resolve before continuing
Promise.all(promiseArray)
  .then(data => {
    // Deconstruct data so that users and genres can be used when creating books
    const [ users, genres ] = data
    return Promise.all([
      Books.create([...]),
      // Along with books, pass users down to the next then block
      users
      ])
    })
  .then(data => {
    // Deconstruct data so that books and users can be used when creating loans
    const [ books, users ] = data
    return Loan.create([...])
  })

You can’t perform that action at this time.