use of mapstatetoprops and mapdispatchtoprops in react final form - redux-form

I have an application where I have used redux-form version 5.3.6, now I want to migrate from it to react-final-form. As I have mostly used Redux to manage form data while using redux-form, now while I am migrating, I find it very hard to integrate my redux codes with the final form implementation mentioned in the migration doc: https://final-form.org/docs/react-final-form/migration/redux-form
I am finding it difficult to use my mapStateToProps, mapDispatchToProps, and other redux-related functions with the final-form implementation.
Here is my current form container code from where form data is getting connected with redux via redux-form
import React from 'react';
import PropTypes from 'prop-types';
import { reduxForm } from 'redux-form';
import moment from 'moment';
import { startFromCart, endFromCart } from '../../lib/datesFromCart';
import { validateAllSearchParams } from './helpers/validate';
import cartOverride from './helpers/cartOverride';
import deepClone from '../../lib/deepClone';
import SearchForm from './SearchForm';
import searchAccountValues from '../../helpers/searchAccountValues';
import { createCarSearch } from '../../actions/car';
import { resetApp } from '../../actions/common';
const initialFields = [
'pickUpDate',
'dropOffDate',
'pickUpTime',
'dropOffTime',
'airportSearch',
'pickUpLocationAirportCode',
'pickUpGeoLocationAddress',
'pickUpGeoLocationLatitude',
'pickUpGeoLocationLongitude',
'dropOffLocationAirportCode',
'dropOffGeoLocationAddress',
'dropOffGeoLocationLatitude',
'dropOffGeoLocationLongitude',
'driverAge',
'vendorPreferences.codes',
'vendorPreferences.type',
];
const reduxFormConfig = {
form: 'carSearchForm',
destroyOnUnmount: false,
fields: [],
};
const mapStateToProps = (state, ownProps) => {
const { adults } = state.common.travelerCount;
const fields = deepClone(initialFields);
const cartHasCar = state.common.cart.items.some(item => item.type === 'car');
if (ownProps.searchAccounts && ownProps.searchAccounts.cars) {
Object.keys(ownProps.searchAccounts.cars).forEach(key => fields.push(`accountSettings.${key}`));
}
const startDateFromCart = startFromCart(state);
const endDateFromCart = endFromCart(state);
let vendorsPreferences = []
if (ownProps.globalPolicyPreferences.carRentalCompanies.length > 0) {
vendorsPreferences = ownProps.globalPolicyPreferences.carRentalCompanies
}
const defaultSearchParams = {
accountSettings: searchAccountValues(ownProps.searchAccounts.cars),
airportSearch: false,
driverAge: ownProps.currentUserAge || 32,
pickUpDate:
startDateFromCart ||
state.cars.defaultCarSearchParams.pickUpDate ||
moment()
.add(7, 'days')
.startOf('day')
.format('YYYY-MM-DD'),
dropOffDate: endDateFromCart || state.cars.defaultCarSearchParams.dropOffDate,
pickUpTime: '12:00',
dropOffTime: '11:00',
vendorPreferences: state.cars.defaultCarSearchParams.vendorPreferences || {
codes: vendorsPreferences,
type: 'exclusive',
},
};
const searchForm = state.form.carSearchForm;
const preselectTravelers = state.common.preselectTravelers.travelers;
const cartHasItems = state.common.cart.items.length > 0;
return {
fields,
cartHasCar,
initialValues: {
...defaultSearchParams,
...ownProps.searchParams,
...state.cars.defaultCarSearchParams,
...cartOverride(adults),
},
validate: validateAllSearchParams(ownProps.availableChannels.cars, ownProps.minHoursInFuture),
defaultSearchParams,
preselectTravelers,
searchForm,
cartHasItems,
travelerLoggedIn: ownProps.travelerLoggedIn,
};
};
const mapDispatchToProps = dispatch => ({
createCarSearch: (params, callbacks) => dispatch(createCarSearch(params, callbacks)),
resetApp: () => dispatch(resetApp('/cars')),
});
const ReduxForm = reduxForm(reduxFormConfig, mapStateToProps, mapDispatchToProps)(SearchForm);
const SearchFormWithContext = (props, context) => <ReduxForm {...props} {...context} />;
SearchFormWithContext.contextTypes = {
availableChannels: PropTypes.object,
searchAccounts: PropTypes.object,
laymanMode: PropTypes.bool,
currency: PropTypes.string,
callbackParams: PropTypes.object,
minHoursInFuture: PropTypes.number.isRequired,
currentUserAge: PropTypes.number.isRequired,
globalPolicyPreferences: PropTypes.shape({
airlines: PropTypes.arrayOf(PropTypes.number).isRequired,
carRentalCompanies: PropTypes.arrayOf(PropTypes.number).isRequired,
}).isRequired,
};
export default SearchFormWithContext;
The component is
import React, { useEffect, Fragment, useState } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import moment from 'moment';
import { I18nText } from '#wtag/react-comp-lib';
import { Menu, MenuItem } from '#wtag/rcl-menu';
import Alert from '#wtag/rcl-alert';
import SelectBox from '#wtag/rcl-select-box';
import Input from '#wtag/rcl-input';
import Button from '#wtag/rcl-button';
import Icon from '#wtag/rcl-icon';
import LocationSearchInput from 'sharedWebpack/LocationSearchInput';
import DatePickerWithFocusedStateWrapper from 'sharedWebpack/DatePickerWithFocusedStateWrapper';
import TimeSelectBox from 'sharedWebpack/TimeSelectBox';
import availableChannelsShape from '../../shapes/availableChannels';
import noChannelSelected from '../../lib/helpers/noChannelSelected';
import showInvalidDateError from '../../helpers/showInvalidDateError';
import history from '../../lib/history';
import { showError } from '../../actions/common';
import AirportAutoComplete from '../AirportAutoComplete';
import VendorSelectBox from './VendorSelectBox';
import SearchAccounts from '../../components/SearchAccounts';
import searchAccountsShape from '../../shapes/searchAccounts';
import PreselectTravelers from '../PreselectTravelers';
import preparePreselectTravelers from '../PreselectTravelers/preparePreselectTravelers';
import useSearchFormContainer from '../../helpers/hooks/useSearchFormContainer';
const SearchForm = props => {
const {
fields: {
airportSearch,
pickUpLocationAirportCode,
dropOffLocationAirportCode,
pickUpGeoLocationAddress,
pickUpGeoLocationLatitude,
pickUpGeoLocationLongitude,
dropOffGeoLocationAddress,
dropOffGeoLocationLatitude,
dropOffGeoLocationLongitude,
pickUpDate,
dropOffDate,
pickUpTime,
dropOffTime,
driverAge,
vendorPreferences,
accountSettings,
},
submitting,
handleSubmit,
laymanMode,
availableChannels,
cartHasCar,
createCarSearch,
defaultSearchParams,
initializeForm,
globalPolicyPreferences,
resetApp,
searchForm,
preselectTravelers,
setIsCollapsed,
cartHasItems,
formWithoutChildren,
resetBookingAppContext,
travelerLoggedIn,
travelArrangerPresent,
} = props;
const [isLocationMenuVisible, setIsLocationMenuVisible] = useState(false);
const [isVendorMenuVisible, setIsVendorMenuVisible] = useState(false);
const [isAgeMenuVisible, setIsAgeMenuVisible] = useState(false);
const onLocationMenuOutsideClick = () => {
setIsLocationMenuVisible(false);
};
const onVendorMenuOutsideClick = () => {
setIsVendorMenuVisible(false);
};
const onAgeMenuOutsideClick = () => {
setIsAgeMenuVisible(false);
};
const showCarFormContainer = useSearchFormContainer();
const filterTypeOptions = [
{
value: 'preferred',
label: I18n.t('components.ibe.search_form.car.label.vendor_preferences_type.preferred'),
isDisabled: laymanMode
? availableChannels.cars.includes('galileo')
: accountSettings && accountSettings.galileo && accountSettings.galileo.value.length > 0,
},
{
value: 'exclusive',
label: I18n.t('components.ibe.search_form.car.label.vendor_preferences_type.exclusive'),
},
];
const disableVendorPreference = globalPolicyPreferences.carRentalCompanies.length > 0;
const isNoChannelSelected = noChannelSelected(
laymanMode,
availableChannels,
accountSettings,
'cars',
);
const onPickUpLocationSelect = (address, latitude, longitude) => {
if (!dropOffGeoLocationAddress.touched) {
dropOffGeoLocationAddress.onChange(address);
dropOffGeoLocationLatitude.onChange(latitude);
dropOffGeoLocationLongitude.onChange(longitude);
}
};
const onPickUpAirportChange = airport => {
pickUpLocationAirportCode.onChange(airport);
if (!dropOffLocationAirportCode.touched) {
dropOffLocationAirportCode.onChange(airport);
}
};
const onPickUpDateChange = date => {
if (date === '') {
pickUpDate.onChange('');
} else {
const pickUpDateValue = moment.utc(date);
pickUpDate.onChange(pickUpDateValue.format());
if (!dropOffDate.touched) {
const nextValue = pickUpDateValue.add(7, 'days').format();
dropOffDate.onChange(nextValue);
}
}
};
const handleFormSubmission = (values, dispatch, ownProps) => {
history.push('/cars/searching');
createCarSearch(
{
...values,
currency: ownProps.currency,
preselectTravelers: preparePreselectTravelers(preselectTravelers),
},
ownProps.callbackParams,
)
.then(data => data && data.id && history.push(`/cars/searches/${data.id}`))
.catch(error => dispatch(showError('Search failed for unknown reasons', error)));
setIsCollapsed();
};
const resetToDefaultSearchParams = () => {
initializeForm(defaultSearchParams);
};
const findTimeSelectBoxValue = value => {
if (value === 'anytime') {
return { label: <I18nText id="departure_times.anytime" />, value };
}
return { label: value, value };
};
const onDropOffDateChange = date => {
if (date === '') {
dropOffDate.onChange('');
} else if (date) {
dropOffDate.onChange(moment.utc(date).format());
}
};
const minDropOffDate = () => {
if (pickUpDate.value) return moment(pickUpDate.value).add(1, 'days');
return moment()
.add(1, 'days')
.format('YYYY/MM/DD');
};
const updateDriverAgeFromPreselectedTraveler = () => {
const driver = preselectTravelers[0];
if (driver.traveler.value.age) {
driverAge.onChange(driver.traveler.value.age);
}
};
const loggedInTravellerConditions = travelerLoggedIn && travelArrangerPresent;
const guestTravellerConditions = !travelerLoggedIn && !cartHasItems && formWithoutChildren;
const showPreselectTravellerButton = () => {
if (
laymanMode &&
(loggedInTravellerConditions || guestTravellerConditions) &&
!props.forceAdvancedSearch
) {
return <PreselectTravelers type="car" />;
}
return null;
};
useEffect(() => {
const vendorCodes = globalPolicyPreferences.carRentalCompanies;
if (vendorCodes.length > 0) {
vendorPreferences.codes.onChange(vendorCodes);
vendorPreferences.type.onChange('exclusive');
}
}, []);
useEffect(() => {
if (!searchForm) {
resetToDefaultSearchParams();
}
}, [searchForm]);
useEffect(() => {
if (!driverAge.touched && preselectTravelers.length > 0) {
updateDriverAgeFromPreselectedTraveler();
}
}, [preselectTravelers]);
return (
<div
className={classNames('grid car-form__container', {
'car-form__container--show': showCarFormContainer,
})}
>
<div className="col-grid direction-row col-12 car-form__specifier-container wrap">
<div className="search-menu__container search-menu__container--location">
<Menu
className="search-menu__container--location-dropdown"
isVisible={isLocationMenuVisible}
size="small"
popOverDirection="bottom-left"
onOutsideClick={onLocationMenuOutsideClick}
label={
<div className="search-menu__menu-label">
{airportSearch.value
? I18n.t('components.ibe.search_form.car.label.search_by.airport')
: I18n.t('components.ibe.search_form.car.label.search_by.location')}
<div
className="search-menu__menu-label--icon"
onClick={() => setIsLocationMenuVisible(prevState => !prevState)}
role="button"
tabIndex={0}
onKeyPress={() => setIsLocationMenuVisible(prevState => !prevState)}
>
<Icon name="iconDownChevron" size="tiny" />
</div>
</div>
}
>
<MenuItem
onClick={() => {
airportSearch.onChange(false);
setIsLocationMenuVisible(false);
}}
>
{I18n.t('components.ibe.search_form.car.label.search_by.location')}
</MenuItem>
<MenuItem
onClick={() => {
airportSearch.onChange(true);
setIsLocationMenuVisible(false);
}}
>
{I18n.t('components.ibe.search_form.car.label.search_by.airport')}
</MenuItem>
</Menu>
</div>
<div className="search-menu__container search-menu__container--vendors">
<Menu
className="search-menu__container--vendors-dropdown"
isVisible={isVendorMenuVisible}
size="small"
popOverDirection="bottom-center"
onOutsideClick={onVendorMenuOutsideClick}
label={
<div className="search-menu__menu-label">
{I18n.t('components.ibe.search_form.car.label.vendor_preferences')}
<div
className="search-menu__menu-label--icon"
onClick={() => setIsVendorMenuVisible(prevState => !prevState)}
role="button"
tabIndex={0}
onKeyPress={() => setIsVendorMenuVisible(prevState => !prevState)}
>
<Icon name="iconDownChevron" size="tiny" />
</div>
</div>
}
>
<div className="search-menu-item__container">
<div className="search-menu-item__specifier">
<div className="col-12 col-bleed">
<div className="search-menu-item__specifier--add-padding-bottom">
<VendorSelectBox
value={vendorPreferences.codes.value}
onChange={value => vendorPreferences.codes.onChange(value || [])}
errorMsg={vendorPreferences.codes.error && vendorPreferences.codes.error[0]}
isMulti={true}
width="full"
size="tiny"
placeholderText={I18n.t('components.ibe.search_form.car.label.choose_vendor')}
isDisabled={disableVendorPreference}
/>
</div>
<SelectBox
width="full"
size="tiny"
value={filterTypeOptions.find(
filter => filter.value === vendorPreferences.type.value,
)}
isDisabled={
disableVendorPreference || vendorPreferences.codes.value.length <= 0
}
options={filterTypeOptions}
onChange={selectedOption =>
vendorPreferences.type.onChange(selectedOption ? selectedOption.value : '')
}
placeholderText={I18n.t('components.ibe.search_form.car.label.filter_type')}
isClearable={false}
/>
</div>
</div>
</div>
</Menu>
</div>
<div className="search-menu__container search-menu__container--drivers-age">
<Menu
className="search-menu__container--drivers-age-dropdown"
isVisible={isAgeMenuVisible}
size="small"
popOverDirection="bottom-center"
onOutsideClick={onAgeMenuOutsideClick}
label={
<div
className={classNames('search-menu__menu-label', {
'search-menu__label--error': driverAge.touched && !driverAge.valid,
})}
>
{I18n.t('components.ibe.search_form.car.label.driver_age')}
<div
className="search-menu__menu-label--icon"
onClick={() => setIsAgeMenuVisible(prevState => !prevState)}
role="button"
tabIndex={0}
onKeyPress={() => setIsAgeMenuVisible(prevState => !prevState)}
>
<Icon name="iconDownChevron" size="tiny" />
</div>
</div>
}
>
<div className="search-menu-item__container">
<div className="search-menu-item__specifier">
<div className="col-12 col-bleed">
<Input
placeholder={I18n.t('components.ibe.search_form.car.label.driver_age')}
size="tiny"
type="number"
{...driverAge}
disabled={cartHasCar}
/>
</div>
</div>
<Alert
className="col-12 col-bleed car-form__error"
hideClose={true}
isLocationMenuVisible={cartHasCar}
type="warning"
>
{I18n.t('components.ibe.search_form.error.change_number_of_travelers')}
</Alert>
</div>
</Menu>
</div>
</div>
<div className="col-12 col-bleed-y">
{airportSearch.value ? (
<Fragment>
<div className="col-12 col-sm-6 col-bleed car-form__field--add-padding-right">
<AirportAutoComplete
title={I18n.t('components.ibe.search_form.car.label.pick_up.airport')}
error={pickUpLocationAirportCode.touched && pickUpLocationAirportCode.error}
currentAirport={pickUpLocationAirportCode.value}
{...pickUpLocationAirportCode}
onChange={onPickUpAirportChange}
fullWidth={true}
size="tiny"
/>
</div>
<div className="col-12 col-sm-6 col-bleed car-form__field--add-padding-left">
<AirportAutoComplete
title={I18n.t('components.ibe.search_form.car.label.drop_off.airport')}
error={dropOffLocationAirportCode.error}
currentAirport={dropOffLocationAirportCode.value}
{...dropOffLocationAirportCode}
fullWidth={true}
/>
</div>
</Fragment>
) : (
<Fragment>
<div className="col-12 col-sm-6 col-bleed car-form__field--add-padding-right">
<LocationSearchInput
address={pickUpGeoLocationAddress.value}
placeholder={I18n.t('components.ibe.search_form.car.label.pick_up.location')}
fullWidth={true}
onAddressChange={pickUpGeoLocationAddress.onChange}
onLatitudeChange={pickUpGeoLocationLatitude.onChange}
onLongitudeChange={pickUpGeoLocationLongitude.onChange}
onSelect={onPickUpLocationSelect}
size="small"
touched={pickUpGeoLocationAddress.touched}
error={pickUpGeoLocationAddress.error}
/>
</div>
<div className="col-12 col-sm-6 col-bleed car-form__field--add-padding-left">
<LocationSearchInput
address={dropOffGeoLocationAddress.value}
placeholder={I18n.t('components.ibe.search_form.car.label.drop_off.location')}
fullWidth={true}
onAddressChange={dropOffGeoLocationAddress.onChange}
onLatitudeChange={dropOffGeoLocationLatitude.onChange}
onLongitudeChange={dropOffGeoLocationLongitude.onChange}
size="small"
touched={dropOffGeoLocationAddress.touched}
error={dropOffGeoLocationAddress.error}
/>
</div>
</Fragment>
)}
</div>
<div className="col-12">
<div className="col-12 col-sm-6 col-bleed car-form__field--add-padding-right">
<DatePickerWithFocusedStateWrapper
pastYearsCount={0}
futureYearsCount={1}
size="small"
hideMonthsAndYearsWithNoActiveDates={true}
id={Math.random()}
placeholder={I18n.t('components.ibe.search_form.car.label.pick_up.date')}
onChange={onPickUpDateChange}
fullWidth={true}
error={showInvalidDateError(pickUpDate)}
minDate={moment()
.add(1, 'days')
.format('YYYY/MM/DD')}
date={pickUpDate.value}
locale={moment().locale()}
showClearDate={true}
hideNavButtons={false}
/>
</div>
<div className="col-12 col-sm-6 col-bleed car-form__field--add-padding-left">
<TimeSelectBox
placeholderText={I18n.t('components.ibe.search_form.car.label.pick_up.time')}
width="full"
onChange={selectedOption => selectedOption && pickUpTime.onChange(selectedOption.value)}
errorMsg={pickUpTime.touched && pickUpTime.error}
value={findTimeSelectBoxValue(pickUpTime.value)}
isClearable={false}
size="small"
/>
</div>
</div>
<div className="col-12 col-bleed-y">
<div className="col-12 col-sm-6 col-bleed car-form__field--add-padding-right">
<DatePickerWithFocusedStateWrapper
pastYearsCount={0}
futureYearsCount={1}
size="small"
hideMonthsAndYearsWithNoActiveDates={true}
id={Math.random()}
placeholder={I18n.t('components.ibe.search_form.car.label.drop_off.date')}
onChange={onDropOffDateChange}
fullWidth={true}
error={showInvalidDateError(dropOffDate)}
minDate={minDropOffDate()}
date={dropOffDate.value}
locale={moment().locale()}
showClearDate={true}
hideNavButtons={false}
/>
</div>
<div className="col-12 col-sm-6 col-bleed car-form__field--add-padding-left">
<TimeSelectBox
placeholderText={I18n.t('components.ibe.search_form.car.label.drop_off.time')}
width="full"
onChange={selectedOption =>
selectedOption && dropOffTime.onChange(selectedOption.value)
}
errorMsg={dropOffTime.touched && dropOffTime.error}
value={findTimeSelectBoxValue(dropOffTime.value)}
isClearable={false}
size="small"
/>
</div>
</div>
{!props.laymanMode && (
<div className="col-12 car-form__search-accounts">
<div className="accounts">
<SearchAccounts
searchAccounts={props.searchAccounts.cars}
selectedAccounts={accountSettings}
disabled={false}
selectAll={props.selectAll}
toggleSelectAll={props.toggleSelectAll}
selectGds={props.selectGds}
toggleSelectGds={props.toggleSelectGds}
selectDirect={props.selectDirect}
toggleSelectDirect={props.toggleSelectDirect}
/>
</div>
</div>
)}
<Alert
className="col-12 col-bleed hotel-form__error"
hideClose={true}
isLocationMenuVisible={isNoChannelSelected}
type="danger"
>
{I18n.t('components.ibe.search_form.error.no_channel_selected')}
</Alert>
<div className="col-grid direction-row align-center justify-space-between hotel-form__action-wrapper">
<div className="car-form__preselect-traveler">{showPreselectTravellerButton()}</div>
<div className="car-form__action">
<div className="hotel-form__field--add-padding-right">
<Button
label={I18n.t('components.ibe.search_form.label.reset')}
size="normal"
version="v2"
onClick={() => {
resetApp();
resetBookingAppContext();
}}
/>
</div>
<div className="hotel-form__field--add-padding-left">
<Button
label={I18n.t('components.ibe.search_form.label.search')}
size="normal"
version="v2"
disabled={!!submitting || isNoChannelSelected}
onClick={handleSubmit(handleFormSubmission)}
type="primary"
/>
</div>
</div>
</div>
</div>
);
};
export default SearchForm;
the corresponding reducer is
import { combineReducers } from 'redux';
import defaultCarSearchParams from './defaultCarSearchParams';
import searchFilterParamsBySearchId from './searchFilterParamsBySearchId';
import searchParamsBySearchId from './searchParamsBySearchId';
import searchResultsBySearchId from './searchResultsBySearchId';
import searchStatsBySearchId from './searchStatsBySearchId';
import totalResultsBySearchId from './totalResultsBySearchId';
import vendorsByCode from './vendorsByCode';
import totalResultsPerSupplier from './totalResultsPerSupplier'
const carsReducer = combineReducers({
defaultCarSearchParams,
searchFilterParamsBySearchId,
searchParamsBySearchId,
searchResultsBySearchId,
searchStatsBySearchId,
totalResultsBySearchId,
vendorsByCode,
totalResultsPerSupplier,
});
export default carsReducer;
Now I am not sure how to make migration among these from redux-form to react-final-form using my Redux codes.

