Related
I'm pretty new to javascript, and I've been following a tutorial to create an e-commerce website. For some reason I am now getting a TypeError: Cannot read property 'category' of undefined error in reference to:
export const listProducts = ({category = '' }) => async (dispatch)
part of my productAction.js document. I've included the rest of my productAction.js document below, as well as the other important documents. I would really appreciate any help or guidance on this issue.
productAction.js
import Axios from 'axios';
import {
PRODUCT_CREATE_FAIL,
PRODUCT_CREATE_REQUEST,
PRODUCT_CREATE_SUCCESS,
PRODUCT_DETAILS_FAIL,
PRODUCT_DETAILS_REQUEST,
PRODUCT_DETAILS_SUCCESS,
PRODUCT_LIST_FAIL,
PRODUCT_LIST_REQUEST,
PRODUCT_LIST_SUCCESS,
PRODUCT_UPDATE_REQUEST,
PRODUCT_UPDATE_SUCCESS,
PRODUCT_UPDATE_FAIL,
PRODUCT_DELETE_REQUEST,
PRODUCT_DELETE_FAIL,
PRODUCT_DELETE_SUCCESS,
PRODUCT_REVIEW_CREATE_REQUEST,
PRODUCT_REVIEW_CREATE_SUCCESS,
PRODUCT_REVIEW_CREATE_FAIL,
PRODUCT_CATEGORY_LIST_SUCCESS,
PRODUCT_CATEGORY_LIST_REQUEST,
PRODUCT_CATEGORY_LIST_FAIL,
/*PRODUCT_SAVE_REQUEST,*/
} from '../constants/productConstants';
export const listProducts = ({name = '', category = '' }) => async (dispatch) => {
dispatch({
type: PRODUCT_LIST_REQUEST,
});
try {
const { data } = await Axios.get(`/api/products?name=${name}category=${category}`);
dispatch({ type: PRODUCT_LIST_SUCCESS, payload: data });
} catch (error) {
dispatch({ type: PRODUCT_LIST_FAIL, payload: error.message });
}
};
export const listProductCategories = () => async (dispatch) => {
dispatch({
type: PRODUCT_CATEGORY_LIST_REQUEST,
});
try {
const { data } = await Axios.get(`/api/products/categories`);
dispatch({ type: PRODUCT_CATEGORY_LIST_SUCCESS, payload: data });
} catch (error) {
dispatch({ type: PRODUCT_CATEGORY_LIST_FAIL, payload: error.message });
}
};
export const detailsProduct = (productId) => async (dispatch) => {
dispatch({ type: PRODUCT_DETAILS_REQUEST, payload: productId });
try {
const { data } = await Axios.get(`/api/products/${productId}`);
dispatch({ type: PRODUCT_DETAILS_SUCCESS, payload: data });
} catch (error) {
dispatch({
type: PRODUCT_DETAILS_FAIL,
payload:
error.response && error.response.data.message
? error.response.data.message
: error.message,
});
}
};
export const createProduct = () => async (dispatch, getState) => {
dispatch({ type: PRODUCT_CREATE_REQUEST });
const {
userSignin: { userInfo },
} = getState();
try {
const { data } = await Axios.post(
'/api/products',
{},
{
headers: { Authorization: `Bearer ${userInfo.token}` },
}
);
dispatch({
type: PRODUCT_CREATE_SUCCESS,
payload: data.product,
});
} catch (error) {
const message =
error.response && error.response.data.message
? error.response.data.message
: error.message;
dispatch({ type: PRODUCT_CREATE_FAIL, payload: message });
}
};
export const updateProduct = (product) => async (dispatch, getState) => {
dispatch({ type: PRODUCT_UPDATE_REQUEST, payload: product });
const {
userSignin: { userInfo },
} = getState();
try {
const { data } = await Axios.put(`/api/products/${product._id}`, product, {
headers: { Authorization: `Bearer ${userInfo.token}` },
});
dispatch({ type: PRODUCT_UPDATE_SUCCESS, payload: data });
} catch (error) {
const message =
error.response && error.response.data.message
? error.response.data.message
: error.message;
dispatch({ type: PRODUCT_UPDATE_FAIL, error: message });
}
};
export const deleteProduct = (productId) => async (dispatch, getState) => {
dispatch({ type: PRODUCT_DELETE_REQUEST, payload: productId });
const {
userSignin: { userInfo },
} = getState();
try {
const {data} = Axios.delete(`/api/products/${productId}`, {
headers: { Authorization: `Bearer ${userInfo.token}` },
});
dispatch({ type: PRODUCT_DELETE_SUCCESS });
} catch (error) {
const message =
error.response && error.response.data.message
? error.response.data.message
: error.message;
dispatch({ type: PRODUCT_DELETE_FAIL, payload: message });
}
};
export const createReview = (productId, review) => async (
dispatch,
getState
) => {
dispatch({ type: PRODUCT_REVIEW_CREATE_REQUEST });
const {
userSignin: { userInfo },
} = getState();
try {
const { data } = await Axios.post(
`/api/products/${productId}/reviews`,
review,
{
headers: { Authorization: `Bearer ${userInfo.token}` },
}
);
dispatch({
type: PRODUCT_REVIEW_CREATE_SUCCESS,
payload: data.review,
});
} catch (error) {
const message =
error.response && error.response.data.message
? error.response.data.message
: error.message;
dispatch({ type: PRODUCT_REVIEW_CREATE_FAIL, payload: message });
}
};
productReducer.js
const {
PRODUCT_LIST_REQUEST,
PRODUCT_LIST_SUCCESS,
PRODUCT_LIST_FAIL,
PRODUCT_CATEGORY_LIST_REQUEST,
PRODUCT_CATEGORY_LIST_SUCCESS,
PRODUCT_CATEGORY_LIST_FAIL,
} = require('../constants/productConstants');
export const productListReducer = (
state = { loading: true, products: [] },
action
) => {
switch (action.type) {
case PRODUCT_LIST_REQUEST:
return { loading: true };
case PRODUCT_LIST_SUCCESS:
return { loading: false, products: action.payload };
case PRODUCT_LIST_FAIL:
return { loading: false, error: action.payload };
default:
return state;
}
};
export const productCategoryListReducer = (
state = { loading: true, products: [] },
action
) => {
switch (action.type) {
case PRODUCT_CATEGORY_LIST_REQUEST:
return { loading: true };
case PRODUCT_CATEGORY_LIST_SUCCESS:
return { loading: false, categories: action.payload };
case PRODUCT_CATEGORY_LIST_FAIL:
return { loading: false, error: action.payload };
default:
return state;
}
};
Backend
productModel.js
import mongoose from 'mongoose';
const reviewSchema = new mongoose.Schema(
{
name: { type: String, required: true },
comment: { type: String, required: true },
rating: { type: Number, required: true },
},
{
timestamps: true,
}
);
const productSchema = new mongoose.Schema(
{
name: { type: String, required: true, unique: true },
image: { type: String, required: true },
brand: { type: String, required: true },
category: { type: String, required: true },
description: { type: String, required: true },
price: { type: Number, required: true },
countInStock: { type: Number, required: true },
rating: { type: Number, required: true },
numReviews: { type: Number, required: true },
reviews: [reviewSchema],
},
{
timestamps: true,
}
);
const Product = mongoose.model('Product', productSchema);
export default Product;
productRouter.js
import express from 'express';
import expressAsyncHandler from 'express-async-handler';
import data from '../data.js';
import Product from '../models/productModel.js';
import { isAdmin, isAuth } from '../utils.js';
const productRouter = express.Router();
productRouter.get(
'/',
expressAsyncHandler(async (req, res) => {
const name = req.query.name || '';
const category = req.query.category || '';
const nameFilter = name ? { name: { $regex: name, $options: 'i' } } : {};
const categoryFilter = category ? { category } : {};
const products = await Product.find({
...nameFilter,
...categoryFilter,
}).res.send(products);
})
);
productRouter.get(
'/categories',
expressAsyncHandler(async (req, res) => {
const categories = await Product.find().distinct('category');
res.send(categories);
})
);
productRouter.get(
'/seed',
expressAsyncHandler(async (req, res) => {
// await Product.remove({});
const createdProducts = await Product.insertMany(data.products);
res.send({ createdProducts });
})
);
productRouter.get(
'/:id',
expressAsyncHandler(async (req, res) => {
const product = await Product.findById(req.params.id);
if (product) {
res.send(product);
} else {
res.status(404).send({ message: 'Product Not Found' });
}
})
);
productRouter.post(
'/',
isAuth,
isAdmin,
expressAsyncHandler(async (req, res) => {
const product = new Product({
name: 'sample name ' + Date.now(),
image: '/images/p1.jpg',
price: 0,
category: 1,
brand: 'sample brand',
countInStock: 0,
rating: 0,
numReviews: 0,
description: 'sample description',
});
const createdProduct = await product.save();
res.send({ message: 'Product Created', product: createdProduct });
})
);
productRouter.put(
'/:id',
isAuth,
isAdmin,
expressAsyncHandler(async (req, res) => {
const productId = req.params.id;
const product = await Product.findById(productId);
if (product) {
product.name = req.body.name;
product.price = req.body.price;
product.image = req.body.image;
product.category = req.body.category;
product.brand = req.body.brand;
product.countInStock = req.body.countInStock;
product.description = req.body.description;
const updatedProduct = await product.save();
res.send({ message: 'Product Updated', product: updatedProduct });
} else {
res.status(404).send({ message: 'Product Not Found' });
}
})
);
export default productRouter;
Update:
So, I think the issue was my listProducts constant. In my HomeScreen.js document (not previously included), I realized that I had forgot to add {} within dispatch(listProducts());
useEffect(() =>{
dispatch(listProducts({}));
}, [dispatch]);
My only issue is that while my page now loads, I now have a Request failed with status code 500 error.
I've included the rest of my HomeScreen.js document and my other documents that include listProducts. Any additional guidance would be greatly appreciated.
HomeScreen.js
import React, { useEffect } from 'react';
import Product from '../components/Product';
import LoadingBox from '../components/LoadingBox';
import MessageBox from '../components/MessageBox';
import { useDispatch, useSelector } from 'react-redux';
import { listProducts } from '../actions/productActions';
import { Link } from 'react-router-dom';
export default function HomeScreen() {
const dispatch = useDispatch();
const productList = useSelector((state) => state.productList);
const { loading, error, products } = productList;
useEffect(() =>{
dispatch(listProducts({}));
}, [dispatch]);
return (
<div>
{loading ? (
<LoadingBox></LoadingBox>
) : error ? (
<MessageBox variant="danger">{error}</MessageBox>
) : (
<>
{products.length === 0 && <MessageBox>No Product Found</MessageBox>}
<div className="row center">
{products.map((product) => (
<Product key={product._id} product={product}></Product>
))}
</div>
</>
)}
</div>
);
}
SearchScreen.js
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Link, useParams } from 'react-router-dom';
import { listProducts } from '../actions/productActions';
import LoadingBox from '../components/LoadingBox';
import MessageBox from '../components/MessageBox';
import Product from '../components/Product';
export default function SearchScreen(props) {
const { name = 'all', category = 'all' } = useParams();
const dispatch = useDispatch();
const productList = useSelector((state) => state.productList);
const { loading, error, products } = productList;
const productCategoryList = useSelector((state) => state.productCategoryList);
const {
loading: loadingCategories,
error: errorCategories,
categories,
} = productCategoryList;
useEffect(() => {
dispatch(
listProducts({
name: name !== 'all' ? name : '',
category: category !== 'all' ? category : '',
})
);
}, [category, dispatch, name]);
const getFilterUrl = (filter) => {
const filterCategory = filter.category || category;
const filterName = filter.name || name;
return `/search/category/${filterCategory}/name/${filterName}`;
};
return (
<div>
<div className="row">
{loading ? (
<LoadingBox></LoadingBox>
) : error ? (
<MessageBox variant="danger">{error}</MessageBox>
) : (
<div>{products.length} Results</div>
)}
</div>
<div className="row top">
<div className="col-1">
<h3>Department</h3>
{loadingCategories ? (
<LoadingBox></LoadingBox>
) : errorCategories ? (
<MessageBox variant="danger">{errorCategories}</MessageBox>
) : (
<ul>
{categories.map((c) => (
<li key={c}>
<Link
className={c === category ? 'active' : ''}
to={getFilterUrl({ category: c })}
>
{c}
</Link>
</li>
))}
</ul>
)}
</div>
<div className="col-3">
{loading ? (
<LoadingBox></LoadingBox>
) : error ? (
<MessageBox variant="danger">{error}</MessageBox>
) : (
<>
{products.length === 0 && (
<MessageBox>No Product Found</MessageBox>
)}
<div className="row center">
{products.map((product) => (
<Product key={product._id} product={product}></Product>
))}
</div>
</>
)}
</div>
</div>
</div>
);
}
ProductListScreen.js
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
createProduct,
deleteProduct,
listProducts,
} from '../actions/productActions';
import LoadingBox from '../components/LoadingBox';
import MessageBox from '../components/MessageBox';
import {
PRODUCT_CREATE_RESET,
PRODUCT_DELETE_RESET,
} from '../constants/productConstants';
export default function ProductListScreen(props) {
const productList = useSelector((state) => state.productList);
const { loading, error, products } = productList;
const productCreate = useSelector((state) => state.productCreate);
const {
loading: loadingCreate,
error: errorCreate,
success: successCreate,
product: createdProduct,
} = productCreate;
const productDelete = useSelector((state) => state.productDelete);
const {
loading: loadingDelete,
error: errorDelete,
success: successDelete,
} = productDelete;
const dispatch = useDispatch();
useEffect(() => {
if (successCreate) {
dispatch({ type: PRODUCT_CREATE_RESET });
props.history.push(`/product/${createdProduct._id}/edit`);
}
if (successDelete) {
dispatch({ type: PRODUCT_DELETE_RESET });
}
dispatch(listProducts());
/// TODO: dispatch delete action
}, [createdProduct, dispatch, props.history, successCreate, successDelete]);
const deleteHandler = (product) => {
if (window.confirm('Are you sure to delete?')) {
dispatch(deleteProduct(product._id));
}
};
const createHandler = () => {
dispatch(createProduct());
};
return (
<div>
<div className="row">
<h1>Products</h1>
<button type="button" className="primary" onClick={createHandler}>
Create Product
</button>
</div>
{loadingDelete && <LoadingBox></LoadingBox>}
{errorDelete && <MessageBox variant="danger">{errorDelete}</MessageBox>}
{loadingCreate && <LoadingBox></LoadingBox>}
{errorCreate && <MessageBox variant="danger">{errorCreate}</MessageBox>}
{loading ? (
<LoadingBox></LoadingBox>
) : error ? (
<MessageBox variant="danger">{error}</MessageBox>
) : (
<table className="table">
<thead>
<tr>
<th>ID</th>
<th>NAME</th>
<th>PRICE</th>
<th>CATEGORY</th>
<th>BRAND</th>
<th>ACTIONS</th>
</tr>
</thead>
<tbody>
{products.map((product) => (
<tr key={product._id}>
<td>{product._id}</td>
<td>{product.name}</td>
<td>{product.price}</td>
<td>{product.category}</td>
<td>{product.brand}</td>
<td>
<button
type="button"
className="small"
onClick={() =>
props.history.push(`/product/${product._id}/edit`)
}
>
Edit
</button>
<button
type="button"
className="small"
onClick={() => deleteHandler(product)}
>
Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
);
}
Based on the error it seems you may be calling listProducts and not passing any argument. If this is expected then you can provide an initial value for the argument to destructure category from.
export const listProducts = ({ category = '' } = {}) => async (dispatch) => {
dispatch({
type: PRODUCT_LIST_REQUEST,
});
try {
const { data } = await Axios.get(`/api/products?category=${category}`);
dispatch({ type: PRODUCT_LIST_SUCCESS, payload: data });
} catch (error) {
dispatch({ type: PRODUCT_LIST_FAIL, payload: error.message });
}
};
SO the most plausible answer according to me is (based on the error provided),
The error is causing because you are not passing anything. Due to which the javascript is trying to spread nothing and giving you the error "Cannot read property 'category' of undefined".
I know this is a bit confusing let me explain it with an example
let's say you have a function
const test = ({context = ""}) => {
console.log(context)
}
and if you call the test function without passing an object like below
test();
this will produce the error "Cannot read property 'category' of undefined" as the function definition is trying to spread an object and get a context property out of that object but you are passing nothing.
instead, if you call the function
test({});
or
test({context=1});
this will not cause you any error as function definition will get an object to spread.
for more details on object spread syntax, you can refer MDN docs here
Hope its helpful.
I present my problem to you
with the following code I make a payment,
I would like that if the payment is refused I redirect to a page otherwise if the payment is accept I redirect to another page
do you have any idea how to do this?
I tried to simplify the code so that it is as clear as possible
Thanks for your help Neff
import { Redirect } from 'react-router-dom';
import React, { Component } from 'react';
import { CardNumberElement, CardExpiryElement, CardCVCElement, injectStripe } from 'react-
stripe-elements';
import { CardText, Col, Container, CardTitle, Button, Input, Card } from 'reactstrap';
import './Payment.scss'
const entrypoint = process.env.REACT_APP_API_ENTRYPOINT + '/api';
class _Form extends Component {
constructor(props) {
super(props);
this.state = {
alertMessage: '',
alertStyle: '',
randomID: null,
redirect: false,
loading: false,
};
}
handleChange = (e) => {
this.setState({ [e.target.name]: e.target.value });
}
postbackend = () => {
const newItems = this.props.items.map((item) => {
const { title, quantity, } = item;
return {
title,
quantity,
};
});
const config = {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ ...this.state, items: newItems }),
};
const url = entrypoint + "/alluserpls";
fetch(url, config)
.then(res => res.json())
.then(res => {
if (res.error) {
alert(res.error);
} else {
alert(`film ajouté avec l'ID ${res}!`);
}
}).catch(e => {
console.error(e);
}).finally(() => this.setState({ redirect: true }));
}
handleSubmit = async (ev) => {
ev.preventDefault();
this.setState({ loading: true });
this.props.stripe.createToken({ name: this.props.clientName })
.then(result => {
if (typeof (result.error) !== 'undefined') {
this.setState({ error: result.error.message, success: '' });
this.postbackend();
} else {
this.stripeCreateCharge(result.token, this.props.totalPrice).then(() => {
this.postbackend();
});
}
});
}
stripeCreateCharge(token, amount) {
const params = { token: token.id, amount: amount };
const qParams = queryString.stringify(params);
const url = [entrypoint + "/stripe", qParams].join('?');
return fetch(url)
.then(response => response.json())
.then(val => {
if (val.ok) {
return val.message;
} else {
throw val.message;
}
})
.then(success => {
this.setState({ alertMessage: success, alertStyle: 'success' });
return Promise.resolve()
})
.catch(error => this.setState({ alertMessage: error, alertStyle: 'danger' }));
}
render() {
const { loading } = this.state;
const redirect = this.state.redirect;
if (redirect) {
return <Redirect to="/OderSummaryScreen" />
}
else {
return (
<div >
<form method="post" onSubmit={(ev) => this.handleSubmit(ev)}>
<Container>
<CardTitle className='cardTitlePaymentFormComponent'>Entre ton moyen de paiement</CardTitle>
<Card className="CardPaymenntFormComponenentCard">
{/* <Alert msg={this.state.alertMessage} style={this.state.alertStyle} /> */}
<div className="col12PaymentFormComponent">
<div className="col8PaymentFormComponent">
<CardText className='justOnlyForFunckingPaddindOnTextForPaymentFormComponent'>Numéro de Carte</CardText>
<CardNumberElement className="cardNumberElementPaymentFormComponent" />
</div>
<div className="col4PaymentFormComponent">
<CardText className='justOnlyForFunckingPaddindOnTextForPaymentFormComponent'> Expiration</CardText>
<CardExpiryElement className="cardExpiryElementPaymentFormComponent" />
</div>
</div>
<div className="col12PaymentFormComponent">
<div className="col8PaymentFormComponent">
<ClienInfo />
</div>
<div className="col4PaymentFormComponent">
<CardText className='justOnlyForFunckingPaddindOnTextForPaymentFormComponent'>Cryptogramme</CardText>
<CardCVCElement className="cardCVCElementPaymentFormComponent" />
</div>
</div>
</Card>
</Container>
<Container className='containerPaymentFooterContainer' >
<Col sm="12" className='col12PaymentsFooter'>
<Input onClick={this.handleClick} type="checkbox" required className="inputCheckboxPaymentsFooter" />{' '}
<CardText className='cardTextPaymentsFooter' > Je reconnais avoir lu et pris connaissance des Termes, de la Charte de Confidentialité et des CGU, et les accepte.</CardText>
</Col>
</Container>
<Col sm="12" className='col12PaymentsFooterButtonCol12' >
{!loading && <div >
<Button className='buttonPaymentsFooterbutton' type="submit" value="Envoyer" disabled={loading} >
<CardTitle className="buttonPaymentsCardTitleFooter">PAYER </CardTitle>
<CardTitle className="buttonPaymentsCardTitleFooter" >{this.props.total} € </CardTitle>
</Button>
</div>}
{loading && <div className="wtfloadingLogoHandeSpinerBro" ><Handespiner /> </div>}
</Col>
</Form>
)}
</Formik>
</Col>
</Container>
</form>
</div>
)
};
}
}
const mapStateToProps = (state) => {
return {
items: state.addedItems,
}
}
export default connect(mapStateToProps)(injectStripe(_Form))
In your postbackend function you can update your fetch request like this:
fetch(url, config)
.then(res => res.json())
.then(res => {
if (res.error) {
alert(res.error);
this.props.history.replace("/"); // Your Error Page
} else {
alert(`film ajouté avec l'ID ${res}!`);
this.props.history.push("/"); // Your Success Page
}
}).catch(e => {
console.error(e);
this.props.history.replace("/"); // Your Error Page
}).finally(() => this.setState({
redirect: true
}));
The catch case will be invoked if case of any error. While your else part will take care of unsuccessful response from the server.
I have code as shown below. My question is regarding react and redux usage. I am not able to get a state "categories" via mapstate method. It is showing undefined when I try to console categories via this.props.categories in my Product.js. I have attached product.js file which has reducer and another is my main component Product.js.
Here is my Product.js code:
import React from 'react'
import {connect} from 'react-redux'
import withStyles from 'isomorphic-style-loader/lib/withStyles'
import s from './Product.css'
import { Select, Button, Table, Tooltip, InputNumber, Card} from 'antd'
import ProductForm from './ProductForm'
import {withCurrency} from '../../components'
import debounce from 'lodash/debounce'
import {
changeSelectedProduct,
getProducts,
addProduct,
changeMarkupFactor,
updateProduct,
currencyClicked,
editRetailPrice,
retailPriceUpdated,
handleChange
} from '../../reducers/product'
const children = ["Home","Health","Beauty","Pregnancy","Kits","Animals","Travel","Cooking"]
class Product extends React.Component {
constructor(props) {
super(props)
this.getProducts = debounce(props.getProducts, 800);
}
componentWillMount() {
// TODO get initial list of popular products
this.props.getProducts()
}
handleChange = (values) => {
console.log(values)
let Values = children.filter( (eachone,index) =>{
var includes = values.includes(`${index}`)
if(includes){
return true
}
}
)
this.props.handleChange(Values)
}
render() {
const {
products,
loading,
selectedProduct,
onSubmit,
formRef,
markupFactor,
changeMarkupFactor,
changeSelectedProduct,
currencies,
totalPrice,
currency,
clearForms,
addRetailPrice,
currencyClicked,
editRetailPrice,
retailPriceUpdated,
categories
} = this.props;
console.log('entire state', this.props.state)
const columns = [
{
key: 'name',
dataIndex: 'name',
width: 130,
className: s.nameColumn,
},
{
key: 'price',
dataIndex: 'price',
className: s.priceColumn,
render: (price, row) => {
if (row.id === 'retail-price' && addRetailPrice) {
return (
<InputNumber
value={totalPrice.retail_price[currency.key]}
onChange={(value) => editRetailPrice(value, currency.key)}
defaultValue={price[currency.key]}
onBlur={() => retailPriceUpdated(currency.key)}
/>
);
}
return (
<Tooltip
placement='topRight'
title={
currencies.map(item =>
currency.key !== item.key ? (
<div key={item.key}>
{withCurrency(item, price[item.key])}
</div>
) : null
)
}
>
<div onClick={() => currencyClicked(row)}>
{withCurrency(currency, price[currency.key])}
</div>
</Tooltip>
)
}
}
]
let data = []
if (totalPrice) {
data = [
{id:'receipe', name: 'Recipe', price: totalPrice.recipe.total_price},
{id:'container', name: 'Container', price: totalPrice.container.total_price},
{id:'wholesale-price', name: 'Wholesale Price', price: totalPrice.wholesale_price},
{id:'retail-price', name: 'Retail Price', price: totalPrice.retail_price},
]
}
return (
<Card
className={s.container}
title={'Product'}
bodyStyle={{minHeight: 409}}
extra = {<a onClick={clearForms}>{'Clear'}</a>}
>
{/* <div className={s.container}> */}
{/* <div className={s.headerWrapper}>
<h3 className={s.header}>{'Product'}</h3> */}
{/*use clear here below */}
{/* <a onClick={clearForms}>{'Clear'}</a>
</div> */}
<div className={s.containerSearchWrapper}>
<Select
className = {s.search}
showSearch
allowClear
value={selectedProduct ? `${selectedProduct.id}` : undefined}
className={s.productSearch}
placeholder='Search'
notFoundContent={loading.products ? <Spin size='small'/> : null}
filterOption={false}
onSearch={(search) => this.getProducts({search})}
onChange={(id) => {
const newProduct = products.find(item => item.id === +id)
changeSelectedProduct(newProduct)
}}
>
{products.map(item =>
<Select.Option key={item.id}>{item.name}</Select.Option>
)}
</Select>
</div>
<ProductForm
markupFactor={markupFactor}
changeMarkupFactor={changeMarkupFactor}
ref={formRef}
children={children}
handleChange={this.handleChange}
/>
<Table
className={s.table}
columns={columns}
dataSource={data}
loading={loading.totalPrice}
size='small'
rowKey={(record, i) => i}
pagination={false}
showHeader={false}
locale={{emptyText: 'Total price'}}
/>
<div className={s.actions}>
<Button
onClick={() => onSubmit('update')}
loading={loading.updatingProduct}
disabled={!selectedProduct}
>
{'Save'}
</Button>
<Button
onClick={() => onSubmit('add')}
loading={loading.addingProduct}
>
{'Create new'}
</Button>
</div>
{/* </div> */}
</Card>
)
}
}
const mapState = state => ({
...state.product,
currency: state.global.currency,
currencies: state.global.currencies,
categories: state.global.categories,
})
const mapDispatch = {
getProducts,
addProduct,
updateProduct,
changeMarkupFactor,
changeSelectedProduct,
currencyClicked,
editRetailPrice,
retailPriceUpdated,
handleChange,
}
export default connect(mapState, mapDispatch)(withStyles(s)(Product))
Here is my product.js code
import createReducer, {RESET_STORE} from '../createReducer'
import {getToken} from './user'
import qs from 'query-string'
import _ from 'lodash';
import {message} from 'antd'
import messages from '../messages'
import {getFieldValue} from '../utils'
// ------------------------------------
// Constants
// ------------------------------------
export const GET_PRODUCT_REQUEST = 'Product.GET_PRODUCT_REQUEST'
export const GET_PRODUCT_SUCCESS = 'Product.GET_PRODUCT_SUCCESS'
export const GET_PRODUCT_FAILURE = 'Product.GET_PRODUCT_FAILURE'
export const ADD_PRODUCT_REQUEST = 'Product.ADD_PRODUCT_REQUEST'
export const ADD_PRODUCT_SUCCESS = 'Product.ADD_PRODUCT_SUCCESS'
export const ADD_PRODUCT_FAILURE = 'Product.ADD_PRODUCT_FAILURE'
export const UPDATE_PRODUCT_REQUEST = 'Product.UPDATE_PRODUCT_REQUEST'
export const UPDATE_PRODUCT_SUCCESS = 'Product.UPDATE_PRODUCT_SUCCESS'
export const UPDATE_PRODUCT_FAILURE = 'Product.UPDATE_PRODUCT_FAILURE'
export const GET_TOTAL_PRICE_REQUEST = 'Product.GET_TOTAL_PRICE_REQUEST'
export const GET_TOTAL_PRICE_SUCCESS = 'Product.GET_TOTAL_PRICE_SUCCESS'
export const GET_TOTAL_PRICE_FAILURE = 'Product.GET_TOTAL_PRICE_FAILURE'
export const CHANGE_SELECTED_PRODUCT = 'Product.CHANGE_SELECTED_PRODUCT'
export const CHANGE_MARKUP_FACTOR = 'Product.CHANGE_MARKUP_FACTOR'
export const CHANGE_RETAIL_PRICE = 'Product.CHANGE_RETAIL_PRICE';
export const EDITING_RETAIL_PRICE = 'Product.EDITING_RETAIL_PRICE';
export const RETAIL_PRICE_UPDATED = 'Product.RETAIL_PRICE_UPDATED';
export const CLEAR = 'Product.CLEAR'
export const ADD_CATEGORIES = 'Product.ADD_CATEGORIES'
// ------------------------------------
// Actions
// ------------------------------------
// ------------------------------------
// Actions
// ------------------------------------
export const changeSelectedProduct = (selectedProduct) => (dispatch, getState) => {
dispatch({type: CHANGE_SELECTED_PRODUCT, selectedProduct})
// dispatch(changeSelectedComponents(selectedProduct ? selectedProduct.components : []))
}
export const currencyClicked = (row) => (dispatch) => {
if (!row.id === 'retail-price') return;
dispatch({type: CHANGE_RETAIL_PRICE})
}
export const editRetailPrice = (value, currencyKey) => (dispatch, getState) => {
const { totalPrice } = getState().product;
totalPrice.retail_price[currencyKey] = value;
dispatch({type: EDITING_RETAIL_PRICE, totalPrice});
}
export const retailPriceUpdated = (currencyKey) => (dispatch, getState) => {
const { totalPrice } = getState().product;
const markupFactor = parseFloat(totalPrice.retail_price[currencyKey] / totalPrice.wholesale_price[currencyKey]).toFixed(2);
dispatch({type: RETAIL_PRICE_UPDATED, totalPrice});
dispatch({type: CHANGE_MARKUP_FACTOR, markupFactor});
}
export const getProducts = (params = {}) => (dispatch, getState, {fetch}) => {
dispatch({type: GET_PRODUCT_REQUEST, params})
const {token} = dispatch(getToken())
const {search, ordering} = getState().product
return fetch(`/pands/products/?${qs.stringify({
search,
ordering,
})}`, {
method: 'GET',
token,
success: (res) => dispatch({type: GET_PRODUCT_SUCCESS, res}),
failure: (err) => dispatch({type: GET_PRODUCT_FAILURE}),
})
}
export const addProduct = (values) => (dispatch, getState, {fetch}) => {
dispatch({type: ADD_PRODUCT_REQUEST})
const {token} = dispatch(getToken())
const {currencies} = getState().global
const {selectedRecipe} = getState().recipe
const {selectedContainer} = getState().container
return fetch(`/pands/products/`, {
method: 'POST',
body: {
...values,
recipe: selectedRecipe.id,
container: selectedContainer.id,
currencies: currencies.map(item => item.key),
},
token,
success: (selectedProduct) => {
dispatch({type: ADD_PRODUCT_SUCCESS, selectedProduct})
message.success(messages.addProductSuccess)
},
failure: (err) => {
dispatch({type: ADD_PRODUCT_FAILURE})
message.error(messages.addProductError)
},
})
}
export const updateProduct = (values) => (dispatch, getState, {fetch}) => {
dispatch({type: UPDATE_PRODUCT_REQUEST})
const {token} = dispatch(getToken())
const {currencies} = getState().global
const {selectedProduct} = getState().product
const {selectedRecipe} = getState().recipe
const {selectedContainer} = getState().container
return fetch(`/pands/products/${selectedProduct.id}/`, {
method: 'PATCH',
body: {
...values,
recipe: selectedRecipe.id,
container: selectedContainer.id,
currencies: currencies.map(item => item.key),
},
token,
success: (selectedProduct) => {
dispatch({type: UPDATE_PRODUCT_SUCCESS, selectedProduct})
message.success(messages.updateProductSuccess)
},
failure: (err) => {
dispatch({type: UPDATE_PRODUCT_FAILURE})
message.error(messages.updateProductError)
},
})
}
export const getTotalPrice = () => (dispatch, getState, {fetch}) => {
const {token} = dispatch(getToken())
dispatch({type: GET_TOTAL_PRICE_REQUEST})
const {currencies} = getState().global
const {markupFactor} = getState().product
const {fields: {ingredients}} = getState().rawMaterials
const {fields: {components}} = getState().components
return fetch(`/pands/products/calculate/`, {
method: 'POST',
token,
body: {
recipe: {
ingredients: getFieldValue(ingredients),
},
container: {
components: getFieldValue(components),
},
markup_factor: +markupFactor,
currencies: currencies.map(item => item.key),
},
success: (totalPrice) => dispatch({type: GET_TOTAL_PRICE_SUCCESS, totalPrice}),
failure: (error) => dispatch({type: GET_TOTAL_PRICE_FAILURE, error}),
})
}
export const changeMarkupFactor = (markupFactor) => (dispatch, getState) => {
dispatch({type: CHANGE_MARKUP_FACTOR, markupFactor})
dispatch(getTotalPrice())
}
export const clear = () => ({type: CLEAR})
// -----------------------------------
export const handleChange = (values) => (dispatch) => {
dispatch({type: ADD_CATEGORIES,values});
}
// ------------------------------------
// Reducer
// -----------------------------------
const initialState = {
loading: {
products: false,
addingProduct: false,
updatingProduct: false,
totalPrice: false,
},
products: [],
selectedProduct: null,
error: null,
totalPrice: null,
markupFactor: 1.3,
addRetailPrice: false,
categories:[],
}
export default createReducer(initialState, {
[GET_PRODUCT_REQUEST]: (state, {params}) => ({
search: _.has(params, 'search') ? params.search : state.search,
ordering: params.sorter ? `${params.sorter.order === 'descend' ? '-' : ''}${params.sorter.field}` : state.ordering,
filters: params.filters || state.filters,
loading: {
...state.loading,
products: false,
},
}),
[GET_PRODUCT_SUCCESS]: (state, {res: {results}}) => ({
products: results,
loading: false,
}),
[GET_PRODUCT_FAILURE]: (state, action) => ({
loading: false,
}),
[ADD_PRODUCT_REQUEST]: (state, action) => ({
loading: {
...state.loading,
addingProduct: true,
},
}),
[ADD_PRODUCT_SUCCESS]: (state, {selectedProduct}) => ({
selectedProduct,
loading: {
...state.loading,
addingProduct: false,
},
}),
[ADD_PRODUCT_FAILURE]: (state, action) => ({
loading: {
...state.loading,
addingProduct: false,
},
}),
[UPDATE_PRODUCT_REQUEST]: (state, action) => ({
loading: {
...state.loading,
updatingProduct: true,
},
}),
[UPDATE_PRODUCT_SUCCESS]: (state, {selectedProduct}) => ({
selectedProduct,
loading: {
...state.loading,
updatingProduct: false,
},
}),
[UPDATE_PRODUCT_FAILURE]: (state, action) => ({
loading: {
...state.loading,
updatingProduct: false,
},
}),
[GET_TOTAL_PRICE_REQUEST]: (state, action) => ({
totalPrice: null,
loading: {
...state.loading,
totalPrice: true,
},
}),
[GET_TOTAL_PRICE_SUCCESS]: (state, {totalPrice}) => ({
totalPrice,
loading: {
...state.loading,
totalPrice: false,
}
}),
[EDITING_RETAIL_PRICE]: (state, {totalPrice}) => ({
totalPrice
}),
[RETAIL_PRICE_UPDATED]: (state, {totalPrice}) => ({
totalPrice,
addRetailPrice: false
}),
[GET_TOTAL_PRICE_FAILURE]: (state, {error}) => ({
error,
loading: {
...state.loading,
totalPrice: false,
},
}),
[CHANGE_MARKUP_FACTOR]: (state, {markupFactor}) => ({
markupFactor,
}),
[CHANGE_SELECTED_PRODUCT]: (state, {selectedProduct}) => ({
selectedProduct,
}),
[CHANGE_RETAIL_PRICE]: (state) => ({
addRetailPrice: true,
}),
[ADD_CATEGORIES]: (state,{ categories }) => ({
categories
}),
[CLEAR]: (state, action) => RESET_STORE,
})
In your every reducer you have to return previous state and a new state.
[ACTION]:(state, ...) => ({
...state, <------ previouse state
... < ------- your new data (loading, others)
}),
Right now you don't return previous store state, instead you overwrite the store with only new data
[GET_PRODUCT_REQUEST]: (state, {params}) => ({
...state,
...
}),
[GET_PRODUCT_SUCCESS]: (state, {res: {results}}) => ({
...state,
...
products: results,
loading: false,
}),
so I've been working on an admin control panel for a page that displays a list of cards that once clicked, redirect the user to either a video ora text-based link. I must also add a CRUD feature that allows me to edit / remove posts from the API provided.
The API is locally hosted, so requesting "localhost:3000/videos/{Id}" will load up the object with the following fields: "id", "url", "title" and "thumbnail"
I have a class that is used for making the card, called HelpCard.tsx The code is as follows:
import React, { Component } from "react";
import "../help/HelpCard.css";
import "../help/HelpList";
import spinner from "../help/spinner.gif";
import { string, number } from "prop-types";
import { Link } from "react-router-dom";
interface Props {
url: string;
title: string;
thumbnail: string;
}
interface State {
title: string;
thumbnail: string;
id: string;
imageLoading?: boolean;
tooManyRequests?: boolean;
}
export default class HelpCard extends Component<Props, State> {
state = {
id: "",
title: "",
imageLoading: true,
tooManyRequests: false,
thumbnail: "",
deleteProduct: true
};
componentDidMount() {
const { url, title, thumbnail } = this.props;
const id = url.split("/")[url.split("/").length - 2];
this.setState({
id,
title,
thumbnail,
imageLoading: true,
tooManyRequests: false
});
}
render() {
const isThumbnail = this.state.thumbnail;
const adminhelpcard = this.state;
return (
<div>
{isThumbnail ? (
<div className="horizontalCard">
<div className="innerCard">
<div className="leftImage">
<img
className="Sprite"
onLoad={() => this.setState({ imageLoading: false })}
onError={() => this.setState({ tooManyRequests: true })}
src={this.state.thumbnail}
style={
this.state.tooManyRequests
? { display: "none" }
: this.state.imageLoading
? { display: "null" }
: { display: "null" }
}
/>
</div>
<div className="rightText">
<div className="card-body">
{this.state.title}
<div className="cardButtons">
<button className="btn btn-update btn-outline-primary">Update</button>
<button
onClick={() => adminhelpcard.deleteProduct(this.state.id)}
className="btn btn-outline-primary">
Delete
</button>
</div>
</div>
</div>
</div>
</div>
And then I have the HelpList.tsx module that is responsible for displaying the cards in the form of a list. and the code is as follows:
import React, { Component } from "react";
import HelpCard from "./HelpCard";
import "../help/HelpCard.css";
import axios from "axios";
import InfiniteScroll from "react-infinite-scroller";
import { Button } from "components/ui";
interface State {
url: string;
adminhelpcard: SingleAdminHelpCard[];
error: null;
response: {};
}
interface SingleAdminHelpCard {
id: string;
url: string;
title: string;
thumbnail: string;
}
interface Props {}
export default class HelpList extends Component<Props, State> {
state = {
id: "",
url: "http://localhost:3000/videos/",
adminhelpcard: [],
itemsCountPerPage: 1,
activePage: 1,
error: null,
response: {}
};
loadAdminHelpCard = () => {
axios
.get(this.state.url)
.then((res) => {
this.setState((prevState) => {
const adminhelpcard = prevState.adminhelpcard;
return {
adminhelpcard: [...prevState.adminhelpcard, ...res.data],
url: res.data.next
};
});
})
.catch(function(error) {
// handle error
console.log(error);
});
};
async componentDidMount() {
const apiUrl = "http://localhost:3000/videos/";
const res = await axios.get(this.state.url);
this.setState({ adminhelpcard: res.data });
fetch(apiUrl)
.then((res) => res.json())
.then(
(result) => {
this.setState({
adminhelpcard: result
});
},
(error) => {
this.setState({ error });
}
);
}
deleteProduct(id: any) {
const { adminhelpcard } = this.state;
const apiUrl = `http://localhost:3000/videos/${this.state.id}`;
const options = {
method: "DELETE"
};
fetch(apiUrl, options)
.then((res) => res.json())
.then(
(result) => {
this.setState({
response: result,
adminhelpcard: adminhelpcard.filter((adminhelpcard: SingleAdminHelpCard) => adminhelpcard.id !== id)
});
},
(error) => {
this.setState({ error });
}
);
}
render() {
console.log(this.state.adminhelpcard);
return (
<div>
<React.Fragment>
{this.state.adminhelpcard ? (
<div className="row">
<InfiniteScroll
pageStart={1}
loadMore={this.loadAdminHelpCard}
hasMore={this.state.url ? true : false}
threshold={0}
loader={
<div className="loader" key={0}>
Loading ...
</div>
}>
{this.state.adminhelpcard.map((adminhelpcard: SingleAdminHelpCard, i) => (
<HelpCard
key={adminhelpcard.id + i}
title={adminhelpcard.title}
url={adminhelpcard.url}
thumbnail={adminhelpcard.thumbnail}
/>
))}
</InfiniteScroll>
</div>
) : (
<h1>Loading Cards</h1>
)}
</React.Fragment>
</div>
);
}
}
I get the following error:
This expression is not callable.
Type 'Boolean' has no call signatures.ts(2349)
from the "deleteProduct" function in the line of code:
onClick={() => adminhelpcard.deleteProduct(this.state.id)}
When I try to click the delete button on the helpCards, it says that the no function called "deleteProduct" is recognized. How do I go about fixing this?
--------------------------EDIT----------------------------
Error given for adding deleteProduct to HelpCard component.
"No overload matches this call.
Overload 1 of 2, '(props: Readonly): HelpCard', gave the following error.
Type '{ key: string; title: string; url: string; thumbnail: string; deleteProduct: (id: any) => void; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes & Readonly & Readonly<{ children?: ReactNode; }>'.
Property 'deleteProduct' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes & Readonly & Readonly<{ children?: ReactNode; }>'."
First, you need to pass the deleteProduct function as a prop to the HelpCard component.
So in your HelpList.tsx, add another prop to the <HelpCard/> element as follows.
<HelpCard
key={adminhelpcard.id + i}
title={adminhelpcard.title}
url={adminhelpcard.url}
thumbnail={adminhelpcard.thumbnail}
deleteProduct={this.deleteProduct.bind(this)}
/>
Then you need to use it from the onClick handler within the HelpCard component.
<button
onClick={() => this.props.deleteProduct(this.state.id)}
className="btn btn-outline-primary">
Delete
</button>
Probably you may need to change the Props interface in your HelpCard to cater to this new prop as well.
interface Props {
url: string;
title: string;
thumbnail: string;
deleteProduct: (id: any) => void;
}
I am experiencing a very weird react behavior. There is this component which gets message and emailSubmitedText. In render method based on some condition it should render either first or the other one.
Now at first it is message. I click on the submit of the form. All the functions happen.
The component rerenders. In the console log I can see this time it should render emailSubmitedText. In react devtools it show the right text.
However in the actual html and html inspector it still shows the previos text.
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import Details from './Details'
class DefaultMessage extends Component {
inputRef = null
renderEmailForm = () => {
return (
<form
className='b2c_email-form input-field'
onSubmit={e => {
e.preventDefault()
const { projectId, visitSessionId } = this.props
this.setState({ email: this.inputRef.value })
this.props.onSubmitEmail({
email: this.inputRef.value,
convertedPage: window.location.href, projectId, visitSessionId
})
}}
>
<div className="input-field">
<input ref={elem => this.inputRef = elem} id='email' type='email' className='validate' value={this.props.email} />
<label htmlFor='email' data-error='Invalid email address'>E-mail</label>
</div>
<button
className='b2c_email-form-button waves-effect waves-light btn'
type='submit'
style={{
backgroundColor: this.props.companyColor || '#63bc78'
}}
>Submit</button>
</form>
)
}
render = () => {
console.log('...> ', this.props.error || !this.props.contactId && this.props.message || this.props.emailSubmitedText)
return (
<div className='b2c_chat-message'>
<Details
classNames='b2c_chat-message-details__admin'
avatar={this.props.avatar}
name={this.props.name}
date={this.props.date}
/>
<div className='b2c_chat-message-text b2c_chat-message-text__admin b2c_chat-message-default'>
<div className='b2c_chat-message-after b2c_chat-message-after__admin' />
{this.props.error || !this.props.contactId && this.props.message || this.props.emailSubmitedText}
{!this.props.contactId && this.renderEmailForm()}
</div>
</div>
)
}
}
DefaultMessage.propTypes = {
projectId: PropTypes.string.isRequired,
visitSessionId: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
email: PropTypes.string.isRequired,
date: PropTypes.string.isRequired,
message: PropTypes.string.isRequired,
onSubmitEmail: PropTypes.func.isRequired
}
export default DefaultMessage
Here is the direct parent of the component.
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import $ from 'jquery'
import moment from 'moment'
import randomstring from 'randomstring'
import DefaultMessage from './DefaultMessage'
import Message from './Message'
import UserTypingIndicator from '../UserTypingIndicator'
import TypingIndicator from './TypingIndicator'
class Messages extends Component {
chatRef = null
componentDidUpdate () {
this.scrollToTheLastMessage()
}
scrollToTheLastMessage = () => {
const $chat = $(this.chatRef)
const scrollTop = $chat.prop('scrollHeight') - $chat.innerHeight()
$chat.scrollTop(scrollTop)
}
renderDefaultMessage = () => (
<DefaultMessage
contactId={this.props.contactId}
companyColor={this.props.companyColor}
error={this.props.error}
date={moment().format('h:mm A')}
name={this.props.adminName}
avatar={this.props.adminAvatar}
message={this.props.welcomeMessage}
emailSubmitedText={this.props.emailSubmitedText}
projectId={this.props.projectId}
visitSessionId={this.props.visitSessionId}
onSubmitEmail={this.props.onSubmitEmail}
/>
)
renderMessages = () => {
let checkConversationDate = null
const {messages, contactName, adminName, adminAvatar} = this.props
const compareConversationDates = (createdAt) => {
checkConversationDate = moment(createdAt).format("DD.MM.YYYY")
return (
<div key={randomstring.generate()} className='conversationDayDate'>
<span>{checkConversationDate}</span>
</div>
)
}
if (!messages) return null
return messages.map((message, index) => {
return (
<div>
{checkConversationDate !== moment(message.createdAt.$date).format("DD.MM.YYYY") ? compareConversationDates(message.createdAt.$date) : ''}
{/* {index === 0 ? this.renderDefaultMessage() : ''} */}
<Message
isAdmin={message.userId ? true : false}
imageFile={message.imageFile}
key={randomstring.generate()}
companyColor={this.props.companyColor}
contactName={contactName}
adminName={adminName}
avatar={adminAvatar}
message={message.message}
date={moment(message.createdAt.$date).format('h:mm A')}
/>
</div>
)
})
}
renderTypingIndicators = () => {
const arrayToRender = [
this.props.isAdminTyping && <AdminTypingIndicator />,
this.props.isUserTyping && <UserTypingIndicator />
]
return arrayToRender
}
render = () => <div ref={elem => this.chatRef = elem} id='chat' className='chat-body' style={{
height: 'calc(100% - 190px - 3rem)',
overflowY: 'scroll',
margin: '30px 10px 10px 0',
boxSizing: 'border-box'
}}>
{this.renderDefaultMessage()}
{this.renderMessages()}
{this.renderTypingIndicators()}
</div>
}
Messages.propTypes = {
projectId: PropTypes.string.isRequired,
visitSessionId: PropTypes.string.isRequired,
messages: PropTypes.array.isRequired,
adminName: PropTypes.string.isRequired,
contactName: PropTypes.string.isRequired,
onSubmitEmail: PropTypes.func.isRequired
}
export default Messages
And here is where Container with states
import React, { Component } from 'react'
import Sound from 'react-sound'
import ddp from '../../ddp'
import Cookies from 'js-cookie'
import randomstring from 'randomstring'
import ChatContainer from './ChatContainer'
import Icon from './Icon'
import { connect, makeArrayCollectionFromObjectCollection, getNewMessages } from '../../functions'
class View extends Component {
defaultDocumentTitle = null
state = {
contactId: '',
chat: null,
show: false,
newMessagesCount: null,
notStatus: 'STOPPED'
}
newMessageNotification = newMessages => {
if (newMessages.length && newMessages.length > this.state.newMessagesCount) {
this.setState({ notStatus: 'PLAYING' })
document.title = `(${newMessages.length}) ${this.defaultDocumentTitle}`
} else if (!newMessages.length) {
document.title = this.defaultDocumentTitle
}
if (this.state.newMessagesCount !== newMessages.length) {
this.setState({ newMessagesCount: newMessages.length })
}
}
componentWillMount () {
this.defaultDocumentTitle = document.title
}
componentDidMount = async () => {
this.setContactIdFromCookies()
await connect(ddp)
}
setContactIdFromCookies = () => {
window.Cookies = Cookies
console.warn('setContactIdFromCookies')
const contactId = Cookies.get('b2cContactId')
console.log('contactId', contactId)
if (contactId) this.setState({contactId})
}
componentDidUpdate () {
console.warn('componentDidUpdate', this.props)
if (this.state.contactId && !this.state.chat) {
this.getChat(this.state.contactId)
}
if (this.state.chat && this.state.chat.length) {
let newMessages = getNewMessages(this.state.chat)
this.newMessageNotification(newMessages)
}
}
componentWillReceiveProps = (nextProps) => {
console.warn('componentWillReceiveProps', nextProps)
if (!nextProps.contactId) return
if (this.state.chat == null) this.getChat(nextProps.contactId)
}
getChat = async (contactId) => {
console.log('getChat', contactId)
await ddp.subscribe('Messages', {contactId})
const messagesColl = ddp.getCollection('Messages')
console.log('messagesColl', messagesColl)
this.setState({chat: this.getMessages(messagesColl)})
ddp.watch('Messages', (changedDoc, message) => {
console.log('Messages collection item changed', changedDoc, message)
const messagesColl = ddp.getCollection('Messages')
this.setState({chat: this.getMessages(messagesColl)})
})
}
getMessages = collection => {
let messages = []
if (collection) {
messages = makeArrayCollectionFromObjectCollection(collection)
}
console.log('messages', messages)
return messages
}
submitEmail = ({ email, convertedPage, projectId, visitSessionId }) => ddp.call('chat.init', { email, convertedPage, projectId, visitSessionId })
.then(contactId => {
Cookies.set('b2cContactId', contactId, { expires: 90 })
this.setState({ contactId, error: '' })
})
.catch(error => {
console.error('Error >', error)
})
readMessages = () => ddp.call('readMessages', {contactId: this.state.contactId, userId: !null})
.then(res => {
console.log('res', res)
})
.catch(error => {
console.error('Error', error)
})
submitMessage = ({message, visitSessionId, imageFile}) => ddp.call('chat.submitContactMessage', { message, visitSessionId, contactId: this.state.contactId, projectId: this.props.projectId, imageFile: imageFile || null})
.then((res) => {
console.log('res', res)
})
.catch(error => {
console.error('Error', error)
this.setState({error})
})
toggleChat = () => this.setState((state) => ({show: !state.show}))
sendFileToServer = (base64File, resolve, reject) => {
ddp.call('uploadToDropbox', base64File)
.then((res) => {
this.submitMessage({message: '', visitSessionId: this.props.visitSessionId, imageFile: res})
console.log('res', res)
})
.catch(error => {
console.error('Error', error)
})
}
getBase64 = (file, resolve, reject) => {
const self = this
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = function () {
self.sendFileToServer(reader.result, resolve, reject)
}
reader.onerror = function (error) {
console.error('FileReader Error: ', error)
}
}
onFileDrop = files => {
let self = this
files.forEach(file => {
return new Promise((resolve, reject) => {
self.getBase64(file, resolve, reject)
})
})
}
render () {
return (
<div>
<ChatContainer
onFileDrop={this.onFileDrop}
contactId={this.state.contactId}
show={this.state.show}
error={this.state.error && <span style={{color: 'red'}}>{this.state.error}</span>}
chatSettings={this.props.chatSettings}
projectId={this.props.projectId}
visitSessionId={this.props.visitSessionId}
defaultAdminUserName='default defaultAdminUserName'
contactName='You'
supportName='Our Support'
messages={this.state.chat}
onSend={this.submitMessage}
onSubmitEmail={this.submitEmail}
toggleChat={this.toggleChat}
readMessages={this.readMessages}
/>
<Icon
companyColor={this.props.chatSettings.companyColor}
onClick={this.toggleChat}
newMessagesCount={this.state.newMessagesCount}
/>
<Sound
url='https://www.incredo.co/hubfs/b2c/Not%201.wav'
playStatus={this.state.notStatus}
playFromPosition={0}
onFinishedPlaying={() => this.setState({ notStatus: 'STOPPED' })}
/>
</div>
)
}
}
export default View