Search code examples
javascriptreactjsreduxreact-reduxjson-server

Increase view count when each product is opened in React Redux


What i want is to increase the count of each product, when it is opened(viewed), using react redux.

AllProductsPage.js(The page starts here)

import React, { useState } from "react";
import { connect } from "react-redux";
import { bindActionCreators } from "redux";
import { Link } from "react-router-dom";
import ProductList from "./ProductList";
import Pagination from './Pagination'
import * as productActions from "../redux/actions/productActions";
import * as userActions from '../redux/actions/userActions'
import { Button } from "react-bootstrap";
import {FiSearch} from 'react-icons/fi'
import { Container, Row, Col} from "react-bootstrap";

const AllProductsPage =(props)=> {

    const [quantity, showQuantity] = useState(true);
    const [price, showPrice] = useState(true);
    const [manufacturer,showManufacturer] = useState(true);
    const data = {quantity,price,manufacturer};
    const [search,setSearch]=useState("");
    const loggedIn = props.loggedIn;
   
    //Pagination Logic
    const [currentPage,setCurrentPage] = useState(1)
    const postsPerPage = 9
    const indexOfLastPost = currentPage * postsPerPage;
    const indexOfFirstPost = indexOfLastPost - postsPerPage;
    const currentPosts = props.products.slice(indexOfFirstPost,indexOfLastPost)

    //Change the page
    const paginate =(pageNumber)=>{
      setCurrentPage(pageNumber)
    }


     //const filteredSearch = props.products && props.products.filter(product=>product.name.toLowerCase().indexOf(search.toLowerCase())!==-1).sort( (a,b)=>(a.id>b.id)?1:-1 );
    const filteredSearch = currentPosts && currentPosts.filter(product=>product.name.toLowerCase().indexOf(search.toLowerCase())!==-1).sort( (a,b)=>(a.id>b.id)?1:-1 );

    return (
      <div>

        <div style={{"display":"flex","paddingTop":"30px"}} className="container">
         { loggedIn && <Link to="/addProduct"><Button variant="primary">Add Product</Button>{" "}</Link> }

          <span style={{"marginLeft":"auto"}}><input type="text" onChange={event=>setSearch(event.target.value)}/> {" "} <FiSearch size="20px"/> </span>
        </div>

        <div style={{"display":"flex","justifyContent":"flex-end","alignItems":"space-between","paddingTop":"6px"}} className="container" >
          <label style={{"padding":"0px 5px 0px 2px","color":"white"}}><input type="checkbox"  defaultChecked={quantity} onClick={()=>showQuantity(!quantity)}/>{" "}Quantity</label>
          <label style={{"padding":"0px 5px 0px 2px","color":"white"}}><input type="checkbox"  defaultChecked={price} onClick={()=>showPrice(!price)}/>{" "}Price </label>
          <label style={{"padding":"0px 5px 0px 2px","color":"white"}}><input type="checkbox"  defaultChecked={manufacturer} onClick={()=>showManufacturer(!manufacturer)}/>{" "}Manufacturer </label>
        </div>
        
        <hr></hr>

        <div style={{minHeight:"100vh"}}>
        <ProductList 
          products={filteredSearch} 
          data={data} 
          togglePrice={showPrice}
          toggleQuantity={showQuantity}
          toggleManufacturer={showManufacturer}
          loggedIn={props.loggedIn}
        />
        <br />
        <Container>
          <Row>
          <Col></Col>
          <Col xs="auto" sm="auto" md="auto" lg="auto">
            <Pagination postsPerPage={postsPerPage} totalPosts={props.products.length} paginate={paginate} />
          </Col>
          <Col></Col>
          </Row>
        </Container>
        </div>
        <footer>
          <p style={{"textAlign":"center","backgroundColor":"#333","color":"white","padding":"20px"}}>Copyright @2020, Rohit K F</p>
        </footer>
      </div>
    );
}

