diff --git a/.env.example b/.env.example
index 805b716..cb78b1c 100644
--- a/.env.example
+++ b/.env.example
@@ -1,3 +1,4 @@
-CURRENCY_API_URL =
-SALARIES_API_URL =
-COMPANIES_API_URL =
\ No newline at end of file
+CURRENCY_API_URL=
+SALARIES_API_URL=
+COMPANIES_API_URL=
+ENTERPRISES_URL=
\ No newline at end of file
diff --git a/package.json b/package.json
index ea3f4e8..1b24b32 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "gethired-base",
- "version": "1.0.3",
+ "version": "1.1.0",
"main": "index.js",
"license": "MIT",
"scripts": {
@@ -20,7 +20,7 @@
"dependencies": {
"@emotion/react": "11.6.0",
"@emotion/styled": "11.6.0",
- "@master-c8/commons": "^0.1.8",
+ "@master-c8/commons": "^0.1.10",
"@master-c8/icons": "^0.1.3",
"@master-c8/theme": "^0.1.9",
"@mui/icons-material": "5.1.1",
@@ -34,10 +34,12 @@
"@mui/utils": "^5.3.0",
"@reduxjs/toolkit": "^1.7.0",
"chart.js": "^3.6.2",
+ "date-fns": "^2.28.0",
"prop-types": "^15.7.2",
"react": "17.0.2",
"react-chartjs-2": "^4.0.0",
"react-dom": "17.0.2",
+ "react-hook-form": "^7.27.1",
"react-redux": "^7.2.6",
"react-router-dom": "6.0.2",
"redux-persist": "^6.0.0",
@@ -54,11 +56,13 @@
"@testing-library/jest-dom": "^5.16.1",
"@testing-library/react": "^12.1.2",
"@testing-library/user-event": "^13.5.0",
+ "axios": "^0.26.0",
"babel-eslint": "10.1.0",
"babel-jest": "^27.4.2",
"babel-loader": "8.2.3",
"clean-webpack-plugin": "4.0.0",
"css-loader": "6.5.1",
+ "dotenv": "^16.0.0",
"dotenv-webpack": "^7.0.3",
"eslint": "8.3.0",
"eslint-config-airbnb": "19.0.0",
diff --git a/public/calculate.gif b/public/calculate.gif
new file mode 100644
index 0000000..885ae95
Binary files /dev/null and b/public/calculate.gif differ
diff --git a/public/comparate.gif b/public/comparate.gif
new file mode 100644
index 0000000..fe669bb
Binary files /dev/null and b/public/comparate.gif differ
diff --git a/public/favicon-144.png b/public/favicon-144.png
new file mode 100644
index 0000000..236aba0
Binary files /dev/null and b/public/favicon-144.png differ
diff --git a/public/favicon-64.png b/public/favicon-64.png
new file mode 100644
index 0000000..7d6dfbb
Binary files /dev/null and b/public/favicon-64.png differ
diff --git a/public/index.html b/public/index.html
index 0d2e1b8..4f679dd 100644
--- a/public/index.html
+++ b/public/index.html
@@ -4,7 +4,8 @@
GETHIRED | Find your dream job
-
+
+
diff --git a/readme.md b/readme.md
index 3f05d4c..4143a9e 100644
--- a/readme.md
+++ b/readme.md
@@ -1,12 +1,41 @@
-# React template platzi master [c8]
+# Job Placement | Salaries [C8] πΈπΈπΈ
-This project is a template for a React app.
+In salaries you can calculate your salary, compare your salary range with other profiles and when searching for your job you can negotiate your salary. π π°
-This project is configured with webpack, [Material react](https://mui.com), Prettier and eslint.
+## Demo Salaries π
-This project already have a theme file configuration (`src/constants/theme.constant.js`) following the design system from Platzi master C8 in [figma](https://www.figma.com/file/JbToDZz42lRNoZFCdDxya5/Standards?node-id=0%3A1)
+[View Demo](https://salaries.get-hired.work/)
-## Setup and test
+## Demo Job Placement Cell π
+
+[View Demo](https://get-hired.work/)
+
+## Overview π
+
+![Img overview project](public/calculate.gif)
+
+![Img overview project](public/comparate.gif)
+
+## About Project π
+
+This project is part of the Job Placement cell. He also belongs to cohort 8 of Platzi Master π
+
+## Technologies π§
+
+- [React.js](https://reactjs.org/)
+- [Redux](https://redux.js.org/)
+- [Mui](https://mui.com/)
+- [@master-c8/commons](https://www.npmjs.com/package/@master-c8/commons)
+- [@master-c8/icons](https://www.npmjs.com/package/@master-c8/icons)
+- [@master-c8/theme](https://www.npmjs.com/package/@master-c8/theme)
+- [Vercel](https://vercel.com/)
+- [Webpack](https://webpack.js.org/)
+
+## Design System πͺ
+
+You can design system [here](https://www.figma.com/file/JbToDZz42lRNoZFCdDxya5/Standards?node-id=0%3A1)
+
+## Setup and test βοΈ
First, clone the repository and install the dependencies
@@ -16,10 +45,14 @@ Run the project with the script start
`yarn start`
-the page will be loaded on `http://localhost:3000`
+The page will be loaded on `http://localhost:3000`
-
+## Contributors π§
+- [Yadu Lopez](https://www.linkedin.com/in/yadu-lopez/)
+- [Johan Perez](https://www.linkedin.com/in/johannpereze/)
+- [Kevin Farid](https://www.linkedin.com/in/kevfarid/)
+- [Emilio Sanchez](https://www.linkedin.com/in/emlez/)
## How to contribute
@@ -27,5 +60,5 @@ Thank you for being here, we're really happy you decided to contribute to the pr
Before you contribute to the project please make sure to read all items below.
-* [Code of Conduct](/CODE_OF_CONDUCT.md)
-* [Contributing Guide](/CONTRIBUTING.md)
+- [Code of Conduct](/CODE_OF_CONDUCT.md)
+- [Contributing Guide](/CONTRIBUTING.md)
diff --git a/src/app/CalculateSalary/selectors.js b/src/app/CalculateSalary/selectors.js
index 2849a83..0112a25 100644
--- a/src/app/CalculateSalary/selectors.js
+++ b/src/app/CalculateSalary/selectors.js
@@ -39,3 +39,8 @@ export const selectLoadingFormComparison = createSelector(
(state) => state.Calculate.loadingButtonsState.formCompare,
(loading) => loading,
);
+
+export const selectVacancies = createSelector(
+ (state) => state.Calculate.vacancies,
+ (vacancies) => vacancies,
+);
diff --git a/src/app/CalculateSalary/slice.js b/src/app/CalculateSalary/slice.js
index 90fc3f3..a9ffdb7 100644
--- a/src/app/CalculateSalary/slice.js
+++ b/src/app/CalculateSalary/slice.js
@@ -3,23 +3,23 @@ import { getSalaryProfile } from 'Services/salaries';
const initialState = {
formMain: {
- title_id: '',
+ title_name: '',
technologies: [],
- seniority: '',
+ seniority: null,
english_level: '',
- is_remote: false,
+ is_remote: true,
location: '',
},
formComparison: {
- title_id: '',
+ title_name: '',
technologies: [],
- seniority: '',
+ seniority: null,
english_level: '',
- is_remote: false,
+ is_remote: true,
location: '',
},
chartData: [],
- currency: '',
+ currency: 'USD',
comparisonChartData: [],
snackbarShow: false,
loadingButtonsState: {
@@ -67,6 +67,9 @@ const calculateSalary = createSlice({
},
clearFormMain(state) {
state.formMain = initialState.formMain;
+ state.formComparison = initialState.formComparison;
+ state.chartData = initialState.chartData;
+ state.comparisonChartData = initialState.comparisonChartData;
},
deleteChip: (state, action) => {
state.formMain.technologies = state.formMain.technologies.filter((chip) => chip !== action.payload);
@@ -104,7 +107,14 @@ const calculateSalary = createSlice({
},
});
-export const { changesForm, changesFormComparison, clearFormMain, deleteChip, changeCurrency, closeSnackbar } =
- calculateSalary.actions;
+export const {
+ changesForm,
+ changesFormComparison,
+ clearFormMain,
+ deleteChip,
+ changeCurrency,
+ closeSnackbar,
+ changeFilter,
+} = calculateSalary.actions;
export default calculateSalary.reducer;
diff --git a/src/app/Filters/selectors.js b/src/app/Filters/selectors.js
new file mode 100644
index 0000000..de0f594
--- /dev/null
+++ b/src/app/Filters/selectors.js
@@ -0,0 +1,6 @@
+import { createSelector } from '@reduxjs/toolkit';
+
+export const selectFilters = createSelector(
+ (state) => state.Filters.filters,
+ (filters) => filters,
+);
diff --git a/src/app/Filters/slice.js b/src/app/Filters/slice.js
new file mode 100644
index 0000000..a863f4d
--- /dev/null
+++ b/src/app/Filters/slice.js
@@ -0,0 +1,31 @@
+import { createSlice } from '@reduxjs/toolkit';
+
+const initialState = {
+ filters: {
+ typeWork: null ?? '',
+ 'company[]': null,
+ job_location: null,
+ min_salary: null,
+ max_salary: null,
+ },
+};
+
+const filters = createSlice({
+ name: 'Filters',
+ initialState,
+ reducers: {
+ changeFilter(state, action) {
+ state.filters = {
+ ...state.filters,
+ ...action.payload,
+ };
+ },
+ resetFilters(state) {
+ state.filters = initialState.filters;
+ },
+ },
+});
+
+export const { changeFilter, resetFilters } = filters.actions;
+
+export default filters.reducer;
diff --git a/src/app/ListData/selectors.js b/src/app/ListData/selectors.js
index 4251c3f..0d365c9 100644
--- a/src/app/ListData/selectors.js
+++ b/src/app/ListData/selectors.js
@@ -24,3 +24,18 @@ export const selectListCurrencies = createSelector(
(state) => state.ListData.list.Currencies,
(currencies) => currencies.map(({ currency }) => currency),
);
+
+export const selectAllList = createSelector(
+ (state) => state.ListData.list,
+ (all) => all,
+);
+
+export const selectLoading = createSelector(
+ (state) => state.ListData.loading,
+ (load) => load,
+);
+
+export const selectError = createSelector(
+ (state) => state.ListData.error,
+ (error) => error,
+);
diff --git a/src/app/ListData/slice.js b/src/app/ListData/slice.js
index 658031f..f490a0b 100644
--- a/src/app/ListData/slice.js
+++ b/src/app/ListData/slice.js
@@ -2,6 +2,8 @@ import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { getListByName } from 'Services/salaries';
import { getListCurrencies } from 'Services/currency';
+import { getListCompanies } from 'Services/filters';
+import { getListLocations, getListTypeWork } from '../../services/filters';
export const fetchListData = createAsyncThunk('post/fetchListData', async () => ({
Technologies: await getListByName('technologies'),
@@ -9,6 +11,9 @@ export const fetchListData = createAsyncThunk('post/fetchListData', async () =>
English: await getListByName('english'),
Seniority: await getListByName('seniority'),
Currencies: await getListCurrencies(),
+ Companies: await getListCompanies(),
+ TypeWork: await getListTypeWork(),
+ Locations: await getListLocations(),
}));
const dataSlice = createSlice({
@@ -20,11 +25,26 @@ const dataSlice = createSlice({
English: { level: '', texts: [], description: '' },
Seniority: { level: '', texts: [], description: '' },
Currencies: [],
+ Companies: [],
+ TypeWork: [],
+ Locations: [],
},
+ loading: false,
+ error: null,
},
extraReducers: {
[fetchListData.fulfilled]: (state, action) => {
state.list = action.payload;
+ state.loading = false;
+ state.error = null;
+ },
+ [fetchListData.pending]: (state) => {
+ state.loading = true;
+ state.error = null;
+ },
+ [fetchListData.rejected]: (state) => {
+ state.loading = false;
+ state.error = 'Ups! There is an error';
},
},
});
diff --git a/src/app/rootReducer.js b/src/app/rootReducer.js
index 0827494..370a35f 100644
--- a/src/app/rootReducer.js
+++ b/src/app/rootReducer.js
@@ -2,10 +2,12 @@ import { combineReducers } from '@reduxjs/toolkit';
import dataReducer from './ListData/slice';
import calculateReducer from './CalculateSalary/slice';
+import filtersReducer from './Filters/slice';
const rootReducer = combineReducers({
ListData: dataReducer,
Calculate: calculateReducer,
+ Filters: filtersReducer,
});
export default rootReducer;
diff --git a/src/components/Commons/Select/Select.jsx b/src/components/Commons/Select/Select.jsx
index 7e53532..c8206eb 100644
--- a/src/components/Commons/Select/Select.jsx
+++ b/src/components/Commons/Select/Select.jsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { forwardRef } from 'react';
import PropTypes from 'prop-types';
import MenuItem from '@mui/material/MenuItem';
@@ -7,7 +7,7 @@ import FormHelperText from '@mui/material/FormHelperText';
import InputLabel from '@mui/material/InputLabel';
import SelectMUI from '@mui/material/Select';
-const Select = (props) => {
+const Select = forwardRef((props, ref) => {
const {
helperText,
options,
@@ -20,22 +20,32 @@ const Select = (props) => {
name,
fullWidth,
width,
+ children,
...otherProps
} = props;
return (
{label}
-
- {options?.map((option) => (
-
- ))}
+
+ {children ||
+ options?.map((option) => (
+
+ ))}
{!!helperText && {helperText}}
);
-};
+});
Select.propTypes = {
onChange: PropTypes.func.isRequired,
@@ -44,11 +54,12 @@ Select.propTypes = {
disabled: PropTypes.bool,
id: PropTypes.string.isRequired,
helperText: PropTypes.string,
- options: PropTypes.arrayOf(PropTypes.string).isRequired,
+ options: PropTypes.arrayOf(PropTypes.string),
label: PropTypes.string.isRequired,
fullWidth: PropTypes.bool,
- value: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
+ value: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string), PropTypes.number]),
width: PropTypes.number,
+ children: PropTypes.node,
};
Select.defaultProps = {
@@ -58,6 +69,8 @@ Select.defaultProps = {
value: '',
fullWidth: true,
width: null,
+ children: null,
+ options: [],
};
export default Select;
diff --git a/src/components/Currencies/Currencies.jsx b/src/components/Currencies/Currencies.jsx
index 9ef5b01..c76a5a5 100644
--- a/src/components/Currencies/Currencies.jsx
+++ b/src/components/Currencies/Currencies.jsx
@@ -1,38 +1,53 @@
-import React from 'react';
+import React, { Fragment } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
-import { Autocomplete, TextField } from '@mui/material';
+import Skeleton from '@mui/material/Skeleton';
+import Autocomplete from '@mui/material/Autocomplete';
+import TextField from '@mui/material/TextField';
import { changeCurrency } from 'App/CalculateSalary/slice';
import { selectCurrency } from 'App/CalculateSalary/selectors';
-import { selectListCurrencies } from 'App/ListData/selectors';
+import { selectListCurrencies, selectLoading, selectError } from 'App/ListData/selectors';
-const Currencies = ({ handleCurrency, currency, listCurrencies }) => {
+const Currencies = ({ handleCurrency, currency, listCurrencies, error, loading }) => {
const handleCurrencies = (_, values) => handleCurrency(values);
-
+ if (error) return null;
return (
- option === value}
- renderInput={(params) => }
- />
+
+ {loading && !error && }
+ {!loading && (
+ option === value}
+ renderInput={(params) => }
+ />
+ )}
+
);
};
+Currencies.defaultProps = {
+ error: null,
+};
+
Currencies.propTypes = {
handleCurrency: PropTypes.func.isRequired,
currency: PropTypes.string.isRequired,
listCurrencies: PropTypes.arrayOf(PropTypes.string).isRequired,
+ error: PropTypes.string,
+ loading: PropTypes.bool.isRequired,
};
const mapStateToProps = (state) => ({
currency: selectCurrency(state),
listCurrencies: selectListCurrencies(state),
+ loading: selectLoading(state),
+ error: selectError(state),
});
const mapDispatchToProps = (dispatch) => ({
diff --git a/src/components/Filter/Filter.jsx b/src/components/Filter/Filter.jsx
new file mode 100644
index 0000000..1808b2b
--- /dev/null
+++ b/src/components/Filter/Filter.jsx
@@ -0,0 +1,211 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import { useForm, Controller } from 'react-hook-form';
+import PropTypes from 'prop-types';
+
+import Card from '@mui/material/Card';
+import Typography from '@mui/material/Typography';
+import Select from 'Components/Commons/Select';
+import MenuItem from '@mui/material/MenuItem';
+import Autocomplete from '@mui/material/Autocomplete';
+import TextField from '@mui/material/TextField';
+import Box from '@mui/material/Box';
+import Button from '@mui/material/Button';
+import FormControl from '@mui/material/FormControl';
+import Skeleton from '@mui/material/Skeleton';
+
+import { selectAllList, selectLoading, selectError } from 'App/ListData/selectors';
+import { changeFilter, resetFilters } from 'App/Filters/slice';
+import { disabledButton } from 'Helpers';
+
+const defaultValues = {
+ typeWork: null ?? '',
+ min_salary: null,
+ max_salary: null,
+};
+
+const Filter = ({ list, setFilters, resetFilter, loading, error }) => {
+ const { register, handleSubmit, formState, control, getValues, reset } = useForm({
+ defaultValues,
+ });
+ const { errors } = formState;
+
+ const onSubmitFilter = (data) => {
+ const { job_location, company, ...rest } = data;
+ const info = {
+ ...rest,
+ 'company[]': company?.id,
+ job_location: job_location.job_location,
+ };
+ setFilters(info);
+ };
+
+ const handleReset = () => {
+ reset(defaultValues);
+ resetFilter();
+ };
+
+ const hasDisabled = disabledButton(getValues());
+
+ return (
+
+ {error && (
+
+ Not found filters π’
+
+ )}
+ {loading && !error && (
+
+
+
+
+
+ )}
+ {!loading && (
+
+
+ Filters Offers
+
+ (
+
+ )}
+ />
+ data}
+ control={control}
+ defaultValue={{ id: null, name: null }}
+ render={({ field: { onChange, ...field } }) => (
+ onChange(data)}
+ disableClearable
+ options={list.Companies}
+ getOptionLabel={(option) => option?.name ?? ''}
+ renderInput={(params) => }
+ />
+ )}
+ />
+ data}
+ defaultValue={{ job_location: null }}
+ render={({ field: { onChange, ...field } }) => (
+ onChange(data)}
+ freeSolo
+ sx={{ mt: 1 }}
+ disableClearable
+ getOptionLabel={(option) => option?.job_location ?? ''}
+ options={list.Locations}
+ renderInput={(params) => (
+
+ )}
+ />
+ )}
+ />
+
+
+ Number(getValues('max_salary'))
+ ? Number(getValues('max_salary')) >= Number(getValues('min_salary'))
+ : true,
+ })}
+ />
+
+ !Number(getValues('min_salary'))
+ ? Number(getValues('min_salary')) <= Number(getValues('max_salary'))
+ : true,
+ })}
+ />
+
+
+
+
+ )}
+
+ );
+};
+
+Filter.propTypes = {
+ list: PropTypes.shape({
+ TypeWork: PropTypes.arrayOf(
+ PropTypes.shape({
+ id: PropTypes.number,
+ name: PropTypes.string,
+ }),
+ ),
+ Companies: PropTypes.arrayOf(
+ PropTypes.shape({
+ id: PropTypes.number,
+ name: PropTypes.string,
+ }),
+ ),
+ Locations: PropTypes.arrayOf(
+ PropTypes.shape({
+ job_location: PropTypes.string,
+ }),
+ ),
+ }).isRequired,
+ setFilters: PropTypes.func.isRequired,
+ resetFilter: PropTypes.func.isRequired,
+ loading: PropTypes.bool.isRequired,
+ error: PropTypes.string,
+};
+
+Filter.defaultProps = {
+ error: null,
+};
+
+const mapStateToProps = (state) => ({
+ list: selectAllList(state),
+ loading: selectLoading(state),
+ error: selectError(state),
+});
+
+const mapDispatchToProps = (dispatch) => ({
+ setFilters: (data) => dispatch(changeFilter(data)),
+ resetFilter: () => dispatch(resetFilters()),
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(Filter);
diff --git a/src/components/Filter/index.js b/src/components/Filter/index.js
new file mode 100644
index 0000000..7494969
--- /dev/null
+++ b/src/components/Filter/index.js
@@ -0,0 +1 @@
+export { default } from './Filter';
diff --git a/src/components/FormCard/FormCard.jsx b/src/components/FormCard/FormCard.jsx
index 7d8517b..567f523 100644
--- a/src/components/FormCard/FormCard.jsx
+++ b/src/components/FormCard/FormCard.jsx
@@ -1,14 +1,27 @@
-import React, { useEffect } from 'react';
+import React, { useEffect, Fragment } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
-import { Typography, Card, Autocomplete, TextField, Grid } from '@mui/material';
+import Typography from '@mui/material/Typography';
+import Card from '@mui/material/Card';
+import Autocomplete from '@mui/material/Autocomplete';
+import TextField from '@mui/material/TextField';
+import Grid from '@mui/material/Grid';
+import MenuItem from '@mui/material/MenuItem';
import Select from 'Components/Commons/Select';
import { InfoTooltip } from 'Components/Commons/InfoTooltip/InfoTooltip';
-import { selectTechnologies, selectJobs, selectSeniority, selectEnglish } from 'App/ListData/selectors';
+import {
+ selectTechnologies,
+ selectJobs,
+ selectSeniority,
+ selectEnglish,
+ selectLoading,
+ selectError,
+} from 'App/ListData/selectors';
import { fetchListData } from 'App/ListData/slice';
+import FormCardSkeleton from './FormCardSkeleton';
const FormCard = ({
onChange,
@@ -20,11 +33,13 @@ const FormCard = ({
listEnglish,
children,
addListData,
+ loading,
+ error,
}) => {
- const { title_id, technologies, seniority, english_level } = values;
+ const { title_name, technologies, seniority, english_level } = values;
const handleTechnologies = (e, value) => onChange(e, value, 'technologies');
- const handleTitle = (e, value) => onChange(e, value, 'title_id');
+ const handleTitle = (e, value) => onChange(e, value, 'title_name');
useEffect(() => {
addListData();
@@ -32,82 +47,103 @@ const FormCard = ({
return (
-
-
- {!!title && {title}}
-
-
- option === value}
- onChange={handleTitle}
- value={title_id}
- renderInput={(params) => }
- />
-
-
- option === value}
- onChange={handleTechnologies}
- ChipProps={{
- color: 'primary',
- variant: 'outlined',
- size: 'small',
- }}
- defaultValue={[]}
- value={technologies}
- renderInput={(params) => }
- />
-
-
-
-
+ {error && (
+
+ Not found data, try again π’
+
+ )}
+ {loading && !error && }
+ {!loading && (
+
+
+
+ {!!title && {title}}
+
+
+ option === value}
+ onChange={handleTitle}
+ value={title_name}
+ renderInput={(params) => }
+ />
+
+
+ option === value}
+ onChange={handleTechnologies}
+ ChipProps={{
+ color: 'primary',
+ variant: 'outlined',
+ size: 'small',
+ }}
+ defaultValue={[]}
+ value={technologies}
+ renderInput={(params) => (
+
+ )}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
- {children}
-
-
+
+ )}
);
};
FormCard.propTypes = {
+ loading: PropTypes.bool.isRequired,
values: PropTypes.shape({
- title_id: PropTypes.string,
+ title_name: PropTypes.string,
technologies: PropTypes.arrayOf(PropTypes.string),
- seniority: PropTypes.string,
+ seniority: PropTypes.number,
english_level: PropTypes.string,
}).isRequired,
children: PropTypes.node,
@@ -126,11 +162,13 @@ FormCard.propTypes = {
onChange: PropTypes.func.isRequired,
title: PropTypes.string,
addListData: PropTypes.func.isRequired,
+ error: PropTypes.string,
};
FormCard.defaultProps = {
title: null,
children: null,
+ error: null,
};
const mapStateToProps = (state) => ({
@@ -138,6 +176,8 @@ const mapStateToProps = (state) => ({
listJobs: selectJobs(state),
listSeniority: selectSeniority(state),
listEnglish: selectEnglish(state),
+ loading: selectLoading(state),
+ error: selectError(state),
});
const mapDispatchToProps = (dispatch) => ({
diff --git a/src/components/FormCard/FormCardSkeleton.jsx b/src/components/FormCard/FormCardSkeleton.jsx
new file mode 100644
index 0000000..d6c9c32
--- /dev/null
+++ b/src/components/FormCard/FormCardSkeleton.jsx
@@ -0,0 +1,18 @@
+import React, { Fragment } from 'react';
+
+import Skeleton from '@mui/material/Skeleton';
+
+const FormCardSkeleton = () => {
+ return (
+
+
+
+
+
+
+
+
+ );
+};
+
+export default FormCardSkeleton;
diff --git a/src/components/JobOffers/JobCard.jsx b/src/components/JobOffers/JobCard.jsx
new file mode 100644
index 0000000..fbed069
--- /dev/null
+++ b/src/components/JobOffers/JobCard.jsx
@@ -0,0 +1,54 @@
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+
+import Grid from '@mui/material/Grid';
+import Card from '@mui/material/Card';
+import Typography from '@mui/material/Typography';
+import Button from '@mui/material/Button';
+
+import { selectCurrency } from 'App/CalculateSalary/selectors';
+import { helpCurrency } from 'Helpers';
+
+import JobDetailsModal from './JobDetailsModal';
+
+const JobCard = ({ job, currency }) => {
+ const [showDetail, setShowDetail] = useState(false);
+ const handleOpenClose = () => setShowDetail(!showDetail);
+ return (
+
+
+
+
+ {job.name}
+
+ {job.description}
+
+ {helpCurrency(job.salary)} {currency}
+
+
+
+
+
+
+
+
+ );
+};
+
+JobCard.propTypes = {
+ job: PropTypes.shape({
+ name: PropTypes.string.isRequired,
+ description: PropTypes.string.isRequired,
+ salary: PropTypes.number.isRequired,
+ }).isRequired,
+ currency: PropTypes.string.isRequired,
+};
+
+const mapToStateToProps = (state) => ({
+ currency: selectCurrency(state),
+});
+
+export default connect(mapToStateToProps, null)(JobCard);
diff --git a/src/components/JobOffers/JobDetailsModal.jsx b/src/components/JobOffers/JobDetailsModal.jsx
new file mode 100644
index 0000000..7d3c3c2
--- /dev/null
+++ b/src/components/JobOffers/JobDetailsModal.jsx
@@ -0,0 +1,205 @@
+/* eslint-disable camelcase */
+import React from 'react';
+import PropTypes from 'prop-types';
+import Box from '@mui/material/Box';
+import Dialog from '@mui/material/Dialog';
+import DialogContent from '@mui/material/DialogContent';
+import DialogTitle from '@mui/material/DialogTitle';
+import CloseIcon from '@mui/icons-material/Close';
+import { Chip, IconButton, Typography } from '@mui/material';
+import { helpColor, sum, helpCurrency } from '../../helpers';
+
+// import RegisterApplicantForm from '../RegisterApplicantForm';
+
+const JobDetailsModal = ({ showDetail, handleOpenClose, vacancyInfo }) => {
+ const {
+ // id,
+ // company_id,
+ company,
+ job_location,
+ description,
+ skills,
+ name,
+ salary,
+ typeWork,
+ hours_per_week,
+ minimum_experience,
+ postulation_status,
+ applicant_evaluations,
+ } = vacancyInfo;
+
+ // const [openApplyModal, setOpenApplyModal] = useState(false);
+ // const navigate = useNavigate();
+
+ return (
+
+ );
+};
+
+JobDetailsModal.propTypes = {
+ handleOpenClose: PropTypes.func.isRequired,
+ showDetail: PropTypes.bool.isRequired,
+ vacancyInfo: PropTypes.shape({
+ id: PropTypes.number.isRequired,
+ companyName: PropTypes.string,
+ location: PropTypes.string,
+ company: PropTypes.shape({
+ name: PropTypes.string,
+ }),
+ postulation_status: PropTypes.shape({
+ name: PropTypes.string,
+ id: PropTypes.number,
+ }),
+ applicant_evaluations: PropTypes.arrayOf(
+ PropTypes.shape({
+ company_id: PropTypes.number,
+ applicant_name: PropTypes.string,
+ }),
+ ),
+ description: PropTypes.string,
+ skills: PropTypes.arrayOf(PropTypes.string),
+ name: PropTypes.string,
+ postulation_deadline: PropTypes.string,
+ status: PropTypes.bool,
+ salary: PropTypes.number,
+ company_id: PropTypes.number,
+ typeWork: PropTypes.string,
+ job_location: PropTypes.string,
+ hours_per_week: PropTypes.number,
+ minimum_experience: PropTypes.number,
+ created_at: PropTypes.string,
+ updated_at: PropTypes.string,
+ tracking_code: PropTypes.string,
+ }).isRequired,
+};
+
+export default JobDetailsModal;
diff --git a/src/components/JobOffers/JobSkeleton.jsx b/src/components/JobOffers/JobSkeleton.jsx
new file mode 100644
index 0000000..9b4c47e
--- /dev/null
+++ b/src/components/JobOffers/JobSkeleton.jsx
@@ -0,0 +1,38 @@
+import React, { Fragment } from 'react';
+import Grid from '@mui/material/Grid';
+import Card from '@mui/material/Card';
+import Typography from '@mui/material/Typography';
+import Skeleton from '@mui/material/Skeleton';
+
+const JobSkeleton = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const GroupSkeleton = () => (
+
+
+
+
+
+
+
+
+
+);
+
+export default GroupSkeleton;
diff --git a/src/components/JobOffers/Jobs.jsx b/src/components/JobOffers/Jobs.jsx
new file mode 100644
index 0000000..5d3dd64
--- /dev/null
+++ b/src/components/JobOffers/Jobs.jsx
@@ -0,0 +1,70 @@
+import React, { useState, useCallback, useEffect, Fragment } from 'react';
+import Typography from '@mui/material/Typography';
+import { connect } from 'react-redux';
+import { exchangeValueOfObject } from 'Libs/exchange';
+import PropTypes from 'prop-types';
+
+import { selectVacancies, selectCurrency } from 'App/CalculateSalary/selectors';
+import { selectFilters } from 'App/Filters/selectors';
+import { getJobs } from 'Services/jobs';
+import useCurrency from 'Hooks/useCurrency';
+import JobCard from './JobCard';
+import GroupSkeleton from './JobSkeleton';
+
+const Jobs = ({ currency, filters }) => {
+ const [listJobs, setListJobs] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const { currencyValue } = useCurrency(currency);
+
+ const getJobsList = useCallback(
+ async (fil) => {
+ setLoading(true);
+ try {
+ const jobs = await getJobs(fil);
+ setListJobs(jobs.map((job) => exchangeValueOfObject(job, 'salary', currencyValue)));
+ } catch {
+ console.log('');
+ } finally {
+ setLoading(false);
+ }
+ },
+ [currencyValue],
+ );
+
+ useEffect(() => {
+ getJobsList(filters);
+ }, [getJobsList, filters]);
+
+ return (
+
+ {loading && }
+ {listJobs.length < 1 && !loading && (
+
+ No jobs found π₯²
+
+ )}
+ {listJobs?.map((job) => (
+
+ ))}
+
+ );
+};
+
+Jobs.propTypes = {
+ currency: PropTypes.string.isRequired,
+ filters: PropTypes.shape({
+ typeWork: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
+ company: PropTypes.string,
+ job_location: PropTypes.string,
+ min_salary: PropTypes.number,
+ max_salary: PropTypes.number,
+ }).isRequired,
+};
+
+const mapStateToProps = (state) => ({
+ vacancies: selectVacancies(state),
+ currency: selectCurrency(state),
+ filters: selectFilters(state),
+});
+
+export default connect(mapStateToProps)(Jobs);
diff --git a/src/components/JobOffers/index.jsx b/src/components/JobOffers/index.jsx
new file mode 100644
index 0000000..997d624
--- /dev/null
+++ b/src/components/JobOffers/index.jsx
@@ -0,0 +1,26 @@
+import React from 'react';
+import Filter from 'Components/Filter';
+import Grid from '@mui/material/Grid';
+import Typography from '@mui/material/Typography';
+
+import Jobs from './Jobs';
+
+const JobOffers = () => {
+ return (
+
+
+
+ Work offers
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default JobOffers;
diff --git a/src/components/TabCalculate/TabCalculate.jsx b/src/components/TabCalculate/TabCalculate.jsx
index 263da03..3d0acbf 100644
--- a/src/components/TabCalculate/TabCalculate.jsx
+++ b/src/components/TabCalculate/TabCalculate.jsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { Fragment } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
@@ -8,6 +8,7 @@ import LoadingButton from '@mui/lab/LoadingButton';
import FormCard from 'Components/FormCard';
import NormalDistributionChart from 'Components/Charts';
+import JobOffers from 'Components/JobOffers';
import { changesForm, clearFormMain, fetchChartData } from 'App/CalculateSalary/slice';
import {
@@ -19,7 +20,7 @@ import {
import { selectListCurrencies } from 'App/ListData/selectors';
-import { disabled } from 'Helpers';
+import { disabled, isDisabledClear } from 'Helpers';
import useCurrency from 'Hooks/useCurrency';
const TabCalculate = ({
@@ -49,29 +50,37 @@ const TabCalculate = ({
};
return (
-
-
-
-
- Calculate Salary
-
-
-
+
+
+
+
+
+ Calculate Salary
+
+
+
+
+
+
+
-
-
-
-
+
+
);
};
@@ -80,10 +89,10 @@ TabCalculate.propTypes = {
handleCalculate: PropTypes.func.isRequired,
formCalculate: PropTypes.shape({
english_level: PropTypes.string,
- seniority: PropTypes.string,
+ seniority: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
is_remote: PropTypes.bool,
location: PropTypes.string,
- title_id: PropTypes.string,
+ title_name: PropTypes.string,
technologies: PropTypes.arrayOf(PropTypes.string),
}).isRequired,
addChartData: PropTypes.func.isRequired,
diff --git a/src/constants/index.js b/src/constants/index.js
index 774dfd2..e54d3c8 100644
--- a/src/constants/index.js
+++ b/src/constants/index.js
@@ -5,6 +5,7 @@ export const initialValues = {
englishLevel: '',
};
+export const ListSalaries = ['700', '800', '900', '1000', '1200', '1300', '1400', '1500'];
export const values1 = [
{
salariesBottom20: 867,
@@ -26,4 +27,4 @@ export const values2 = [
},
];
-export const currencyName = 'USD';
\ No newline at end of file
+export const currencyName = 'USD';
diff --git a/src/helpers/index.js b/src/helpers/index.js
index 79813da..3143488 100644
--- a/src/helpers/index.js
+++ b/src/helpers/index.js
@@ -1,4 +1,58 @@
export const disabled = (values) => {
- const { title_id, technologies, seniority, english_level } = values;
- return Boolean(!title_id || !technologies.length || !seniority || !english_level);
+ const { title_name, technologies, seniority, english_level } = values;
+ return Boolean(!title_name || !technologies.length || !seniority || !english_level);
+};
+
+export const helpCurrency = (number) => `$ ${Intl.NumberFormat().format(number)}`;
+
+/* eslint-disable camelcase */
+export const countPage = (data) => {
+ const numberPerPage = Math.ceil(data.length / 10);
+ return numberPerPage;
+};
+
+export const scrollTop = () => {
+ if (window === 'undefined') return;
+ window.scroll({ top: 0, behavior: 'smooth' });
+};
+
+export const helpColor = (id) => {
+ const status = {
+ 1: 'info',
+ 2: 'warning',
+ 3: 'success',
+ 4: 'error',
+ };
+ if (status[id]) {
+ return status[id];
+ }
+
+ return null;
+};
+
+export const sum = (app) => {
+ const { company_id, applicant_name, id, applicant_id, created_at, ...rest } = app;
+ const sumTotal = Object.keys(rest).reduce((acc, key) => acc + parseFloat(rest[key] || 0), 0);
+ return (sumTotal / 7).toFixed(1);
+};
+
+export const isDisabledClear = (obj) => {
+ const { is_remote, ...rest } = obj;
+ const empty = Object.entries(rest).some(([, value]) => {
+ if (Array.isArray(value)) {
+ return !!value.length;
+ }
+ return !!value;
+ });
+
+ return !empty;
+};
+
+export const disabledButton = (data) => {
+ return Object.entries(data).every(([, value]) => {
+ if (value && typeof value === 'object') {
+ return Object.entries(value).every(([, val]) => val === null || val === '');
+ }
+ return value === null || value === '';
+ });
};
diff --git a/src/pages/Salaries.jsx b/src/pages/Salaries.jsx
index 7e4d8b5..b3e9d0a 100644
--- a/src/pages/Salaries.jsx
+++ b/src/pages/Salaries.jsx
@@ -1,12 +1,17 @@
import React, { useState, Fragment } from 'react';
import Tabs from '@mui/material/Tabs';
+import Link from '@mui/material/Link';
+import MenuList from '@mui/material/MenuList';
+import MenuItem from '@mui/material/MenuItem';
+import ListItemIcon from '@mui/material/ListItemIcon';
+import Typography from '@mui/material/Typography';
import Tab from '@mui/material/Tab';
import Box from '@mui/material/Box';
import Container from '@mui/material/Container';
-import { Switch, Currency } from '@master-c8/icons';
-import { HeaderJob } from '@master-c8/commons';
+import { Switch, Currency, Home } from '@master-c8/icons';
+import { Header } from '@master-c8/commons';
import { TabPanel } from 'Components/Commons/Tabs';
import TabCompare from 'Components/TabCompare';
@@ -16,14 +21,34 @@ import Currencies from 'Components/Currencies';
const Salaries = () => {
const [tabs, setValue] = useState(0);
- const [open, setOpen] = useState(false);
- const handleOpen = () => setOpen(!open);
const handleChange = (_, newValue) => setValue(newValue);
return (
-
+
+
+
+
+
diff --git a/src/routes/index.jsx b/src/routes/index.jsx
index a2dc263..30049b6 100644
--- a/src/routes/index.jsx
+++ b/src/routes/index.jsx
@@ -5,7 +5,7 @@ import Salaries from 'Pages/Salaries';
import NotFound from 'Pages/NotFound';
const Routes = () => (
-
+
} />
} />
diff --git a/src/services/currency.js b/src/services/currency.js
index 60cf395..c55ab41 100644
--- a/src/services/currency.js
+++ b/src/services/currency.js
@@ -1,8 +1,8 @@
import { get } from 'Services/http';
-const API_URL = process.env.CURRENCY_API_URL;
+const API_URL = process.env.SALARIES_API_URL;
-export const getCurrencyExchange = async(to, value) => {
+export const getCurrencyExchange = async (to, value) => {
const url = `${API_URL}exchange?Code%20Name=${to}&Value%20to%20exchange=${value}`;
const data = await get(url);
return data.converted_currency[0];
diff --git a/src/services/filters.js b/src/services/filters.js
new file mode 100644
index 0000000..95e9287
--- /dev/null
+++ b/src/services/filters.js
@@ -0,0 +1,18 @@
+import { get } from 'Services/http';
+
+const API_URL = process.env.COMPANIES_API_URL;
+
+export const getListCompanies = async () => {
+ const data = await get(`${API_URL}companies/select`);
+ return data.data;
+};
+
+export const getListTypeWork = async () => {
+ const data = await get(`${API_URL}types-work`);
+ return data.data;
+};
+
+export const getListLocations = async () => {
+ const data = await get(`${API_URL}vacancies-job-location`);
+ return data;
+};
diff --git a/src/services/jobs.js b/src/services/jobs.js
new file mode 100644
index 0000000..94be025
--- /dev/null
+++ b/src/services/jobs.js
@@ -0,0 +1,11 @@
+import axios from 'axios';
+
+const API_URL = process.env.COMPANIES_API_URL;
+
+export const getJobs = async (params) => {
+ const {
+ data: { data },
+ } = await axios.post(`${API_URL}filter`, null, { params });
+ const statusActive = data?.filter((vacancy) => vacancy.status === true);
+ return statusActive;
+};
diff --git a/webpack.config.js b/webpack.config.js
index b316c90..bf5c6a4 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -3,7 +3,9 @@ const HtmlWebPackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
-const Dotenv = require('dotenv-webpack');
+// const Dotenv = require('dotenv-webpack');
+const webpack = require('webpack');
+require('dotenv').config({ path: './.env' });
module.exports = {
entry: './src/index.jsx',
@@ -11,7 +13,6 @@ module.exports = {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
chunkFilename: '[name].bundle.js',
- publicPath: '/'
},
resolve: {
extensions: ['.js', '.jsx'],
@@ -25,7 +26,7 @@ module.exports = {
Hooks: path.resolve(__dirname, './src/hooks'),
Helpers: path.resolve(__dirname, './src/helpers'),
Services: path.resolve(__dirname, './src/services'),
- Libs: path.resolve(__dirname, './src/libs')
+ Libs: path.resolve(__dirname, './src/libs'),
},
},
devServer: {
@@ -106,8 +107,16 @@ module.exports = {
analyzerMode: 'disabled',
generateStatsFile: true,
}),
- new Dotenv({
- path: './.env',
+ // new Dotenv({
+ // path: './.env',
+ // }),
+ new webpack.DefinePlugin({
+ 'process.env': JSON.stringify(process.env),
}),
+ // new webpack.DefinePlugin({
+ // 'process.env': {
+ // SALARIES_API_URL: JSON.stringify(process.env.SALARIES_API_URL),
+ // },
+ // }),
],
};
diff --git a/yarn.lock b/yarn.lock
index 0b76859..251c7eb 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -969,6 +969,13 @@
dependencies:
regenerator-runtime "^0.13.4"
+"@babel/runtime@^7.17.2":
+ version "7.17.2"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.2.tgz#66f68591605e59da47523c631416b18508779941"
+ integrity sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw==
+ dependencies:
+ regenerator-runtime "^0.13.4"
+
"@babel/template@^7.16.0", "@babel/template@^7.16.7", "@babel/template@^7.3.3":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155"
@@ -1527,10 +1534,10 @@
"@types/yargs" "^16.0.0"
chalk "^4.0.0"
-"@master-c8/commons@^0.1.8":
- version "0.1.8"
- resolved "https://registry.yarnpkg.com/@master-c8/commons/-/commons-0.1.8.tgz#27ca61e7bb565696f62e4c6fbe68af45d163ca1d"
- integrity sha512-1SH8WD7YEsV0YKE9OYZnmSW15Mf9gOI2X8kABY975UGx0tb3Xj9FvPfWKVuDAUmq7oBk4E4OaKyZy5Ub8K8E9Q==
+"@master-c8/commons@^0.1.10":
+ version "0.1.10"
+ resolved "https://registry.yarnpkg.com/@master-c8/commons/-/commons-0.1.10.tgz#403c5bba46ebd7d77839dd6f1a8884f40471fb8a"
+ integrity sha512-fCEZfGj3Z6TyvD5h8bcE2hB/KAeA/qZ5zyeLSDac+a7eFH4heGcCeoEhRcHNP0GlUAeyJ7HGFD7Qad8Afp/piA==
dependencies:
"@master-c8/icons" "0.1.3"
"@master-c8/theme" "0.1.9"
@@ -1719,11 +1726,11 @@
react-is "^17.0.2"
"@mui/utils@^5.3.0":
- version "5.3.0"
- resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.3.0.tgz#5f31915063d25c56f1d3ba9e289bf447472a868c"
- integrity sha512-O/E9IQKPMg0OrN7+gkn7Ga5o5WA2iXQGdyqNBFPNrYzxOvwzsEtM5K7MtTCGGYKFe8mhTRM0ZOjh5OM0dglw+Q==
+ version "5.4.4"
+ resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.4.4.tgz#bd7dde4f48f60c02b6debf976bd74f3505b188fe"
+ integrity sha512-hfYIXEuhc2mXMGN5nUPis8beH6uE/zl3uMWJcyHX0/LN/+QxO9zhYuV6l8AsAaphHFyS/fBv0SW3Nid7jw5hKQ==
dependencies:
- "@babel/runtime" "^7.16.7"
+ "@babel/runtime" "^7.17.2"
"@types/prop-types" "^15.7.4"
"@types/react-is" "^16.7.1 || ^17.0.0"
prop-types "^15.7.2"
@@ -2584,6 +2591,13 @@ axe-core@^4.3.5:
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.3.5.tgz#78d6911ba317a8262bfee292aeafcc1e04b49cc5"
integrity sha512-WKTW1+xAzhMS5dJsxWkliixlO/PqC4VhmO9T4juNYcaTg9jzWiJsou6m5pxWYGfigWbwzJWeFY6z47a+4neRXA==
+axios@^0.26.0:
+ version "0.26.0"
+ resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.0.tgz#9a318f1c69ec108f8cd5f3c3d390366635e13928"
+ integrity sha512-lKoGLMYtHvFrPVt3r+RBMp9nh34N0M8zEfCWqdWZx6phynIEhQqAdydpyBAAG211zlhX9Rgu08cOamy6XjE5Og==
+ dependencies:
+ follow-redirects "^1.14.8"
+
axobject-query@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
@@ -3418,6 +3432,11 @@ data-urls@^2.0.0:
whatwg-mimetype "^2.3.0"
whatwg-url "^8.0.0"
+date-fns@^2.28.0:
+ version "2.28.0"
+ resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.28.0.tgz#9570d656f5fc13143e50c975a3b6bbeb46cd08b2"
+ integrity sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==
+
debug@2.6.9, debug@^2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
@@ -3702,6 +3721,11 @@ dotenv-webpack@^7.0.3:
dependencies:
dotenv-defaults "^2.0.2"
+dotenv@^16.0.0:
+ version "16.0.0"
+ resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.0.tgz#c619001253be89ebb638d027b609c75c26e47411"
+ integrity sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q==
+
dotenv@^8.2.0:
version "8.6.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b"
@@ -4381,6 +4405,11 @@ follow-redirects@^1.0.0:
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.7.tgz#2004c02eb9436eee9a21446a6477debf17e81685"
integrity sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==
+follow-redirects@^1.14.8:
+ version "1.14.9"
+ resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7"
+ integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==
+
forever-agent@~0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
@@ -7120,6 +7149,11 @@ react-dom@17.0.2:
object-assign "^4.1.1"
scheduler "^0.20.2"
+react-hook-form@^7.27.1:
+ version "7.27.1"
+ resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.27.1.tgz#fe5fbcb6bf58751f66d9569e998d671480cc57f6"
+ integrity sha512-N3a7A6zIQ8DJeThisVZGtOUabTbJw+7DHJidmB9w8m3chckv2ZWKb5MHps9d2pPJqmCDoWe53Bos56bYmJms5w==
+
react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"