---
# System prepended metadata

title: Changes between end of Lecture 4 and Beginning of Lecture 5

---

# Changes between end of Lecture 4 and Beginning of Lecture 5

## Bug Fix for UserEvents

I was accidentally trying to destroy an event within the user_events controller instead of a user_event. I had this:

#### app/controllers/user_events_controller.rb
```rb
def destroy
  user_event = current_user.events.find(params[:id])
  user_event.destroy
  render json: user_event, status: :ok
end
```
Instead, I needed this:

```rb
def destroy
  user_event = current_user.user_events.find(params[:id])
  user_event.destroy
  render json: user_event, status: :ok
end
```

## Added Frontend for Events
### EventsContainer
- load events and groups from API
- Define callbacks for interacting with API
    - removeRsvpToEvent
    - cancelEvent
    - rsvpToEvent
    - createEvent
- Render routes for EventsList and EventDetail and pass appropriate data and callbacks as props

#### client/src/components/EventsContainer.js
```js
import React, { useState, useEffect } from 'react'
import { Switch, Route } from 'react-router-dom'
import EventsList from './EventsList'
import EventDetail from './EventDetail'

function EventsContainer() {
  const [events, setEvents] = useState([])
  const [groups, setGroups] = useState([])
  
  useEffect(() => {
    fetch(`/events`)
      .then(res => res.json())
      .then(events => setEvents(events))
    fetch(`/groups`)
      .then(res => res.json())
      .then(groups => setGroups(groups))
  },[])

  const removeRsvpToEvent = (eventId) => {
    const event = events.find(event => event.id === eventId)
    return fetch(`/user_events/${event.user_event.id}`, {
      method: "DELETE"
    })
      .then(res => {
        if (res.ok) {
          // if the event is the one we just removed an rsvp 
          // for, set its user_event property in state to 
          // undefined; If not, leave the event as it is
          const updatedEvents = events.map((event) => {
            if (event.id === eventId) {
              return {
                ...event,
                user_event: undefined
              }
            } else {
              return event
            }
          })
          setEvents(updatedEvents)
        }
      })
  }

  const cancelEvent = (eventId) => {
    return fetch(`/events/${eventId}`, {
      method: "DELETE"
    })
      .then(res => {
        if (res.ok) {
          const updatedEvents = events.filter(event => event.id !== eventId)
          setEvents(updatedEvents)
        }
      })
  }
  const rsvpToEvent = (eventId) => {
    return fetch('/user_events', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        event_id: eventId
      })
    })
      .then(res => {
        if (res.ok) {
          return res.json()
        } else {
          return res.json().then(errors => Promise.reject(errors))
        }
      })
      .then(userEvent => {
        // if the event is the one we just rsvp'd to
        // add a user_event property in state and set
        // it to the userEvent; if not, leave it as is
        const updatedEvents = events.map((event) => {
          if (event.id === eventId) {
            return {
              ...event,
              user_event: userEvent
            }
          } else {
            return event
          }
        })
        setEvents(updatedEvents)
      })
  }

  const createEvent = (formData) => {
    return fetch("/events", {
      method: "POST",
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify(formData)
    })
      .then(res => {
        if (res.ok) {
          return res.json()
        } else {
          return res.json().then(errors => Promise.reject(errors))
        }
      })
      .then(event => {
        setEvents(events.concat(event))
      })
  }

  return (
    <div>
      <Switch>
        <Route
          exact
          path="/events"
        >
          <EventsList
            events={events}
            groups={groups}
            cancelEvent={cancelEvent}
            removeRsvpToEvent={removeRsvpToEvent}
            rsvpToEvent={rsvpToEvent}
            createEvent={createEvent}
          />
        </Route>
        <Route
          exact
          path="/events/:id"
          render={({ match }) => {
            return <EventDetail
              eventId={match.params.id}
              cancelEvent={cancelEvent}
              removeRsvpToEvent={removeRsvpToEvent}
              rsvpToEvent={rsvpToEvent}
            />
          }}
        />
      </Switch>
    </div>
  )
}

export default EventsContainer
```

### EventsList
- Show list of links to events with a button to submit or cancel an rsvp and a button to cancel the event (if the current user created it)
- Show a form that can be used to add a new event to a group (belonging to the logged in user)
    - form has a `group_name` input that is allowed in `event_params` in the controller to pass through `.new` into the `group_name=` method in the `Event` model that will find or create a group with the given name and associated it with the event (assigning the foreign key for us)
    - Note, this requires that we change the `EventsController` to allow `group_name` through in our `event_params` and also that we define `group_name=(name)` within our `Event` model:

