View this project on Heroku: https://ga-booker.herokuapp.com
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 - Front End - Back End - Testing 11. Future Features 12. Key Learnings |
---|
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).
yarn | npm | |
---|---|---|
Install Dependencies | $ yarn |
$ npm install |
Run Server Locally | $ yarn start:server |
$ npm run start:server |
Run Client Locally | $ yarn start:client |
$ npm run start:client |
Seed Local Database | $ yarn seed |
$ npm run seed |
Run Tests Locally | $ yarn test |
$ npm run test |
Build with Webpack | $ yarn build |
$ npm run build |
View the full list of scripts and dependencies in the package.json
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 |
-
The app must deliver something of value to the end-user with a visually impressive design, ideally should be mobile responsive.
-
It should store user generated content (UGC) by authenticated users who login to upload content they have generated
-
Users must have a fully functional CMS using the MERN stack (Mongo-Express-React-Node) to upload content
-
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
-
The database should store 3-4 data-schemas
-
A minimum of one external API should be integrated
-
A complete CRUD (Create, Read, Update, Delete) cycle must be integrated into the user experience and be tested, demonstrating test-driven-development
-
The project should be deployed (fully-working and bug-free) on Heroku
-
A README file should outline approach and how the project meets technical requirements
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 so that I may borrow books from their libraries
-
confirm 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 so that I can 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
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 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
- 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
- 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
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.
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.
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 |
Homepage | About Page |
---|---|
![]() |
![]() |
Login | Register |
---|---|
![]() |
![]() |
Logged Out |
---|
![]() |
Logged In |
---|
![]() |
Logged Out | Logged In |
---|---|
![]() |
![]() |
Viewing all books, filtering by library, searching for a specific book and then rating and reviewing it. |
---|
![]() |
Adding a new book from the main Books page |
---|
![]() |
Borrowing a book and managing loan requests for books that the logged in user has borrow |
---|
![]() |
Managing loan requests for loans from the logged in user |
---|
![]() |
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 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))
}
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.
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.
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 | ![]() |
Small | ![]() |
View the style SCSS file here
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
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 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 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
})
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)
}
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))
}
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))
}
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))
}
Set up for the environment, port, database URI and secret
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)
For custom error messages and response statuses
// Example: 401 Unauthorized
if (err.message === 'Unauthorized') {
return res.status(401).json({ message: 'Unauthorized' })
}
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)
}
To drop the current database and populate it with:
- 11 users
- 11 genres
- 91 books
- 24 loan requests
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([...])
})
A test resource was created for the books using Chai and Mocha. SuperTest was also installed to make HTTP calls within the test environment. This meant that a local test environment could be created in the test file with 'dummy data'.
The dummy book data used for the test was stored in a variable called bookData. As defined in the book schema, fields such as title and fiction are required and a test would not pass unless these two fields were filled.
const bookData = {
title: 'The Hobbit',
authors: 'J.R.R Tolkien',
image: 'http://www.orjon.com/dev/booker/images/bookcovers/cover-theHobbit.jpeg',
fiction: true,
description: 'In a hole in the ground there lived a hobbit....'
}
As some routes (such as posting a book) were secure routes and required user authentication to access them, the user environment and JSON Web Token had to be imported into the test environment.
beforeEach(done => {
Book.collection.remove()
Book.create(
bookData
)
.then(() => User.remove({}))
.then(() => User.create({
username: 'test',
email: 'test',
password: 'test',
passwordConfirmation: 'test',
location: {
lat: 51.4,
lng: 21
},
libraryName: 'test'
}))
.then(user => {
token = jwt.sign({ sub: user._id }, secret, { expiresIn: '6h' })
done()
})
.catch(done)
})
describe('POST /api/books', () => {
it('should return a 201 response', done => {
api
.post('/api/books')
.set({ 'Accept': 'application/json', 'Authorization': `Bearer ${token}`})
.send(bookData)
.end((err, res) => {
console.log(err)
expect(res.status).to.eq(201)
done()
})
})
Tests can be run from the command line using yarn or npm:
$ yarn test
$ npm run test
Running tests in iTerm2 with yarn |
---|
![]() |
- Select dropdown to filter loans by status
- Button on the Libraries page that links from a library popup to the books page filtered by books belonging to that library
- Messaging between users
- Book loan notifications (i.e. 'New book loan request', 'Book loan approved', 'Book due to be returned in X days', etc.)
- Building a functioning full-stack app where requests can successfully display information on the front end (read) and data can be created/updated/deleted on the back end
- Promises in JavaScript
- Back end testing
- Back end error handling
- Custom SCSS on top of Bulma CSS Framework