function mapStateToProps(state, ownProps) {
  return {
    products: state.products,
    users : state.users
  };
}

function mapDispatchToProps(dispatch) {
  return {
    actions: bindActionCreators(productActions, dispatch),
    userAction : bindActionCreators(userActions,dispatch)
  };
}
export default (connect(mapStateToProps, mapDispatchToProps))(AllProductsPage);

ProductList.js(then it takes each product and passes it to Product.js)

import React from "react";
import Product from "./Product";
import { Container, Row, Col} from "react-bootstrap";

const chunk = (arr, chunkSize = 1, cache = []) => {
  const tmp = [...arr]
  if (chunkSize <= 0) return cache
  while (tmp.length) cache.push(tmp.splice(0, chunkSize))
  return cache
}

const ProductList = (props) => {
  const productsChunks = chunk(props.products, 3)
  
  const rows = productsChunks.map((productChunk, index) => {
        const productsCols = productChunk.map((product, index) => {
          return (
            <Col xs="auto" sm="auto" md="auto" lg="auto" key={product.id} style={{"paddingBottom":"20px"}}>
              <Product 
              key={product.id} 
              id={product.id}
              quantity={product.quantity} 
              price={product.price} 
              name={product.name} 
              description={product.description}
              manufacturer={product.manufacturer}
                {...props}
              />      
            </Col>
          );
        });
    return (
      <Row key={index} style={{"paddingBottom":"20px"}}>
       {productsCols}
      </Row>
            
  )});
    return (
    <Container>
      {rows}
    </Container>
  )
}

export default ProductList;

Product.js(Here we show the each product)

import React,{useState} from "react";
import { Link } from "react-router-dom";
import { Prompt, withRouter } from "react-router";
import { connect } from "react-redux";
import * as productActions from "../redux/actions/productActions";
import { bindActionCreators } from "redux";
import { Card, Button } from "react-bootstrap";
import toastr from "toastr";
import EditProduct from './EditProduct'
import {MdDelete,MdVisibility,MdCreate} from 'react-icons/md'


const Product = (props) => {
  const [show, setShow] = useState(false);
  const handleClose     = () => setShow(false);
  const handleShow      = () => setShow(true);
  
  const isLoggedIn = props.loggedIn
  const checkUser = (e) => {
      if (!isLoggedIn) {
        e.preventDefault();
        toastr.options = { positionClass: "toast-top-full-width",hideDuration: 300,timeOut: 2000,};
        toastr.clear();
        setTimeout(() => toastr.warning("Login to view details"), 0);
      }
  };

  const deleteProduct = () => {
    props.actions.deleteProduct(props.id)
  };
  //<Link to={'/ProductDetail/'+props.id}  >

  const product = {
    id :props.id,name:props.name,quantity:props.quantity,description:props.description,manufacturer:props.manufacturer,price:props.price
  }

  return (
    <>
    <Card style={{ width: "18rem", "borderRadius":"30px","border":"3px solid" }}>
      {isLoggedIn && (
        <Prompt when={isLoggedIn}
          message={(location) => location.pathname.includes("/ProductDetail/") ? `Are you sure you want to view the details ?` : true }
        />
      )}
      <Card.Body>
        <Card.Title style={{"fontSize":"30px","fontWeight":"bold","display":"flex", "justifyContent":"center"}}> {props.name} </Card.Title>
        {props.data.quantity && ( <Card.Text> Quantity : {props.quantity} </Card.Text> )}
        {props.data.manufacturer && <Card.Text> Manufacturer : {props.manufacturer}</Card.Text>}
        {props.data.price && <Card.Text>$ {props.price}</Card.Text>}

        <div style={{ display: "flex", justifyContent: "space-around" }}>
          

          <Link
          to={{
            pathname: `/ProductDetail/${props.id}`,
            productName: {
              id: props.id,
              name: props.name,
              price: props.price,
              quantity: props.quantity,
              description: props.description,
              manufacturer: props.manufacturer,
            },
          }}
        >
            <Button variant="primary" onClick={(event) => checkUser(event)} style={{ "fontWeight":"bold" }} > 
                {!isLoggedIn && <span style={{"paddingRight":"5px"}}>View</span> }
                {!isLoggedIn && <MdVisibility color="black"/> }
                {isLoggedIn && <MdVisibility/>}
            </Button>
          </Link>
          {isLoggedIn &&   <Button variant="success" style={{"fontWeight":"bold"  }} onClick={() => handleShow()} ><MdCreate/></Button> }    
          {isLoggedIn &&     <Button variant="danger" style={{"fontWeight":"bold"  }} onClick={() => deleteProduct()} ><MdDelete/> </Button>}
             
        </div>
      </Card.Body>
    </Card>
    <EditProduct show={show} handleClose={handleClose} actions={props.actions} product={product}/>
    </>
  );
};
function mapStateToProps(state, ownProps) {
  return {
    products: state.products,
  };
}