Related

Adding Leading Zero's in React 'duration' component

I have created a 'working' duration slider, but I'm having trouble inserting 'leading zeros' into the hours and minutes:
const [hours, setHours] = useState([parseFloat(defaultValue.split(':')[0])])
const [minutes, setMinutes] = useState([parseFloat(defaultValue.split(':')[1])])
The above code is working, but will output (9hours & 5mins) as 9:5 (needs to be 09:05)
The code below (commented out in the full code) successfully puts in the leading zero's, but the code errors on line 61 and 97 where it's trying to handle the value
var leadingZeroHour = ('0000'+[parseFloat(defaultValue.split(':')[0])]).slice(-2)
const [hours, setHours] = useState(leadingZeroHour)
var leadingZeroMin = ('0000'+[parseFloat(defaultValue.split(':')[1])]).slice(-2)
const [minutes, setMinutes] = useState(leadingZeroMin)
The full code is below. If you can help me out and point me in the right direction, I'd be very grateful. Many thanks!
import React, { FunctionComponent, useState, useEffect } from 'react'
import { Range } from 'react-range'
type Props = {
className?: string,
defaultValue: string,
title?: string,
onUpdate: (value: string) => void;
[x: string]: any;
}
const defaultProps: Props = {
className: '',
defaultValue: '00:00',
title: '',
onUpdate: (value: any) => {},
}
const DurationInput: FunctionComponent<Props> = ({ className, defaultValue, title, onUpdate, ...rest }) => {
const [hours, setHours] = useState([parseFloat(defaultValue.split(':')[0])])
const [minutes, setMinutes] = useState([parseFloat(defaultValue.split(':')[1])])
// var leadingZeroHour = ('0000'+[parseFloat(defaultValue.split(':')[0])]).slice(-2)
// const [hours, setHours] = useState(leadingZeroHour)
// var leadingZeroMin = ('0000'+[parseFloat(defaultValue.split(':')[1])]).slice(-2)
// const [minutes, setMinutes] = useState(leadingZeroMin)
// console.log(hours)
useEffect(() => {
const duration = `${hours[0]}:${minutes[0]}`
onUpdate(duration)
}, [hours, minutes])
return (
<div className={`w-full ${className}`}>
{title ? <div className="text-base sm:text-xl mb-4 text-center">{title}</div> : <></>}
{/* <div className="grid grid-cols-3 gap-3 mb-8">
<div></div> */}
<div className="mx-auto w-40 sm:w-80 mb-8">
<div className="border border-orange rounded py-3 text-center text-4xl bg-white">
{hours}:{minutes}
</div>
<div></div>
</div>
<div className="mb-4 w-4/5 sm:w-3/5 mx-auto">
<div className="font-bold text-center mb-6">Hours</div>
<Range
step={1}
min={0}
max={23}
values={hours}
onChange={(values) => setHours(values)}
renderTrack={({ props, children }) => (
<div
{...props}
className="bg-orange rounded-full"
style={{
...props.style,
height: '6px',
width: '100%',
}}
>
{children}
</div>
)}
renderThumb={({ props }) => (
<div
{...props}
className="bg-white rounded-full border-4 border-orange"
style={{
...props.style,
height: '42px',
width: '42px',
}}
/>
)}
/>
</div>
<div className=" w-4/5 sm:w-3/5 mx-auto">
<div className="font-bold text-center mb-6">Minutes</div>
<Range
step={5}
min={0}
max={59}
values={minutes}
onChange={(values) => setMinutes(values)}
renderTrack={({ props, children }) => (
<div
{...props}
className="bg-orange rounded-full"
style={{
...props.style,
height: '6px',
width: '100%',
}}
>
{children}
</div>
)}
renderThumb={({ props }) => (
<div
{...props}
className="bg-white rounded-full border-4 border-orange"
style={{
...props.style,
height: '42px',
width: '42px',
}}
/>
)}
/>
</div>
</div>
)
}
DurationInput.defaultProps = defaultProps
export default DurationInput

