HomemadEtsy is an E-commerce platform based off on Etsy, a application that focuses on homemade products made with unique craftsman ship that one would not see on other e-commerce sites. HomemadEtsy, like Etsy, aims to connect buyers with sellers offering unique, handcrafted goods. Although right now the products are generic, I would like to expand on them in the future in the future to show off real handcrafted products from around the world. The platform is organized into categories like Jewelry & Accessories, Clothing & Shoes, Home & Living, Wedding & Party, and Craft Supplies to ease the shopping experience.
Check out the live website here.
Creating a state shape that was easily accessible by React components, readable, and efficient in terms of rendering time was a challenging task.
To solve this, I used Jbuilder to structure my JSON responses from the Rails backend. This allowed me to only include the data that was absolutely necessary for the front end, thus speeding up render times. I also wanted to solve the N+1 query issue, in which multiple fetches could be made for one piece of data. This would cause a slower, less interactable app, as well as an increase of fetches, which if fetching from a paid API, could be expensive. In order to solve this, I used the .includes method to load everything needed for a product or any other type of data in one request rather then many
@products.each do |product|
json.set! product.id do
json.extract! product, :id, :title, :description, :price, :stock_quantity
json.photoUrl product.photo.attached? ? product.photo.url : nil
json.user do
json.id product.user.id
json.username product.user.username
end
json.category do
json.id product.category.id
json.name product.category.name
end
end
end
def index
@products = Product.includes(:category, :user).all
render :index
end
Products Page:
The Jbuilder code is what is sent to the front end to be used as a data base for react componenets.
The Products Controller will control what gets sent to the jbuilder when index route is called
Implementing a shopping cart that was both user-specific and CRUD-capable was a considerable challenge.
For tracking cart items based on the user, I utilized Rails sessions tied to unique user IDs. CRUD operations were then handled using Rails routes and controllers to ensure a seamless user experience.
export const fetchCart = () => async (dispatch) => {
const res = await fetch("/api/cart_items");
if(res.ok){
const data = await res.json();
dispatch(receiveCart(data));
}else{
const errorMessage = await res.json();
console.error("Failed to fetch cart", errorMessage.message || "Unknown Error")
}
}
export const addToCart = (productId, quantity) => async(dispatch) => {
const res = await csrfFetch("/api/cart_items", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({product_id: productId, quantity: quantity})
})
if(res.ok){
const data = await res.json();
dispatch(receiveCartItem(data));
return true;
}else{
const errorMessage = await res.json();
console.error("Failed to add to cart", errorMessage.message || "Unknown Error");
return false;
}
}
export const updateToCart = (cartItemId, quantity) => async(dispatch) => {
const res = await csrfFetch(`/api/cart_items/${cartItemId}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({cartItemId: cartItemId, quantity: quantity})
})
if(res.ok){
const data = await res.json();
dispatch(receiveCartItem(data));
return true;
}else{
const errorMessage = await res.json();
console.error("Failed to update cart", errorMessage.message || "Unknown Error");
return false;
}
}
export const deleteCartItem = (cartItemId) => async(dispatch) => {
const res = await csrfFetch(`/api/cart_items/${cartItemId}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json"
}
})
if(res.ok){
dispatch(removeCartItem(cartItemId));
}else{
const errorMessage = await res.json();
console.error("Failed to delete cart item", errorMessage.message || "Unknown Error");
}
}
export const cartReducer = (state = {}, action) => {
const nextState = Object.assign({}, state);
switch (action.type) {
case RECEIVE_CART:
return{...state, ...action.payload}
case RECEIVE_CART_ITEM:
nextState[action.payload.id] = action.payload;
return nextState;
case REMOVE_CART_ITEM:
delete nextState[action.payload]
return nextState;
case REMOVE_CURRENT_USER:
return {};
default:
return state;
}
}
Cart Screen Shot:
These are my thunk actions and reducer for my cart