function mapDispatchToProps(dispatch) {
  return {
    actions: bindActionCreators(productActions, dispatch),
  };
}

export default connect(mapStateToProps,mapDispatchToProps)(withRouter(Product));

ProductDetail.js(When clicked on View, it goes to this page to view details of the product)

import React from 'react';
import { Link} from 'react-router-dom';
import {withRouter} from 'react-router'
import {Button, Card} from 'react-bootstrap'


const ProductDetail=(props)=>{
    console.log(props)
    const style={"display":"flex", "justifyContent":"center","alignItems":"center"}
    return(
            <div style={style}>
                <Card style={{ width: "18rem","borderRadius":"30px" }}>
                    <Card.Body style={{style}}>
                        <Card.Title style={{"fontSize":"30px","fontWeight":"bold","display":"flex", "justifyContent":"center"}}> {props.location.productName.name} </Card.Title>
                        <Card.Text><strong>Quantity    :</strong>{props.location.productName.quantity}</Card.Text>
                        <Card.Text><strong>Price       :</strong>{props.location.productName.price}</Card.Text>
                        <Card.Text><strong>Manufacturer:</strong>{props.location.productName.manufacturer}</Card.Text>
                        <Card.Text><strong>Description :</strong>{props.location.productName.description}</Card.Text>
                        <div>
                        <Link to="/"><Button variant="primary" style={{ height: "6vh","fontWeight":"bold" }}>Back</Button></Link>
                        </div>
                    </Card.Body>
                </Card>
            </div>
        );
}
export default withRouter(ProductDetail); 

ProductReducer.js

import initialState from "./initialState";
import * as actionTypes from "../actions/actionTypes";

export default function productReducer(state = initialState.products, action) {
  switch (action.type) {
    case actionTypes.INIT:
      return action.products;

    case actionTypes.ADD:
      return [...state, Object.assign({}, action.product)];

    case actionTypes.DELETE:
      return [...state.filter((product) => product.id !== action.id)];

    case actionTypes.UPDATE:
      return [
        ...state.filter((product) => product.id !== action.product.id),
        Object.assign({}, action.product),
      ];

    case actionTypes.VIEW:
      return [
        ...state[action.product.id],
        Object.assign({},action.product.view)
      ]

    default:
      return state;
  }
}

ProductActions.js

import dataApi from "../../server/dataAPI";
import * as actionTypes from "../actions/actionTypes";

//======================LOADING A PRODUCT
export function loadProduct() {
  return function (dispatch) {
    return dataApi
      .getAllProducts()
      .then((products) => {
        dispatch({ type: actionTypes.INIT, products });
      })
      .catch((error) => {
        throw error;
      });
  };
}
//==========================ADDING A PRODUCT
export function addProduct(product) {
  return function (dispatch) {
    return dataApi
      .addProduct(product)
      .then((product) => {
        dispatch({ type: actionTypes.ADD, product });
      })
      .catch((error) => {
        throw error;
      });
  };
}

