/sports-with-strangers

Full-stack single-page rails application with react-redux frontend

Primary LanguageRuby

SportsWithStrangers

Sports With Strangers is a full-stack web application taking inspiration from Tea With Strangers. The application was built using Ruby-on-Rails with PostgreSQL for backend data management, and React.js with Redux to contain state on the frontend.

Sports With Strangers Live

Overview

Sports With Strangers is a meet-up application based around sporting events, with the aim to give users the ability to find and meet others who share their passion for a given sport, league, or team. The smaller number of cities and events helps to create a tight host-based community, wherein the host users from each city can build a positive reputation and in doing so encourage new users to attend meet-ups for the first time.

home image

Implementation of Features

React & React Router

Sports with Strangers is served entirely in a single page. Thanks to react-router, the application responds to the address bar normally, while never having to make a new HTTP-Request until reload. Restricted paths are still protected, as certain routes can only be accessed when the application state meets a given criteria.

Assigning Home Cities To Users

When a user decides chooses a city upon sign-up or changes it upon profile edit, the frontend only has access to the name of the city the user submitted. When the API request reaches the backend, Active Record queries the database for the city matching that name, and assigns ther user's city_id prior to database validation.

before_validation :set_city_id, :format_email, :set_default_image...

def set_city_id
  city = City.find_by(name: self.location)
  self.city_id = city.id if city
end

###Listing Events by Host or City

In the database events hold an indexed foreign_key of city_id, as well as host_id which corresponds to a column in the users table. Because GET requests to api/events are all handled by EventsController#index, the controller responds based on the parameters it is given.

$.ajax({
  method: 'GET',
  url: `api/events?event[host_id]=${id}`
})

Because events are fetched by their database associations and not their primary_key, the AJAX GET request sends a long the foreign_key as well in the form of a query string. The controller responds with either a city's events or a host's events depending on the parameters it receives.

def index
  if event_params[:city_id]
    city = City.find(event_params[:city_id])
    render json: ['City not found'], status: 404 if city.nil?
    @events = city.events
    render json: ["No events scheduled in #{city.name}."], status: 422 if @events.empty?
  elsif event_params[:host_id]
    host = User.find(event_params[:host_id])
    render json: ['User not found'], status: 404 if host.nil?
    @events = host.events
    render json: ["#{user.name} isn't hosting any events."], status: 422 if @events.empty?
  end
end

Listing A User's Attended Events

Attendance for events is stored in the database using an attendees table, which holds an event_id, and a user_id to link users to their attending events. When a user logs in, an AJAX call is made to retrieve and store a user's information as well as the events they are attending. The information is sent back in JSON format via jbuilder. The decision to store the current user's events was made to avoid making new requests every time a user visited their dashboard. The EventList react component more frequently makes requests to fetch a new list by host, or by user, but a user's attendance only needs to be fetched once upon login.

json.extract! @user, :email, :name, :location, :image, :city, :blurb
json.set! :attending, @user.attending do |event|
  json.title event.title
  json.time event.time
  json.address event.address
  json.capacity event.capacity
  json.sport event.sport
  json.city_id event.city_id
  json.id event.id
end

Security and Privacy

When requests are made to the backend to change a user's information, Rails checks the current user's id against the id of the user being updated. This prevents a malicious user from changing another user's information by making an outside PATCH request.

if @user.id != current_user.id
  errors.add(:user, 'validation failed')
end

However, in the case of host users, their information is public on their host page. Therefore prior to having their information being sent to the frontend to render their host page, rails checks if it is either the current user making the request, or the user is a host. This way, a user's email is only made public when they choose to become a host, and is protected against any requests made from the client-side attempting to attain others' email.

Joining Events & Event Capacity

When user visits a city's page, react first checks that the user is logged in before continuing. Then an AJAX call is made using the react-router params to fetch the correct events. These events are rendered in a list component. Each event has a capacity cell in the database, and is returned from the backend with the number of attendees, so it may be compared with the capacity.

```ruby
@events.attendees.length
```

dashboard image When each event item is rendered the difference is calculated, and the button is disabled if the capacity has been met. The current user's id is also checked against the host id, in order to disable the button if the current user is hosting that event.

if (this.props.attending.includes(event.id)) {
  return (
    <div>
    <input type='submit' value={'Leave this Event'}
      className='button'
      onClick={() => this.leaveEvent(this.props.currentUserId, event.id)} />
    <p>You are attending this event</p></div>
  );
} else if (event.host.id === this.props.currentUserId) {
  return (
    <div>
    <input type='button' disabled value={'Cannot Join Own Event'}
      className='button-disabled'/>
    <p>You are hosting this event</p></div>
  );
} else if (event.attending >= event.capacity) {
  return (
    <div>
    <input type='button' disabled value={'No Spaces Left'}
      className='button-disabled'/>
  </div>
);
}

The challenge was to retrieve the information needed to achieve each state of each event item, but without retrieving and storing unnecessary objects. Only the an array of id's from the attending users are fetched to be compared with the host and the length counted against capacity, no user objects.

User Dashboard and Editing User Information

One of the great things about react is the ease in having a single path for all users to share. You are able to display and edit the current user's information without ever having to make another request after initial login. The 'edit profile' form is pre-filled with the information contained in the redux-store, and is reflected in the react component's state. Upon logout, the data used to fill the personal information is discarded by the frontend, and there is no risk of another user gaining access to someone else's dashboard by URL tampering.

dashboard image

React also allows you to set boolean values in JSX to be compiled into HTML. I am able to disable a form until the user decides they want to interact with it, something that cannot be done in HTML so eloquently.

  <input type='email'
  disabled={this.state.disabled}
  value={user.email}
  onChange={this.update('email')}/>

I chose to handle any errors with the update form without dispatching actions to the redux store. It made it simple to display a success message when the PATCH request was successful, and let the user know what they missed otherwise. I utilized setTimeout with setState's optional callback to display the success/failure message for five seconds.

handleSubmit() {
  this.props.updateUser(this.state).then(
    res => this.setState({prompt: 'Successfully Updated'},
     () => setTimeout(() => this.setState({prompt: ''}), 5000)),
    res => this.setState({prompt: res.responseJSON.map(err => ` ${err}. `)},
    () => setTimeout(() => this.setState({prompt: ''}), 5000)));
}

While normally data from the backend would be stored in state, utilizing success and failure callbacks in this manner worked well in this particular case since the data would be discarded almost immediately anyway.

##Future Directions for Project

Personalize Event Lists

  • Filter Events by Sport
  • List preferred Sports for Users

Hosts Search

  • Search Hosts by Sport
  • Search Events by Host, or Sport

API Integration

  • Integrate a public API to show sports statistics, or schedules if available.
  • Integrate Google Maps API for event location.

Smart Events

  • Suggestions based on favorite team, or previous attendance.