Commit dc9fd10f authored by Florent Chehab's avatar Florent Chehab

dropped(redux)

parent ee66c6a0
......@@ -35,7 +35,7 @@ check_back:
check_front:
<<: *only-default
stage: check
image: registry.gitlab.utc.fr/rex-dri/rex-dri/frontend:v1.1.1
image: registry.gitlab.utc.fr/rex-dri/rex-dri/frontend:v1.2.0
before_script:
- cd frontend && cp -R /usr/src/deps/node_modules .
script:
......@@ -77,7 +77,7 @@ test_back:
test_frontend:
<<: *only-default
stage: test
image: registry.gitlab.utc.fr/rex-dri/rex-dri/frontend:v1.1.1
image: registry.gitlab.utc.fr/rex-dri/rex-dri/frontend:v1.2.0
before_script:
- cd frontend && cp -R /usr/src/deps/node_modules .
script:
......@@ -97,7 +97,7 @@ flake8:
eslint:
<<: *only-default
stage: lint
image: registry.gitlab.utc.fr/rex-dri/rex-dri/frontend:v1.1.1
image: registry.gitlab.utc.fr/rex-dri/rex-dri/frontend:v1.2.0
before_script:
- cd frontend && cp -R /usr/src/deps/node_modules .
script:
......
......@@ -68,7 +68,7 @@ services:
# Service to handle frontend live developpments and building
frontend:
# Get the image from the registry
image: registry.gitlab.utc.fr/rex-dri/rex-dri/frontend:v1.1.1
image: registry.gitlab.utc.fr/rex-dri/rex-dri/frontend:v1.2.0
# To use a locally built one, comment above, uncomment bellow.
# build: ./frontend
# On startup, we retrieve the dependencies from the image and start the developpement server
......
......@@ -49,12 +49,8 @@
"react-dom": "^16.9.0",
"react-mapbox-gl": "^4.6.0",
"react-markdown": "^4.1.0",
"react-redux": "^7.1.1",
"react-router-dom": "^5.0.1",
"recompose": "^0.30.0",
"redux": "^4.0.4",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.3.0",
"regenerator-runtime": "^0.13.3",
"typeface-roboto": "0.0.75",
"uuid": "^3.3.3"
......
......@@ -9,7 +9,7 @@ import ListItem from "@material-ui/core/ListItem";
import ListItemIcon from "@material-ui/core/ListItemIcon";
import ListItemText from "@material-ui/core/ListItemText";
import PropTypes from "prop-types";
import RequestParams from "../../redux/api/RequestParams";
import RequestParams from "../../utils/api/RequestParams";
import { withErrorBoundary } from "../common/ErrorBoundary";
import toDateFr from "../../utils/dateToFr";
import withNetworkWrapper, { NetWrapParam } from "../../hoc/withNetworkWrapper";
......
......@@ -2,7 +2,7 @@ import React from "react";
import { compose } from "recompose";
import PropTypes from "prop-types";
import { withErrorBoundary } from "../common/ErrorBoundary";
import RequestParams from "../../redux/api/RequestParams";
import RequestParams from "../../utils/api/RequestParams";
import withNetworkWrapper, { NetWrapParam } from "../../hoc/withNetworkWrapper";
import NotificationService from "../../services/NotificationService";
......
import React, { useEffect } from "react";
import PropTypes from "prop-types";
import useDeleteOne from "../../hooks/useDeleteOne";
import AlertService from "../../services/AlertService";
import { useApiDelete } from "../../hooks/wrappers/api";
// TODO component is Useless...
function DeleteHandler({ performClose, route, id }) {
const performDelete = useDeleteOne(route);
const performDelete = useApiDelete(route);
useEffect(() => {
AlertService.open({
info: false,
......
import React from "react";
import PropTypes from "prop-types";
import { setDisplayName } from "recompose";
import compose from "recompose/compose";
import { connect } from "react-redux";
import { withRouter } from "react-router-dom";
import RequestParams from "../../redux/api/RequestParams";
import getActions from "../../redux/api/getActions";
import APP_ROUTES from "../../config/appRoutes";
import AlertService from "../../services/AlertService";
import NavigationService from "../../services/NavigationService";
import { useApiCreate } from "../../hooks/wrappers/api";
function clear() {
return {
......@@ -31,8 +27,7 @@ class ErrorBoundary extends React.Component {
const data = "stack" in error ? { componentStack: error.stack } : errorInfo;
const params = RequestParams.Builder.withData(data).build();
this.props.logErrorOnServer(params);
this.props.logErrorOnServer(data);
}
render() {
......@@ -53,7 +48,7 @@ class ErrorBoundary extends React.Component {
handleResponse: agreed => {
this.setState(clear());
// May need to click twice, but there seem to be no other ways
if (!agreed) this.props.history.push(APP_ROUTES.base);
if (!agreed) NavigationService.goHome();
}
});
......@@ -67,23 +62,9 @@ class ErrorBoundary extends React.Component {
ErrorBoundary.propTypes = {
children: PropTypes.node.isRequired,
logErrorOnServer: PropTypes.func.isRequired,
history: PropTypes.object.isRequired
logErrorOnServer: PropTypes.func.isRequired
};
const mapDispatchToProps = dispatch => ({
logErrorOnServer: params =>
dispatch(getActions("frontendErrors").create(params))
});
const ConnectedErrorBoundary = compose(
connect(
() => ({}),
mapDispatchToProps
),
withRouter
)(ErrorBoundary);
/**
* HOC (higher order component) wrapper to provide an error boundary to the sub components.
*
......@@ -95,10 +76,14 @@ export function withErrorBoundary() {
setDisplayName("error-boundary")(
// We need to forward the ref otherwise the styles are not correctly applied.
// eslint-disable-next-line react/display-name
React.forwardRef((props, ref) => (
<ConnectedErrorBoundary>
<Component {...props} ref={ref} />
</ConnectedErrorBoundary>
))
React.forwardRef((props, ref) => {
const logErrorOnServer = useApiCreate("frontendErrors");
return (
<ErrorBoundary logErrorOnServer={logErrorOnServer}>
<Component {...props} ref={ref} />
</ErrorBoundary>
);
})
);
}
......@@ -3,8 +3,8 @@
import React from "react";
import parseMoney from "../../../utils/parseMoney";
import convertAmountToEur from "../../../utils/convertAmountToEur";
import BaseMarkdown from "./BaseMarkdown";
import CurrencyService from "../../../services/data/CurrencyService";
function compileSource(source) {
let compiled = "";
......@@ -18,7 +18,7 @@ function compileSource(source) {
if (currency === "EUR") {
compiled += `${amount}€`;
} else {
const converted = convertAmountToEur(amount, currency);
const converted = CurrencyService.convertAmountToEur(amount, currency);
compiled += `${amount} ${currency} `;
if (converted === null) {
compiled += `*(\`${currency}\` n'a pas été reconnue comme le code d'une monnaie ; nous n'avons pas pu procéder à une conversion automatique)*`;
......
......@@ -3,7 +3,7 @@ import PropTypes from "prop-types";
import MuiThemeProvider from "@material-ui/core/styles/MuiThemeProvider";
import { getTheme, updatePhoneStatusBarColor } from "./utils";
import RequestParams from "../../../redux/api/RequestParams";
import RequestParams from "../../../utils/api/RequestParams";
import { CURRENT_USER_ID } from "../../../config/user";
import withNetworkWrapper, {
NetWrapParam
......
......@@ -232,7 +232,7 @@ class Editor extends React.Component {
Editor.propTypes = {
subscribeToModuleWrapper: PropTypes.func,
rawModelData: PropTypes.object.isRequired,
// props added in subclasses but are absolutely required to handle redux
// props added in subclasses but are absolutely required to handle state managment
savingHasError: PropTypes.object.isRequired,
clearSaveError: PropTypes.func.isRequired,
lastUpdateTimeInModel: PropTypes.string,
......
......@@ -10,16 +10,16 @@ import KeyboardArrowRight from "@material-ui/icons/KeyboardArrowRight";
import { makeStyles } from "@material-ui/styles";
import Typography from "@material-ui/core/Typography";
import Divider from "@material-ui/core/Divider";
import RequestParams from "../../redux/api/RequestParams";
import RequestParams from "../../utils/api/RequestParams";
import FullScreenDialogService from "../../services/FullScreenDialogService";
import useStepper from "../../hooks/useStepper";
import useInvalidateAll from "../../hooks/useInvalidateAll";
import FullScreenDialogFrame from "../common/FullScreenDialogFrame";
import dateTimeStrToStr from "../../utils/dateTimeStrToStr";
import withNetworkWrapper, {
getApiPropTypes,
NetWrapParam
} from "../../hoc/withNetworkWrapper";
import { useApiInvalidateAll } from "../../hooks/wrappers/api";
const useStyles = makeStyles(theme => ({
editButton: {
......@@ -88,7 +88,7 @@ function History({
editFromVersion,
rawModelDataEx
}) {
const resetVersions = useInvalidateAll("versions");
const resetVersions = useApiInvalidateAll("versions");
useEffect(() => {
return resetVersions(); // only on unmount
}, []);
......
......@@ -6,14 +6,14 @@ import Button from "@material-ui/core/Button";
import Typography from "@material-ui/core/Typography";
import Divider from "@material-ui/core/Divider";
import { makeStyles } from "@material-ui/styles";
import RequestParams from "../../redux/api/RequestParams";
import RequestParams from "../../utils/api/RequestParams";
import FullScreenDialogService from "../../services/FullScreenDialogService";
import FullScreenDialogFrame from "../common/FullScreenDialogFrame";
import withNetworkWrapper, {
getApiPropTypes,
NetWrapParam
} from "../../hoc/withNetworkWrapper";
import useInvalidateAll from "../../hooks/useInvalidateAll";
import { useApiInvalidateAll } from "../../hooks/wrappers/api";
const useStyles = makeStyles(theme => ({
editButton: {
......@@ -77,7 +77,7 @@ function PendingModeration({
renderCore,
userCanModerate
}) {
const resetData = useInvalidateAll("pendingModerationObj");
const resetData = useApiInvalidateAll("pendingModerationObj");
useEffect(() => {
return resetData();
......
......@@ -4,12 +4,12 @@ import TextField from "./TextField";
import MultiSelectField from "./MultiSelectField";
import UniversityService from "../../../services/data/UniversityService";
import MarkdownField from "./MarkdownField";
import { getLatestReadDataFromStore } from "../../../redux/api/utils";
import getObjModerationLevel from "../../../utils/getObjModerationLevels";
import SelectField from "./SelectField";
import CountryService from "../../../services/data/CountryService";
import CurrencyService from "../../../services/data/CurrencyService";
import LanguageService from "../../../services/data/LanguageService";
import { getLatestApiReadData } from "../../../hooks/usePersistentState";
export function TitleField() {
return <TextField required fieldMapping="title" label="Titre" />;
......@@ -71,7 +71,7 @@ export function LanguageField() {
export function ObjModerationLevelField() {
// hack to access directly the store and get the value we need.
const userData = getLatestReadDataFromStore("userDataOne");
const userData = getLatestApiReadData("userData-one");
const possibleObjModeration = getObjModerationLevel(
userData.owner_level,
true
......
import React, { useEffect } from "react";
import { useDispatch } from "react-redux";
import ExpansionPanel from "@material-ui/core/ExpansionPanel";
import ExpansionPanelSummary from "@material-ui/core/ExpansionPanelSummary";
import ExpansionPanelDetails from "@material-ui/core/ExpansionPanelDetails";
......@@ -8,11 +6,11 @@ import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
import Typography from "@material-ui/core/Typography";
import uuid from "uuid/v4";
import { makeStyles } from "@material-ui/styles";
import { saveSelectedUniversities } from "../../redux/actions/filter";
import DownshiftMultiple from "../common/DownshiftMultiple";
import usePersistentState from "../../hooks/usePersistentState";
import FilterService from "../../services/FilterService";
import FilterStatus from "./FilterStatus";
import { useSetSelectedUniversities } from "../../hooks/wrappers/useSelectedUniversities";
const useStyles = makeStyles(theme => ({
root: {
......@@ -43,7 +41,7 @@ const DOWNSHIFT_MAJORS_ID = uuid();
*/
function Filter() {
const classes = useStyles();
const dispatch = useDispatch();
const saveSelectedUniversities = useSetSelectedUniversities();
const [isOpened, setIsOpened] = usePersistentState("filter-open", false);
const [countries, setCountries] = usePersistentState("filter-countries", []);
......@@ -66,10 +64,7 @@ function Filter() {
const hasSelection = [countries, semesters, majorMinors].some(
arr => arr.length !== 0
);
dispatch(
saveSelectedUniversities(hasSelection ? selectedUniversities : null)
);
saveSelectedUniversities(hasSelection ? selectedUniversities : null);
}, [countries, semesters, majorMinors]);
const {
......
......@@ -2,7 +2,7 @@ import InfoIcon from "@material-ui/icons/InfoOutlined";
import Typography from "@material-ui/core/Typography";
import React from "react";
import { makeStyles } from "@material-ui/styles";
import { useSelector } from "react-redux";
import { useSelectedUniversities } from "../../hooks/wrappers/useSelectedUniversities";
const useStyles = makeStyles(theme => ({
infoFilter: {
......@@ -27,9 +27,7 @@ function getMessage(selectedUniversities) {
function FilterStatus() {
const classes = useStyles();
const selectedUniversities = useSelector(
state => state.app.selectedUniversities
);
const [selectedUniversities] = useSelectedUniversities();
const hasSelection = selectedUniversities !== null;
return (
......
......@@ -18,7 +18,7 @@ const Map = ReactMapboxGl({
* If an id is provided, the state of the map will be automatically saved and regenerated.
*/
class BaseMap extends Component {
// Static variable to hold the map center in a generic way without redux
// Static variable to hold the map center in a generic way without globalState
static allMaps = {};
// campusesMarkers = [];
......
import React, { useMemo } from "react";
import { useSelector } from "react-redux";
import uuid from "uuid/v4";
import BaseMap from "./BaseMap";
import "./map.scss";
import UniversityService from "../../services/data/UniversityService";
import { useSelectedUniversities } from "../../hooks/wrappers/useSelectedUniversities";
const MAIN_MAP_ID = uuid();
......@@ -13,7 +13,7 @@ const MAIN_MAP_ID = uuid();
* Main map of the application (map tab)
*/
function MainMap() {
const listUnivSel = useSelector(state => state.app.selectedUniversities);
const [listUnivSel] = useSelectedUniversities();
const mainCampusesSelection = useMemo(() => {
const out = [];
......@@ -43,7 +43,7 @@ function MainMap() {
});
return out;
});
}, [listUnivSel]);
// create all the markers
return <BaseMap id={MAIN_MAP_ID} campuses={mainCampusesSelection} />;
......
......@@ -6,7 +6,7 @@ import { makeStyles } from "@material-ui/styles";
import { withErrorBoundary } from "../common/ErrorBoundary";
import { withPaddedPaper } from "./shared";
import EditModuleGeneralPreviousExchangeFeedback from "../university/modules/previousExchangeFeedback/edit/EditModuleGeneralFeedback";
import RequestParams from "../../redux/api/RequestParams";
import RequestParams from "../../utils/api/RequestParams";
import EditModuleCoursesFeedback from "../university/modules/previousExchangeFeedback/edit/EditModuleCoursesFeedback";
import CustomLink from "../common/CustomLink";
import APP_ROUTES from "../../config/appRoutes";
......
......@@ -4,9 +4,8 @@ import Paper from "@material-ui/core/Paper";
import { compose } from "recompose";
import { Typography } from "@material-ui/core";
import { makeStyles } from "@material-ui/styles";
import RequestParams from "../../redux/api/RequestParams";
import RequestParams from "../../utils/api/RequestParams";
import Pictures from "../user/Pictures";
import useInvalidateAll from "../../hooks/useInvalidateAll";
import withNetworkWrapper, { NetWrapParam } from "../../hoc/withNetworkWrapper";
const useStyles = makeStyles(theme => ({
......@@ -36,13 +35,12 @@ const buildFilesParams = match =>
// eslint-disable-next-line no-unused-vars
function PageFiles({ pictures, files }) {
const classes = useStyles();
const invalidate = useInvalidateAll("pictures", "files");
return (
<>
<Paper className={classes.paper}>
<Typography variant="h3">Photos</Typography>
<Pictures pictures={pictures} onSomethingWasSaved={invalidate} />
<Pictures pictures={pictures} onSomethingWasSaved={() => {}} />
</Paper>
{/* <Paper style={theme.myPaper}> */}
{/* <Typography variant={"h3"}>Fichiers</Typography> */}
......
import React, { useCallback } from "react";
import { compose } from "recompose";
import { useDispatch, useSelector } from "react-redux";
import Typography from "@material-ui/core/Typography";
import ListItemText from "@material-ui/core/ListItemText";
import ListItemIcon from "@material-ui/core/ListItemIcon";
......@@ -15,26 +14,28 @@ import Loading from "../common/Loading";
import { withErrorBoundary } from "../common/ErrorBoundary";
import { withPaddedPaper } from "./shared";
import { CURRENT_USER_ID } from "../../config/user";
import RequestParams from "../../redux/api/RequestParams";
import getActions from "../../redux/api/getActions";
import RequestParams from "../../utils/api/RequestParams";
import compareSemesters from "../../utils/compareSemesters";
import TextLink from "../common/TextLink";
import UniversityService from "../../services/data/UniversityService";
import useInvalidateAll from "../../hooks/useInvalidateAll";
import NavigationService from "../../services/NavigationService";
import withNetworkWrapper, { NetWrapParam } from "../../hoc/withNetworkWrapper";
import {
useApi,
useApiInvalidateAll,
useApiRead
} from "../../hooks/wrappers/api";
// TODO check if reload still works
function Introduction() {
const dispatch = useDispatch();
const invalidateData = useInvalidateAll("exchanges");
const invalidateData = useApiInvalidateAll("exchanges");
const [, read] = useApiRead("updateStudentExchanges", "all");
const requestReload = useCallback(() => {
dispatch(
getActions("updateStudentExchanges").readAll(
RequestParams.Builder.withOnSuccessCallback(() =>
invalidateData()
).build()
)
read(
RequestParams.Builder.withOnSuccessCallback(() =>
invalidateData()
).build()
);
}, []);
......@@ -70,8 +71,9 @@ function Introduction() {
* Page that lists the previous exchange of a user
*/
function PageMyExchanges({ exchanges }) {
const isReloadingFromEnt = useSelector(
state => state.api.updateStudentExchangesAll.isReading
const [{ isReading: isReloadingFromEnt }] = useApi(
"updateStudentExchanges",
"all"
);
exchanges.sort(
......
......@@ -17,12 +17,11 @@ import APP_ROUTES from "../../config/appRoutes";
import SimplePopupMenu from "../common/SimplePopupMenu";
import LinkToUser from "../common/LinkToUser";
import { CURRENT_USER_ID } from "../../config/user";
import useCreateOne from "../../hooks/useCreateOne";
import useDeleteOne from "../../hooks/useDeleteOne";
import useInvalidateAll from "../../hooks/useInvalidateAll";
import NavigationService from "../../services/NavigationService";
import withNetworkWrapper, { NetWrapParam } from "../../hoc/withNetworkWrapper";
import { useApiCreate, useApiDelete } from "../../hooks/wrappers/api";
const emptyList = {
title: "Une nouvelle liste",
is_public: false,
......@@ -52,13 +51,12 @@ function SelectListSubPage({ lists }) {
const [display, setDisplay] = useState("owned");
const classes = useStyles();
const createList = useCreateOne("recommendationLists");
const deleteList = useDeleteOne("recommendationLists");
const invalidateData = useInvalidateAll("recommendationLists");
const createList = useApiCreate("recommendationLists");
const deleteList = useApiDelete("recommendationLists");
const goToList = useCallback(listId => {
NavigationService.goToRoute(APP_ROUTES.forList(listId));
});
}, []);
const ownedLists = lists.filter(list => list.is_user_owner);
const followedLists = lists.filter(list => !list.is_user_owner);
......@@ -88,7 +86,6 @@ function SelectListSubPage({ lists }) {
onClick={() =>
createList(emptyList, data => {
goToList(data.id);
invalidateData();
})
}
className={classes.button}
......@@ -130,10 +127,7 @@ function SelectListSubPage({ lists }) {
{
disabled: false,
label: "Confirmer",
onClick: () =>
deleteList(list.id, () => {
invalidateData();
})
onClick: () => deleteList(list.id)
}
]}
renderHolder={({ onClick }) => (
......
......@@ -3,7 +3,7 @@ import PropTypes from "prop-types";
import { compose } from "recompose";
import Button from "@material-ui/core/Button";
import ArrowBackIcon from "@material-ui/icons/ArrowBack";
import RequestParams from "../../redux/api/RequestParams";
import RequestParams from "../../utils/api/RequestParams";
import List from "./view/View";
import withNetworkWrapper, {
getApiPropTypes,
......
......@@ -6,13 +6,11 @@
*
* Some hacks to optimize performances and keep performances on the top
*/
import React, { useEffect, useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
import PropTypes from "prop-types";
import Button from "@material-ui/core/Button";
import AddIcon from "@material-ui/icons/Add";
import { compose } from "recompose";
import { connect } from "react-redux";
import { Typography } from "@material-ui/core";
import IconButton from "@material-ui/core/IconButton";
import KeyboardArrowDownIcon from "@material-ui/icons/KeyboardArrowDown";
......@@ -33,12 +31,16 @@ import CopyToClipboard from "../../common/CopyToClipboard";
import SimplePopupMenu from "../../common/SimplePopupMenu";
import { appBarHeight } from "../../../config/sharedStyles";
import SaveButton from "../../common/SaveButton";
import RequestParams from "../../../redux/api/RequestParams";
import getActions from "../../../redux/api/getActions";
import UnivBlock from "./UnivBlock";
import TextBlock from "./TextBlock";
import LinkToUser from "../../common/LinkToUser";
import LicenseNotice from "../../common/LicenseNotice";
import {
useApiDelete,
useApiInvalidateAll,
useApiInvalidateOne,
useApiUpdate
} from "../../../hooks/wrappers/api";
const FORGET_SAVE_MESSAGE = "Des changements n'ont pas été enregistré !";
......@@ -674,40 +676,35 @@ const styles = theme => ({
}
});
const mapDispatchToProps = dispatch => ({
updateListOnServer: (data, onSuccess) => {
const params = RequestParams.Builder.withOnSuccessCallback(onSuccess)
.withId(data.id)
.withData(data)
.build();
return dispatch(getActions("recommendationLists").update(params));
},
followList: (listId, onSuccess) => {
const params = RequestParams.Builder.withOnSuccessCallback(onSuccess)
.withId(listId)
.build();
return dispatch(
getActions("recommendationListChangeFollower").update(params)
);
},
unFollowList: (listId, onSuccess) => {
const params = RequestParams.Builder.withOnSuccessCallback(onSuccess)
.withId(listId)
.build();
return dispatch(
getActions("recommendationListChangeFollower").delete(params)
);
},
invalidateList: () =>
dispatch(getActions("recommendationLists").invalidateOne()),
invalidateListsSummary: () =>
dispatch(getActions("recommendationLists").invalidateAll())
});
const StyledView = withStyles(styles)(View);
export default compose(
withStyles(styles),
connect(
() => ({}),
mapDispatchToProps
)
)(View);
// eslint-disable-next-line react/prop-types
export default ({ list }) => {
const invalidateList = useApiInvalidateOne("recommendationLists");
const invalidateListsSummary = useApiInvalidateAll("recommendationLists");
const updateListOnServerInt = useApiUpdate("recommendationLists");
const updateListOnServer = useCallback(
(data, onSuccess) => updateListOnServerInt(data.id, data, onSuccess),
[]
);
const followListInt = useApiUpdate("recommendationListChangeFollower");
const followList = useCallback(
(listId, onSuccess) => followListInt(listId, {}, onSuccess),
[]
);
const unFollowList = useApiDelete("recommendationListChangeFollower");
return (
<StyledView
list={list}
invalidateList={invalidateList}
invalidateListsSummary={invalidateListsSummary}
updateListOnServer={updateListOnServer}
followList={followList}
unFollowList={unFollowList}
/>
);
};
import React, { useCallback, useMemo, useState } from "react";
import TextField from "@material-ui/core/TextField";
import { useSelector } from "react-redux";
import fuzzysort from "fuzzysort";
import Typography from "@material-ui/core/Typography";
import { makeStyles } from "@material-ui/styles";
import UnivList from "./UnivList";
import UniversityService from "../../services/data/UniversityService";
import { useSelectedUniversities } from "../../hooks/wrappers/useSelectedUniversities";
const useStyles = makeStyles({
inputCentered: {
......@@ -26,9 +25,7 @@ function Search() {
setInputValue(e.target.value);
}, []);
const selectedUniversities = useSelector(
state => state.app.selectedUniversities
);
const [selectedUniversities] = useSelectedUniversities();
const suggestions = useMemo(() => {
const universities = UniversityService.getUniversities();
......
/* eslint-disable react/sort-comp */
// Inspired by from https://github.com/mui-org/material-ui/blob/master/docs/src/pages/style/color/ColorTool.js
import React from "react";
import React, { useCallback } from "react";