DEV Community

alliecaton
alliecaton

Posted on • Edited on

React-Redux Smoothie E-commerce Site

Screen Shot 2021-05-07 at 10.41.43 AM

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:
Screen Shot 2021-05-07 at 2.46.21 PM

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);
Enter fullscreen mode Exit fullscreen mode

My CartList, CartItem, CartTotal, and CheckoutButton simply renders buttons and data from my global store.
Screen Shot 2021-05-07 at 2.51.22 PM

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 >

Enter fullscreen mode Exit fullscreen mode

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: ''
    }
Enter fullscreen mode Exit fullscreen mode

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())
})
Enter fullscreen mode Exit fullscreen mode
    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"})
        }
    }
Enter fullscreen mode Exit fullscreen mode

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})
            })
        }
}
Enter fullscreen mode Exit fullscreen mode

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

    }

}
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode
class Product < ApplicationRecord
    has_many :product_ingredients
    has_many :ingredients, through: :product_ingredients
    belongs_to :order
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)