//==========================DELETE A PRODUCT
export function deleteProduct(id) {
  return function (dispatch) {
    return dataApi
      .deleteProduct(id)
      .then((product) => {
        dispatch({ type: actionTypes.DELETE, id});
      })
      .catch((error) => {
        throw error;
      });
  };
}

//==========================UPDATE A PRODUCT
export function updateProduct(product) {
    return function (dispatch) {
      return dataApi
        .updateProduct(product)
        .then((product) => {
          dispatch({ type: actionTypes.UPDATE, product });
        })
        .catch((error) => {
          throw error;
        });
    };
  }

  //Increase View Count of product
  export function addView(product){
    return function (dispatch){
      return dataApi.addView(product)
      .then(product=>{
        dispatch({type:actionTypes.VIEW, product})
      })
    }
  }

dataAPI.js(to add,delete,update to json server with axios)

import axios from 'axios'

class dataAPI {
    static  getAllProducts() {
        return axios.get('http://localhost:4000/products?_sort=id&_order=asc').then(response=>response.data);
    }

    static addProduct(product) {
        return axios.post('http://localhost:4000/products',product).then(response=>response.data);
    }
    
    static updateProduct(product){
        return axios.patch('http://localhost:4000/products/'+product.id,product)
        .then(response=>response.data);
    }

    static deleteProduct(id){
        return axios.delete(`http://localhost:4000/products/${id}`).then(response=>response.data);
    }

    static getAllUsers(){
        return axios.get('http://localhost:4000/users').then(response=>response.data);
    }

    static addUser(user) {
        return axios.post('http://localhost:4000/users',user).then(response=>response.data);
    }
}

export default dataAPI;

db.json(the file that contains all the data)

