For my final React-Redux project for Flatiron's Software Engineering bootcamp, I created a demo e-commerce site for a fake smoothie shop. The main features are the Build A Smoothie and cart features. In this blog post, I'm going to demo the code that makes up my cart functionality. It's a very barebones shopping cart, which hopefully will be helpful for anyone trying to quickly throw together a cart feature.
To start off, I broke down my shopping cart into 5 different components:
I also created a redux cart reducer to handle my actions, and mapped my redux state and dispatch actions to a component called CartContainer. Within my CartContainer, I passed redux state/dispatch actions to the appropriate components:
import React, { Component } from 'react';
import CartList from '../components/cart/CartList'
import { connect } from 'react-redux'
import CartTotal from '../components/cart/CartTotal'
import CheckoutButton from '../components/cart/CheckoutButton'
import { Container } from 'react-bootstrap'
import { removeCartItem } from '../actions/cartActions.js'
class CartContainer extends Component {
render() {
return (
<Container>
<div className="body-wrapper">
<div className="inner-wrapper">
<div>
<CartList removeCartItem={this.props.removeCartItem} items={this.props.items} />
<CartTotal cartTotal={this.props.cartTotal} />
<CheckoutButton items={this.props.items} totalPrice={this.props.cartTotal} />
</div>
</div>
</div>
</Container>
);
}
}
const mapStateToProps = (state) => ({
items: state.cartReducer.cartItems,
ingredientIds: state.cartReducer.cartItems.ingredientIds,
cartTotal: state.cartReducer.cartTotal
})
const mapDispatchToProps = dispatch => ({
removeCartItem: (id) => dispatch(removeCartItem(id)),
})
export default connect(mapStateToProps, mapDispatchToProps)(CartContainer);
My CartList
, CartItem
, CartTotal
, and CheckoutButton
simply renders buttons and data from my global store.
Within my CheckoutContainer
, I created a very standard HTML form to collect checkout information:
<form className="checkout-form">
<label>Please Enter Checkout Details</label><br></br>
<Row>
<br></br>
<Col>
<label htmlFor="customerName">* Name:</label><br></br>
<input id="customerName" type="text" name="customerName" onChange={this.handleChange} /><br></br>
<label htmlFor="address">* Address:</label><br></br>
<textarea id="address" type="textarea" name="address" onChange={this.handleChange} /><br></br>
<label htmlFor="note">Customer Note:</label><br></br>
<input id="note" type="text" name="note" onChange={this.handleChange} /><br></br>
</Col>
<Col>
<label htmlFor="cardnum">* Card Number:</label>
<input id="cardnum" type="text" name="cardNumber" onChange={this.handleChange} /><br></br>
<label htmlFor="cardexp">* Expiration Date (MM/YY):</label>
<input id="cardexp" type="text" name="cardExp" onChange={this.handleChange} /><br></br>
<label htmlFor="cardsec">* Security Code:</label>
<input id="cardsec" type="password" name="cardSecurityNum" onChange={this.handleChange} /><br></br>
<br></br>
<Button variant="success" onClick={(e) => this.handleSubmit(e)} >Submit Order</Button>
</Col>
</Row>
</form >
The CheckoutForm
component keeps track of it's own state to pass along to the global redux store which is ultimately what gets POSTed to the database upon checkout finalization. Card information is commented out as I didn't actually want to collect any real card information since this is a fake shop.
state = {
items: this.props.items,
totalPrice: this.props.totalPrice,
customerName: '',
address: '',
// cardNumber: '',
// cardExp: '',
// cardSecurityNum: '',
note: '',
message: ''
}
I mapped my checkout dispatch functions to my CheckoutForm
container and utilized them within my submit event handlers:
const mapDispatchToProps = dispatch => ({
checkout: (data) => dispatch(checkout(data)),
emptyCart: () => dispatch(emptyCart())
})
handleChange = (e) => {
this.setState((prevState) => ({
...prevState,
[e.target.name]: e.target.value,
}))
}
handleSubmit = (e) => {
e.preventDefault()
if (this.state.items.length > 0 && this.state.customerName !=='' && this.setState.address !== '') {
this.props.checkout(this.state)
this.props.emptyCart()
this.setState({message: "Your order is on the way!"})
} else {
this.setState({message: "Please fill out all required fields"})
}
}
My checkout POST action utilizes Thunk to incorporate my dispatches.
export const checkout = (data) => {
return (dispatch) => {
dispatch({type: 'LOADING_POST'})
return fetch('https://boiling-earth-59543.herokuapp.com/orders', {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(humps.decamelizeKeys({order: data}))
})
.then(resp => resp.json())
.then(json => {
console.log('this is the posted checkout obj', json)
dispatch({type: 'CHECKOUT', payload: data})
})
}
}
And my reducer handles saving this checkout item to the global store.
export default function cartReducer(state= {
order: []}, action) {
switch (action.type) {
case 'CHECKOUT':
const newOrder = {
ingredientIds: action.payload.items,
totalPrice: action.payload.totalPrice,
customerName: action.payload.customerName,
address: action.payload.address,
cardNumber: action.payload.cardNumber,
cardExp: action.payload.cardExp,
cardSecurityNum: action.payload.cardSecurityNum,
note: action.payload.note
}
return {
order: [...state.order, newOrder]
}
default:
return state
}
}
As far as my backend, things are pretty simple. My backend is a Rails api. My model relationships that pertain to the cart functionality are very simple:
class Order < ApplicationRecord
has_many :products
class Product < ApplicationRecord
has_many :product_ingredients
has_many :ingredients, through: :product_ingredients
belongs_to :order
I save Products to the database at the same time as Orders, as there was no real reason for my to be making extra fetches to the database when creating smoothie prodcuts on the frontend until they needed to be associated with an order. My create method looks like this:
def create
@order = Order.create(order_params)
Product.add_ingredients(params[:order][:items], @order)
if @order.save
render json: @order, status: :created, location: @order
else
render json: @order.errors, status: :unprocessable_entity
end
end
I created the class method add_ingredients
to help create Prodcuts alongside Orders:
def self.add_ingredients(items, order)
items.each do |item|
product = Product.create
item[:ingredient_ids].each do |id|
ingredient = Ingredient.find_by(id: id)
product.ingredients << ingredient
product.order_id = order.id
product.save
end
end
end
And that's it! Order's are persisted to the databases along with their Products (smoothies) and the ingredients that make them up. Thanks for reading!
Top comments (0)