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` -Screen Shot 2021-11-21 at 11 41 08 PM +## 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) => ( - - {option} - - ))} + + {children || + options?.map((option) => ( + + {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) => ( + + )} + /> + + + + + + + + + + + + + + + level)} - /> + + + {children} + - - - - - - -