Mod 5 Final Project- the technicals

For my final project, I created a social networking site for houseplant lovers called ~PlantiVerse~. I used React/Redux for my frontend, and a rails server for my backend.

Here’s what it does:

  • A user can login in /create an account

For my UI , I depended on this neat library called Material UI, which has a lot of styled templates and skeletons, neat buttons, and cool responsive forms.

I scraped about 200 or more plants from Tropicoia; there were no helpful houseplant APIs that I could have used.

One of the caveats I ran into while scraping, was that this website had inconsistent class labeling, which resulted in a lot of custom and nested logic. I used the Nokogiri gem for this bad-boy.

Here’s an example of -some- of what I had to do in order to get a title, common name, image url, and origin.

find_common_names= plant_array.find_index("-")
common_names_idx = find_common_names + 1
common_names = plant_array.slice(common_names_idx...).join(" ").gsub("<!-- Common name : -->", "")find_origin = plant.css('.ar12D')[14].inner_text.split(" ").join(" ")find_origin== "Origin :" ? origin= plant.css('.ar12D')[15].inner_text.split(" ").join(" ") : origin= find_originimg_src= plant.css('.ar12D').css('img').attr('src').value
img_url= "http://www.tropicopia.com/house-plant" +img_src.gsub("..", "")

...very ugly.

Since my plant data was so exhaustive I had to Paginate my backend data- otherwise every time I’d render the Plant Index, I would make expensive fetches to all the plants. This can slow down your program if your server is continuously running, and it wasn’t necessary to get every single piece of data.

For this task, I used Pagy. This ruby gem has great documentation, as well as a live chatroom from the developer. I posted a question on it, and received answer a few hours later. A m a z i n g. Pagy does this thing where it overrides your key transforms. To get around it, I manually set my keys in my serializer methods.

def imgUrl
self.object.img_url
end

Since my backend API was paginated, that means I had to workout how to paginate on my frontend, naturally.

I wanted to use Material UI’s pagination template, so I grabbed some code from the router integration . But BAM. It became an expedition trying to figure it out. Here’s what happened:

  • The url params wouldn’t change whenever i went to different pages

I had to figure out how to connect my front end with my backend params.

To get cracking on this issue, I had to make sure that my router was set up to take in params for the next page.

<Route path="/plants/:id" component={PlantShow} />
<Route path="/plants?page=:id" component={PlantIndex} />

Then I had to configure my action creator to check which page of JSON plant data I am going to be grabbing from my backend. Thus, my action creator had to take in an argument of an id (or searchQuery) for this case. It checks to see whether or not an id is passed.

export const getPlants = (searchQuery) => {
const url= !searchQuery? "http://localhost:3000/plants" :
`http://localhost:3000/plants${searchQuery}`return dispatch => fetch(url)
.then(res => res.json())
.then(plants => {
dispatch({type: "GET_PLANTS", payload: plants.plant_records})}
)
}

The pagination links at the bottom of my plant index have an event listener that checks the value of the page number clicked, sets a state of the page number that was requested, and then uses the useHistory hook to redirect to a param.

const history = useHistory()
const [page, setPage] = useState(`?page=1`)
const handleChange = (e, value) => {
e.preventDefault()
setPage(`?page=${value}`);
history.push(`/plants?page=${value}`)
};

In order to get the correct plants to render upon mounting, I used the useLocation hook.

const location =useLocation()useEffect(()=> { !location.search ? getPlants(page) :
getPlants(location.search)
},[page, getPlants, location.search])

Here, this code is checking if location.search is provided- location.search returns the query portion (params) of a URL including the Question mark (?). Remember the history.push() we did in our handleChange function. So with the above lines, you are going to your action creator -> sending over the default page 1 if you have no search query-> or sending over the location that you redirected from in history.push().

The next thing that had to be done was to make sure that the buttons rendered on the correct page/redirect. If I manually went to, say, page 3 of my plants data, the pagination buttons would default to page 1.

This code solves it:

const pageFromParams = !location.search ? 
1: parseInt(location.search.match(/\d/)[0])
<Pagination count={9} color="primary" onChange={handleChange} page ={pageFromParams}/>

The page in Pagination represents the page that it’s currently on. pageFromParams checks to see if location.search() was handed in, and if it does, sets the page number for visual display.

Last thing I want to talk about is the importance of having the correct state set up in your reducer, that’s grabbing the correctly formatted data.

I ran into a problem where whenever I added a plant to my collection, the button that added the plant didn’t toggle to the remove. The plant was correctly being added to the user’s collection, and I could only get the ‘remove plant’ option appear when I refreshed the page.

In react/redux, whenever there is a change in state, the page should automatically re-render.

After looking at my redux developer tools, I found that whenever I’d add a plant to my collection, the state of plants would updated with a nested values instead of an individual new plant. The payload data I was receiving from my backend was the whole array of plants that the user owns.

Instead of

case "ADD_PLANT_TO_USER":
return {...state,
user: {...state.user,
plants: [...state.user.plants, payload]}}

I wrote

case "ADD_PLANT_TO_USER":
return {...state,
user: {...state.user,
plants: payload}}

The same with my delete action, my backend sent me all of the user’s plants.

Original code:

case "DELETE_PLANT_FROM_USER":
return {...state, user: {...state.user, plants:
[...state.user.plants.filter(plant => plant.id !== payload.id)]}};

Working code:

case "DELETE_PLANT_FROM_USER":
return {...state, user: {...state.user,
plants: payload}};

There you have it! Those are a few of the big issues that took me a while to figure out.

My ideas to add to this project in the future:

  • A user can delete their own comments

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store