{
  "products": [
    {
      "id": 1,
      "name": "Moto G5 Ultra",
      "quantity": 3,
      "price": 10000,
      "description": "Moto G5",
      "manufacturer": "Motorola",
      "views" : 0
    },
    {
      "id": 2,
      "name": "Racold Geyser",
      "quantity": 2,
      "price": 60000,
      "description": "Moto G5",
      "manufacturer": "Motorola",
      "views" : 0
    },
    {
      "name": "Lenovo G5",
      "quantity": 3,
      "price": 55000,
      "manufacturer": "Lenovo",
      "description": "A gaming laptop",
      "id": 3,
      "views" : 0
    },
    {
      "name": "Acer Swift ",
      "quantity": 5,
      "price": 35000,
      "manufacturer": "Acer",
      "description": "Business Laptop",
      "id": 4,
      "views" : 0
    },
    {
      "name": "Acer Nitro 7",
      "quantity": 4,
      "price": 75000,
      "manufacturer": "Acer",
      "description": "A gaming laptop",
      "id": 5,
      "views" : 0
    },
    "users": [
    {
      "id": 1,
      "email": "vi@gmail.com",
      "password": "truth",
      "name": {
        "firstName": "Rick",
        "lastName": "Garner"
      },
      "location": "Canada",
      "mobile": "55643980"
    },
    {
      "id": 2,
      "email": "t@t.com",
      "password": "123",
      "name": {
        "firstName": "Ram",
        "lastName": "Shankar"
      },
      "location": "Delhi",
      "mobile": "9895454860"
    },
    {
      "email": "e@e.com",
      "password": "123456789",
      "name": {
        "firstName": "RAGAV",
        "lastName": "Shant"
      },
      "location": "Karnataka",
      "mobile": "1234567891",
      "id": 3
    },
    {
      "email": "k@k.com",
      "password": "123456789",
      "name": {
        "firstName": "sd",
        "lastName": "dv"
      },
      "location": "dfv",
      "mobile": "12345678231",
      "id": 4
    }
    
  ]
}

Solution

  • So what i did was I added a useEffect() to my ProductDetail.js file and fired the Action from there.

    ProductDetail.js

    import React,{useEffect} from 'react';
    import { Link} from 'react-router-dom';
    import {withRouter} from 'react-router'
    import {Button, Card} from 'react-bootstrap'
    import { connect } from "react-redux";
    import * as productActions from "../redux/actions/productActions";
    import { bindActionCreators } from "redux";
    
    
    const ProductDetail=(props)=>{
        useEffect(() => {
    
            console.log("PROPIES ",props.location.productName.id+" "+props.location.productName.views)
            props.actions.addView(props.location.productName.id,props.location.productName.views)
        
        },[props.actions,props.location.productName.id,props.location.productName.views])
        const style={"display":"flex", "justifyContent":"center","alignItems":"center","minHeight":"100vh"}
        return(
                <div style={style}>
                    <Card style={{ width: "18rem","borderRadius":"30px" }} >
                        <Card.Body style={{style}}>
                            <Card.Title style={{"fontSize":"30px","fontWeight":"bold","display":"flex", "justifyContent":"center"}}> {props.location.productName.name} </Card.Title>
                            <Card.Text><strong>Quantity    :</strong>{props.location.productName.quantity}</Card.Text>
                            <Card.Text><strong>Price       :</strong>{props.location.productName.price}</Card.Text>
                            <Card.Text><strong>Manufacturer:</strong>{props.location.productName.manufacturer}</Card.Text>
                            <Card.Text><strong>Description :</strong>{props.location.productName.description}</Card.Text>
                            <div>
                            <Link to="/"><Button variant="primary" style={{ height: "6vh","fontWeight":"bold" }}>Back</Button></Link>
                            </div>
                        </Card.Body>
                    </Card>
                </div>
            );
    }
    
    function mapStateToProps(state, ownProps) {
        return {
          products: state.products,
        };
      }
      
      function mapDispatchToProps(dispatch) {
        return {
          actions: bindActionCreators(productActions, dispatch),
        };
      }
    
    export default connect(mapStateToProps,mapDispatchToProps)(withRouter(ProductDetail)); 
    

    It then fires this action

    //Increase View Count of product
      export function addView(id,count){
        console.log("func called")
        return function (dispatch){
          console.log("api to be called")
          return dataApi.addView(id,count)
          .then((product)=>{
            console.log("dispatched")
            dispatch({type:actionTypes.VIEW, id: product.id})
          })
        }
      }
    

    So it updates the view on the server first and then in the reducer state

    dataAPI.js

    static addView(id,count){
          return axios.patch('http://localhost:4000/products/'+id,{views:count+1})
            .then(response=>response.data);
        }
    

    productReducer.js

    import initialState from "./initialState";
    import * as actionTypes from "../actions/actionTypes";
    
    export default function productReducer(state = initialState.products, action) {
      switch (action.type) {
        case actionTypes.INIT:
          return action.products;
    
        case actionTypes.ADD:
          return [...state, Object.assign({}, action.product)];
    
        case actionTypes.DELETE:
          return [...state.filter((product) => product.id !== action.id)];
    
        case actionTypes.UPDATE:
          return [
            ...state.filter((product) => product.id !== action.product.id),
            Object.assign({}, action.product),
          ].sort( (a,b)=>(a.id>b.id)?1:-1 );
    
        case actionTypes.VIEW:
          let prod = [...state][action.id-1];
          prod.views++;
          //eslint-disable-next-line
          let addView =()=>( [
               ...state.filter(product => product.id !== action.id),
               Object.assign({}, prod)
             ])
          return state;
    
        default:
          return state;
      }
    }
    
    

    I had to write the ActionType.VIEW case in switch like this

    case actionTypes.VIEW:
          let prod = [...state][action.id-1];
          prod.views++;
          //eslint-disable-next-line
          let addView =()=>( [
               ...state.filter(product => product.id !== action.id),
               Object.assign({}, prod)
             ])
          return state;
    

    I had to put the state modification part inside a function called addView(), otherwise I saw that the function was repeatedly getting called infinitly. I'd appreciate it is someone could help me with a around that