winstonchan94 / decentreads

DecentReads is a full-stack collecting and reviewing site for books, inspired by goodReads

Home Page:http://decent-reads.herokuapp.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

DecentReads

DecentReads is a single-page web application for organizing and reviewing books. Registered users are able to use the site to shelve books according to their organizational standards, review books they have read, and browse books and reviews by other users.

Technologies Used

  • Backend
    • Database: PostgreSQL (v 0.18)
    • Language: Ruby (v 2.5.0)
    • Routing, Controllers, and Model: Rails (v 5.1.5)
    • Authentication using BCrypt (v 3.1.7)
  • Frontend
    • React/Redux
    • Ajax requests via JQuery
    • jSON jBuilder for backend to frontend data flow
    • SCSS for styling

Design Docs

Wiki

Features

  • Books
    • Users can browse through an index of books as well as books on their respective shelves.
    • Users can examine individual books to get general book information as well as access to user reviews and a personal review submission form
  • Shelves
    • Accounts come with auto-generated default shelves
    • Users can create custom shelves for personal organizational preferences
    • Users can add books to shelves from wherever books are displayed.
  • Reviews
    • Users may submit reviews for books, complete with a text body and a rating out of 5
    • Individual book pages house a list of existing reviews by users

Functionality

Login-status-aware component rendering

Authentication was implemented through a combination of frontend and backend logic. On the front end, logic was implemented in order to ensure that certain components such as the Read-status Bar and the Shelves Sidebar would only be rendered to a logged-in user.

Logged out state sans buttons and sidebar

Imgur

Logged in state with both aforementioned features

Imgur

This was done by making sure the session slice of state was mapped to the props of every potentially offending component, and consequently code was written to make sure the component would not be rendered if nobody was logged in, as per the following example snippet:

let statusContainer;
if (currentUser) {
  statusContainer = (<ReadStatusContainer className="book-show-status" book={book}/>);
}
return(
  <div className="book-details-box">
    <div className="book-details-left">
      <img className="book-cover" src={book.coverUrl}></img>
      {statusContainer}
    </div>
    <div className="book-details">
      <header>
        <h2 className="book-title">{book.title}</h2>
        <p className="book-author">by {book.author.name}</p>
        <h6 className="book-avg-score">avg. rating <strong>{book.averageScore}</strong></h6>
      </header>
      <p className="book-description">{description}</p>
      <p className="book-publish-info">Published {book.publish_date} by {book.publisher}</p>
    </div>

  </div>
);

Similar lines of code were used on both the Book Index as well as Review Form components.

Default Shelves vs. Non-default deletion & shelf creation form toggle

Users have the ability to create new shelves with customized names from the side bar, as well as delete them, but are not permitted to delete default shelves. These shelves are created upon account creation and handled by the backend controllers.

Imgur

To achieve this, the following lines of code were written to ensure that the deletion button would not appear next to default shelves:

  shelves.map(
    shelf => {
      if (!['Read', 'Currently Reading', 'Want to Read'].includes(shelf.name))
        {
          return (<li key={shelf.id} className="shelf-list-item">
            <Link
              className="shelf-list-link"
              to={`/mybooks/shelf/${shelf.id}`}>{shelf.name} ({shelf.bookIds.length})</Link>
            <button
              className="shelf-delete-button"
              onClick={() => {
                this.props.destroyShelf(shelf.id);
              }}>x</button>
          </li>);
        }

The toggling was implemented through alteration of a local state attribute within the form component:

constructor(props) {
  super(props);
  this.state = {
    name: '',
    userId: this.props.currentUser.id,
    toggleForm: false,
  };
  this.handleSubmit = this.handleSubmit.bind(this);
  this.showForm = this.showForm.bind(this);
  this.hideForm = this.hideForm.bind(this);

}

update(field) {
  return e => this.setState({
    [field]: e.currentTarget.value
  });
}

showForm() {
  this.setState({toggleForm: true});

}

hideForm() {
  this.setState({toggleForm: false});
}

handleSubmit(e) {
  e.preventDefault();
  const shelf = Object.assign({}, {name: this.state.name, user_id: this.state.userId});
  this.hideForm();
  this.props.processForm(shelf);
  this.setState({name: ''});
}

Dynamic book shelving

When a user is examining a default shelf, say "Currently Reading," for example, they are able to move any of the contained books to a different default shelf. It was paramount in such a situation that said book would disappear from the current page, as the following backend code would destroy any conflicting shelvings occurring upon creation of a new shelving:

def ensure_individuality
  defaults = ['Read', 'Currently Reading', 'Want to Read']
  @do_remove = false
  user = self.user
  encroaching_defaults = user.shelves.select do |shelf|
    defaults.include?(shelf.name) &&
    shelf.books.include?(self.book) &&
    shelf.id != self.shelf.id &&
    defaults.include?(self.shelf.name)
  end

  user.shelvings.each do |shelving|
    if (encroaching_defaults.any?{|shelf| shelf.shelvings.include?(shelving)} &&
      shelving.book_id == self.book_id &&
      defaults.include?(shelving.shelf.name))
        @shelf_to_remove_from = shelving.shelf_id
        shelving.destroy
    end
  end
end

To make sure shelves were visibly altered in this scenario, the following logic in the reducer for shelves was implemented:

case ADD_SHELVING:
  newState = merge({}, state);
  if (action.shelfToRemoveFrom) {
    if (newState[action.shelfToRemoveFrom]) {
      delete newState[action.shelfToRemoveFrom].bookIds[newState[action.shelfToRemoveFrom].bookIds.indexOf(action.bookId)];
    }
  }
  return newState;

The above code removed the offending book's id from the bookIds array of the shelf's slice of state, thus altering the props in the component and affecting the objects rendered.

Future Plans

Future updates will include but are not limited to:

  • Implementing book search
  • Implementing user search
  • Implementing user profiles
  • Integrating shelvings with reviews: i.e. adding books to 'Read' shelf upon review submission
    • Removal of books from shelves manually, whereupon associated reviews will be destroyed as well
  • Implementing a recommendation list via backend algorithm to be rendered to logged in users
  • Caching for performance

About

DecentReads is a full-stack collecting and reviewing site for books, inspired by goodReads

http://decent-reads.herokuapp.com


Languages

Language:HTML 60.7%Language:JavaScript 18.1%Language:CSS 14.4%Language:Ruby 6.6%Language:CoffeeScript 0.2%