Plus and Minus the Quantity of selected item only in the Cart and total price - React Redux

I want to plus or minus the quantity of an item which is added in the cart. The issue is when I add another item to the cart and then when I plus or minus the quantity then the other item's quantity also changes. Also when quantity changes I want to add the total price of the quantity. I am using React Redux. Please help.
CART.JS
import React, { useState } from "react";
import "./cart.css";
import { useDispatch, useSelector } from "react-redux";
import {
clearCart,
minusQuantity,
plusQuantity,
removeFromCart,
} from "../../../redux/actions/CartActions";
const Cart = ({ id }) => {
const dispatch = useDispatch();
const product = useSelector((state) => state.addToCartReducer.products);
const qty = useSelector((state) => state.addToCartReducer.quantity);
// const minus = useSelector((state) => state.addToCartReducer.minus);
console.log(qty);
// console.log(minus);
const [quantity, setQuantity] = useState(1);
return (
<>
{product && (
<div
className={`dvCart ${id ? `col-lg-3` : `col-lg-2`} d-none d-lg-block`}
>
<div className="sticky-top" style={{ top: "90px" }}>
<div className="row">
<div className="col-sm-12 mb-1">
<h5 className="heading-5 d-inline-block mr-2">
Cart {product.length} items
</h5>
<span
className={`paragraph cp ${
product.length === 0 ? `d-none` : ``
}`}
onClick={() => dispatch(clearCart({}))}
>
clear all
</span>
</div>
{product && product.length === 0 ? (
<div className="dvCartEmpty col-12 d-none- mb-2">
<h5 className="heading-5 text-danger">Cart is Empty.</h5>
<p className="paragraph text-danger">
All Good No Bad! Go ahead, order some items from the menu.
</p>
{/* <!-- <img src="images/cart-empty.png" className="img-fluid" width="200" alt=""> --> */}
</div>
) : (
<div className="dvCartItems col-sm-12 mb-2">
<div className="row">
<div className="scrollbar mr-3">
{product.map((item) => {
const { id, heading, img, price, pack, size } = item;
return (
<div key={id} className="item col-sm-12 mb-2 pr-0">
<div className="bg-light pt-2 pb-2">
<div className="col-12">
<h5 className="heading-5">{heading}</h5>
<span className="paragraph mb-1 mr-2">
{size}
</span>
<span
className={`paragraph mb-1 ${
pack === `pack of 0` ? `d-none` : ``
}`}
>
{pack}
</span>
</div>
<div className="col-sm-12">
<div className="d-flex justify-content-between align-items-center">
<div className="addBtn d-flex justify-content-center align-items-center flex-1">
<div className="flex-1 text-center">
<i
className="fa fa-minus mr-1 cp p-1"
onClick={() =>
dispatch(
minusQuantity({ quantity: 1 })
)
}
></i>
</div>
<div className="flex-3 text-center">
<input
type="text"
value={qty}
onChange={(e) =>
setQuantity(e.target.value)
}
className="form-control text-center p-0"
/>
</div>
<div className="flex-1 text-center">
<i
className="fa fa-plus ml-1 cp p-1"
onClick={() =>
dispatch(
plusQuantity({ quantity: 1, id })
)
}
></i>
</div>
</div>
<div className="paragraph text-right">
<i className="fa fa-inr"></i>
{price}
</div>
</div>
</div>
<button
onClick={() =>
dispatch(
removeFromCart({ id, heading, price, size })
)
}
className="btn btn-remove"
>
<i className="fa fa-close"></i>
</button>
</div>
</div>
);
})}
</div>
</div>
</div>
)}
<div className="dvSubTotal col-sm-12 mb-2">
<div className="d-flex justify-content-between">
<div>
<h5 className="heading-5 text-success">Subtotal</h5>
</div>
<div>
<p className="heading-5 text-success">
<i className="fa fa-inr"></i>
{product
.map((item) => item.price)
.reduce((prev, curr) => prev + curr, 0)}
.00
</p>
</div>
</div>
<p className="paragraph">Extra charges may apply.</p>
</div>
<div className="dvProceed col-sm-12 mb-2">
<button
disabled={product.length !== 0 ? false : true}
className="btn btn-black w-100"
>
Proceed
</button>
</div>
</div>
</div>
</div>
)}
</>
);
};
export default Cart;
PRODUCTLIST.JS
import React, { useState } from "react";
import { Link } from "react-router-dom";
import "./productlist.css";
import ProductNotFound from "../../other/product-not-found/ProductNotFound";
import { useDispatch, useSelector } from "react-redux";
import { addToCart } from "../../../redux/actions/CartActions";
const ProductList = ({
id,
img,
pack,
price,
size,
heading,
description,
notfound,
}) => {
const dispatch = useDispatch();
// const [isDisabled, setIsDisabled] = useState(false);
const products = useSelector((state) => state.addToCartReducer.products);
// const isDisabled = useSelector((state) => state.addToCartReducer.isDisabled);
// console.log(isDisabled);
// console.log(products);
return (
<>
{notfound ? (
<ProductNotFound />
) : (
<div className="col-6 col-md-4 col-lg-6 col-xl-3 mb-4">
<div className="border border-light shadow-sm p-1 h-100">
<div className="image">
<p className={pack !== "pack of 0" ? "packs" : "d-none"}>
{pack}
</p>
<div className="bg-light text-center pt-2 pb-2 mb-1">
<Link className="d-inline-block" to={`/${id}`}>
<img src={img} className="img-fluid" alt={heading} />
</Link>
</div>
<h5 className="heading-5 text-center">{heading}</h5>
</div>
<div className="description d-flex justify-content-between mb-1">
<div className="paragraph">
<p>{size}</p>
</div>
<div className="paragraph mr-2">
<span>
<i className="fa fa-inr"></i>
<span>{price}</span>
</span>
</div>
</div>
<div className="addBtn text-center">
<button
onClick={() =>
dispatch(
addToCart({
id,
img,
pack,
price,
size,
heading,
description,
})
)
}
className="btn btn-white w-100"
disabled={products.find((item) => item.id === id) && true}
>
Add to Bag
</button>
</div>
</div>
</div>
)}
</>
);
};
export default ProductList;
CART ACTIONS.JS
import { actionTypes } from "../constants/action-types";
//ADD TO CART
export const addToCart = (item) => {
return {
type: actionTypes.ADD_TO_CART,
payload: item,
};
};
//PLUS QUANTITY
export const plusQuantity = (item) => {
return {
type: actionTypes.PLUS_QUANTITY,
payload: item,
};
};
//MINUS QUANTITY
export const minusQuantity = (item) => {
return {
type: actionTypes.MINUS_QUANTITY,
payload: item,
};
};
//REMOVE FROM CART
export const removeFromCart = (item) => {
return {
type: actionTypes.REMOVE_FROM_CART,
payload: item,
};
};
//CLEAR CART
export const clearCart = (item) => {
return {
type: actionTypes.CLEAR_CART,
payload: item,
};
};
CART REDUCER.JS
import { actionTypes } from "../constants/action-types";
const addToCartiState = {
products: [],
quantity: 1,
// minus: 0,
};
//CART REDUCER
export const addToCartReducer = (state = addToCartiState, action) => {
console.log(state);
console.log(action);
switch (action.type) {
case actionTypes.ADD_TO_CART:
return {
...state,
products: [...state.products, action.payload],
};
case actionTypes.PLUS_QUANTITY:
return {
...state,
//i am not sure if this logic is correct
quantity: state.quantity + action.payload.quantity,
};
case actionTypes.MINUS_QUANTITY:
return {
...state,
//i am not sure if this logic is correct
quantity:
state.quantity > 1 ? state.quantity - action.payload.quantity : 1,
};
case actionTypes.REMOVE_FROM_CART:
const filteredID = state.products.filter(
(item) => item.id !== action.payload.id
);
return {
...state,
products: filteredID,
};
case actionTypes.CLEAR_CART:
return {
...state,
products: [],
};
default:
return state;
}
};
Total cart quantity should be derived state and not stored in state. You should instead store an item quantity and compute a total quantity when rendering.
Add a quantity property when adding an item to the cart. If the item has already been added then simply update the quantity instead of adding a duplicate item.
Example item object:
{
id,
img,
pack,
price,
size,
heading,
description,
}
Cart reducer
const addToCartiState = {
products: [],
};
export const addToCartReducer = (state = addToCartiState, action) => {
switch (action.type) {
case actionTypes.ADD_TO_CART:
const isInCart = state.products.some(item => item.id === action.payload.id);
if (isInCart) {
// update existing item in cart
return {
...state,
products: state.products.map(
item => item.id === action.payload.id
? {
...item,
quantity: item.quantity + 1,
}
: item
),
};
}
// add new item to cart
return {
...state,
products: [
...state.products,
{
...action.payload,
quantity: 1
},
],
};
case actionTypes.PLUS_QUANTITY:
return {
...state,
products: state.products.map(
item => item.id === action.payload.id
? {
...item,
quantity: item.quantity + action.payload.quantity
}
:item
),
};
case actionTypes.MINUS_QUANTITY:
const item = state.products.find(item => item.id === action.payload.id);
if (item?.quantity === 1) {
// new quantity is 0, remove item from cart
return {
...state,
products: state.products.filter(item => item.id !=== action.payload.id),
};
}
// decrement quantity
return {
...state,
products: state.products.map(
item => item.id === action.payload.id
? {
...item,
quantity: item.quantity - action.payload.quantity
}
:item
),
};
case actionTypes.REMOVE_FROM_CART:
return {
...state,
products: state.products.filter(
(item) => item.id !== action.payload.id
),
};
case actionTypes.CLEAR_CART:
return {
...state,
products: [],
};
default:
return state;
}
};
Compute the derived cart total item quantity in the UI.
const Cart = ({ id }) => {
const dispatch = useDispatch();
const products = useSelector((state) => state.addToCartReducer.products);
const quantity = products.reduce((total, { quantity }) => total + quantity, 0);
console.log(quantity);

How to Fetch the Data on Button Click using React Hooks

I want to display the Child component on click on Button, But here I am getting the Data when the page loads,I need to write a condition like when Button Clicked , then only the Child component show display otherwise a default text should display there.
const DetailsCard = () => {
const [employee, setemployee] = useState([])
const [data, setData] = useState()
useEffect(() => {
fetch("http://localhost:3000/users").then(res => res.json()).then((result) => {
setemployee(result);
}, [])
})
const handleChange = () => {
setData(true);
}
return (
<div>
<h1>LordShiva</h1>
<div className="container">
<div className="row">
<div className="col">
<div className="card">
<div className="card-header bg-primary text-white">
<p className="h4">Birthday APP</p>
<button className="btn btn-red true1 " onClick={handleChange} >HappyBirthday</button>
</div>
<div className="card-body">
<TableCard data={employee} />
</div>
</div>
</div>
</div>
</div>
</div>
)
}
export default DetailsCard;
const DetailsCard = () => {
const [employee, setemployee] = useState([]);
const [showChild, toggleShowChild] = useState(false);
useEffect(() => {
fetch("http://localhost:3000/users")
.then((res) => res.json())
.then((result) => {
setemployee(result);
}, []);
});
const handleChange = () => {
toggleShowChild(!showChild);
};
return (
<div>
<h1>LordShiva</h1>
<div className="container">
<div className="row">
<div className="col">
<div className="card">
<div className="card-header bg-primary text-white">
<p className="h4">Birthday APP</p>
<button className="btn btn-red true1 " onClick={handleChange}>
HappyBirthday
</button>
</div>
{/* Only show child when `showChild` is true. */}
{showChild && (
<div className="card-body">
<TableCard data={employee} />
</div>
)}
</div>
</div>
</div>
</div>
</div>
);
};
export default DetailsCard;
Codesandbox
import React, { useState, useEffect } from "react";
const FetchAPI = () => {
const [data, setData] = useState([]);
const newData = data.slice(0, 12);
const [showData, setShowData] = useState(false);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/todos")
.then((response) => response.json())
// .then((json) => console.log(json));
.then((json) => setData(json));
}, []);
const onClickhandler = () => {
setShowData(!showData);
};
return (
<>
<h1>Search</h1>
<ul>
{newData.map((item) => {
return showData ? (
<li style={{ listStyle: "none", marginTop: "10px" }} key={item.id}>
{item.title}
</li>
) : (
""
);
})}
</ul>
<button onClick = {onClickhandler}>
Fetch
</button>
</>
);
};
export default FetchAPI;