#### app/controllers/events_controller.rb

```rb
def event_params
  params.permit(:title, :description, :location, :start_time, :end_time, :group_name, :cover_image_url)
end
```

#### app/models/event.rb
```rb
def group_name=(group_name) 
  self.group = Group.find_or_create_by(name: group_name)
end
```
#### src/components/EventsList.js
```js
import React, { useState } from 'react'
import { Link } from 'react-router-dom'

function EventsList({ events, groups, removeRsvpToEvent, cancelEvent, rsvpToEvent, createEvent }) {
  const now = new Date();
  now.setMinutes(now.getMinutes() - now.getTimezoneOffset());
  const [title, setTitle] = useState('')
  const [description, setDescription] = useState('')
  const [location, setLocation] = useState('')
  const [startTime, setStartTime] = useState(now.toISOString().slice(0, 16))
  const [endTime, setEndTime] = useState('')
  const [groupName, setGroupName] = useState('')


  const rsvpOrCancelButton = (event) => {
    if (event.user_event) {
      return <button onClick={() => removeRsvpToEvent(event.id)}>Cancel RSVP</button>
    } else {
      return <button onClick={() => rsvpToEvent(event.id)}>RSVP for event</button>
    }
  }

  const cancelEventButton = (event) => {
    if (event.user_is_creator) {
      return <button onClick={() => cancelEvent(event.id)}>Cancel Event</button>
    }
  }

  const handleSubmit = (e) => {
    e.preventDefault()
    createEvent({
      title,
      description,
      location,
      start_time: startTime,
      end_time: endTime,
      group_name: groupName
    })
    setTitle('')
    setDescription('')
    setLocation('')
    setStartTime('')
    setEndTime('')
    setGroupName('')
  }
  
  return (
    <div>
      <h1>Events</h1>
      {events.map(event => (
        <p><Link to={`events/${event.id}`}>{event.title}</Link> --- {rsvpOrCancelButton(event)} --- {cancelEventButton(event)}</p>
      ))}
      <h3>Add Event</h3>
      <form onSubmit={handleSubmit}>
        <p>
          <label htmlFor="title">Title </label>
          <input
            type="text"
            value={title}
            onChange={(e) => setTitle(e.target.value)}
            name="title"
          />
        </p>
        <p>
          <label htmlFor="description"> Description </label>
          <textarea
            value={description}
            onChange={(e) => setDescription(e.target.value)}
            name="description"
          />
        </p>
        <p>
          <label htmlFor="name"> Location </label>
          <input
            type="text"
            value={location}
            onChange={(e) => setLocation(e.target.value)}
            name="location"
          />
        </p>
        <p>
          <label htmlFor="start_time"> Start Time </label>
          <input
            type="datetime-local"
            value={startTime}
            onChange={(e) => setStartTime(e.target.value)}
            name="start_time"
          />
        </p>
        <p>
          <label htmlFor="end_time"> End Time </label>
          <input
            type="datetime-local"
            value={endTime}
            onChange={(e) => setEndTime(e.target.value)}
            name="end_time"
          />
        </p>
        <p>
          <label htmlfor="group_name">Group Name </label>
          <input
            type="text"
            name="group_name"
            value={groupName}
            list="groups"
            onChange={(e) => setGroupName(e.target.value)}
          />
          <datalist id="groups">
            {groups.map(group => <option>{group.name}</option>)}
          </datalist>
        </p>
        {" "}<button type="submit">Add Event</button>
      </form>
    </div>
  )
}

export default EventsList
```

