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
- A user can add/remove a plant to their collection from the seeded plants data
- The plant show page has a picture, description, and origin details.
- The plant data was scraped from a houseplant database
- There’s a message board for users to post on. Users can upload pictures of their plants and ask for advice/ questions about their plants.
- Other users can post comments in each topic.
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 + 1common_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
- Had issues where whenever I’d manually type in the url, it would only render from the first set of data (page 1 of my backend pagination).
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
- Search bar option
- A map on a user’s show page that has pins where in the world their plants in their collections are from
- A service that sends emails to users to confirm sign up
- A service that sends reminders on when a user needs to water their plants