How to Display search result on another page in react hooks

I m developing an e-commerce application with react hooks in search component result is shown beneath the component I want to render on search Result component how to redirect search result on the search Result component
import React, { useState, useEffect } from "react";
import { Link, Redirect } from "react-router-dom";
import { getCategories, list } from "./apiCore";
import SearchResult from "./SearchResult";
const Search = () => {
const [data, setData] = useState({
categories: [],
category: "",
search: "",
results: [],
searched: false,
});
const { categories, category, search, results, searched } = data;
const loadCategories = () => {
getCategories().then(data => {
if (data.error) {
console.log(data.error);
} else {
setData({ ...data, categories: data });
}
});
};
useEffect(() => {
loadCategories();
}, []);
const searchData = () => {
// console.log(search, category);
if (search) {
list({ search: search || undefined, category: category }).then(
response => {
if (response.error) {
console.log(response.error);
} else {
setData({ ...data, results: response, searched: true });
}
}
);
}
};
const searchSubmit = e => {
e.preventDefault();
searchData();
};
const handleChange = name => event => {
setData({ ...data, [name]: event.target.value, searched: false });
};
const searchMessage = (searched, results) => {
if (searched && results.length > 0) {
return `Found ${results.length} products`;
}
if (searched && results.length < 1) {
return `No products found`;
}
};
const searchedProducts = (results = []) => {
return (
<div>
<h2 className="text-muted mb-4">
{searchMessage(searched, results)}
</h2>
<div className="row">
{results.map((product, i) => (
<CarouselCard key={i} product={product} />
))}
</div>
</div>
);
};
return (
<div className="site-navbar-top">
<div className="container">
<div className="row align-items-center">
<div className="col-6 col-md-4 order-2 order-md-1 site-search-icon text-left">
<form className="site-block-top-search" onSubmit={searchSubmit}>
<span className="icon icon-search2"></span>
<input type="text" className="form-control border-0" placeholder="Search" onChange={handleChange("search")} />
</form>
</div>
<div className="col-12 mb-3 mb-md-0 col-md-4 order-1 order-md-2 text-center">
<div className="site-logo">
<Link to="/" className="js-logo-clone">Shoppers</Link>
</div>
</div>
<div className="col-6 col-md-4 order-3 order-md-3 text-right">
<div className="site-top-icons">
<ul>
<li><Link to="#"><span className="icon icon-person" /></Link></li>
<li><Link to="#"><span className="icon icon-heart-o" /></Link></li>
<li>
<Link to="cart.html" className="site-cart">
<span className="icon icon-shopping_cart" />
<span className="count">2</span>
</Link>
</li>
</ul>
</div>
</div>
</div>
</div>
{searchedProducts(results)}
</div>
)
}
export default Search;
The main part of the hook is pretty straightforward, but I'm having a difficult time finding a nice way to handle that redirect. The current working solution is to wrap the functional component with withRouter, then pass props.history