## EventDetail
- Display details for an event including the creator, a link to the group that's hosting the event, a description, time (start and end) and a list of the attendees. 
- Display buttons for rsvping to an event or canceling an existing rsvp (if logged in user has already rsvp'd)
    - to support this functionality, we need to update our EventSerializer to use the current_user method to retrieve the user_event for this event
    #### app/serializers/event_index_serializer.rb
    - ```
        class EventIndexSerializer < ActiveModel::Serializer
          attributes :id, :title, :description, :location, :start_time, :end_time, :user_event, :user_is_creator, :time

          def user_event
            current_user.user_events.find_by(event_id: object.id)
          end

          def user_is_creator
            current_user == object.user
          end

          def time
            "From #{object.start_time.strftime('%A, %m/%d/%y at %I:%m %p')} to #{object.end_time.strftime('%A, %m/%d/%y at %I:%m %p')}"
          end
        end
        ``` 
    - If our API responds with no user_event for an event, it means the current user has not rsvp'd for that event. 
    - If they have rsvp'd the `user_event` will include the `id` that we'll need to include in the URL of the delete request to remove the rsvp.

- We'll be displaying a button here for canceling the event if the currently logged in user is the one that created the event. The `user_is_created` attribute that we're adding to the serializer will support this functionality server side. We'll still need to ensure on the server side that only the user who created an event will be able to delete it.

#### client/src/components/EventDetail.js

- fetch details for the Event from the /events/:id show endpoint.
    - we're doing this within a useCallback hook so that we can trigger the fetch both on initial load and also after the user updates their rsvp.
- display button to cancel event (only to user who created the event).
    - redirect to /events upon cancellation
- display an rsvp button (cancel button to those who already have rsvp'd)
    - send another fetch to the callback to update the event in state after invoking the callback for creating or deleting the user_event 
    - we need this currently because the piece of state for `events` in the container component is updated by invoking the appropriate callback, but the `event` piece of state in the `EventDetail` component is coming from the API upon loading the component within `useEffect` (it's not coming in as a prop from the parent that would update and trigger a re-render after the callback)
```js
import React, { useState, useEffect, useCallback } from 'react'
import { useHistory } from 'react-router-dom'
import { Link } from 'react-router-dom'

function EventDetail({ eventId, removeRsvpToEvent, rsvpToEvent, cancelEvent }) {
  const [event, setEvent] = useState(null)
  const history = useHistory();

  const fetchEventCallback = useCallback(
    () => {
      fetch(`/events/${eventId}`)
        .then(res => res.json())
        .then(event => setEvent(event))
    },
    [eventId],
  )

  useEffect(() => {
    fetchEventCallback()
  }, [fetchEventCallback])


  const cancelEventButton = (event) => {
    if (event.user_is_creator) {
      return (
        <p>
          <button
            onClick={handleCancel}>Cancel Event</button>
        </p>
      )
    }
  }

  const handleCancel = (e) => {
    cancelEvent(event.id);
    history.push('/events')
  }

  const rsvpButton = (event) => {
    if (event.user_event) {
      return (
        <button
          onClick={() => {
            removeRsvpToEvent(event.id).then(() => fetchEventCallback())
          }
        }>
          Cancel RSVP
        </button >
      )
    } else {
      return (
        <button
          onClick={() => {
            rsvpToEvent(event.id).then(() => fetchEventCallback())
          }
        }>
          RSVP for event
        </button>
      )
    }
  }
  
  if(!event) { return <div></div>}
  return (
    <div>
      <h1>{event.title}</h1>
      {cancelEventButton(event)}
      <small>Created by {event.creator} for <Link to={`/groups/${event.group.id}`}>{event.group.name}</Link></small>
      <p>{event.description}</p>
      <p>{event.time}</p>
      <p>Location: {event.location}</p>
      <p>{rsvpButton(event)}</p>
      <ul>
        {event.attendees.map(attendee => (
          <li>{attendee.username}</li>
        ))}
      </ul>


    </div>
  )
}

export default EventDetail
```
## Higher Level changes
I added a route and link to App.js
#### client/src/App.js
```js
import './App.css';
import GroupsContainer from './components/GroupsContainer'
import EventsContainer from './components/EventsContainer'
import { Switch, Route, NavLink, BrowserRouter as Router } from 'react-router-dom'

function App() {
  return (
    <div className="App">
      <Router>
        <NavLink to="/groups">Groups</NavLink>{" - "}
        <NavLink to="/events">Events</NavLink>
        <Switch>
          <Route path="/groups">
            <GroupsContainer />
          </Route>
          <Route path="/events">
            <EventsContainer />
          </Route>
        </Switch>
      </Router>
    </div>
  );
}

export default App;
```

I added some styling to the forms and background in App.css
#### client/src/App.css
```css
body {
  background-color: #ddffaa;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120' viewBox='0 0 120 120'%3E%3Cpolygon fill='%23AE9' points='120 120 60 120 90 90 120 60 120 0 120 0 60 60 0 0 0 60 30 90 60 120 120 120 '/%3E%3C/svg%3E");
}
.App {
  background-color: rgba(255,255,255, 0.95);
  width: 80%;
  margin: 2em auto;
  padding: 2em;
  min-height: 100vh;
}

.App-logo {
  height: 40vmin;
  pointer-events: none;
}

form input, form textarea {
  width: 16rem;
  padding: 0.5rem;
}

form label {
  display: inline-block;
  width: 6rem;
  vertical-align: top;
  padding-top: 0.5rem;
}

@media (prefers-reduced-motion: no-preference) {
  .App-logo {
    animation: App-logo-spin infinite 20s linear;
  }
}

.App-header {
  background-color: #282c34;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
  color: white;
}

.App-link {
  color: #61dafb;
}

@keyframes App-logo-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

```

### Here's what my serializers/controllers were updated to:

#### app/serializers/event_index_serializer.rb
```rb
class EventIndexSerializer < ActiveModel::Serializer
  attributes :id, :title, :description, :location, :start_time, :end_time, :user_event, :user_is_creator, :time

  def user_event
    current_user.user_events.find_by(event_id: object.id)
  end

  def user_is_creator
    current_user == object.user
  end

  def time
    "From #{object.start_time.strftime('%A, %m/%d/%y at %I:%m %p')} to #{object.end_time.strftime('%A, %m/%d/%y at %I:%m %p')}"
  end
end
```

#### app/serializers/event_serializer.rb

```rb
class EventSerializer < EventIndexSerializer
  has_many :attendees
  belongs_to :group, serializer: EventGroupSerializer
  attribute :creator

  def creator
    object.user.username
  end
end
```

#### app/serializers/event_group_serializer.rb

```rb
class EventGroupSerializer < ActiveModel::Serializer
  attributes :id, :name
end
```

#### app/controllers/events_controller.rb
- Add includes user_events here to avoid N+1 query.
```rb
class EventsController < ApplicationController
  def index
    events = Event.all.includes(:user_events)
    render json: events, each_serializer: EventIndexSerializer
  end

  def show
    event = Event.find(params[:id])
    render json: event
  end

  def create
    event = current_user.created_events.new(event_params)
    if event.save
      render json: event, status: :created
    else
      render json: event.errors, status: :unprocessable_entity
    end
  end

  def update
    event = current_user.events.find(params[:id])
    if event.update(event_params)
      render json: event, status: :ok
    else
      render json: event.errors, status: :unprocessable_entity
    end
  end

  def destroy
    event = current_user.created_events.find(params[:id])
    event.destroy
    # we'll render the event as json in case we want to enable undo functionality from the frontend.
    render json: event, status: :ok
  end

  private

  def event_params
    params.permit(:title, :description, :location, :start_time, :end_time, :group_name, :cover_image_url)
  end

end
```

## Updating the way we handle groups to incorporate the current_user into the serializers to handle joining/leaving groups without the reduce on the client side

### Controllers and Serializers

#### app/controllers/groups_controller.rb

```rb
class GroupsController < ApplicationController
  def index
    groups = Group.all.includes(:user_groups)
    render json: groups, each_serializer: GroupIndexSerializer
  end

  def show
    render json: Group.find(params[:id])
  end

  def create
    group = Group.new(group_params)
    if group.save
      render json: group, status: :created
    else
      render json: group.errors, status: :unprocessable_entity
    end
  end

  private

  def group_params
    params.permit(:name, :location)
  end
end
```

#### app/serializers/group_index_serializer.rb
```rb
class GroupIndexSerializer < ActiveModel::Serializer
  attributes :id, :name, :location, :user_group

  def user_group
    current_user.user_groups.find_by(group_id: object.id)
  end
end
```

I removed the `GroupShowSerializer` and chose to use the default `GroupSerializer` instead.

#### app/serializers/group_serializer.rb
```rb
class GroupSerializer < GroupIndexSerializer
  has_many :events
  has_many :members
end
```

### Frontend changes for Groups

Main change was to remove the userGroups from the `GroupsContainer` in favor of allowing the API to manage the user_group relationship between the current_user and each group we get back from our API request. This means we don't need the reduce clientside anymore. 

#### client/src/components/GroupsContainer.js

Changes:
- remove `userGroups` from state 
- remove fetch call to `/user_groups` in useEffect
- remove `groupsWithMembership()` function
- pass `groups` piece of state directly into `GroupsList` (instead of `groupsWithMembership()`)
- Adjust promise callbacks in `joinGroup` and `leaveGroup` to update the `groups` piece of state and the `user_group` property for the appropriate group instead of updating `userGroups` in state.
- pass the `leaveGroup` and `joinGroup` callbacks to the `GroupDetail` component as well as `GroupsList`


```js
import React, { useState, useEffect } from 'react'
import { Switch, Route } from 'react-router-dom'
import GroupsList from './GroupsList'
import GroupDetail from './GroupDetail'

function GroupsContainer() {
  const [groups, setGroups] = useState([]);

  useEffect(() => {
    fetch("/groups")
      .then(res => res.json())
      .then(groups => setGroups(groups))
  }, [])


  const leaveGroup = (groupId) => {
    let userGroupId = groups.find(group => group.id === groupId).user_group.id
    return fetch(`/user_groups/${userGroupId}`, {
      method: 'DELETE'
    })
      .then(res => {
        if (res.ok) {
          const updatedGroups = groups.map(group => {
            if (group.id === groupId) {
              return {
                ...group,
                user_group: undefined
              }
            } else {
              return group
            }
          })
          setGroups(updatedGroups)
        }
      })
  }

  const joinGroup = (groupId) => {
    return fetch('/user_groups', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        group_id: groupId
      })
    })
      .then(res => {
        if (res.ok) {
          return res.json()
        } else {
          return res.json().then(errors => Promise.reject(errors))
        }
      })
      .then(userGroup => {
        const updatedGroups = groups.map(group => {
          if (group.id === groupId) {
            return {
              ...group,
              user_group: userGroup
            }
          } else {
            return group
          }
        })
        setGroups(updatedGroups)
      })
  }

  const createGroup = (formData) => {
    return fetch("/groups", {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(formData)
    })
      .then(res => {
        if (res.ok) {
          return res.json()
        } else {
          return res.json().then(errors => Promise.reject(errors))
        }
      })
      .then(group => {
        setGroups(groups.concat(group))
      })
  }

  return (
    <div>
      <Switch>
        <Route
          exact
          path="/groups"
        >
          <GroupsList
            groups={groups}
            leaveGroup={leaveGroup}
            joinGroup={joinGroup}
            createGroup={createGroup}
          />
        </Route>
        <Route
          exact
          path="/groups/:id"
          render={({ match }) => {
            return (
              <GroupDetail
                groupId={match.params.id}
                leaveGroup={leaveGroup}
                joinGroup={joinGroup}
              />
            )
          }}
        />
      </Switch>
    </div>
  )
}

export default GroupsContainer
```

#### client/src/components/GroupsList.js
Changes
- on line 9 replaced
```js
if (group.userGroup) {
```
with 
```js
if (group.user_group) {
```
and that was it!

#### client/src/components/GroupDetail.js

Changes:
- import { Link } from react router.
- Accept `leaveGroup` and `joinGroup` callbacks as props
- Add `useCallback` hook to wrap fetch to groups in `fetchGroupCallback` 
    - this callback will be used both with `useEffect` and also within promise callbacks that are triggered when the callbacks to join/leave a group resolve (both callbacks return promises)
- add a `leaveOrJoinButton` function that will render the correct button based on whether the `group` has a `user_group` or not.
- reorganize conditional render based on the loading status of the group
- add call to `leaveOrJoinButton` function to ensure the button appears in the UI
- Add Links to list of the groups events.


```js
import React, { useState, useEffect, useCallback } from 'react'
import { Link } from 'react-router-dom'

function GroupDetail({ groupId, leaveGroup, joinGroup }) {
  const [group, setGroup] = useState(null)

  const fetchGroupCallback = useCallback(() => {
    fetch(`/groups/${groupId}`)
      .then(res => res.json())
      .then(group => setGroup(group))
  }, [groupId])
  
  useEffect(() => {
    fetchGroupCallback()
  }, [fetchGroupCallback])

  const leaveOrJoinButton = (group) => {
    if (group.user_group) {
      return (
        <button
          onClick={() => leaveGroup(group.id).then(() => fetchGroupCallback())}
        >
          Leave Group
        </button>
      )
    } else {
      return (
        <button
          onClick={() => joinGroup(group.id).then(() => fetchGroupCallback())}
        >
          Join Group
        </button>
      )
    }
  }

  if(!group){ return <div></div>}
  
  return (
    <div>
      <h1>{group.name}</h1>
      {leaveOrJoinButton(group)}
      <h2>Members</h2>
      <ul>
        {group.members?.map(member => <li>{member.username}</li>)}
      </ul>
      <h2>Events</h2>
      <ul>
        {group.events?.map((event) => <li><Link to={`/events/${event.id}`}>{event.title}</Link></li>)}
      </ul>
    </div>
  )
  
}

export default GroupDetail
```

Next, I proceeded to add in the client side changes necessary to support authentication.

Check that out [here](https://hackmd.io/@dlm/phase4-lecture5-adding-authentication)

