Simple Webshop

Goals

  • Building a simple webshop to proof that I understand the concepts of react.js and redux

  • Applying best practices from tutorials and communities.

  • Add an advanced element into the application like redux-saga, to show that I can enhance react application in a scalable and maintainable fashion

Mocking Data

- Shop products

- Delivery Address

Break The UI Into A Component Hierarchy

Simple happy application flow

ActionTypes

ActionCreators

export const ADD_PRODUCT_TO_CART = 'ADD_PRODUCT_TO_CART';
export const REMOVE_PRODUCT_FROM_CART = 'REMOVE_PRODUCT_FROM_CART';
export const OPEN_CART_LIST_DRAWER = 'OPEN_CART_LIST_DRAWER';
export const CLOSE_CART_LIST_DRAWER = 'CLOSE_CART_LIST_DRAWER';
export const SET_PRODUCTS = 'SET_PRODUCTS';
export const REMOVE_PRODUCT = 'REMOVE_PRODUCT';
export const ADD_PRODUCT = 'ADD_PRODUCT';
export const SET_VIEW_PRODUCT_ID = 'SET_VIEW_PRODUCT_ID';
export const SET_SELECTED_PRODUCT = 'SET_SELECTED_PRODUCT';
import {
    ADD_PRODUCT_TO_CART, CLOSE_CART_LIST_DRAWER, OPEN_CART_LIST_DRAWER, SET_PRODUCTS,
    SET_SELECTED_PRODUCT, SET_VIEW_PRODUCT_ID
} from "./actionTypes";
import {change} from 'redux-form';

export const initShopProducts = (products) => {
    return {type: SET_PRODUCTS, products}
};

export const selectProductToView = (selectedProduct) => {
    return {type: SET_SELECTED_PRODUCT, selectedProduct}
};

export const selectProductIdToView = (productId) => {
    return {type: SET_VIEW_PRODUCT_ID, productId};
};

export const addProductToCart =(product) => {
    return {type: ADD_PRODUCT_TO_CART, product};
};

export const openCartListDrawer = () => {
    return {type: OPEN_CART_LIST_DRAWER}
};

export const closeCartListDrawer = () => {
    return {type: CLOSE_CART_LIST_DRAWER}
};

export const setLatForAddress = (lat) => {
    return change('deliveryAddress', 'lat', lat);
};

export const setLngForAddress = (lng) => {
    return change('deliveryAddress', 'lng', lng);
};

export const fillInStreet = (street) => {
    return change('deliveryAddress', 'street', street);
};

export const fillInCity = (city) => {
    return change('deliveryAddress', 'city', city);
};

export const fillInAddress = (data) => {
    return change('deliveryAddress', 'address', data);
};

Shop reducers

import {ADD_PRODUCT, REMOVE_PRODUCT, SET_PRODUCTS, SET_SELECTED_PRODUCT, SET_VIEW_PRODUCT_ID} from "../actionTypes";

const defaultState = {
    products: [],
    selectedProduct: {
        image: null,
        description: null,
        title: null,
        price: null
    }
};

export default function shop(state = defaultState, action) {
    switch (action.type) {
        case SET_PRODUCTS:
            return {...state, products: action.products}
        case REMOVE_PRODUCT:
            return Object.assign({}, state, {
                products: [
                    ...state.products.filter(product => product.id !== action.product.id)
                ]
            })
        case ADD_PRODUCT:
            return Object.assign({}, state, {
                products: [...state.products, action.product]
            })
        case SET_VIEW_PRODUCT_ID:
            return Object.assign({}, state, {
                productId: action.productId
            })
        case SET_SELECTED_PRODUCT:
            return Object.assign({}, state, {
                selectedProduct: action.selectedProduct
            })
        default:
            return state
    }
}

Cart Reducers

import {products} from '../MockApi';
import {sample} from "lodash";
import {
    ADD_PRODUCT_TO_CART, CLOSE_CART_LIST_DRAWER, OPEN_CART_LIST_DRAWER,
    REMOVE_PRODUCT_FROM_CART
} from "../actionTypes";

const defaultState = {
    products: [sample(products)]
};

export default function (state = defaultState, action) {
    switch (action.type) {
        case ADD_PRODUCT_TO_CART:
            return {
                ...state,
                products: [...state.products.map((product) => {
                    product.animated = false;
                    return product;
                }), {...action.product, animated: true}]
            };
        case REMOVE_PRODUCT_FROM_CART:
            return {
                ...state,
                products: [...state.products.filter(product => product.id !== action.product.id)]
            };
        case OPEN_CART_LIST_DRAWER:
            return {
                ...state,
                cartListDrawerOpened: true
            };
        case CLOSE_CART_LIST_DRAWER:
            return {
                ...state,
                cartListDrawerOpened: false
            };
        default:
            return state
    }
}
export function* getAllProducts() {
    const products = yield call(Api.getAllProducts);
    yield put(initShopProducts(products));
}

export default function* rootSaga() {
    yield fork(getAllProducts);
    yield fork(watchViewProductDetail);
    yield fork(watchCompleteAddressInput);
}
export function* watchViewProductDetail() {
    while (true) {
        const {productId} = yield take(SET_VIEW_PRODUCT_ID);
        const selectedProduct = yield call(Api.getProduct, parseInt(productId));
        yield put(selectProductToView(selectedProduct));
    }
}

export default function* rootSaga() {
    yield fork(getAllProducts);
    yield fork(watchViewProductDetail);
    yield fork(watchCompleteAddressInput);
}
function* watchCompleteAddressInput() {
    while (true) {
        const {meta} = yield take(formActionTypes.CHANGE);
        if (meta.field === 'address') {
            const {zipcode, housenumber, street, city, address} =
                yield select((state) => state.form['deliveryAddress'].values);

            if (zipcode && street && housenumber && city && address) {
                try {
                    const {data} = yield call(Api.getGeoCodeByAddress, `${street},${housenumber},${city}`);
                    const lat = data.results[0]['geometry'].location.lat;
                    const lng = data.results[0]['geometry'].location.lng;
                    yield put(setLatForAddress(lat));
                    yield put(setLngForAddress(lng));
                } catch (e) {

                }
            }
        }
    }
}


export default function* rootSaga() {
    yield fork(getAllProducts);
    yield fork(watchViewProductDetail);
    yield fork(watchCompleteAddressInput);
}

End of the example showcase

Next challenge would be

  • Unit Testing
  • Use LocalStorage to store cart session
  • Remove product from cart
  • Change quantity cart items

Simple Webshop with react.js and redux saga

By tlimpanont

Simple Webshop with react.js and redux saga

  • 1,772