first commit

This commit is contained in:
ryanwong
2022-11-26 01:23:44 -05:00
commit 02843b95c9
2776 changed files with 102795 additions and 0 deletions
@@ -0,0 +1,28 @@
import React, { memo, ReactNode, useCallback } from 'react';
import { areEqual } from 'Utils/equalityChecks';
import { Button } from 'Components/Button';
interface Props {
children: ReactNode;
item: any;
disabled?: boolean;
onClick: (e: any) => void;
}
const ActionButtonWrapperContainer = ({ children, item, disabled, onClick }: Props) => {
const onItemClick = useCallback(() => onClick(item), [item]);
return (
<Button onClick={onItemClick} disabled={disabled}>
{children}
</Button>
);
};
ActionButtonWrapperContainer.defaultProps = {
disabled: false,
};
const ActionButtonWrapperContainerMemo = memo(ActionButtonWrapperContainer, areEqual);
export { ActionButtonWrapperContainerMemo as ActionButtonWrapperContainer };
@@ -0,0 +1 @@
export { ActionButtonWrapperContainer as ActionButtonWrapper } from './ActionButtonWrapper';
@@ -0,0 +1,104 @@
import React, { memo, useCallback, useEffect, useState } from 'react';
import { SingleUnitAdd } from 'Containers/SingleUnitAdd';
import { MultiUnitAdd } from 'Containers/MultiUnitAdd';
import { areEqual } from 'Utils/equalityChecks';
import { useDispatch, useSelector } from 'react-redux';
import { setUnitType } from 'Containers/Projects';
import { useHistory, useLocation } from 'react-router-dom';
import { ADD_LOCATIONS, MULTI_UNIT, PHOTO_MANAGEMENT, SINGLE } from 'Utils/constants';
import { MultiUnitAddRoom } from 'Containers/Project/AddLocations/MultiUnitAddRoom';
import { setUnit } from 'Containers/Project/Unit/actions';
import { multiUnitSelector, singleUnitSelector } from 'Containers/Project/Unit/selector';
import { projectMultiSelector, propertyMultiSelector, propertySingleSelector } from 'Containers/Project/selectors';
import { ChoosePropertyType } from './ChoosePropertyType';
import { setSelectedUnitTypeUrl } from '.';
const AddLocationTabContainer = () => {
const dispatch = useDispatch();
const location = useLocation();
const history = useHistory();
const { pathname } = location;
const { selectedProjectId, projectStore } = useSelector(projectMultiSelector, areEqual);
const singleUnit: any = useSelector(singleUnitSelector, areEqual);
const multiUnit: any = useSelector(multiUnitSelector, areEqual);
const property: any = useSelector(propertySingleSelector, areEqual);
const propertyMulti: any = useSelector(propertyMultiSelector, areEqual);
const [unitType] = useState({
singleUnit: 1,
multiUnit: 2,
});
const [singleUnitExists, setSingleUnitExists] = useState(false);
const [multiUnitExists, setMultiUnitExists] = useState(false);
// TODO::replace these with redux
useEffect(() => {
const { id: singlUnitId } = singleUnit;
const { id: multiUnitId } = multiUnit ?? '';
const { id: propertyId } = property;
const { id: propertyMultiId } = propertyMulti;
// single unit exist check
const exists = Number.isSafeInteger(singlUnitId) && Number.isSafeInteger(propertyId);
setSingleUnitExists(exists);
// Dispatch
if (exists) {
dispatch(setUnit(property, singleUnit, selectedProjectId));
}
// multi unit exist check
const existsMulti = Number.isSafeInteger(multiUnitId) && Number.isSafeInteger(propertyMultiId);
setMultiUnitExists(existsMulti);
}, [singleUnit, multiUnit, propertyMulti, property]);
const addSingleUnit = useCallback(() => {
// Creat a unit here.
// Create the property adn Unit, based on the selected location type
const projectAddress = projectStore.find((elem: any) => elem.id.toString() === selectedProjectId);
// we'll create single unit once, we'll skip if the user already has a single unit
if (!singleUnitExists) {
dispatch(setUnitType(selectedProjectId, projectAddress?.address?.address, unitType.singleUnit));
}
history.push(`/projects${PHOTO_MANAGEMENT}${ADD_LOCATIONS}${SINGLE}`);
dispatch(setSelectedUnitTypeUrl('singleUnit'));
}, [projectStore, selectedProjectId, singleUnitExists]);
const addMultiUnit = useCallback(() => {
history.push(`/projects${PHOTO_MANAGEMENT}${ADD_LOCATIONS}${MULTI_UNIT}`);
dispatch(setSelectedUnitTypeUrl('multiUnit'));
}, []);
const view = {
mainView: <ChoosePropertyType onSingleUnitTileClick={addSingleUnit} onMultiUnitTileClick={addMultiUnit} />,
singleUnitAddView: <SingleUnitAdd />,
multiUnitAddView: <MultiUnitAdd />,
multiUnitAddRoomView: <MultiUnitAddRoom />,
};
const [tabView, setTabView] = useState(view.mainView);
useEffect(() => {
if (pathname.includes(`/projects${PHOTO_MANAGEMENT}${ADD_LOCATIONS}${SINGLE}`)) {
setTabView(view.singleUnitAddView);
} else if (pathname.includes(`/projects${PHOTO_MANAGEMENT}${ADD_LOCATIONS}${MULTI_UNIT}/add`)) {
setTabView(view.multiUnitAddRoomView);
} else if (pathname.includes(`/projects${PHOTO_MANAGEMENT}${ADD_LOCATIONS}${MULTI_UNIT}`)) {
setTabView(view.multiUnitAddView);
} else if (!singleUnitExists && !multiUnitExists) {
setTabView(view.mainView);
}
}, [pathname]);
return tabView;
};
const AddLocationTabContainerMemo = memo(AddLocationTabContainer, areEqual);
export { AddLocationTabContainerMemo as AddLocationTab };
@@ -0,0 +1,26 @@
import React, { memo } from "react";
import { Icon } from "Components/Icons";
import { ImageTile } from "Containers/ImageTile";
import { areEqual } from "Utils/equalityChecks";
import classes from "./choosePropertyType.module.css";
interface Props {
onSingleUnitTileClick: () => void;
onMultiUnitTileClick: () => void;
}
const ChoosePropertyType = ({ onSingleUnitTileClick, onMultiUnitTileClick }: Props) => (
<div className={classes.addLocationsWrapper}>
<h1 className={classes.addLocationsTitle}>This Property Is...</h1>
<div className={`d-flex justify-content-center align-items-center ${classes.addLocationsContent}`}>
<ImageTile caption="Single Unit" onTileClick={onSingleUnitTileClick} icon={<Icon type="singleHome" />} />
<ImageTile caption="Multi Unit" onTileClick={onMultiUnitTileClick} icon={<Icon type="highrise" />} />
</div>
</div>
);
const ChoosePropertyTypeMemo = memo(ChoosePropertyType, areEqual);
export { ChoosePropertyTypeMemo as ChoosePropertyType };
@@ -0,0 +1,18 @@
.addLocationsWrapper {
margin-top: 16px;
padding-bottom: 421px;
}
.addLocationsTitle {
font-family: IBM Plex Sans;
font-style: normal;
font-weight: 700;
font-size: 32px;
line-height: 40px;
color: #0e1c4a;
margin-left: 30px;
}
.addLocationsContent {
margin-top: 115px;
}
@@ -0,0 +1 @@
export { ChoosePropertyType } from "./ChoosePropertyType";
@@ -0,0 +1,19 @@
// types
export const SET_UNIT_TYPE_URL = "SET_UNIT_TYPE_URL";
interface ActionTypes {
SET_UNIT_TYPE_URL: string;
}
interface MessageAction {
type: keyof ActionTypes;
payload: string;
}
// TODO::ADD THUNKS FOR DIFFERENT TYPES OF SPINNERS
export const setSelectedUnitTypeUrl = (payload: string) => ({
type: SET_UNIT_TYPE_URL,
payload,
});
export type SetUnitTypes = MessageAction;
@@ -0,0 +1,3 @@
export { AddLocationTab } from "./AddLocationTab";
export { addLocationsReducer } from "./reducer";
export { setSelectedUnitTypeUrl } from "./actions";
@@ -0,0 +1,37 @@
// types
import { SET_UNIT_TYPE_URL, SetUnitTypes } from './actions';
const routeBasePath: string = '/projects/photoManagement/';
const singleUnitTypeRoute: string = 'addLocations/single';
const multiUnitTypeRoute: string = 'addLocations/multiUnit/add';
const allLocationsRoute: string = 'allLocations';
let selectedRoute = `${routeBasePath}${allLocationsRoute}`;
// state
const initialState = {
selectedUnitTypeUrl: selectedRoute,
};
// we we'll use this for system variables, etc. loaders
export const addLocationsReducer = (state = initialState, action: SetUnitTypes) => {
switch (action.type) {
case SET_UNIT_TYPE_URL: {
const selectedPath: string = action.payload;
if (selectedPath === singleUnitTypeRoute) {
selectedRoute = `${routeBasePath}${singleUnitTypeRoute}`;
}
if (selectedPath === multiUnitTypeRoute) {
selectedRoute = `${routeBasePath}${multiUnitTypeRoute}`;
}
return {
...state,
selectedUnitTypeUrl: selectedRoute,
};
}
default:
return state;
}
};
@@ -0,0 +1 @@
export const selectedUnitTypeUrlSelector = ({ addLocations: { selectedUnitTypeUrl } }) => selectedUnitTypeUrl;
@@ -0,0 +1,154 @@
import React, { memo, useCallback, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { areEqual } from 'Utils/equalityChecks';
import { listCountries, setProvincesList } from 'Containers/Address/actions';
import { CountryModel } from 'Containers/Address/CountryAutocomplete/Models/CountryModel';
import { CountryAutocomplete } from 'Components/Address';
// set country value if passed from the parent (redux variable)
const getCountryLongName = (countriesList: Array<CountryModel>, value: string) =>
countriesList.find((country: CountryModel) => country.alpha_2.toLocaleLowerCase() === value.toLocaleLowerCase());
const countriesSelector = ({ address: { countries } }: any) => countries;
interface Props {
country: string;
setSelectedCountry: (e: any) => void;
invalid?: boolean;
showCaretIcon?: boolean;
}
const CountryAutocompleteContainer = ({ country, setSelectedCountry, invalid, showCaretIcon }: Props) => {
const dispatch = useDispatch();
const [countryLocal, setCountryLocal] = useState('');
const [countries, setCountries] = useState([]);
const [showDropDown, setShowDropDown] = useState(false);
// API data
const countriesList: Array<CountryModel> = useSelector(countriesSelector, areEqual);
useEffect(() => {
if (countriesList.length === 0) {
dispatch(listCountries());
}
}, []);
// we'll use this to prevent the if condition being running onSelectItem useCallback
const [clear, setClear] = useState(true);
useEffect(() => {
// if the country field sent, we'll set the local country state and provinces
// this happen when a user choose an address from google
if (country && countriesList.length > 0 && clear) {
let provinces = [];
const lowercase = country.toLocaleLowerCase();
// if the country selected from google autocomplete we do the following
if (country.length <= 3) {
setCountryLocal(getCountryLongName(countriesList, country)?.name);
const { provinces: provinceList } = countriesList.find(
(country: CountryModel) => country.alpha_2.toLocaleLowerCase() === lowercase
);
provinces = provinceList;
} else {
setCountryLocal(country);
provinces = countriesList.find(
(country: CountryModel) => country.name.toLocaleLowerCase() === lowercase
)?.provinces;
}
if (provinces) {
dispatch(setProvincesList(provinces));
}
}
}, [country, countriesList]);
useEffect(() => {
if (countriesList.length > 0) {
setCountries(countriesList);
}
}, [countriesList]);
const onChangeCountry = useCallback(
(e: any) => {
e.preventDefault();
setClear(false);
const { value } = e.target;
if (value.length > 0) {
const lowercase = value.toLocaleLowerCase();
const countries = countriesList.filter((country: CountryModel) =>
country.name.toLocaleLowerCase().includes(lowercase)
);
setCountries(countries);
setShowDropDown(countries.length > 0);
} else {
dispatch(setProvincesList([]));
}
setSelectedCountry(value);
setCountryLocal(value);
},
[countriesList]
);
const onSelectItem = useCallback(
(e: any) => {
e.preventDefault();
const { id = '', name = '' } = e.target.dataset;
setCountryLocal(name);
setSelectedCountry(name);
const { provinces } = countriesList.find((country: CountryModel) => country.id.toString() === id);
if (provinces) {
dispatch(setProvincesList(provinces));
}
setShowDropDown(false);
// set this to false, so the if condition in the first useEffect will fail
setClear(false);
},
[countriesList]
);
const onClickCaretIcon = useCallback(() => {
setShowDropDown((prevState) => !prevState);
}, []);
// we'll close the dropdown onBlur event outside the dropdown
// const onBlur = useCallback((e: any) => {
// e.preventDefault();
// setShowDropDown(false);
// }, []);
return (
<CountryAutocomplete
country={countryLocal}
onChangeCountry={onChangeCountry}
countries={countries}
onSelectItem={onSelectItem}
onClickCaretIcon={onClickCaretIcon}
showCaretIcon={showCaretIcon}
showDropDown={showDropDown}
invalid={invalid}
/>
);
};
CountryAutocompleteContainer.defaultProps = {
invalid: false,
showCaretIcon: false,
};
const CountryAutocompleteContainerMemo = memo(CountryAutocompleteContainer, areEqual);
export { CountryAutocompleteContainerMemo as CountryAutocompleteContainer };
@@ -0,0 +1,11 @@
/* eslint-disable */
import { ProvinceModel } from 'Containers/Address/ProvinceAutocomplete/Models/ProvinceModel';
export type CountryModel = {
id: number;
name: string;
alpha_2: string;
alpha_3: string;
provinces: Array<ProvinceModel>;
};
@@ -0,0 +1 @@
export type { CountryModel } from './CountryModel';
@@ -0,0 +1 @@
export { CountryAutocompleteContainer as CountryAutocomplete } from './CountryAutocomplete';
@@ -0,0 +1,172 @@
import React, { memo, useCallback, useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import { useHistory } from 'react-router-dom';
import usePlacesService from 'react-google-autocomplete/lib/usePlacesAutocompleteService';
import { areEqual } from 'Utils/equalityChecks';
import { setAddressRequest } from 'Containers/Address/actions';
import { GoogleAutocomplete } from 'Components/Address';
import { componentRestrictions } from 'Utils/google';
const googleAutoCompleteConfig = {
debounce: 700,
};
const GoogleAutocompleteContainer = () => {
const dispatch = useDispatch();
const history = useHistory();
const { placesService, placePredictions, getPlacePredictions } = usePlacesService(googleAutoCompleteConfig);
// local variables
const [address, setAddress]: any = useState('');
const [showDropDown, setShowDropDown]: any = useState(false);
const [placeDetails, setPlacesDetails]: any = useState('');
const [localPlacePredictions, setLocalPlacePredictions]: any = useState([]);
const [placeId, setPlaceId]: any = useState('');
// we'll use these objects to get accurate data from the google
const [autocompleteAddress, setAutocompleteAddress] = useState({
street_number: '',
route: '',
locality: '',
administrative_area_level_1: '',
country: '',
postal_code: '',
});
const [addressComponents] = useState({
street_number: 'short_name',
route: 'long_name',
locality: 'long_name',
administrative_area_level_1: 'long_name',
country: 'short_name',
postal_code: 'short_name',
});
useEffect(() => {
// prepare the local data object, add or modify any property as needed
if (placePredictions.length) {
setLocalPlacePredictions(
placePredictions.map((place: any) => ({
placeId: place.place_id,
mainText: place?.structured_formatting?.main_text,
secondaryText: place?.structured_formatting?.secondary_text,
}))
);
}
}, [placePredictions]);
useEffect(() => {
// show the dropdown once the data is prepared
setShowDropDown(localPlacePredictions.length > 0);
}, [localPlacePredictions]);
// on set placeId, we'll call google API to get place details
useEffect(() => {
if (placeId) {
placesService?.getDetails({ placeId }, (placeDetails) => {
setPlacesDetails(placeDetails);
});
}
}, [placeId]);
// this is where we set our local variables, redux variables
useEffect(() => {
if (placeDetails) {
setAddress(placeDetails?.formatted_address);
const address: any = {};
const components: any = placeDetails?.address_components || [];
components.forEach((component: any) => {
const [addressType = ''] = component?.types;
if (addressType.length > 0) {
if (addressComponents[addressType]) {
// set address objects with google data
address[addressType] = addressType.length > 0 ? component[addressComponents[addressType]] : '';
}
}
});
// update state
setAutocompleteAddress({
...autocompleteAddress,
...address,
});
}
}, [placeDetails]);
// here we'll update the redux and trigger the route change
useEffect(() => {
if (autocompleteAddress.country) {
dispatch(
setAddressRequest({
google_places_id: placeId,
country: autocompleteAddress.country,
state: autocompleteAddress.administrative_area_level_1,
city: autocompleteAddress.locality,
zip: autocompleteAddress.postal_code,
address: `${autocompleteAddress.street_number} ${autocompleteAddress.route}`,
latitude: placeDetails?.geometry?.location?.lat(),
longitude: placeDetails?.geometry?.location?.lng(),
})
);
setShowDropDown(false);
history.push('/projects/editAddress');
}
}, [autocompleteAddress]);
const onChangeAddress = useCallback((e: any) => {
const { value } = e.target;
// this will call the google API and get the predictions
getPlacePredictions({ input: value, componentRestrictions });
// to set local input value
setAddress(value);
}, []);
const onSelectItem = useCallback(
(e: any) => {
// set placeId
const tempPlaceId = e.target.dataset?.id;
if (tempPlaceId) {
setPlaceId(tempPlaceId);
} else {
// sometimes id not present in the target element, so we'll get the id from the parent(Anchor) tag
// this happen when they click on the Span tag
setPlaceId(e.target.parentElement.dataset.id);
}
// close the dropdown
setShowDropDown(false);
},
[localPlacePredictions]
);
// close the dropdown when click outside the dropdown
const onblur = useCallback(() => {
setShowDropDown(false);
}, []);
return (
<GoogleAutocomplete
address={address}
onChangeAddress={onChangeAddress}
placePredictions={localPlacePredictions}
onSelectItem={onSelectItem}
showDropDown={showDropDown}
onblur={onblur}
/>
);
};
const GoogleAutocompleteContainerMemo = memo(GoogleAutocompleteContainer, areEqual);
export { GoogleAutocompleteContainerMemo as GoogleAutocompleteContainer };
@@ -0,0 +1 @@
export { GoogleAutocompleteContainer as GoogleAutocomplete } from "./GoogleAutocomplete";
@@ -0,0 +1,7 @@
/* eslint-disable */
export type ProvinceModel = {
id: number;
name: string;
alpha_2: string;
};
@@ -0,0 +1 @@
export type { ProvinceModel } from './ProvinceModel';
@@ -0,0 +1,99 @@
import React, { memo, useCallback, useEffect, useState } from 'react';
import { areEqual } from 'Utils/equalityChecks';
import { ProvinceAutocomplete } from 'Components/Address';
import { useSelector } from 'react-redux';
interface Props {
state: string;
setSelectedProvince: (e: any) => void;
invalid?: boolean;
showCaretIcon?: boolean;
}
const provincesSelector = ({ address: { provinces } }: any) => provinces;
const ProvinceAutocompleteContainer = ({ state, setSelectedProvince, invalid, showCaretIcon }: Props) => {
const [province, setProvince] = useState('');
const [provinces, setProvinces] = useState([]);
const [showDropDown, setShowDropDown] = useState(false);
// API data
const provincesList = useSelector(provincesSelector, areEqual);
// set province value if passed from the parent (redux variable)
useEffect(() => {
setProvince(state);
}, [state]);
useEffect(() => {
if (provincesList.length > 0) {
setProvinces(provincesList);
}
}, [provincesList]);
const onChangeProvince = useCallback(
(e: any) => {
e.preventDefault();
const { value } = e.target;
const lowercase = value.toLocaleLowerCase();
setProvinces(
value.length > 0
? provincesList.filter((province: any) => province.name.toLocaleLowerCase().includes(lowercase))
: []
);
setProvince(value);
setSelectedProvince(value);
setShowDropDown(value.length > 0);
},
[provincesList]
);
const onSelectItem = useCallback((e: any) => {
e.preventDefault();
const { name = '' } = e.target.dataset;
setProvince(name);
setSelectedProvince(name);
setShowDropDown(false);
}, []);
// useEffect(() => {
// setShowDropDown(province.length > 0 && provinces.length > 0);
// }, [provinces]);
const onClickCaretIcon = useCallback(() => {
setShowDropDown((prevState) => !prevState);
}, []);
// const onBlur = useCallback((e: any) => {
// e.preventDefault();
// setShowDropDown(false);
// }, []);
return (
<ProvinceAutocomplete
province={province}
onChangeProvince={onChangeProvince}
provinces={provinces}
onSelectItem={onSelectItem}
showDropDown={showDropDown}
invalid={invalid}
showCaretIcon={showCaretIcon}
onClickCaretIcon={onClickCaretIcon}
/>
);
};
ProvinceAutocompleteContainer.defaultProps = {
invalid: false,
showCaretIcon: false,
};
const ProvinceAutocompleteContainerMemo = memo(ProvinceAutocompleteContainer, areEqual);
export { ProvinceAutocompleteContainerMemo as ProvinceAutocompleteContainer };
@@ -0,0 +1 @@
export { ProvinceAutocompleteContainer as ProvinceAutocomplete } from './ProvinceAutocomplete';
+112
View File
@@ -0,0 +1,112 @@
/* eslint-disable */
import { handleApiRequest } from 'Utils/handleApiRequest';
// types
import { CountryModel } from 'Containers/Address/CountryAutocomplete/Models/CountryModel';
import { ProvinceModel } from 'Containers/Address/ProvinceAutocomplete/Models/ProvinceModel';
import { userDetails } from 'Containers/User';
import { projectsCreate } from 'Containers/Projects/actions';
import { setProjectAddressObject, setSimpleProjectAddress } from 'Containers/RocketScan/actions';
export const SET_COUNTRIES = 'SET_COUNTRIES';
export const SET_PROVINCES = 'SET_PROVINCES';
export const SET_ADDRESS_REQUEST = 'SET_ADDRESS_REQUEST';
export const RESET_ADDRESS_REQUEST = 'RESET_ADDRESS_REQUEST';
interface ActionTypes {
SET_COUNTRIES: Array<CountryModel> | null;
SET_PROVINCES: Array<ProvinceModel> | null;
SET_ADDRESS_REQUEST: any;
RESET_ADDRESS_REQUEST: any;
}
interface MessageAction {
type: keyof ActionTypes;
payload: any;
}
export type SetAddressTypes = MessageAction;
export const listCountries =
(url = 'countries', type = 'get', requestData = {}) =>
async (dispatch: any, _getState = null, utils: any) => {
const response = await handleApiRequest(dispatch, utils.Api[type](url, requestData));
if (response?.data) {
const { data } = response;
dispatch({
type: SET_COUNTRIES,
payload: data,
});
}
};
// create address for project
export const addressCreate =
(requestData = {}, companyId: number, projectStatusId = '', url = 'addresses', type = 'post') =>
async (dispatch: any, _getState = null, utils: any) => {
const response = await handleApiRequest(dispatch, utils.Api[type](url, requestData));
if (response?.data) {
const { data } = response;
dispatch(
projectsCreate(
{
company_id: companyId,
project_status_id: projectStatusId,
address_id: data.id,
},
`companies/${companyId}/projects`
)
);
}
};
// update address
export const addressUpdate =
(addressId: number, requestData = {}, onAddressUpdated?: any) =>
async (dispatch: any, _getState = null, utils: any) => {
const response = await handleApiRequest(dispatch, utils.Api.put(`addresses/${addressId}`, requestData));
if (response?.data) {
const { data } = response;
dispatch(setProjectAddressObject(data));
dispatch(setSimpleProjectAddress(data.address));
}
if (onAddressUpdated) {
onAddressUpdated();
}
};
/*
* Non API thunks
* */
export const setProvincesList =
(data = []) =>
async (dispatch) => {
dispatch({
type: SET_PROVINCES,
payload: data,
});
};
export const setAddressRequest =
(data = {}) =>
async (dispatch) => {
dispatch({
type: SET_ADDRESS_REQUEST,
payload: data,
});
};
export const resetAddressRequest = () => async (dispatch) => {
dispatch({
type: RESET_ADDRESS_REQUEST,
});
};
+3
View File
@@ -0,0 +1,3 @@
export { CountryAutocomplete } from "./CountryAutocomplete";
export { ProvinceAutocomplete } from "./ProvinceAutocomplete";
export { GoogleAutocomplete } from "./GoogleAutocomplete";
+51
View File
@@ -0,0 +1,51 @@
// state
import {
SET_ADDRESS_REQUEST,
RESET_ADDRESS_REQUEST,
SET_COUNTRIES,
SET_PROVINCES,
SetAddressTypes,
} from "Containers/Address/actions";
const initialState = {
countries: [],
provinces: [],
addressRequest: {
google_places_id: "",
address: "",
addressTwo: "",
city: "",
state: "",
zip: "",
country: "",
latitude: "",
longitude: "",
},
};
export const addressReducer = (state = initialState, action: SetAddressTypes) => {
switch (action.type) {
case SET_COUNTRIES:
return {
...state,
countries: action.payload,
};
case SET_PROVINCES:
return {
...state,
provinces: action.payload,
};
case SET_ADDRESS_REQUEST:
return {
...state,
addressRequest: { ...state.addressRequest, ...action.payload },
};
case RESET_ADDRESS_REQUEST:
return {
...state,
addressRequest: initialState.addressRequest,
};
default:
return state;
}
};
@@ -0,0 +1,15 @@
import React, { memo, ReactNode } from 'react';
import { areEqual } from 'Utils/equalityChecks';
import { GuestLayout } from 'Components/Layouts';
interface Props {
children: ReactNode;
}
const GuestWrapperContainer = ({ children }: Props) => <GuestLayout>{children}</GuestLayout>;
const GuestWrapperContainerMemo = memo(GuestWrapperContainer, areEqual);
export { GuestWrapperContainerMemo as GuestWrapperContainer };
@@ -0,0 +1 @@
export { GuestWrapperContainer as GuestWrapper } from "./GuestWrapper";
+145
View File
@@ -0,0 +1,145 @@
/* eslint-disable */
// types
import { SMS_SENT } from 'Containers/SignIn/PhoneVerification/actions';
export const SET_AUTH_TOKEN = 'SET_AUTH_TOKEN';
export const SET_AUTHENTICATED = 'SET_AUTHENTICATED';
export const SET_AUTHENTICATION_TYPE = 'SET_AUTHENTICATION_TYPE';
export const SOCIAL_LOGIN_ERRORS = 'SOCIAL_LOGIN_ERRORS';
export const SET_RESET_AUTH = 'SET_RESET_AUTH';
interface ActionTypes {
SET_AUTH_TOKEN: string;
SET_AUTHENTICATED: boolean;
SET_AUTHENTICATION_TYPE: string;
SOCIAL_LOGIN_ERRORS: object;
SET_RESET_AUTH: string;
}
interface MessageAction {
type: keyof ActionTypes;
payload: any;
}
export type SetAuthTypes = MessageAction;
import { userDetails } from 'Containers/User';
// API
import { handleApiRequest } from 'Utils/handleApiRequest';
import { CODE_VERIFIED } from 'Containers/SignIn/PhoneVerificationCode';
import { createPhoneRecord, SET_USER_STATUS, SET_USER_VERIFICATION } from 'Containers/User/actions';
export const login =
(url: string, type = 'post', requestData = {}) =>
async (dispatch: any, _getState = null, utils: any) => {
const data = await handleApiRequest(dispatch, utils.Api[type](url, requestData));
if (data) {
const { token } = data;
// update the axios headers
utils.Api.setAuthorizationToken(token);
// we'll fetch user details on a successful sign in and set redux variable for route changes
dispatch(userDetails());
// set the redux state with the token --sanctum auth bearer token
dispatch({
type: SET_AUTH_TOKEN,
payload: token,
});
}
};
export const register =
(url = 'auth/register', type = 'post', requestData = {}) =>
async (dispatch: any, _getState = null, utils: any) => {
const data = await handleApiRequest(dispatch, utils.Api[type](url, requestData));
if (data) {
const { token } = data;
// update the axios headers
utils.Api.setAuthorizationToken(token);
dispatch(userDetails());
// set the redux state with the token --sanctum auth bearer token
dispatch({
type: SET_AUTH_TOKEN,
payload: token,
});
// set the redux state with the authentication status
dispatch({
type: SET_AUTHENTICATED,
payload: true,
});
}
};
export const smsSendVerification =
(url = 'auth/sms-send-verification', type = 'post', requestData: any, userID: number) =>
async (dispatch: any, _getState = null, utils: any) => {
const data = await handleApiRequest(dispatch, utils.Api[type](url, requestData));
if (typeof data === 'string') {
const { phone } = requestData;
// create a phone record so we can attach it to the user
dispatch(
createPhoneRecord(
'phones',
'post',
{
value: phone,
extension: '',
is_primary: true,
type: 'mobile',
},
userID
)
);
dispatch({
type: SMS_SENT,
payload: true,
});
}
};
export const smsVerifyCode =
(url = 'auth/sms-verify-code', type = 'post', requestData = {}) =>
async (dispatch: any, _getState = null, utils: any) => {
const data = await handleApiRequest(dispatch, utils.Api[type](url, requestData));
if (typeof data === 'string') {
dispatch({
type: CODE_VERIFIED,
payload: true,
});
}
};
export const logout =
(url = 'auth/logout') =>
async (dispatch, _getState = null, utils) => {
// clear the axios headers
utils.Api.resetAuthorizationToken();
await utils.Api.delete(url, {});
// clear auth data
dispatch({
type: SET_RESET_AUTH,
});
// clear user data, important for route changes
dispatch({
type: SET_USER_VERIFICATION,
payload: {
authenticated: false,
sms: false,
company: false,
approved: false,
isNew: false,
},
});
};
+3
View File
@@ -0,0 +1,3 @@
export { GuestWrapper } from "./GuestWrapper";
export { login, logout } from "./actions";
export { AuthReducer } from "./reducer";
+52
View File
@@ -0,0 +1,52 @@
// types
import {
SET_AUTH_TOKEN,
SET_AUTHENTICATED,
SET_AUTHENTICATION_TYPE,
SET_RESET_AUTH,
SetAuthTypes,
SOCIAL_LOGIN_ERRORS,
} from './actions';
// state
const initialState = {
token: '',
authenticated: false,
authenticationType: 'basic',
socialLoginErrors: {
errors: false,
message: '',
},
};
export const AuthReducer = (state = initialState, action: SetAuthTypes) => {
switch (action.type) {
case SET_AUTH_TOKEN:
return {
...state,
token: action.payload,
};
case SET_AUTHENTICATED:
return {
...state,
authenticated: action.payload,
};
case SET_AUTHENTICATION_TYPE:
return {
...state,
authenticationType: action.payload,
};
case SOCIAL_LOGIN_ERRORS:
return {
...state,
socialLoginErrors: {
...state.socialLoginErrors,
...action.payload,
},
};
case SET_RESET_AUTH:
return initialState;
default:
return state;
}
};
+1
View File
@@ -0,0 +1 @@
export const authenticatedSelector = ({ auth: { authenticated: value = false } }: any) => value;
@@ -0,0 +1,58 @@
import React, { memo, useEffect, useRef } from 'react';
import { areEqual } from 'Utils/equalityChecks';
import { TextBox } from 'Components/TextBox';
import formClasses from 'Themes/form/form.module.css';
interface Props {
name: string;
ariaLabel: string;
placeholder: string;
value: string;
autoFocus: boolean;
className?: string;
isValid: boolean;
onValueChange: (e: any) => void;
}
const AutoFocusInputContainer = ({
name,
ariaLabel,
placeholder,
value,
autoFocus,
className,
isValid,
onValueChange,
}: Props) => {
const ref = useRef(null);
useEffect(() => {
if (autoFocus) {
ref.current.focus();
}
}, [autoFocus]);
return (
<TextBox
ref={ref}
type="text"
className={`mb-0 pb-0 ${formClasses.validateField} ${
isValid ? formClasses.invalidField : formClasses.validField
} ${isValid ? 'is-invalid' : ''} ${className || ''}`}
placeholder={placeholder}
ariaLabel={ariaLabel}
name={name}
value={value}
onChange={onValueChange}
/>
);
};
AutoFocusInputContainer.defaultProps = {
className: undefined,
};
const AutoFocusInputContainerMemo = memo(AutoFocusInputContainer, areEqual);
export { AutoFocusInputContainerMemo as AutoFocusInputContainer };
@@ -0,0 +1 @@
export { AutoFocusInputContainer as AutoFocusInput } from './AutoFocusInput';
@@ -0,0 +1,59 @@
import React, { memo, useEffect, useRef } from 'react';
import { areEqual } from 'Utils/equalityChecks';
import formClasses from 'Themes/form/form.module.css';
import { TextArea } from 'Components/TextArea';
interface Props {
name: string;
ariaLabel: string;
placeholder: string;
value: string;
autoFocus: boolean;
className?: string;
isValid: boolean;
onValueChange: (e: any) => void;
}
const AutoFocusTextAreaContainer = ({
name,
ariaLabel,
placeholder,
value,
autoFocus,
className,
isValid,
onValueChange,
}: Props) => {
const ref = useRef(null);
useEffect(() => {
if (autoFocus) {
ref.current.focus();
}
}, [autoFocus]);
return (
<TextArea
ref={ref}
minRows={1}
maxRows={6}
className={`mb-0 pb-0 ${formClasses.validateField} ${
isValid ? formClasses.invalidField : formClasses.validField
} ${isValid ? 'is-invalid' : ''} ${className || ''}`}
placeholder={placeholder}
ariaLabel={ariaLabel}
name={name}
value={value}
onChange={onValueChange}
/>
);
};
AutoFocusTextAreaContainer.defaultProps = {
className: undefined,
};
const AutoFocusTextAreaContainerMemo = memo(AutoFocusTextAreaContainer, areEqual);
export { AutoFocusTextAreaContainerMemo as AutoFocusTextAreaContainer };
@@ -0,0 +1 @@
export { AutoFocusTextAreaContainer as AutoFocusTextArea } from './AutoFocusTextArea';
@@ -0,0 +1,17 @@
import React, { memo, useState, useCallback } from 'react';
import { areEqual } from 'Utils/equalityChecks';
import { ProjectScreenButton } from 'Components/Button';
const ProjectScreenButtonContainer = () => {
const [isModalOpen, setIsModalOpen] = useState(false);
const setModalStatus = useCallback(() => {
setIsModalOpen((isModalOpen) => !isModalOpen);
}, []);
return <ProjectScreenButton onClick={setModalStatus} isModalOpen={isModalOpen} setModalStatus={setModalStatus} />;
};
const ProjectScreenButtonContainerMemo = memo(ProjectScreenButtonContainer, areEqual);
export { ProjectScreenButtonContainerMemo as ProjectScreenButton };
@@ -0,0 +1 @@
export { ProjectScreenButton } from './ProjectScreenButton';
+247
View File
@@ -0,0 +1,247 @@
import { handleApiRequest } from 'Utils/handleApiRequest';
export const COMPANY_UPDATED = 'COMPANY_UPDATED';
export const COMPANY_PHONES = 'COMPANY_PHONES';
export const COMPANY_PHONE = 'COMPANY_PHONE';
export const COMPANY_PHONE_CREATED = 'COMPANY_PHONE_CREATED';
export const COMPANY_PHONE_UPDATED = 'COMPANY_PHONE_UPDATED';
export const COMPANY_ADDRESS = 'COMPANY_ADDRESS';
export const COMPANY_ADDRESS_UPDATED = 'COMPANY_ADDRESS_UPDATED';
export const COMPANY_ADDRESS_CREATED = 'COMPANY_ADDRESS_CREATED';
export const COMPANY_UPDATE_ERRORS = 'COMPANY_UPDATE_ERRORS';
export const COMPANY_PHONE_UPDATE_ERRORS = 'COMPANY_PHONE_UPDATE_ERRORS';
export const COMPANY_ADDRESS_UPDATE_ERRORS = 'COMPANY_ADDRESS_UPDATE_ERRORS';
export const SET_LOGO_UPLOADING = 'SET_LOGO_UPLOADING';
export const REFRESH_LOGO = 'REFRESH_LOGO';
export const COMPANY_PHOTO_CATEGORIES = 'COMPANY_PHOTO_CATEGORIES';
export const FETCHING_COMPANY_PHOTO_CATEGORIES = 'FETCHING_COMPANY_PHOTO_CATEGORIES';
export const COMPANY_PHOTO_CATEGORIES_UPDATED = 'COMPANY_PHOTO_CATEGORIES_UPDATED';
interface ActionTypes {
COMPANY_UPDATED: boolean;
COMPANY_PHONES: any;
COMPANY_PHONE: any;
COMPANY_PHONE_CREATED: boolean;
COMPANY_PHONE_UPDATED: boolean;
COMPANY_ADDRESS: any;
COMPANY_ADDRESS_CREATED: boolean;
COMPANY_ADDRESS_UPDATED: boolean;
COMPANY_UPDATE_ERRORS: any;
COMPANY_PHONE_UPDATE_ERRORS: any;
COMPANY_ADDRESS_UPDATE_ERRORS: any;
SET_LOGO_UPLOADING: boolean;
REFRESH_LOGO: boolean;
COMPANY_PHOTO_CATEGORIES: any[];
COMPANY_PHOTO_CATEGORIES_UPDATED: boolean;
FETCHING_COMPANY_PHOTO_CATEGORIES: boolean;
}
interface MessageAction {
type: keyof ActionTypes;
payload: any;
}
export type SetCompanyTypes = MessageAction;
export const setCompanyUpdated = (value: boolean) => (dispatch) => {
dispatch({
type: COMPANY_UPDATED,
payload: value,
});
};
export const setCompanyPhone = (value: any) => (dispatch) => {
dispatch({
type: COMPANY_PHONE,
payload: value,
});
};
export const setCompanyPhoneCreated = (value: boolean) => (dispatch) => {
dispatch({
type: COMPANY_PHONE_CREATED,
payload: value,
});
};
export const setCompanyPhoneUpdated = (value: boolean) => (dispatch) => {
dispatch({
type: COMPANY_PHONE_UPDATED,
payload: value,
});
};
export const setCompanyAddress = (value: any) => (dispatch) => {
dispatch({
type: COMPANY_ADDRESS,
payload: value,
});
};
export const setCompanyAddressCreated = (value: boolean) => (dispatch) => {
dispatch({
type: COMPANY_ADDRESS_CREATED,
payload: value,
});
};
export const setCompanyAddressUpdated = (value: boolean) => (dispatch) => {
dispatch({
type: COMPANY_ADDRESS_UPDATED,
payload: value,
});
};
export const setLogoUploading = (payload: any) => (dispatch) => {
dispatch({
type: SET_LOGO_UPLOADING,
payload,
});
};
export const resetLogoUploading = (payload: any) => (dispatch) => {
dispatch({
type: SET_LOGO_UPLOADING,
payload,
});
};
export const setCompanyPhotoCategoriesUpdated = (payload: boolean) => (dispatch) => {
dispatch({
type: COMPANY_PHOTO_CATEGORIES_UPDATED,
payload,
});
};
/* eslint-disable */
export const getCompanyPhones =
(companyId: number) =>
async (dispatch: any, _getState = null, utils: any) => {
const response = await handleApiRequest(dispatch, utils.Api.get(`companies/${companyId}/phones`));
if (response?.data) {
const { data } = response;
dispatch({
type: COMPANY_PHONES,
payload: data,
});
}
};
export const createCompanyPhone =
(companyId: number, requestData) =>
async (dispatch: any, _getState = null, utils: any) => {
const response = await handleApiRequest(
dispatch,
utils.Api.post(`companies/${companyId}/phones`, requestData),
COMPANY_PHONE_UPDATE_ERRORS
);
if (response?.data) {
const { data } = response;
dispatch(setCompanyPhone(data));
dispatch(setCompanyPhoneCreated(true));
}
};
export const updateCompanyPhone =
(phoneId: number, requestData = {}) =>
async (dispatch: any, _getState = null, utils: any) => {
const response = await handleApiRequest(
dispatch,
utils.Api.put(`phones/${phoneId}`, requestData),
COMPANY_PHONE_UPDATE_ERRORS
);
if (response?.data) {
const { data } = response;
dispatch(setCompanyPhone(data));
dispatch(setCompanyPhoneUpdated(true));
}
};
export const createCompanyAddress =
(companyId: number, requestData) =>
async (dispatch: any, _getState = null, utils: any) => {
const response = await handleApiRequest(
dispatch,
utils.Api.post(`companies/${companyId}/addresses`, requestData),
COMPANY_ADDRESS_UPDATE_ERRORS
);
if (response?.data) {
const { data } = response;
dispatch(setCompanyAddress(data));
dispatch(setCompanyAddressCreated(true));
}
};
export const updateCompanyAddress =
(addressId: number, requestData = {}) =>
async (dispatch: any, _getState = null, utils: any) => {
const response = await handleApiRequest(
dispatch,
utils.Api.put(`addresses/${addressId}`, requestData),
COMPANY_ADDRESS_UPDATE_ERRORS
);
if (response?.data) {
const { data } = response;
dispatch(setCompanyAddress(data));
dispatch(setCompanyAddressUpdated(true));
}
};
export const updateCompanyDetails =
(companyId: number, requestData = {}) =>
async (dispatch: any, _getState = null, utils: any) => {
const response = await handleApiRequest(
dispatch,
utils.Api.put(`companies/${companyId}`, requestData),
COMPANY_UPDATE_ERRORS
);
if (response?.data) {
if (requestData) {
dispatch(setCompanyUpdated(true));
}
}
};
export const getCompanyPhotoCategories =
(companyId: number) =>
async (dispatch: any, _getState = null, utils: any) => {
const response = await handleApiRequest(
dispatch,
utils.Api.get(`companies/${companyId}/albums`),
'',
FETCHING_COMPANY_PHOTO_CATEGORIES
);
if (response?.data) {
const { data } = response;
dispatch({
type: COMPANY_PHOTO_CATEGORIES,
payload: data,
});
}
};
export const updateCompanyPhotoCategories =
(categories: any[], setFetching: any) =>
async (dispatch: any, _getState = null, utils: any) => {
setFetching(true);
return await Promise.all(
categories.map((category) => handleApiRequest(dispatch, utils.Api.put(`albums/${category.id}`, category)))
)
.then(() => {
dispatch(setCompanyPhotoCategoriesUpdated(true));
setFetching(false);
return true;
})
.catch(() => {
setFetching(false);
return false;
});
};
+1
View File
@@ -0,0 +1 @@
export { companyReducer } from './reducers';
+128
View File
@@ -0,0 +1,128 @@
// types
import {
SetCompanyTypes,
COMPANY_UPDATED,
COMPANY_PHONES,
COMPANY_PHONE,
COMPANY_PHONE_UPDATED,
COMPANY_PHONE_CREATED,
COMPANY_ADDRESS,
COMPANY_ADDRESS_CREATED,
COMPANY_ADDRESS_UPDATED,
COMPANY_UPDATE_ERRORS,
COMPANY_PHONE_UPDATE_ERRORS,
COMPANY_ADDRESS_UPDATE_ERRORS,
SET_LOGO_UPLOADING,
REFRESH_LOGO,
COMPANY_PHOTO_CATEGORIES,
COMPANY_PHOTO_CATEGORIES_UPDATED,
FETCHING_COMPANY_PHOTO_CATEGORIES,
} from 'Containers/Company/actions';
// state
const initialState = {
companyUpdated: false,
companyPhones: [],
companyPhone: undefined,
companyPhoneCreated: false,
companyPhoneUpdated: false,
companyAddress: undefined,
companyAddressUpdated: false,
companyAddressCreated: false,
companyUpdateErrors: undefined,
companyPhoneUpdateErrors: undefined,
companyAddressUpdateErrors: undefined,
logoUploading: false,
refreshAvatar: false,
fetchingPhotoCategories: false,
photoCategories: [],
photoCategoriesUpdated: false,
};
export const companyReducer = (state = initialState, action: SetCompanyTypes) => {
const { type, payload } = action;
switch (type) {
case COMPANY_UPDATED:
return {
...state,
companyUpdated: payload,
};
case COMPANY_PHONES:
return {
...state,
companyPhones: payload,
};
case COMPANY_PHONE:
return {
...state,
companyPhone: payload,
};
case COMPANY_PHONE_UPDATED:
return {
...state,
companyPhoneUpdated: payload,
};
case COMPANY_PHONE_CREATED:
return {
...state,
companyPhoneCreated: payload,
};
case COMPANY_ADDRESS:
return {
...state,
companyAddress: payload,
};
case COMPANY_ADDRESS_UPDATED:
return {
...state,
companyAddressUpdated: payload,
};
case COMPANY_ADDRESS_CREATED:
return {
...state,
companyAddressCreated: payload,
};
case COMPANY_UPDATE_ERRORS:
return {
...state,
companyUpdateErrors: payload,
};
case COMPANY_PHONE_UPDATE_ERRORS:
return {
...state,
companyPhoneUpdateErrors: payload,
};
case COMPANY_ADDRESS_UPDATE_ERRORS:
return {
...state,
companyAddressUpdateErrors: payload,
};
case SET_LOGO_UPLOADING:
return {
...state,
logoUploading: payload,
};
case REFRESH_LOGO:
return {
...state,
refreshLogo: payload,
};
case FETCHING_COMPANY_PHOTO_CATEGORIES:
return {
...state,
fetchingPhotoCategories: payload,
};
case COMPANY_PHOTO_CATEGORIES:
return {
...state,
photoCategories: payload,
};
case COMPANY_PHOTO_CATEGORIES_UPDATED:
return {
...state,
photoCategoriesUpdated: payload,
};
default:
return state;
}
};
@@ -0,0 +1,32 @@
export const companyPhonesSelector = ({ company: { companyPhones: value = [] } }: any) => value;
export const companyPhoneCreatedSelector = ({ company: { companyPhoneCreated: value = false } }: any) => value;
export const companyAddressCreatedSelector = ({ company: { companyAddressCreated: value = false } }: any) => value;
export const companyUpdatedSelector = ({ company: { companyUpdated: value = false } }: any) => value;
export const companyAddressUpdatedSelector = ({ company: { companyAddressUpdated: value = false } }: any) => value;
export const companyPhoneUpdatedSelector = ({ company: { companyPhoneUpdated: value = false } }: any) => value;
export const companyLogoUploading = ({ company: { logoUploading: value = false } }) => value;
export const fetchingPhotoCategoriesSelector = ({ company: { fetchingPhotoCategories: value = true } }) => value;
export const photoCategoriesSelector = ({ company: { photoCategories: value = [] } }) => value;
export const photoCategoriesUpdatedSelector = ({ company: { photoCategoriesUpdated: value = false } }) => value;
// form errors
export const companyNameErrorSelector = ({ company: { companyUpdateErrors } }: any) => companyUpdateErrors?.name || [];
export const companyPhoneErrorSelector = ({ company: { companyPhoneUpdateErrors } }: any) =>
companyPhoneUpdateErrors?.value || [];
export const companyWebsiteErrorSelector = ({ company: { companyUpdateErrors } }: any) =>
companyUpdateErrors?.website || [];
export const companyAddressErrorSelector = ({ company: { companyAddressUpdateErrors } }: any) =>
companyAddressUpdateErrors?.address || [];
export const companyAddressSecondErrorSelector = ({ company: { companyAddressUpdateErrors } }: any) =>
companyAddressUpdateErrors?.address_2 || [];
export const companyCountryErrorSelector = ({ company: { companyAddressUpdateErrors } }: any) =>
companyAddressUpdateErrors?.country || [];
export const companyStateErrorSelector = ({ company: { companyAddressUpdateErrors } }: any) =>
companyAddressUpdateErrors?.state || [];
export const companyCityErrorSelector = ({ company: { companyAddressUpdateErrors } }: any) =>
companyAddressUpdateErrors?.city || [];
export const companyCodeErrorSelector = ({ company: { companyAddressUpdateErrors } }: any) =>
companyAddressUpdateErrors?.zip || [];
+86
View File
@@ -0,0 +1,86 @@
// types
export const SET_FETCHING = 'SET_FETCHING';
export const SET_REDIRECT_PATH = 'SET_REDIRECT_PATH';
export const SET_TOASTER = 'SET_TOASTER';
export const SIDE_BAR = 'SIDE_BAR';
export const FORM_ERRORS = 'FORM_ERRORS';
export const RESET_TOAST = 'RESET_TOAST';
export const APP_INITIAL_LOADING = 'APP_INITIAL_LOADING';
export const SET_PUSHER = 'SET_PUSHER';
interface ActionTypes {
SET_FETCHING: boolean;
SET_REDIRECT_PATH: string;
SET_TOASTER: object;
SIDE_BAR: boolean;
FORM_ERRORS: any;
RESET_TOAST: undefined;
APP_INITIAL_LOADING: boolean;
SET_PUSHER: any;
}
interface MessageAction {
type: keyof ActionTypes;
payload: any;
}
export type SetCoreTypes = MessageAction;
export const setAppInitialLoading = (payload: any) => (dispatch) => {
dispatch({
type: APP_INITIAL_LOADING,
payload,
});
};
export const setAppRedirectPathLocal = (value: string) => localStorage.setItem('appRedirectPath', value);
export const getAppRedirectPathLocal = () => localStorage.getItem('appRedirectPath');
export const removeAppRedirectPathLocal = () => localStorage.removeItem('appRedirectPath');
// TODO::ADD THUNKS FOR DIFFERENT TYPES OF SPINNERS
export const setFetching =
(payload: boolean, type = 'SET_FETCHING') =>
(dispatch: any) => {
dispatch({
type,
payload,
});
};
export const setToaster =
(message: string, success = true, timeout = 3000) =>
(dispatch: any) => {
dispatch({
type: SET_TOASTER,
payload: {
show: true,
message,
success,
timeout,
},
});
};
export const resetToaster = () => async (dispatch: any) => {
dispatch({
type: RESET_TOAST,
});
};
export const setFormErrors =
(errors: any, type = 'FORM_ERRORS') =>
(dispatch: any) => {
dispatch({
type: type || FORM_ERRORS,
payload: errors,
});
};
export const setPusher = (payload: any) => (dispatch) => {
dispatch({
type: SET_PUSHER,
payload,
});
};
+2
View File
@@ -0,0 +1,2 @@
export { CoreReducer } from "./reducer";
export { setToaster } from "./actions";
+85
View File
@@ -0,0 +1,85 @@
// types
import {
SET_FETCHING,
SET_REDIRECT_PATH,
SET_TOASTER,
SIDE_BAR,
FORM_ERRORS,
RESET_TOAST,
SetCoreTypes,
APP_INITIAL_LOADING,
SET_PUSHER,
} from 'Containers/Core/actions';
// state
const initialState = {
fetching: false,
redirectPath: '/',
toast: {
show: false,
message: '',
icon: false,
success: true,
timeout: 3000,
},
sideBar: false,
formErrors: {},
appInitialLoading: true,
pusher: undefined,
};
// we we'll use this for system variables, etc. loaders
export const CoreReducer = (state = initialState, action: SetCoreTypes) => {
const { type, payload } = action;
switch (type) {
case SET_FETCHING:
return {
...state,
fetching: payload,
};
case SET_REDIRECT_PATH:
return {
...state,
redirectPath: payload,
};
case SET_TOASTER:
return {
...state,
toast: {
...state.toast,
...payload,
},
};
case SIDE_BAR:
return {
...state,
sideBar: payload,
};
case FORM_ERRORS:
return {
...state,
formErrors:
typeof payload === 'object'
? {
...payload,
}
: payload,
};
case APP_INITIAL_LOADING:
return {
...state,
appInitialLoading: payload,
};
case SET_PUSHER:
return {
...state,
pusher: payload,
};
case RESET_TOAST: {
return initialState;
}
default:
return state;
}
};
+4
View File
@@ -0,0 +1,4 @@
export const appInitialLoadingSelector = ({ core: { appInitialLoading: value = true } }: any) => value;
export const appRedirectPathSelector = ({ core: { appRedirectPath: value } }: any) => value;
export const coreFetchingSelector = ({ core: { fetching = false } }: any) => fetching;
export const pusherSelector = ({ core: { pusher: value } }: any) => value;
+143
View File
@@ -0,0 +1,143 @@
import React, { memo, useCallback, useEffect, useState } from 'react';
import { areEqual } from 'Utils/equalityChecks';
import { useUser } from 'Context/User';
import { UserModel } from 'Containers/User/Models/UserModel';
import { useDispatch, useSelector } from 'react-redux';
import {
employeeRemovedSelector,
employeesAttachedSelector,
fetchingMembersSelector,
membersSelector,
} from 'Containers/Crew/selectors';
import { listProjectMembers, setEmployeesAttached } from 'Containers/Crew/actions';
import { projectIdSelector } from 'Containers/RocketScan/selectors';
import { Spinner } from 'Components/Spinner';
import { GeneralToast } from 'Components/Toast';
import { AvatarOrInitials } from 'Components/Avatar';
import { CrewPlaceholder } from 'Components/Crew/CrewPlaceholder';
import { SelectMembersModal, Members } from 'Containers/Crew';
import { CrewWrapper, Crew } from 'Components/Crew';
import { convertWordsFirstLetterUppercase } from 'Utils/helpers';
import classes from './crew.module.css';
const CrewContainer = () => {
const {
avatar_url: avatar = '',
first_name: firstName,
last_name: lastName,
full_name: fullName,
}: UserModel = useUser();
const dispatch = useDispatch();
// user object
const { id: userId } = useUser();
// local variables
const [isOpenSelectMembersModal, setIsOpenSelectMembersModal] = useState(false);
const [showToast, setShowToast] = useState(false);
const [toastMessage, setToastMessage] = useState('');
// selectors
const employeesAttached = useSelector(employeesAttachedSelector, areEqual);
const employeeRemoved = useSelector(employeeRemovedSelector, areEqual);
const members = useSelector(membersSelector, areEqual);
const projectId = useSelector(projectIdSelector, areEqual);
const fetching = useSelector(fetchingMembersSelector, areEqual);
// api
const getProjectMembers = useCallback(() => {
dispatch(listProjectMembers(projectId, userId));
}, [projectId]);
// initial api call
useEffect(() => {
if (projectId) {
getProjectMembers();
}
}, [projectId]);
const onClickAddCrew = useCallback(() => {
setIsOpenSelectMembersModal(true);
}, []);
const onClickMemberModalCloseClick = useCallback((e: any) => {
e.preventDefault();
setIsOpenSelectMembersModal(false);
}, []);
const onClickToastClose = useCallback(() => setShowToast(false), []);
useEffect(() => {
if (employeesAttached) {
setIsOpenSelectMembersModal(false);
setToastMessage('Crew member(s) added');
setShowToast(true);
setTimeout(() => {
setShowToast(false);
}, 2500);
}
}, [employeesAttached]);
useEffect(() => {
if (!isOpenSelectMembersModal && employeesAttached) {
getProjectMembers();
}
return () => {
if (!isOpenSelectMembersModal && employeesAttached) {
dispatch(setEmployeesAttached(false));
}
};
}, [isOpenSelectMembersModal, employeesAttached]);
useEffect(() => {
if (typeof employeeRemoved === 'string') {
dispatch(listProjectMembers(projectId, userId));
setToastMessage(`${convertWordsFirstLetterUppercase(employeeRemoved)} Removed From Project`);
setShowToast(true);
setTimeout(() => {
setShowToast(false);
}, 2500);
}
}, [employeeRemoved]);
return (
<CrewWrapper>
<Crew
onClickAddCrew={onClickAddCrew}
hasCrew={members.length > 0}
currentUserAvatar={<AvatarOrInitials avatar={avatar || ''} firstName={firstName} lastName={lastName} />}
currentUserName={fullName}
>
<Spinner loading={fetching} />
{members.length === 0 && !fetching && <CrewPlaceholder onClickAddCrew={onClickAddCrew} />}
{!fetching && (
<SelectMembersModal isOpen={isOpenSelectMembersModal} onClickModalCloseClick={onClickMemberModalCloseClick} />
)}
{members.length > 0 && <Members members={members} />}
<GeneralToast
id="crew-toast"
className={classes.toast}
show={showToast}
message={toastMessage}
closeToast={onClickToastClose}
/>
</Crew>
</CrewWrapper>
);
};
const CrewContainerMemo = memo(CrewContainer, areEqual);
export { CrewContainerMemo as Crew };
@@ -0,0 +1,3 @@
.toast {
width: calc(100% - 339px);
}
+1
View File
@@ -0,0 +1 @@
export { Crew } from './Crew';
@@ -0,0 +1,57 @@
import React, { memo, useCallback } from 'react';
import { areEqual } from 'Utils/equalityChecks';
import { UserModel } from 'Containers/User/Models/UserModel';
import { EmployeeCard } from 'Components/People/PeopleTabs/EmployeeCard';
import { AvatarOrInitials } from 'Components/Avatar';
import { formatPhoneInternationalWithCountryCode } from 'Utils/helpers';
interface Props {
member: UserModel;
onClickMember: (e: any) => void;
}
const MemberContainer = ({ member, onClickMember }: Props) => {
const {
id,
first_name: firstName,
last_name: lastName,
full_name: fullName,
avatar_url: avatar,
email,
phones,
roles,
} = member;
let formattedPhoneNumber;
let extension;
if (phones.length > 0) {
const [phone] = phones;
const { value: phoneNumber, country_code: code } = phone;
extension = phone.extension;
formattedPhoneNumber = formatPhoneInternationalWithCountryCode(code, phoneNumber);
}
const onClick = useCallback(() => {
onClickMember(member);
}, []);
return (
<EmployeeCard
id={id.toString()}
avatar={<AvatarOrInitials avatar={avatar || ''} firstName={firstName} lastName={lastName} />}
name={fullName}
email={email}
phone={formattedPhoneNumber}
extension={extension}
roles={roles}
selectCardClick={onClick}
/>
);
};
const MemberContainerMemo = memo(MemberContainer, areEqual);
export { MemberContainerMemo as MemberContainer };
@@ -0,0 +1 @@
export { MemberContainer as Member } from './Member';
@@ -0,0 +1,153 @@
import React, { memo, useCallback, useEffect, useState } from 'react';
import { areEqual } from 'Utils/equalityChecks';
import { UserModel } from 'Containers/User/Models/UserModel';
import { countries } from 'Utils/data';
import { Member } from 'Containers/Crew';
import { RemoveMemberModal } from 'Components/Crew/RemoveMemberModal';
import { AvatarOrInitials } from 'Components/Avatar';
import { useDispatch, useSelector } from 'react-redux';
import { listProjectMembers, removeEmployeeFromProject, setEmployeeRemoved } from 'Containers/Crew/actions';
import { projectIdSelector } from 'Containers/RocketScan/selectors';
import { employeeRemovedSelector, removingEmployeeSelector } from 'Containers/Crew/selectors';
import { useUser } from 'Context/User';
import { formatPhoneNumberInternational } from 'Utils/helpers';
import classes from './members.module.css';
interface Props {
members: Array<UserModel>;
}
const MembersContainer = ({ members }: Props) => {
const dispatch = useDispatch();
// user object
const { id: userId } = useUser();
const user: UserModel = useUser();
const [isOpenModal, setIsOpenModal] = useState(false);
const [id, setId] = useState('');
const [name, setName] = useState('');
const [avatar, setAvatar] = useState('');
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [email, setEmail] = useState('');
const [phone, setPhone] = useState('');
const [extension, setExtension] = useState('');
const [countryFlag, setCountryFlag] = useState('usa');
const [roles, setRoles] = useState([]);
const projectId = useSelector(projectIdSelector, areEqual);
const employeeRemoved = useSelector(employeeRemovedSelector, areEqual);
const fetching = useSelector(removingEmployeeSelector, areEqual);
const onClickMember = useCallback(
({
id,
first_name: firstName,
last_name: lastName,
full_name: fullName,
avatar_url: avatar,
email,
phones,
roles,
}: UserModel) => {
setId(id.toString());
setName(fullName);
setAvatar(avatar);
setFirstName(firstName);
setLastName(lastName);
setEmail(email);
const [firstPhone] = phones;
const { value, extension } = firstPhone;
setPhone(formatPhoneNumberInternational(value));
setExtension(extension);
setRoles(roles);
setIsOpenModal(true);
},
[]
);
const onModalCloseClick = useCallback((e: any) => {
e.preventDefault();
setIsOpenModal(false);
}, []);
const onClickRemoveProject = useCallback(() => {
dispatch(removeEmployeeFromProject(projectId, id));
}, [projectId, id]);
useEffect(() => {
if (employeeRemoved) {
setIsOpenModal(false);
}
}, [employeeRemoved]);
useEffect(() => {
if (!isOpenModal && employeeRemoved) {
dispatch(listProjectMembers(projectId, userId));
dispatch(setEmployeeRemoved(name));
}
return () => {
if (!isOpenModal && employeeRemoved) {
dispatch(setEmployeeRemoved(undefined));
}
};
}, [isOpenModal, employeeRemoved]);
// get country code and flag
useEffect(() => {
if (user?.id) {
const { companies } = user;
if (companies.length > 0) {
const [company] = companies;
const { country_alpha_2: countryAlphaTwo } = company;
const countryAlpha = countryAlphaTwo;
const companyCountry = countries.find((country) => country.alpha_2 === countryAlpha);
if (companyCountry?.id) {
const { flag } = companyCountry;
setCountryFlag(flag);
}
}
}
}, [user, phone]);
return (
<>
<div className={classes.membersBase}>
{members.map((member) => (
<Member key={member.id} member={member} onClickMember={onClickMember} />
))}
</div>
<RemoveMemberModal
id={id}
name={name}
flag={countryFlag}
phone={phone}
email={email}
extension={extension}
roles={roles}
avatar={
<AvatarOrInitials
avatarClassName={classes.avatar}
avatar={avatar || ''}
firstName={firstName}
lastName={lastName}
/>
}
isOpen={isOpenModal}
fetching={fetching}
modalCloseClick={onModalCloseClick}
onClickRemoveProject={onClickRemoveProject}
/>
</>
);
};
const MembersContainerMemo = memo(MembersContainer, areEqual);
export { MembersContainerMemo as MembersContainer };
@@ -0,0 +1 @@
export { MembersContainer as Members } from './Members';
@@ -0,0 +1,12 @@
.membersBase {
display: flex;
flex-wrap: wrap;
width: 100%;
padding: 0 30px;
}
.avatar {
width: 56px;
height: 56px;
margin-right: 24px;
}
@@ -0,0 +1,172 @@
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { areEqual } from 'Utils/equalityChecks';
import { debounce } from 'Utils/debounce';
import { addOrRemoveFromArray } from 'Utils/helpers';
import {
attachingEmployeesSelector,
employeesAttachedSelector,
employeesSelector,
fetchingCrewEmployeesSelector,
} from 'Containers/Crew/selectors';
import { listEmployees, setAttachingEmployees, syncEmployeeToProject } from 'Containers/Crew/actions';
import { projectCompanyIdSelector, projectIdSelector } from 'Containers/RocketScan/selectors';
import { useUser } from 'Context/User';
import { Modal } from 'Components/Modal';
import { SearchBox } from 'Components/SearchBox';
import { EmployeeDirectory, EmployeesEmptyPlaceholder } from 'Components/Crew';
import { Spinner } from 'Components/Spinner';
import classes from './selectMembersModal.module.css';
interface Props {
isOpen: boolean;
onClickModalCloseClick: (e: any) => void;
}
const SelectMembersModalContainer = ({ isOpen, onClickModalCloseClick }: Props) => {
const dispatch = useDispatch();
const textBoxRef = useRef(undefined);
// user object
const { id: userId } = useUser();
// local variables
const [searchValue, setSearchValue] = useState('');
const [selectedMembers, setSelectedMembers] = useState([]);
const [showNoEmployees, setShowNoEmployees] = useState(true);
const [showError, setShowError] = useState(false);
const [initialMount, setInitialMount] = useState(false);
// selectors
const employees = useSelector(employeesSelector, areEqual);
const companyId = useSelector(projectCompanyIdSelector, areEqual);
const projectId = useSelector(projectIdSelector, areEqual);
const fetching = useSelector(fetchingCrewEmployeesSelector, areEqual);
const disableButton = useSelector(attachingEmployeesSelector, areEqual);
const employeesAttached = useSelector(employeesAttachedSelector, areEqual);
useEffect(() => {
setInitialMount(true);
}, []);
// api call
const getEmployees = useCallback(
(search = '') => {
dispatch(listEmployees(companyId, userId, search));
},
[companyId, userId]
);
// initial api call
useEffect(() => {
if (isOpen && companyId && employees.length === 0) {
getEmployees();
}
}, [isOpen, companyId]);
// handle search box value change
const handleSearchValueChange = ({ target: { value } }: any) => {
if (value.length <= 36) {
setInitialMount(false);
setSearchValue(value);
if (value.length >= 2) {
getEmployees(value);
}
if (value.length === 0) {
getEmployees();
}
}
};
// show no employees only once on initial mount
useEffect(() => {
if (initialMount) {
setShowNoEmployees(employees.length === 0);
}
}, [initialMount, employees]);
// debounce function on search value change
const onChangeSearchValue = useMemo(() => debounce(handleSearchValueChange, 300), [companyId]);
const onClickClearButton = useCallback(() => {
getEmployees();
setSearchValue('');
textBoxRef.current.value = '';
textBoxRef.current.focus();
setShowError(false);
}, [companyId, userId, textBoxRef]);
// generate members id array
const onClickMemberRow = useCallback(({ currentTarget: { id } }: any) => {
setSelectedMembers((prevIds) => addOrRemoveFromArray(prevIds, id));
}, []);
// form submit api call
const onClickSelectCrew = useCallback(() => {
setShowError(selectedMembers.length === 0);
if (selectedMembers.length > 0) {
dispatch(setAttachingEmployees(true));
// no bulk users submit so we use a loop to submit multiple members
selectedMembers.forEach((selectedMember: string, index: number) =>
dispatch(syncEmployeeToProject(projectId, selectedMember, selectedMembers.length - 1 === index))
);
}
}, [selectedMembers, projectId]);
// refresh employees
useEffect(() => {
if (employeesAttached) {
setSelectedMembers([]);
getEmployees();
}
}, [employeesAttached]);
return (
<Modal
isOpen={isOpen}
title="Select Crew"
id="select-crew"
modalCloseClick={onClickModalCloseClick}
modalHeader
classes={classes}
>
{!showNoEmployees && (
<SearchBox
ref={textBoxRef}
id="crew-search"
name="search"
ariaLabel="Search a crew member"
value={searchValue}
onChangeValue={onChangeSearchValue}
onClickClearButton={onClickClearButton}
/>
)}
{!showNoEmployees && (
<EmployeeDirectory
employees={employees}
selectedMembers={selectedMembers}
showError={showError}
searchValue={searchValue}
disableButton={disableButton}
onClickMemberRow={onClickMemberRow}
onClickSelectCrew={onClickSelectCrew}
/>
)}
{!fetching && showNoEmployees && <EmployeesEmptyPlaceholder />}
<Spinner loading={fetching} />
</Modal>
);
};
const SelectMembersModalContainerMemo = memo(SelectMembersModalContainer, areEqual);
export { SelectMembersModalContainerMemo as SelectMembersModalContainer };
@@ -0,0 +1 @@
export { SelectMembersModalContainer as SelectMembersModal } from './SelectMembersModal';
@@ -0,0 +1,26 @@
.modalTitle {
font-family: IBM Plex Sans;
font-style: normal;
font-weight: 600;
font-size: 20px;
line-height: 32px;
text-align: center;
color: #5B476B;
width: 100%;
}
.modalHeader {
text-align: center;
border-bottom: none!important;
}
.modalBody {
margin: 0 24px;
padding: 8px 0 24px;
border-top: 1px solid #E8E7ED;
position: relative;
}
.modalDialog {
max-width: 600px;
}
+142
View File
@@ -0,0 +1,142 @@
import { handleApiRequest } from 'Utils/handleApiRequest';
export const SET_CREW_EMPLOYEES = 'SET_CREW_EMPLOYEES';
export const FETCHING_CREW_EMPLOYEES = 'FETCHING_CREW_EMPLOYEES';
export const EMPLOYEES_ATTACHED = 'EMPLOYEES_ATTACHED';
export const ATTACHING_EMPLOYEES = 'ATTACHING_EMPLOYEES';
export const SET_PROJECT_MEMBERS = 'SET_PROJECT_MEMBERS';
export const FETCHING_PROJECT_MEMBERS = 'FETCHING_PROJECT_MEMBERS';
export const EMPLOYEE_REMOVED = 'EMPLOYEE_REMOVED';
export const REMOVING_EMPLOYEE = 'REMOVING_EMPLOYEE';
interface ActionTypes {
SET_CREW_EMPLOYEES: any;
FETCHING_CREW_EMPLOYEES: boolean;
EMPLOYEES_ATTACHED: boolean;
ATTACHING_EMPLOYEES: boolean;
SET_PROJECT_MEMBERS: any;
FETCHING_PROJECT_MEMBERS: boolean;
EMPLOYEE_REMOVED: boolean;
REMOVING_EMPLOYEE: boolean;
}
interface MessageAction {
type: keyof ActionTypes;
payload: any;
}
export type SetCrewTypes = MessageAction;
/*
* NON ASYNC THUNKS
* */
export const setEmployeesAttached = (value: any) => (dispatch) => {
dispatch({
type: EMPLOYEES_ATTACHED,
payload: value,
});
};
export const setAttachingEmployees = (value: any) => (dispatch) => {
dispatch({
type: ATTACHING_EMPLOYEES,
payload: value,
});
};
export const setEmployeeRemoved = (value: any) => (dispatch) => {
dispatch({
type: EMPLOYEE_REMOVED,
payload: value,
});
};
/* eslint-disable */
export const listEmployees =
(companyId: number, userId: number, search = '') =>
async (dispatch: any, _getState = null, utils: any) => {
const response = await handleApiRequest(
dispatch,
utils.Api.get(`companies/${companyId}/users`, {
params: {
'filter[search]': search,
include: 'roles',
},
}),
'',
FETCHING_CREW_EMPLOYEES
);
if (response?.data) {
const { data } = response;
dispatch({
type: SET_CREW_EMPLOYEES,
payload: {
data,
userId,
},
});
}
};
export const syncEmployeeToProject =
(projectId: number, userId: string, last: boolean) =>
async (dispatch: any, _getState = null, utils: any) => {
const response = await handleApiRequest(dispatch, utils.Api.post(`projects/${projectId}/users/${userId}`, {}));
if (typeof response === 'string') {
if (last) {
dispatch(setAttachingEmployees(false));
dispatch(setEmployeesAttached(true));
}
} else {
if (last) {
dispatch(setAttachingEmployees(false));
}
}
};
export const listProjectMembers =
(projectId: number, userId: number, page = 1) =>
async (dispatch: any, _getState = null, utils: any) => {
const response = await handleApiRequest(
dispatch,
utils.Api.get(`projects/${projectId}/users`, {
params: {
limit: 100,
page,
include: 'roles',
},
}),
'',
FETCHING_PROJECT_MEMBERS
);
if (response?.data) {
const { data } = response;
dispatch({
type: SET_PROJECT_MEMBERS,
payload: {
data,
userId,
},
});
}
};
export const removeEmployeeFromProject =
(projectId: number, userId: string) =>
async (dispatch: any, _getState = null, utils: any) => {
const response = await handleApiRequest(
dispatch,
utils.Api.delete(`projects/${projectId}/users/${userId}`),
'',
REMOVING_EMPLOYEE
);
if (typeof response === 'string') {
dispatch(setEmployeeRemoved(true));
}
};
+4
View File
@@ -0,0 +1,4 @@
export { Crew } from './Crew';
export { SelectMembersModal } from './SelectMembersModal';
export { Members } from './Members';
export { Member } from './Member';
+77
View File
@@ -0,0 +1,77 @@
import {
ATTACHING_EMPLOYEES,
EMPLOYEE_REMOVED,
EMPLOYEES_ATTACHED,
FETCHING_CREW_EMPLOYEES,
FETCHING_PROJECT_MEMBERS,
REMOVING_EMPLOYEE,
SET_CREW_EMPLOYEES,
SET_PROJECT_MEMBERS,
SetCrewTypes,
} from 'Containers/Crew/actions';
const initialState = {
employees: [],
fetchingCrewEmployees: true,
employeesAttached: false,
attachingEmployees: false,
members: [],
fetchingMembers: true,
employeeRemoved: undefined,
removingEmployee: false,
};
export const crewReducer = (state = initialState, action: SetCrewTypes) => {
const { type, payload } = action;
switch (type) {
case SET_CREW_EMPLOYEES: {
const { userId, data: employees } = payload;
return {
...state,
employees: employees.filter(({ id }: any) => id !== userId), // removing the current user
};
}
case FETCHING_CREW_EMPLOYEES:
return {
...state,
fetchingCrewEmployees: payload,
};
case EMPLOYEES_ATTACHED:
return {
...state,
employeesAttached: payload,
};
case ATTACHING_EMPLOYEES:
return {
...state,
attachingEmployees: payload,
};
case SET_PROJECT_MEMBERS: {
const { userId, data: members } = payload;
return {
...state,
members: members.filter(({ id }: any) => id !== userId), // removing the current user
};
}
case FETCHING_PROJECT_MEMBERS:
return {
...state,
fetchingMembers: payload,
};
case EMPLOYEE_REMOVED:
return {
...state,
employeeRemoved: payload,
};
case REMOVING_EMPLOYEE:
return {
...state,
removingEmployee: payload,
};
default:
return state;
}
};
+8
View File
@@ -0,0 +1,8 @@
export const employeesSelector = ({ crew: { employees: value = [] } }: any) => value;
export const fetchingCrewEmployeesSelector = ({ crew: { fetchingCrewEmployees: value = true } }: any) => value;
export const employeesAttachedSelector = ({ crew: { employeesAttached: value = false } }: any) => value;
export const attachingEmployeesSelector = ({ crew: { attachingEmployees: value = false } }: any) => value;
export const membersSelector = ({ crew: { members: value = [] } }: any) => value;
export const fetchingMembersSelector = ({ crew: { fetchingMembers: value = true } }: any) => value;
export const employeeRemovedSelector = ({ crew: { employeeRemoved: value } }: any) => value;
export const removingEmployeeSelector = ({ crew: { removingEmployee: value = true } }: any) => value;
@@ -0,0 +1,17 @@
import React, { memo } from 'react';
import { areEqual } from 'Utils/equalityChecks';
import { Dashboard } from 'Components/Dashboard';
const DashboardContainer = () => {
return (
<>
<Dashboard />
</>
);
};
const DashboardContainerMemo = memo(DashboardContainer, areEqual);
export { DashboardContainerMemo as DashboardContainer };
@@ -0,0 +1 @@
export { DashboardContainer as Dashboard } from './Dashboard';
@@ -0,0 +1,23 @@
import React, { memo, useCallback } from 'react';
import { areEqual } from 'Utils/equalityChecks';
import { useHistory } from 'react-router-dom';
import { CreateProjectButton } from 'Components/Dashboard';
interface Props {
className?: string;
}
const CreateProjectButtonContainer = ({ className }: Props) => {
const history = useHistory();
return <CreateProjectButton className={className} />;
};
CreateProjectButtonContainer.defaultProps = {
className: null,
};
const CreateProjectButtonContainerMemo = memo(CreateProjectButtonContainer, areEqual);
export { CreateProjectButtonContainerMemo as CreateProjectButtonContainer };
@@ -0,0 +1 @@
export { CreateProjectButtonContainer as CreateProjectButton } from "./CreateProjectButton";
@@ -0,0 +1,65 @@
import React, { memo, ReactNode, useCallback, useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { areEqual } from 'Utils/equalityChecks';
import { width } from 'Utils/screen';
import { DashboardLayout } from 'Components/Layouts';
import { IntercomProvider } from 'react-use-intercom';
import { MobileWarningModal } from 'Containers/MobileWarningModal';
import { HubSpotModal } from 'Containers/HubSpotModal';
interface Props {
children: ReactNode;
isFirstTimer?: boolean;
}
const INTERCOM_ID: string = process.env.REACT_INTERCOM_ID;
const DashboardWrapperContainer = ({ children, isFirstTimer }: Props) => {
const location = useLocation();
const { pathname } = location;
// this is to slide the sidebar on desktop view
const [sideBarDesktop, setSideBarDesktop] = useState(false);
// this is to slide the sidebar on mobile view
const [sideBarMobile, setSideBarMobile] = useState(false);
const toggleSideBar = useCallback(() => {
if (width <= 991) {
setSideBarMobile((prev) => !prev);
} else {
setSideBarDesktop((prev) => !prev);
}
}, []);
// reset the sidebar menu on route change
useEffect(() => {
setSideBarMobile(false);
}, [pathname]);
// dashboard wrapper is wrapped with intercom provider, so we can access its' features
return (
<IntercomProvider appId={INTERCOM_ID}>
<DashboardLayout
toggleSideBar={toggleSideBar}
sideBarMobile={sideBarMobile}
sideBarDesktop={sideBarDesktop}
pathname={pathname}
>
{children}
<HubSpotModal visible={isFirstTimer} />
<MobileWarningModal />
</DashboardLayout>
</IntercomProvider>
);
};
DashboardWrapperContainer.defaultProps = {
isFirstTimer: false,
};
const DashboardWrapperContainerMemo = memo(DashboardWrapperContainer, areEqual);
export { DashboardWrapperContainerMemo as DashboardWrapperContainer };
@@ -0,0 +1 @@
export { DashboardWrapperContainer as DashboardWrapper } from './DashboardWrapper';
@@ -0,0 +1,34 @@
import React, { memo } from 'react';
import { areEqual } from 'Utils/equalityChecks';
import { SideBar } from 'Components/SideBar';
import { navItems } from 'Utils/navItems';
import { useUser } from 'Context/User';
interface Props {
sideBarDesktop: boolean;
sideBarMobile: boolean;
toggleSideBar: () => void;
pathname: string;
}
const SideBarContainer = ({ sideBarDesktop, sideBarMobile, toggleSideBar, pathname }: Props) => {
const user = useUser();
const { companies } = user;
return (
<SideBar
sideBarDesktop={sideBarDesktop}
sideBarMobile={sideBarMobile}
toggleSideBar={toggleSideBar}
navItems={navItems}
pathname={pathname}
companyName={companies?.[0]?.name}
/>
);
};
const SideBarContainerMemo = memo(SideBarContainer, areEqual);
export { SideBarContainerMemo as SideBarContainer };
@@ -0,0 +1 @@
export { SideBarContainer as SideBar } from "./SideBar";
@@ -0,0 +1,3 @@
export { DashboardWrapper } from './DashboardWrapper';
export { SideBar } from './SideBar';
export { CreateProjectButton } from './CreateProjectButton';
@@ -0,0 +1,32 @@
export const SET_ACTIVE_PROJECT = 'SET_ACTIVE_PROJECT';
export const SET_MOBILE_WARNING_MODAL_SHOWN = 'SET_MOBILE_WARNING_MODAL_SHOWN';
interface ActionTypes {
SET_ACTIVE_PROJECT: string;
SET_MOBILE_WARNING_MODAL_SHOWN: boolean;
}
interface MessageAction {
type: keyof ActionTypes;
payload: string | number | boolean | null;
}
export type setActiveProjectTypes = MessageAction;
/*
* NON-API THUNKS
* */
export const setActiveProject = (value: any) => async (dispatch: any) => {
dispatch({
type: SET_ACTIVE_PROJECT,
payload: value,
});
};
export const setMobileWarningModalShown = (state: any) => (dispatch: any) => {
dispatch({
type: SET_MOBILE_WARNING_MODAL_SHOWN,
payload: state,
});
};
+2
View File
@@ -0,0 +1,2 @@
export { DashboardWrapper, CreateProjectButton, SideBar } from './DashboardWrapper';
export { Dashboard } from './Dashboard';
@@ -0,0 +1,21 @@
import { SET_ACTIVE_PROJECT, setActiveProjectTypes, SET_MOBILE_WARNING_MODAL_SHOWN } from './actions';
const initialState = {
activeProject: null,
mobileWarningModalShown: false,
};
export const dashboardReducer = (state = initialState, action: setActiveProjectTypes) => {
switch (action.type) {
case SET_ACTIVE_PROJECT:
return { ...state, activeProject: action.payload };
case SET_MOBILE_WARNING_MODAL_SHOWN: {
return {
...state,
mobileWarningModalShown: action.payload,
};
}
default:
return state;
}
};
@@ -0,0 +1,8 @@
export const activeProjectSelector = ({ dashboard: { activeProject: value = false } }: any) => value;
export const myProjectsSelector = ({ projects: { myProjects: value = {} } }: any) => value;
export const fetchingMyProjectsSelector = ({ projects: { fetchingMyProjects: value = false } }: any) => value;
export const mobileWarningModalShownSelector = ({ dashboard: { mobileWarningModalShown: value = false } }: any) =>
value;
@@ -0,0 +1,57 @@
import React, { memo, useCallback, useEffect, useRef } from 'react';
import { areEqual } from 'Utils/equalityChecks';
import { DropDownItem } from 'Components/DropDown/DropDownItem';
import { MiniDropdownItem } from 'Components/DropDown/MiniDropdownItem';
import { dropdownSizes } from 'Components/DropDown';
interface Props {
id: number;
name: string;
selected: number;
showDropDown: boolean;
size?: string;
shouldScroll?: boolean;
onSelectItem: (e: any) => void;
}
const DropDownItemContainer = ({ id, name, selected, onSelectItem, showDropDown, size, shouldScroll }: Props) => {
const ref = useRef(null);
const onClick = useCallback((e: any) => {
e.preventDefault();
if (onSelectItem) onSelectItem(id);
}, []);
// pre select 0 on floor numbers
// TODO::To be refactor into a separate container
const scrollToItem = useCallback(() => {
if (typeof selected !== 'undefined') {
if (selected === id && showDropDown) {
ref.current.scrollIntoView();
}
}
}, [showDropDown]);
useEffect(() => {
if (shouldScroll) {
scrollToItem();
}
}, [showDropDown, shouldScroll]);
switch (size) {
case dropdownSizes.small:
return <MiniDropdownItem ref={ref} id={id} name={name} selected={selected} onSelectItem={onClick} />;
default:
return <DropDownItem ref={ref} id={id} name={name} selected={selected} onSelectItem={onClick} />;
}
};
DropDownItemContainer.defaultProps = {
size: 'default',
shouldScroll: false,
};
const DropDownItemContainerMemo = memo(DropDownItemContainer, areEqual);
export { DropDownItemContainerMemo as DropDownItem };
@@ -0,0 +1 @@
export { DropDownItem } from './DropDownItem';
+1
View File
@@ -0,0 +1 @@
export { DropDownItem } from './DropDownItem';
+115
View File
@@ -0,0 +1,115 @@
import React, { memo, useCallback } from 'react';
import { areEqual } from 'Utils/equalityChecks';
import { awsStore } from 'Utils/awsStore';
import { Dropzone } from 'Components/DropZone';
import { useDropzone } from 'react-dropzone';
import { useDispatch, useSelector } from 'react-redux';
import { handleApiRequest } from 'Utils/handleApiRequest';
import { Api } from 'Utils/api';
import { showToast } from 'Containers/PhotoErrorToast/actions';
import { attachAlbumPhoto, setFileUploadingRoom } from 'Containers/DropZone/actions';
interface Props {
roomId: number;
albumId?: number;
hasImages: boolean;
setImageList?: (imgPath: object) => void;
}
// This constructs the urls to view in the PhotoViewer
const imageData = (id: number, raw: string | ArrayBuffer, large: string | ArrayBuffer) => ({
id,
thumbnailSrcUrl: large,
srcUrl: raw,
});
const postTransformObject = (projectId: number | string, contentType: string, response: any) =>
// Create a new object to match the json schema for the back end request
({
uuid: response.uuid,
s3_key: response.key,
bucket: response.bucket,
file_name: `${response.name}${response.extension}`,
file_extension: response.extension,
content_type: contentType,
project_id: projectId,
});
const projectIdSelector = ({ projects: { selectedProjectId } }) => selectedProjectId;
const DropzoneContainer = ({ roomId, albumId, hasImages, setImageList }: Props) => {
const projectId = useSelector(projectIdSelector, areEqual);
const dispatch = useDispatch();
const onDrop = useCallback(async (acceptedFiles: any[], rejectedFiles: any[]) => {
if (rejectedFiles.length > 0) {
dispatch(showToast());
}
// this is to show the spinner on specific room and album
dispatch(setFileUploadingRoom({ roomId, albumId }));
const numberOfFiles = acceptedFiles.length;
acceptedFiles.forEach(async (file: any, index: number) => {
const reader = new FileReader();
// For future reference
// reader.onabort = () => {
// //send a message to the Error Messages component
// };
// For future reference
// reader.onerror = () => {
// //send a message to the Error Messages component
// };
reader.onload = async () => {
// Do whatever you want with the file contents
const binaryStr = reader.result;
// setImageList(imageData(null, binaryStr, binaryStr));
const response = await awsStore(file, binaryStr);
// Create a new object to match the back end structure
const transformed = postTransformObject(projectId, file.type, response);
const apiResponse = await handleApiRequest(dispatch, Api.post(`/rooms/${roomId}/photos`, transformed));
if (apiResponse?.data) {
const {
data: {
id,
sizes: { raw, large },
},
} = apiResponse;
// this will attach each photo to a album, and also give the signal to refresh the specific gallery
dispatch(attachAlbumPhoto(id, albumId, { roomId, albumId, refresh: index === numberOfFiles - 1 }));
// will be used for story
if (setImageList) {
setImageList(imageData(id, raw, large));
}
}
};
reader.readAsDataURL(file);
});
}, []);
const { getRootProps, getInputProps } = useDropzone({ onDrop, accept: 'image/jpeg, image/png' });
return (
<>
<Dropzone hasImages={hasImages} getRootProps={getRootProps} getInputProps={getInputProps} />
</>
);
};
DropzoneContainer.defaultProps = {
albumId: undefined,
setImageList: undefined,
};
const DropzoneContainerMemo = memo(DropzoneContainer, areEqual);
export { DropzoneContainerMemo as Dropzone };
+54
View File
@@ -0,0 +1,54 @@
/* eslint-disable */
import { handleApiRequest } from 'Utils/handleApiRequest';
export const FILE_UPLOADING_ROOM_ALBUM = 'FILE_UPLOADING_ROOM_ALBUM';
export const REFRESH_ROOM_PHOTOS = 'REFRESH_ROOM_PHOTOS';
interface ActionTypes {
FILE_UPLOADING_ROOM_ALBUM: number;
REFRESH_ROOM_PHOTOS: any;
}
interface MessageAction {
type: keyof ActionTypes;
payload: any;
}
export type setDropZoneActionTypes = MessageAction;
export const attachAlbumPhoto =
(photoId: number, albumId: number, refreshPhotos?: any) =>
async (dispatch: any, _getState = null, utils: any) => {
const response = await handleApiRequest(
dispatch,
utils.Api.post(
`photos/${photoId}/albums/${albumId}
`,
{}
)
);
// refresh photo gallery photos once the last item attached to the album
if (typeof response === 'string' && refreshPhotos?.refresh) {
dispatch(setRefreshRoomPhotos(refreshPhotos));
}
};
/*
* NON API THUNKS
* */
export const setFileUploadingRoom = (roomAndAlbum: any) => async (dispatch: any) => {
dispatch({
type: FILE_UPLOADING_ROOM_ALBUM,
payload: roomAndAlbum,
});
};
export const setRefreshRoomPhotos = (value: any) => async (dispatch: any) => {
dispatch({
type: REFRESH_ROOM_PHOTOS,
payload: value,
});
};
+1
View File
@@ -0,0 +1 @@
export { Dropzone } from "./DropZone";
+25
View File
@@ -0,0 +1,25 @@
import { FILE_UPLOADING_ROOM_ALBUM, REFRESH_ROOM_PHOTOS, setDropZoneActionTypes } from 'Containers/DropZone/actions';
const initialState = {
fileUploadingRoomAndRoom: undefined,
refreshRoomPhotos: {},
};
export const dropZoneReducer = (state = initialState, action: setDropZoneActionTypes) => {
const { type, payload } = action;
switch (type) {
case FILE_UPLOADING_ROOM_ALBUM:
return {
...state,
fileUploadingRoomAndRoom: payload,
};
case REFRESH_ROOM_PHOTOS:
return {
...state,
refreshRoomPhotos: payload,
};
default:
return state;
}
};
@@ -0,0 +1,2 @@
export const fileUploadingRoomAndRoomSelector = ({ dropzone: { fileUploadingRoomAndRoom: value } }) => value;
export const refreshRoomPhotosSelector = ({ dropzone: { refreshRoomPhotos: value } }) => value;
@@ -0,0 +1,88 @@
import React, { memo, useCallback, useEffect, useState } from 'react';
import { areEqual } from 'Utils/equalityChecks';
import { FloorDropDown } from 'Components/FloorDropDown';
import { floorNumbers } from 'Utils/helpers';
interface Props {
floorNumber: number;
invalid?: boolean;
hideDropDown: boolean;
setFloorNumber?: (e: any) => void;
}
const FloorDropDownContainer = ({ floorNumber, invalid, hideDropDown, setFloorNumber }: Props) => {
const [selectedValue, setSelectedValue]: any = useState('0');
const [numbers, setNumbers] = useState(floorNumbers());
const [showDropDown, setShowDropDown] = useState(false);
const floorList = floorNumbers();
useEffect(() => {
if (floorNumber.toString() !== '0') {
setNumbers([]);
setSelectedValue(floorNumber);
setNumbers(floorNumbers());
}
}, [floorNumber]);
useEffect(() => {
if (hideDropDown) {
setShowDropDown(false);
}
}, [hideDropDown]);
const onChangeFloorSelect = useCallback(
(e: any) => {
const { value } = e.target;
if (value.length > 0 && floorList.length > 0) {
setShowDropDown(true);
setNumbers(floorNumbers().filter((floor: any) => floor.name.includes(value)));
} else {
setNumbers(floorList);
}
setSelectedValue(value);
},
[numbers]
);
const onSelectItem = useCallback(
(id: number) => {
setFloorNumber(id);
if (numbers.length > 0) {
const floor = numbers.find((floor: any) => floor.id === id);
setSelectedValue(floor.name);
}
setShowDropDown(false);
},
[numbers]
);
const onClickIcon = useCallback(() => {
setShowDropDown((prevState) => !prevState);
}, []);
return (
<FloorDropDown
floorSelect={selectedValue}
selectedFloor={floorNumber}
numbers={numbers}
showDropDown={showDropDown}
invalid={invalid}
onChangeFloorSelect={onChangeFloorSelect}
onSelectItem={onSelectItem}
onClickIcon={onClickIcon}
/>
);
};
FloorDropDownContainer.defaultProps = {
invalid: false,
setFloorNumber: undefined,
};
const FloorDropDownContainerMemo = memo(FloorDropDownContainer, areEqual);
export { FloorDropDownContainerMemo as FloorDropDown };
@@ -0,0 +1 @@
export { FloorDropDown } from './FloorDropDown';
+39
View File
@@ -0,0 +1,39 @@
import React, { memo, ReactNode } from 'react';
import { areEqualShallow } from 'Utils/equalityChecks';
import { Form } from 'Components/Form';
interface Props {
id?: string;
className?: string;
noValidate?: boolean;
submitButton?: ReactNode;
children: any;
onSubmit: (formValues: any) => void;
}
/*
Note:
The form can use either a Submit button or the internal onChange to send form data back to the user
A form button needs to be pass in through props and not children. We need a way to detect if a button
is being used or not to handle the form submission
*/
const FormContainer = ({ id, className, noValidate = true, submitButton, children, onSubmit }: Props) => (
// Using a reference is how we are going to do things the React way, with Bootstrap.
<Form id={id} className={className} noValidate={noValidate} onSubmit={onSubmit}>
{children}
{submitButton && <div className="col-12">{submitButton}</div>}
</Form>
);
FormContainer.defaultProps = {
id: undefined,
className: undefined,
noValidate: true,
submitButton: undefined,
};
const FormContainerMemo = memo(FormContainer, areEqualShallow);
export { FormContainerMemo as Form };
+1
View File
@@ -0,0 +1 @@
export { Form } from './Form';
@@ -0,0 +1,60 @@
import React, { memo, useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { Icon } from 'Components/Icons';
import { LevelSelector } from 'Containers/RocketScan/RoomsView/RoomContent/LevelSelector/LevelSelector';
import { updateRoomLevel } from 'Containers/Project/Unit/Rooms/actions';
import { areEqual } from 'Utils/equalityChecks';
import classes from './galleryHeader.module.css';
interface Props {
roomName: string;
roomId: number;
levelName?: string;
}
// temporary user feedback during development
// const temporaryButtonAction = () => alert("Temporarily disabled for development");
const GalleryHeaderContainer = ({ roomName, roomId, levelName }: Props) => {
const dispatch = useDispatch();
const onLevelChange = useCallback((e: any) => {
e.preventDefault();
const { id } = e.currentTarget;
// dispatch update room
dispatch(updateRoomLevel(roomId, id));
}, []);
return (
<div className={`container-fluid d-flex flex-row justify-content-start px-0 py-2 ${classes.headerWrapper}`}>
<div className="col d-flex flex-row justify-content-start align-items-baseline">
<div className={classes.imageWrapper}>
<Icon type="kitchen" />
</div>
<h2 className={classes.roomName}>{roomName}</h2>
</div>
<div className="col d-flex flex-row justify-content-end align-items-center position-relative">
<LevelSelector
defaultSelectedItem={levelName}
defaultAccordionCollapse
accordionId={`${roomId}-accordion`}
levelMenuHeading={`${roomId}-heading`}
levelOptions={`${roomId}-options`}
onLevelChange={onLevelChange}
/>
{/* <Icon type="actionsdefault" onClick={temporaryButtonAction} /> */}
</div>
</div>
);
};
GalleryHeaderContainer.defaultProps = {
levelName: 'Main Level',
};
const GalleryHeaderContainerMemo = memo(GalleryHeaderContainer, areEqual);
export { GalleryHeaderContainerMemo as GalleryHeader };
@@ -0,0 +1,26 @@
.headerWrapper {
border-bottom: 1px solid rgba(154, 0, 255, 0.5);
}
.imageWrapper {
transform: scale(0.55);
transform-origin: left;
width: 35px;
height: 35px;
}
.roomName {
font-family: IBM Plex Sans;
font-style: normal;
font-weight: 600;
font-size: 20px;
line-height: 30px;
color: #6d00e6;
margin: 0;
}
.menuWrapper {
padding: 3px 1px 5px;
background-color: #e8e7ed;
border-radius: 25px;
}
@@ -0,0 +1 @@
export { GalleryHeader } from "./GalleryHeader";
@@ -0,0 +1,46 @@
import React, { memo, useEffect, useState } from 'react';
import { areEqual } from 'Utils/equalityChecks';
import { HubSpotModal } from 'Components/HubSpotModal';
const HubSpotModalContainer = ({ visible }: { visible: boolean }) => {
const [isOpen, setIsOpen] = useState(false);
const [chosenAction, setChosenAction] = useState(null);
useEffect(() => {
if (visible) {
const script = document.createElement('script');
script.src = 'https://static.hsappstatic.net/MeetingsEmbed/ex/MeetingsEmbedCode.js';
document.body.appendChild(script);
script.addEventListener('load', () => {
setIsOpen(true);
});
}
return () => isOpen && setIsOpen(false);
}, [visible]);
const modalCloseClick = React.useCallback(() => setIsOpen(false), []);
const chooseBook = React.useCallback(() => setChosenAction(1), []);
const chooseSkip = React.useCallback(() => setChosenAction(0), []);
const closeAll = React.useCallback(() => {
setChosenAction(-1);
setIsOpen(false);
}, []);
return (
visible && (
<HubSpotModal
closeAll={closeAll}
chosenAction={chosenAction}
chooseBook={chooseBook}
chooseSkip={chooseSkip}
isOpen={isOpen}
modalCloseClick={modalCloseClick}
/>
)
);
};
const HubSpotModalContainerMemo = memo(HubSpotModalContainer, areEqual);
export { HubSpotModalContainerMemo as HubSpotModalContainer };
@@ -0,0 +1 @@
export { HubSpotModalContainer as HubSpotModal } from './HubSpotModal';
@@ -0,0 +1,34 @@
import React, { memo, useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { areEqual } from 'Utils/equalityChecks';
import { ImageDeleteModal } from 'Components/ImageDeleteModal';
import { deletePhoto } from 'Containers/Thumbnail/actions';
interface Props {
id: number;
isOpen: boolean;
modalCloseClick: (e: any) => void;
}
const ImageDeleteModalContainer = ({ id, isOpen, modalCloseClick }: Props) => {
const dispatch = useDispatch();
const onDeleteButtonClick = useCallback(() => {
dispatch(deletePhoto(id));
}, [id]);
return (
<ImageDeleteModal
id={id}
isOpen={isOpen}
modalCloseClick={modalCloseClick}
onDeleteButtonClick={onDeleteButtonClick}
/>
);
};
const ImageDeleteModalContainerMemo = memo(ImageDeleteModalContainer, areEqual);
export { ImageDeleteModalContainerMemo as ImageDeleteModal };
@@ -0,0 +1 @@
export { ImageDeleteModal } from "./ImageDeleteModal";
@@ -0,0 +1,30 @@
import React, { memo, ReactNode } from 'react';
import { areEqual } from 'Utils/equalityChecks';
import classes from './imageTile.module.css';
interface Props {
caption?: string;
icon?: ReactNode;
sizeSmall?: boolean;
onTileClick?: (e: any) => void;
}
const ImageTileContainer = ({ caption, icon, sizeSmall, onTileClick }: Props) => (
<button className={`${classes.imageTileCard} ${sizeSmall ? classes.sizeSmall : ''}`} onClick={onTileClick}>
<div className={classes.iconImage}>{icon}</div>
<h2 className={classes.imageCaption}>{caption}</h2>
</button>
);
ImageTileContainer.defaultProps = {
caption: undefined,
icon: undefined,
sizeSmall: false,
onTileClick: undefined,
};
const ImageTileContainerMemo = memo(ImageTileContainer, areEqual);
export { ImageTileContainerMemo as ImageTile };
@@ -0,0 +1,42 @@
.imageTileCard {
height: 199px;
width: 199px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border: 1px solid #d2cfda;
border-radius: 10px;
margin: 0 0.75em;
background-color: unset !important;
}
.imageTileCard:hover {
box-shadow: 0px 6px 12px rgba(142, 134, 163, 0.2);
border-color: rgba(154, 0, 255, 0.5);
transition: ease-in-out 0.2s;
}
.iconImage {
}
.imageCaption {
font-family: IBM Plex Sans;
font-style: normal;
font-weight: 600;
font-size: 20px;
line-height: 31px;
text-align: center;
}
.sizeSmall {
width: 156px;
height: 156px;
padding-top: 36px;
margin: 0;
justify-content: flex-start;
}
.sizeSmall .imageCaption {
font-size: 16px;
line-height: 24px;
}
+1
View File
@@ -0,0 +1 @@
export { ImageTile } from './ImageTile';
@@ -0,0 +1,134 @@
import React, { memo, useCallback, useState, useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { areEqual } from 'Utils/equalityChecks';
import { InviteEmployees } from 'Components/InviteEmployees';
import { firstCompanyIdSelector } from 'Containers/Projects/selectors';
import { sendInviteSelector, emailInviteLinkSelector, emailErrorSelector } from './selectors';
import { sendInviteLink, setInviteSent, getInviteURL } from './actions';
interface Props {
isOpen?: boolean;
modalCloseClick: (e: any) => void;
header?: any;
footer?: any;
}
const InviteEmployeesContainer = ({ isOpen, modalCloseClick, header, footer }: Props) => {
const dispatch = useDispatch();
const sent = useSelector(sendInviteSelector, areEqual);
const firstCompanyId = useSelector(firstCompanyIdSelector, areEqual);
const emailLink = useSelector(emailInviteLinkSelector, areEqual);
const [isButtonDisabled, setIsButtonDisabled] = useState(true);
const [email, setEmail] = useState('');
const [toastMessage, setToastMessage] = useState('');
const [showToast, setShowToast] = useState(false);
const [tempEmail, setTempEmail] = useState('');
// email field is only cleared when email is sent and the modal is closed
const [canClearEmail, setCanClearEmail] = useState(false);
// API errors
const errors = {
email: useSelector(emailErrorSelector, areEqual),
};
useEffect(() => {
dispatch(getInviteURL(firstCompanyId));
}, []);
useEffect(
() => () => {
if (sent) {
setCanClearEmail(true);
dispatch(setInviteSent(false));
}
},
[sent]
);
useEffect(() => {
if (!isOpen && canClearEmail) {
setTempEmail('');
setCanClearEmail(false);
}
}, [isOpen, canClearEmail]);
const onCopyClick = useCallback((e: any) => {
e.preventDefault();
setToastMessage('Link Copied');
setShowToast(true);
}, []);
const onEmailChange = useCallback((e: any) => {
const { value } = e.target;
setIsButtonDisabled(value.length === 0);
setTempEmail(value);
}, []);
const onFormSubmit = useCallback((formData: any) => {
const { email } = formData;
setEmail(email);
}, []);
const onSendClick = useCallback(
(e: any) => {
e.preventDefault();
dispatch(sendInviteLink(firstCompanyId, email));
setToastMessage('Link Sent');
},
[email]
);
useEffect(() => {
if (sent) {
setTimeout(() => {
dispatch(setInviteSent(false));
}, 1500);
setShowToast(true);
}
}, [sent]);
// Toast timeout
useEffect(() => {
if (showToast) {
setTimeout(() => {
setShowToast(false);
}, 1500);
}
}, [showToast]);
return (
<div>
<InviteEmployees
header={header}
footer={footer}
emailLink={emailLink}
inviteEmail={tempEmail}
onEmailChange={onEmailChange}
isButtonDisabled={isButtonDisabled}
onCopyClick={onCopyClick}
onFormSubmit={onFormSubmit}
isOpen={isOpen}
onSendClick={onSendClick}
formErrors={errors}
showToast={showToast}
toastMessage={toastMessage}
onClickCloseInviteEmployees={modalCloseClick}
/>
</div>
);
};
InviteEmployeesContainer.defaultProps = {
isOpen: false,
header: null,
footer: null,
};
// This allows for default props if they exist
const InviteEmployeesContainerMemo = memo(InviteEmployeesContainer, areEqual);
export { InviteEmployeesContainerMemo as InviteEmployees };
@@ -0,0 +1,57 @@
/* eslint-disable */
import { handleApiRequest } from 'Utils/handleApiRequest';
export const SET_INVITE_SENT = 'SET_INVITE_SENT';
export const GET_EMAIL_INVITE = 'GET_EMAIL_INVITE';
interface ActionTypes {
SET_INVITE_SENT: object;
GET_EMAIL_INVITE: object;
}
interface Payload {
email: string;
}
export interface MessageAction {
type: keyof ActionTypes;
payload: Payload | undefined;
}
export type sendInviteActionTypes = MessageAction;
export const sendInviteLink =
(companyId: string, email: string) =>
async (dispatch: any, _getState = null, utils: any) => {
const response = await handleApiRequest(
dispatch,
utils.Api.post(`/dispatch-company-invite/${companyId}`, { email })
);
if (typeof response === 'string') {
dispatch(setInviteSent(true));
}
};
export const setInviteSent = (status: boolean) => async (dispatch: any) => {
dispatch({
type: SET_INVITE_SENT,
payload: status,
});
};
export const getInviteURL =
(companyId: string) =>
async (dispatch: any, _getState = null, utils: any) => {
const response = await handleApiRequest(dispatch, utils.Api.get(`/show-company-invite-urls/${companyId}`));
if (response?.data) {
const { data } = response;
const emailInviteLink = data?.shortened_deep_link?.javascript_redirect;
dispatch({
type: GET_EMAIL_INVITE,
payload: emailInviteLink,
});
}
};
@@ -0,0 +1 @@
export { InviteEmployees } from './InviteEmployees';
@@ -0,0 +1,24 @@
import { SET_INVITE_SENT, GET_EMAIL_INVITE, sendInviteActionTypes } from './actions';
const initialState = {
sent: false,
emailInviteLink: undefined,
};
export const employeeInviteReducer = (state = initialState, action: sendInviteActionTypes) => {
const { type, payload } = action;
switch (type) {
case SET_INVITE_SENT:
return {
...state,
sent: payload,
};
case GET_EMAIL_INVITE:
return {
...state,
emailInviteLink: payload,
};
default:
return state;
}
};
@@ -0,0 +1,4 @@
export const sendInviteSelector = ({ employeeInvite: { sent: value = false } }) => value;
export const emailInviteLinkSelector = ({ employeeInvite: { emailInviteLink: value = '' } }) => value;
export const emailErrorSelector = ({ core: { formErrors } }: any) => formErrors?.email || [];
@@ -0,0 +1,38 @@
import React, { memo, ReactNode, useCallback } from 'react';
import { areEqual } from 'Utils/equalityChecks';
import { useHistory } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { projectIdSelector, projectUnavailableSelector } from 'Containers/RocketScan/selectors';
import { ProjectTabsLayout } from 'Components/Layouts/ProjectTabsLayout';
interface Props {
tab: string;
children: ReactNode;
}
const ProjectTabsLayoutContainer = ({ children, tab }: Props) => {
const history = useHistory();
const projectId = useSelector(projectIdSelector);
const projectUnavailable = useSelector(projectUnavailableSelector);
// redirect to the specific tab route
const onTabClick = useCallback(
(tab: string) => {
history.push(`/projects/${projectId}/${tab}`);
},
[projectId]
);
return (
<ProjectTabsLayout tab={tab} projectUnavailable={projectUnavailable} onTabClick={onTabClick}>
{children}
</ProjectTabsLayout>
);
};
const ProjectTabsLayoutContainerMemo = memo(ProjectTabsLayoutContainer, areEqual);
export { ProjectTabsLayoutContainerMemo as ProjectTabsLayout };
@@ -0,0 +1 @@
export { ProjectTabsLayout } from './ProjectTabsLayout';

Some files were not shown because too many files have changed in this diff Show More