/solution-base

An idea exchange app built with Rails, React, Redux, Bootstrap

Primary LanguageJavaScript

SolutionBase

SolutionBase is a social platform for users to address global sustainability issues like global warming, plastic free etc. Platform allows user to contribute their ideas/solutions to different topics and problems. Read more about sustainable development goals here.

Live Site

SolutionBase

Design Documents

SolutionBase Wiki

Wireframes and Style Guide

Design alt text

Site Features

Sorting

One main feature is users can filter and sort posts in multiple combination. Their preferences are stored so that when they returned to homepage, it is still in sorted order and/or filtered.

  1. We have a slice of state to keep track of sorting and filtering, separated by topic page and homepage.

  2. For sorting, we use thunk action updateSort to update Redux store, and then, depending on the key, either fetchTopic or fetchPosts using the data object, which becomes the parameter Topic or Post controller receives.

// frontend/actions/filter_actions.js
export const updateSort = (id, data, key) => dispatch => {
  dispatch(receiveSort(id, data, key));
  if (key === 'topic') {
    return fetchTopic(id, data)(dispatch);
  } else if (key === 'homepage') {
    return fetchPosts(data)(dispatch);
  }
};
  1. The data object gets evaluated in the Topic or Post controller in index and calls Post's class method sort_filter. This method checks the sorting type and calls the responsible class methods, which then uses ActiveRecord to query the database.
# app/models/post.rb
def self.sort_filter(sort)
  case sort[:sort]
  when 'most recent'
    self.most_recent(sort[:topic_id], sort[:post_type])
  when 'most comments'
    self.most_commented(sort[:topic_id], sort[:post_type])
  when 'most votes'
    self.most_votes(sort[:topic_id], sort[:post_type])
  end
end
  1. When posts are received, we also receive a postOrder array, which is used to map posts in the sorted order.

  2. Every sort and filter action is stored and updated in Redux store and passed down to a component with mapStateToProps. So component dispatches updateSort on mount with the latest filter and sorting parameters.

Nested Comments

To have children comments nested under a parent comment, we use recursion and Javascript object to achieve better time complexity.

If we have 1000 comments c1, we do not want to iteratre all 1000 comments checking if its c2.parent_comment_id equals to c1.id. This will be 1000 * 1000 comparisons and an O(n^2) algorithm.

We can achieve this with O(n) time, where n is total number of comments.

  1. When we receive a post, we receive an array of all_comments, which is stored in the comments slice of Redux store. In a selector function that takes in state and the currently viewed post as arguments, we create an object with keys as parent_comment_id and values arrays of their children comments. Comments with no parent have null as their keys.
// frontend/reducers/selectors.js
export const selectCommentsForPost = ({comments}, post) => {
  // handle cases where there are no comments or we have not fetchPost
  if (!Object.keys(comments).length) return {};
  if (post.commentIds.filter(commentId => !comments[commentId]).length) return {};
  // create map-like object
  let hash = {};
  post.commentIds.map(commentId => comments[commentId])
    .forEach((comment) => {
      let parent = comment.parent_comment_id;
      let children = hash[parent];
      let update;
      if (children) {
        update = { [parent] : [...children, comment] }
      } else {
        update = { [parent] : [comment] }
      }
      hash = Object.assign({}, hash, update);
    })
  return hash;
}
  1. In the PostShow component, we have a renderComments method that takes a parent comment ID as an argument. This method lookups the children comments in the hash object created above and map over all the children comments. It makes a recursive call with each child comment ID to get its children comments. We call the function with null (comments with no parent) first, which will go through all their children and then their grandchildren and so on.
// frontend/components/posts/post_show.jsx
  function renderComments(id) {
    return (
      <>
        {comments[id].map(comment => (
          <>
            {singleComment(comment)} // returns <CommentIndexItem />
            <ul>
              {comments[comment.id] && <li>{renderComments(comment.id)}</li>}
            </ul>
          </>
        ))}
      </>
    )
  }
  ...
  return (
    ...
    renderComments(null)
    ...
  )

Usage

Development

  1. Front end

    Use the npm package manager to install dependencies in root directory.

    npm install
    npm run webpack
  2. Back end

    Install rails dependencies in root directory, setup Postgres database and start development server.

    rails bundle exec install
    # create the database, load the schema, and initialize it with the seed data
    rails db:setup
    rails s

    You can view the full application on localhost:3000.

Built With

Front-end:

  • React - Front end framework
  • Redux - State management tool used with React
  • jQuery - DOM Manipulation
  • AJAX - AJAX used for API calls
  • Javascript - Language for site functionality
  • React-Bootstrap - - Used for advanced styling and design system
  • Material-UI - Used for advanced styling
  • Styled-Component - Used for custom styling React components
  • CSS3 - Used in conjunction with Material-UI for styling
  • HTML5 - Used for general structure of webpage

Back-end:

Potential Additions/ Unsolved Issues

  • Lazy loading / Infinite scrolling
  • Profile page
  • Text based search
  • Live time update new posts, comments and their vote counts with WebSockets