Called React-Redux action in prop does not give needed results

I am implementing a component that handles an redux action to add comments but it is not working.No error is generated
I have tried calling the props from other regions in the code but that doesnt seem to work.The addComment Action should add the comments rendered in the DishDetails comments section.However no additions are made.
ActionTypes.js
export const ADD_COMMENT='ADD_COMMENT';
ActionCreators.js
import * as ActionTypes from './ActionTypes';
export const addComment=(dishId,rating, author, comment)=>({
type: ActionTypes.ADD_COMMENT,
payload: {
dishId:dishId,
rating:rating,
author:author,
comment:comment
}
});
comments.js
import { COMMENTS } from '../shared/comments';
import * as ActionTypes from './ActionTypes';
export const Comments= (state= COMMENTS, action) => {
switch(action.type){
case ActionTypes.ADD_COMMENT:
var comment= action.payload;
comment.id= state.length;
comment.date = new Date().toISOString();
return state.concat(comment);
default:
return state;
}
};
MainComponent.js
import React, { Component } from 'react';
import Header from './HeaderComponent';
import Footer from './FooterComponent';
import Menu from './MenuComponent';
import DishDetail from './DishDetail';
import Home from './HomeComponent';
import { Switch, Route, Redirect, withRouter } from 'react-router-dom';
import Contact from './ContactComponent';
import About from './AboutComponent';
import { connect } from 'react-redux';
import {addComment} from '../redux/ActionCreators';
const mapStateToProps = state =>{
return{
dishes: state.dishes,
comments: state.comments,
promotions: state.promotions,
leaders: state.leaders
}
};
const mapDispatchToProps = dispatch => ({
addComment: (dishId,rating, author, comment)=>dispatch(addComment(dishId,rating, author, comment))
});
class Main extends Component {
constructor(props) {
super(props);
}
render() {
const HomePage= ()=>{
return(
<Home dish={this.props.dishes.filter((dish)=>dish.featured)[0]}
promotion={this.props.promotions.filter((promotion)=>promotion.featured)[0]}
leader={this.props.leaders.filter((leader)=>leader.featured)[0]}
/>
);
}
const DishWithId = ({match})=>{
return(
<DishDetail dish={this.props.dishes.filter((dish)=>dish.id === parseInt(match.params.dishId,10))[0]}
comments={this.props.comments.filter((comment)=>comment.dishId=== parseInt(match.params.dishId,10))}
addComment={this.props.addComment}/>
);
}
const AboutPage = ()=>{
return(
<About leaders={this.props.leaders}/>
);
}
return (
<div>
<Header/>
<Switch>
<Route path="/home" component={HomePage} />
<Route exact path="/menu" component={()=><Menu dishes ={this.props.dishes} />} />
<Route path="/menu/:dishId" component={DishWithId}/>
<Route exact path="/aboutus" component={() => <AboutPage leaders={this.props.leaders} />} />}/>
<Route exact path="/contactus" component={Contact}/>
<Redirect to="/home"/>
</Switch>
<Footer/>
</div>
);
}
}
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Main));
DishDetail.js
import React, { Component } from 'react';
import { Card, CardImg, CardImgOverlay, CardText, CardBody, CardTitle, Breadcrumb, BreadcrumbItem , Button, Modal, ModalHeader,ModalBody, Form, FormGroup, Input, Label, Col, Row } from 'reactstrap';
import {Control, LocalForm, Errors} from 'react-redux-form';
import {Link} from 'react-router-dom';
const required = (val) =>val && val.length;
const maxLength = (len) => (val) => !(val) || (val.length <= len);
const minLength = (len) => (val) => val && (val.length >= len);
class DishDetail extends Component{
constructor(props){
super(props);
this.state={
dish:props.dish,
isCommentModalOpen: false,
};
this.toggleCommentModal=this.toggleCommentModal.bind(this);
}
toggleCommentModal(){
this.setState({
isCommentModalOpen:!this.state.isCommentModalOpen
});
}
handleSubmit(props,values){
alert("State" + JSON.stringify(props.addComment(props.dishId, values.rating, values.author, values.comment)));
// this.state.addComment(this.state.dishId, values.rating, values.author, values.comment)
}
render(){
const RenderDish=({dish})=>{
return(
<Card>
<CardImg top src={dish.image} alt={dish.name}/>
<CardBody>
<CardTitle>{dish.name}</CardTitle>
<CardText>{dish.description}</CardText>
</CardBody>
</Card>
);
}
const RenderComments=({comments})=>{
const comment_layout= comments.map((comment)=>{
if(comment.comment!=null){
return(
<div>
{comment.comment}
{comment.author}, {new Intl.DateTimeFormat('en-US',{year:'numeric',month:'short',day:'2-digit'}).format(new Date(Date.parse(comment.date)))}
</div>
);
}else{
return(
<div></div>
);
}
});
return(comment_layout);
}
const CommentForm=()=>{
return(
<Button outline onClick={this.toggleCommentModal}>
<span className="fa fa-edit fa-lg">Submit Comment</span>
</Button>
);
}
if (this.state.dish!==undefined){
return (
<div className="container">
<div className="row">
<Breadcrumb>
<BreadcrumbItem>
<Link to="/menu">Menu</Link>
</BreadcrumbItem>
<BreadcrumbItem active>{this.state.dish.name}
</BreadcrumbItem>
</Breadcrumb>
<div className="col-12">
<h3>{this.state.dish.name}</h3>
<hr/>
</div>
</div>
<div className="row ">
<div className="col-12 col-md-5 m-1">
<RenderDish dish={this.state.dish}/>
</div>
<div className="col-md-5 col-sm-12 m-1">
<h4>Comment</h4>
<RenderComments comments={this.props.comments}
/>
<CommentForm/>
</div>
</div>
<Modal isOpen={this.state.isCommentModalOpen} toggle={this.toggleCommentModal}>
<ModalHeader toggle={this.toggleCommentModal}>
Submit Comment </ModalHeader>
<ModalBody>
<div className="col-12">
<LocalForm onSubmit={(values)=>this.handleSubmit(this.props,values)}>
<Row className="form-group">
<Label htmlFor ="rating" md={2}>Rating</Label>
<Control.select model=".rating" id="rating" name="rating" className="form-control">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
</Control.select>
</Row>
<Row className="form-group">
<Label htmlFor ="name" md={2}>Name</Label>
<Control.text model=".name" className="form-control" id="name" name="name" placeholder="name" validators={{required,minLength: minLength(3),maxLength:maxLength(15)}} />
<Errors className="text-danger"
model=".name"
show="touched"
messages={{
required:'Required',
minLength:'Must be greater than 2 char',
maxLength: 'Must be 15 chars or less'
}}
/>
</Row>
<Row className="form-group">
<Label htmlFor ="feedback" md={2}>Comment</Label>
<Control.textarea model=".message" className="form-control" id="message" name="message" rows="12" />
</Row>
<Row className="form-group">
<Button type="submit" color="primary">
Submit
</Button>
</Row>
</LocalForm>
</div>
</ModalBody>
</Modal>
</div>
);
}else{
return(
<div></div>
);
}
}
}
export default DishDetail;
you are not dispatching to the reducer in the action. Do it like this
export const addComment = (dishId, rating, author, comment) => {
return (dispatch) => {
dispatch({
type: ActionTypes.ADD_COMMENT,
payload: {
dishId: dishId,
rating: rating,
author: author,
comment: comment
}
})
}
};

Resources