From 848668bc3c9e7a1acb2749c9c2749c565a217105 Mon Sep 17 00:00:00 2001
From: maxceem <maxceem@gmail.com>
Date: Thu, 17 Dec 2020 13:31:46 +0200
Subject: [PATCH 01/23] feat: shortlist/reject candidates

ref issue #25
---
 package-lock.json                             | 88 +++++++++++++++++
 package.json                                  |  9 +-
 src/constants/index.js                        | 17 ++++
 src/reducers/index.js                         | 13 +++
 src/root.component.jsx                        | 28 ++++--
 src/routes/PositionDetails/actions/index.js   | 64 ++++++++++++
 .../CandidatesStatusFilter/index.jsx          |  0
 .../CandidatesStatusFilter/styles.module.scss |  0
 .../PositionCandidates/index.jsx              | 66 ++++++++++---
 .../PositionCandidates/styles.module.scss     |  0
 .../hooks/useTeamPositionsState.js            | 38 +++++++
 src/routes/PositionDetails/index.jsx          | 26 ++---
 src/routes/PositionDetails/reducers/index.js  | 99 +++++++++++++++++++
 src/services/teams.js                         | 26 ++++-
 src/store.js                                  | 28 ++++++
 src/styles/main.module.scss                   |  2 +-
 src/styles/main.vendor.scss                   |  4 +
 17 files changed, 468 insertions(+), 40 deletions(-)
 create mode 100644 src/reducers/index.js
 create mode 100644 src/routes/PositionDetails/actions/index.js
 rename src/routes/PositionDetails/{ => components}/CandidatesStatusFilter/index.jsx (100%)
 rename src/routes/PositionDetails/{ => components}/CandidatesStatusFilter/styles.module.scss (100%)
 rename src/routes/PositionDetails/{ => components}/PositionCandidates/index.jsx (73%)
 rename src/routes/PositionDetails/{ => components}/PositionCandidates/styles.module.scss (100%)
 create mode 100644 src/routes/PositionDetails/hooks/useTeamPositionsState.js
 create mode 100644 src/routes/PositionDetails/reducers/index.js
 create mode 100644 src/store.js
 create mode 100644 src/styles/main.vendor.scss

diff --git a/package-lock.json b/package-lock.json
index 9133be21..939b1bc4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5708,6 +5708,11 @@
       "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
       "dev": true
     },
+    "deep-diff": {
+      "version": "0.3.8",
+      "resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-0.3.8.tgz",
+      "integrity": "sha1-wB3mPvsO7JeYgB1Ax+Da4ltYLIQ="
+    },
     "deep-equal": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz",
@@ -7819,6 +7824,14 @@
         "minimalistic-crypto-utils": "^1.0.1"
       }
     },
+    "hoist-non-react-statics": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
+      "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+      "requires": {
+        "react-is": "^16.7.0"
+      }
+    },
     "homedir-polyfill": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
@@ -7999,6 +8012,11 @@
       "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
       "dev": true
     },
+    "immutability-helper": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/immutability-helper/-/immutability-helper-3.1.1.tgz",
+      "integrity": "sha512-Q0QaXjPjwIju/28TsugCHNEASwoCcJSyJV3uO1sOIQGI0jKgm9f41Lvz0DZj3n46cNCyAZTsEYoY4C2bVRUzyQ=="
+    },
     "import-fresh": {
       "version": "3.2.1",
       "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz",
@@ -14295,6 +14313,44 @@
         }
       }
     },
+    "react-redux": {
+      "version": "7.2.2",
+      "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.2.tgz",
+      "integrity": "sha512-8+CQ1EvIVFkYL/vu6Olo7JFLWop1qRUeb46sGtIMDCSpgwPQq8fPLpirIB0iTqFe9XYEFPHssdX8/UwN6pAkEA==",
+      "requires": {
+        "@babel/runtime": "^7.12.1",
+        "hoist-non-react-statics": "^3.3.2",
+        "loose-envify": "^1.4.0",
+        "prop-types": "^15.7.2",
+        "react-is": "^16.13.1"
+      },
+      "dependencies": {
+        "@babel/runtime": {
+          "version": "7.12.5",
+          "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz",
+          "integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==",
+          "requires": {
+            "regenerator-runtime": "^0.13.4"
+          }
+        }
+      }
+    },
+    "react-redux-toastr": {
+      "version": "7.6.5",
+      "resolved": "https://registry.npmjs.org/react-redux-toastr/-/react-redux-toastr-7.6.5.tgz",
+      "integrity": "sha512-tbf8z18O85BGKXhDfYsgNmurbKlP9p7UXjtdBNS1wWqCAY0QNAyW1W5ulChPEFWoBWVzQywgMGIZ5j3an//srg==",
+      "requires": {
+        "classnames": "^2.2.3",
+        "eventemitter3": "^3.1.0"
+      },
+      "dependencies": {
+        "eventemitter3": {
+          "version": "3.1.2",
+          "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz",
+          "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q=="
+        }
+      }
+    },
     "react-universal-interface": {
       "version": "0.6.2",
       "resolved": "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz",
@@ -14393,6 +14449,33 @@
         "strip-indent": "^3.0.0"
       }
     },
+    "redux": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz",
+      "integrity": "sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==",
+      "requires": {
+        "loose-envify": "^1.4.0",
+        "symbol-observable": "^1.2.0"
+      }
+    },
+    "redux-logger": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/redux-logger/-/redux-logger-3.0.6.tgz",
+      "integrity": "sha1-91VZZvMJjzyIYExEnPC69XeCdL8=",
+      "requires": {
+        "deep-diff": "^0.3.5"
+      }
+    },
+    "redux-promise-middleware": {
+      "version": "6.1.2",
+      "resolved": "https://registry.npmjs.org/redux-promise-middleware/-/redux-promise-middleware-6.1.2.tgz",
+      "integrity": "sha512-ZqZu/nnSzGgwTtNbGoGVontpk7LjTOv0kigtt3CcgXI9gpq+8WlfXTXRZD0WTD5yaohRq0q2nYmJXSTjwXs83Q=="
+    },
+    "redux-thunk": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz",
+      "integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw=="
+    },
     "reflect.ownkeys": {
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz",
@@ -16042,6 +16125,11 @@
         }
       }
     },
+    "symbol-observable": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
+      "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ=="
+    },
     "symbol-tree": {
       "version": "3.2.4",
       "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
diff --git a/package.json b/package.json
index e8a9c18e..3a16e2cc 100644
--- a/package.json
+++ b/package.json
@@ -59,6 +59,7 @@
     "axios": "^0.21.0",
     "classnames": "^2.2.6",
     "express": "^4.17.1",
+    "immutability-helper": "^3.1.1",
     "lodash": "^4.17.20",
     "moment": "^2.29.1",
     "prop-types": "^15.7.2",
@@ -67,7 +68,13 @@
     "react-dom": "^16.12.0",
     "react-outside-click-handler": "^1.3.0",
     "react-popper": "^2.2.3",
-    "react-use": "^15.3.4"
+    "react-redux": "^7.2.2",
+    "react-redux-toastr": "^7.6.5",
+    "react-use": "^15.3.4",
+    "redux": "^4.0.5",
+    "redux-logger": "^3.0.6",
+    "redux-promise-middleware": "^6.1.2",
+    "redux-thunk": "^2.3.0"
   },
   "browserslist": [
     "last 1 version",
diff --git a/src/constants/index.js b/src/constants/index.js
index 6ad60999..7e6e5784 100644
--- a/src/constants/index.js
+++ b/src/constants/index.js
@@ -121,3 +121,20 @@ export const CANDIDATES_SORT_OPTIONS = [
   { label: "Skill Matched", value: CANDIDATES_SORT_BY.SKILL_MATCHED },
   { label: "Handle", value: CANDIDATES_SORT_BY.HANDLE },
 ];
+
+/**
+ * All action types
+ */
+export const ACTION_TYPE = {
+  LOAD_POSITION: "LOAD_POSITION",
+  LOAD_POSITION_PENDING: "LOAD_POSITION_PENDING",
+  LOAD_POSITION_SUCCESS: "LOAD_POSITION_SUCCESS",
+  LOAD_POSITION_ERROR: "LOAD_POSITION_ERROR",
+
+  RESET_POSITION_STATE: "RESET_POSITION_STATE",
+
+  UPDATE_CANDIDATE: "UPDATE_CANDIDATE",
+  UPDATE_CANDIDATE_PENDING: "UPDATE_CANDIDATE_PENDING",
+  UPDATE_CANDIDATE_SUCCESS: "UPDATE_CANDIDATE_SUCCESS",
+  UPDATE_CANDIDATE_ERROR: "UPDATE_CANDIDATE_ERROR",
+};
diff --git a/src/reducers/index.js b/src/reducers/index.js
new file mode 100644
index 00000000..7c85976f
--- /dev/null
+++ b/src/reducers/index.js
@@ -0,0 +1,13 @@
+/**
+ * Root Redux Reducer
+ */
+import { combineReducers } from "redux";
+import { reducer as toastrReducer } from "react-redux-toastr";
+import positionDetailsReducer from "../routes/PositionDetails/reducers";
+
+const rootReducer = combineReducers({
+  toastr: toastrReducer,
+  positionDetails: positionDetailsReducer,
+});
+
+export default rootReducer;
diff --git a/src/root.component.jsx b/src/root.component.jsx
index eca449ee..dd6bd948 100644
--- a/src/root.component.jsx
+++ b/src/root.component.jsx
@@ -1,18 +1,32 @@
 import React from "react";
+import { Provider } from "react-redux";
 import { Router } from "@reach/router";
 import MyTeamsList from "./routes/MyTeamsList";
 import MyTeamsDetails from "./routes/MyTeamsDetails";
 import PositionDetails from "./routes/PositionDetails";
-import "./styles/main.module.scss";
+import ReduxToastr from 'react-redux-toastr'
+import store from "./store";
+import "./styles/main.vendor.scss";
+import styles from "./styles/main.module.scss";
 
 export default function Root() {
   return (
-    <div styleName="topcoder-micro-frontends-teams-app">
-      <Router>
-        <MyTeamsList path="/taas/myteams" />
-        <MyTeamsDetails path="/taas/myteams/:teamId" />
-        <PositionDetails path="/taas/myteams/:teamId/positions/:positionId" />
-      </Router>
+    <div className={styles['topcoder-micro-frontends-teams-app']}>
+      <Provider store={store}>
+        <Router>
+          <MyTeamsList path="/taas/myteams" />
+          <MyTeamsDetails path="/taas/myteams/:teamId" />
+          <PositionDetails path="/taas/myteams/:teamId/positions/:positionId" />
+        </Router>
+
+        {/* Global config for Toastr popups */}
+        <ReduxToastr
+          timeOut={4000}
+          position="bottom-left"
+          transitionIn="fadeIn"
+          transitionOut="fadeOut"
+        />
+      </Provider>
     </div>
   );
 }
diff --git a/src/routes/PositionDetails/actions/index.js b/src/routes/PositionDetails/actions/index.js
new file mode 100644
index 00000000..c5278a01
--- /dev/null
+++ b/src/routes/PositionDetails/actions/index.js
@@ -0,0 +1,64 @@
+/**
+ * Position Details page actions
+ */
+import { getPositionDetails, patchPositionCandidate } from "services/teams";
+import { getAuthUserTokens } from "@topcoder/micro-frontends-navbar-app";
+import { ACTION_TYPE } from "constants";
+
+/**
+ * Load Team Position details (team job)
+ *
+ * @param {string} teamId team id
+ * @param {string} positionId position id
+ *
+ * @returns {Promise<any>} loaded data or error
+ */
+export const loadPosition = (teamId, positionId) => ({
+  type: ACTION_TYPE.LOAD_POSITION,
+  payload: async () => {
+    const tokens = await getAuthUserTokens();
+    const response = await getPositionDetails(
+      tokens.tokenV3,
+      teamId,
+      positionId
+    );
+
+    return response.data;
+  },
+  meta: {
+    teamId,
+    positionId,
+  },
+});
+
+/**
+ * Update candidate on the server and in Redux store
+ *
+ * @param {string} candidateId position candidate id
+ * @param {object} partialCandidateData partial candidate data
+ *
+ * @returns {Promise} updated candidate data or error
+ */
+export const updateCandidate = (candidateId, partialCandidateData) => ({
+  type: ACTION_TYPE.UPDATE_CANDIDATE,
+  payload: async () => {
+    const tokens = await getAuthUserTokens();
+    const response = await patchPositionCandidate(
+      tokens.tokenV3,
+      candidateId,
+      partialCandidateData
+    );
+
+    return response.data;
+  },
+  meta: {
+    candidateId,
+  },
+});
+
+/**
+ * Reset position state
+ */
+export const resetPositionState = () => ({
+  type: ACTION_TYPE.RESET_POSITION_STATE,
+})
diff --git a/src/routes/PositionDetails/CandidatesStatusFilter/index.jsx b/src/routes/PositionDetails/components/CandidatesStatusFilter/index.jsx
similarity index 100%
rename from src/routes/PositionDetails/CandidatesStatusFilter/index.jsx
rename to src/routes/PositionDetails/components/CandidatesStatusFilter/index.jsx
diff --git a/src/routes/PositionDetails/CandidatesStatusFilter/styles.module.scss b/src/routes/PositionDetails/components/CandidatesStatusFilter/styles.module.scss
similarity index 100%
rename from src/routes/PositionDetails/CandidatesStatusFilter/styles.module.scss
rename to src/routes/PositionDetails/components/CandidatesStatusFilter/styles.module.scss
diff --git a/src/routes/PositionDetails/PositionCandidates/index.jsx b/src/routes/PositionDetails/components/PositionCandidates/index.jsx
similarity index 73%
rename from src/routes/PositionDetails/PositionCandidates/index.jsx
rename to src/routes/PositionDetails/components/PositionCandidates/index.jsx
index 6f1a6407..3fde4647 100644
--- a/src/routes/PositionDetails/PositionCandidates/index.jsx
+++ b/src/routes/PositionDetails/components/PositionCandidates/index.jsx
@@ -20,8 +20,8 @@ import User from "components/User";
 import SkillsSummary from "components/SkillsSummary";
 import Button from "components/Button";
 import Pagination from "components/Pagination";
-import IconResume from "../../../assets/images/icon-resume.svg";
-import { skillShape } from "components/SkillsList";
+import IconResume from "../../../../assets/images/icon-resume.svg";
+import { toastr } from "react-redux-toastr";
 
 /**
  * Generates a function to sort candidates
@@ -45,6 +45,7 @@ const createSortCandidatesMethod = (sortBy) => (candidate1, candidate2) => {
 const PositionCandidates = ({
   candidates,
   candidateStatus,
+  updateCandidate,
 }) => {
   const [sortBy, setSortBy] = useState(CANDIDATES_SORT_BY.SKILL_MATCHED);
   const filteredCandidates = useMemo(
@@ -86,6 +87,42 @@ const PositionCandidates = ({
     [setPage]
   );
 
+  const markCandidateShortlisted = useCallback(
+    (candidateId) => {
+      updateCandidate(candidateId, {
+        status: CANDIDATE_STATUS.SHORTLIST,
+      })
+        .then(() => {
+          toastr.success("Candidate is marked as interested.");
+        })
+        .catch((error) => {
+          toastr.error(
+            "Failed to mark candidate as interested",
+            error.toString()
+          );
+        });
+    },
+    [updateCandidate]
+  );
+
+  const markCandidateRejected = useCallback(
+    (candidateId) => {
+      updateCandidate(candidateId, {
+        status: CANDIDATE_STATUS.REJECTED,
+      })
+        .then(() => {
+          toastr.success("Candidate is marked as not interested.");
+        })
+        .catch((error) => {
+          toastr.error(
+            "Failed to mark candidate as not interested",
+            error.toString()
+          );
+        });
+    },
+    [updateCandidate]
+  );
+
   return (
     <div styleName="position-candidates">
       <CardHeader
@@ -125,24 +162,31 @@ const PositionCandidates = ({
                   limit={7}
                 />
                 {candidate.resumeLink && (
-                  <a
-                    href={`${candidate.resumeLink}`}
-                    styleName="resume-link"
-                  >
+                  <a href={`${candidate.resumeLink}`} styleName="resume-link">
                     <IconResume />
                     Download Resume
                   </a>
                 )}
               </div>
               <div styleName="table-cell cell-action">
-                {candidateStatus === CANDIDATE_STATUS.SHORTLIST ? (
-                  <Button type="primary">Schedule Interview</Button>
-                ) : (
+                {candidateStatus === CANDIDATE_STATUS.OPEN && (
                   <>
                     Interested in this candidate?
                     <div styleName="actions">
-                      <Button type="secondary">No</Button>
-                      <Button type="primary">Yes</Button>
+                      <Button
+                        type="secondary"
+                        onClick={() => markCandidateRejected(candidate.id)}
+                        disabled={candidate.updating}
+                      >
+                        No
+                      </Button>
+                      <Button
+                        type="primary"
+                        onClick={() => markCandidateShortlisted(candidate.id)}
+                        disabled={candidate.updating}
+                      >
+                        Yes
+                      </Button>
                     </div>
                   </>
                 )}
diff --git a/src/routes/PositionDetails/PositionCandidates/styles.module.scss b/src/routes/PositionDetails/components/PositionCandidates/styles.module.scss
similarity index 100%
rename from src/routes/PositionDetails/PositionCandidates/styles.module.scss
rename to src/routes/PositionDetails/components/PositionCandidates/styles.module.scss
diff --git a/src/routes/PositionDetails/hooks/useTeamPositionsState.js b/src/routes/PositionDetails/hooks/useTeamPositionsState.js
new file mode 100644
index 00000000..6095d3f6
--- /dev/null
+++ b/src/routes/PositionDetails/hooks/useTeamPositionsState.js
@@ -0,0 +1,38 @@
+/**
+ * State for PositionDetails page
+ */
+import { useCallback, useLayoutEffect } from "react";
+import { useSelector, useDispatch } from "react-redux";
+import { loadPosition, updateCandidate, resetPositionState } from "../actions";
+
+/**
+ * Hook which provides state for PositionDetails page with all possible actions.
+ *
+ * @param {string} teamId team id
+ * @param {string} positionId position id
+ *
+ * @returns {{ state: {}, updateCandidate: Function }} PositionDetails page state and possible actions
+ */
+export const useTeamPositionsState = (teamId, positionId) => {
+  const state = useSelector((state) => state.positionDetails);
+  const dispatch = useDispatch();
+
+  // load team position details on mount
+  useLayoutEffect(() => {
+    dispatch(loadPosition(teamId, positionId));
+
+    // clear state when we leave the page
+    return () => {
+      dispatch(resetPositionState());
+    }
+  }, [dispatch, teamId, positionId]);
+
+  // bind actions to dispatch method
+  const updateCandidateCallback = useCallback(
+    (candidateId, partialCandidateData) =>
+      dispatch(updateCandidate(candidateId, partialCandidateData)),
+    [dispatch]
+  );
+
+  return { state, updateCandidate: updateCandidateCallback };
+};
diff --git a/src/routes/PositionDetails/index.jsx b/src/routes/PositionDetails/index.jsx
index 09d48fa9..b67e0b1b 100644
--- a/src/routes/PositionDetails/index.jsx
+++ b/src/routes/PositionDetails/index.jsx
@@ -9,26 +9,17 @@ import LayoutContainer from "components/LayoutContainer";
 import LoadingIndicator from "components/LoadingIndicator";
 import PageHeader from "components/PageHeader";
 import { CANDIDATE_STATUS } from "constants";
-import { useData } from "hooks/useData";
-import { getPositionDetails } from "services/teams";
-import PositionCandidates from "./PositionCandidates";
+import PositionCandidates from "./components/PositionCandidates";
+import CandidatesStatusFilter from "./components/CandidatesStatusFilter";
+import { useTeamPositionsState } from "./hooks/useTeamPositionsState";
 import "./styles.module.scss";
-import CandidatesStatusFilter from "./CandidatesStatusFilter";
-import { useAsync } from "react-use";
-import {
-  getAuthUserTokens,
-} from "@topcoder/micro-frontends-navbar-app";
 
 const PositionDetails = ({ teamId, positionId }) => {
-  const authUserTokens = useAsync(getAuthUserTokens);
-  const tokenV3 = authUserTokens.value ? authUserTokens.value.tokenV3 : null;
   const [candidateStatus, setCandidateStatus] = useState(CANDIDATE_STATUS.OPEN);
-  const [position, loadingError] = useData(
-    getPositionDetails,
-    tokenV3,
-    teamId,
-    positionId
-  );
+  const {
+    state: { position, error },
+    updateCandidate,
+  } = useTeamPositionsState(teamId, positionId);
 
   const onCandidateStatusChange = useCallback(
     (status) => {
@@ -40,7 +31,7 @@ const PositionDetails = ({ teamId, positionId }) => {
   return (
     <LayoutContainer>
       {!position ? (
-        <LoadingIndicator error={loadingError && loadingError.toString()} />
+        <LoadingIndicator error={error} />
       ) : (
         <>
           <PageHeader
@@ -57,6 +48,7 @@ const PositionDetails = ({ teamId, positionId }) => {
           <PositionCandidates
             candidates={position.candidates}
             candidateStatus={candidateStatus}
+            updateCandidate={updateCandidate}
           />
         </>
       )}
diff --git a/src/routes/PositionDetails/reducers/index.js b/src/routes/PositionDetails/reducers/index.js
new file mode 100644
index 00000000..f522c503
--- /dev/null
+++ b/src/routes/PositionDetails/reducers/index.js
@@ -0,0 +1,99 @@
+/**
+ * Reducer for PositionDetails page
+ */
+import _ from "lodash";
+import update from "immutability-helper";
+import { ACTION_TYPE } from "constants";
+
+const initialState = {
+  position: undefined,
+  loading: false,
+  error: undefined,
+};
+
+/**
+ * Patch candidate inside position state without state mutation
+ *
+ * @param {object} state state
+ * @param {string} candidateId candidate id
+ * @param {object} partialCandidateData partial candidate data
+ *
+ * @returns {object} new state
+ */
+const patchCandidateInState = (state, candidateId, partialCandidateData) => {
+  const candidateIndex = _.findIndex(_.get(state, "position.candidates"), {
+    id: candidateId,
+  });
+
+  if (candidateIndex === -1) {
+    return state;
+  }
+
+  const updatedCandidate = update(state.position.candidates[candidateIndex], {
+    $merge: partialCandidateData,
+  });
+
+  return update(state, {
+    position: {
+      candidates: {
+        $splice: [[candidateIndex, 1, updatedCandidate]],
+      },
+    },
+  });
+};
+
+const reducer = (state = initialState, action) => {
+  switch (action.type) {
+    case ACTION_TYPE.RESET_POSITION_STATE:
+      return initialState;
+
+    case ACTION_TYPE.LOAD_POSITION_PENDING:
+      return {
+        ...state,
+        loading: true,
+        error: undefined,
+      };
+
+    case ACTION_TYPE.LOAD_POSITION_SUCCESS:
+      return {
+        ...state,
+        position: action.payload,
+        loading: false,
+        error: undefined,
+      };
+
+    case ACTION_TYPE.LOAD_POSITION_ERROR:
+      return {
+        ...state,
+        loading: false,
+        error: action.payload,
+      };
+
+    case ACTION_TYPE.UPDATE_CANDIDATE_PENDING:
+      return patchCandidateInState(state, action.meta.candidateId, {
+        updating: true,
+        error: undefined,
+      });
+
+    case ACTION_TYPE.UPDATE_CANDIDATE_SUCCESS:
+      return patchCandidateInState(
+        state,
+        action.meta.candidateId,
+        {
+          updating: false,
+          ...action.payload,
+        }
+      );
+
+    case ACTION_TYPE.UPDATE_CANDIDATE_ERROR:
+      return patchCandidateInState(state, action.meta.candidateId, {
+        updating: false,
+        error: action.payload,
+      });
+
+    default:
+      return state
+  }
+};
+
+export default reducer;
diff --git a/src/services/teams.js b/src/services/teams.js
index 95ffcf7b..65e158bb 100644
--- a/src/services/teams.js
+++ b/src/services/teams.js
@@ -1,7 +1,5 @@
 /**
- * Topcoder Teams Service
- *
- * NOTE: It uses mock at the moment.
+ * Topcoder TaaS Service
  */
 import axios from "axios";
 import config from "../../config";
@@ -62,3 +60,25 @@ export const getPositionDetails = (tokenV3, teamId, positionId) => {
     headers: { Authorization: `Bearer ${tokenV3}` },
   });
 };
+
+/**
+ * Patch Position Candidate
+ *
+ * @param {string} tokenV3 login token
+ * @param {string} candidateId position candidate id
+ *
+ * @returns {Promise<object{}>} position candidate
+ */
+export const patchPositionCandidate = (
+  tokenV3,
+  candidateId,
+  partialCandidateData
+) => {
+  return axios.patch(
+    `${config.API.V5}/jobCandidates/${candidateId}`,
+    partialCandidateData,
+    {
+      headers: { Authorization: `Bearer ${tokenV3}` },
+    }
+  );
+};
diff --git a/src/store.js b/src/store.js
new file mode 100644
index 00000000..9e1ae7a6
--- /dev/null
+++ b/src/store.js
@@ -0,0 +1,28 @@
+/* global process */
+/**
+ * Configure Redux Store
+ */
+import { createStore, applyMiddleware } from "redux";
+import { createLogger } from "redux-logger";
+import thunk from "redux-thunk";
+import { createPromise } from "redux-promise-middleware";
+import rootReducer from "./reducers";
+
+const middlewares = [
+  // if payload of action is promise it would split action into 3 states
+  createPromise({
+    promiseTypeSuffixes: ["PENDING", "SUCCESS", "ERROR"],
+  }),
+  thunk,
+];
+
+// enable Redux Logger in in DEV environment
+if (process.env.NODE_ENV === "development") {
+  const { createLogger } = require("redux-logger");
+  const logger = createLogger();
+  middlewares.push(logger);
+}
+
+const store = createStore(rootReducer, applyMiddleware(...middlewares));
+
+export default store;
diff --git a/src/styles/main.module.scss b/src/styles/main.module.scss
index cfc5164b..84a0cbc3 100644
--- a/src/styles/main.module.scss
+++ b/src/styles/main.module.scss
@@ -4,5 +4,5 @@
 
 .topcoder-micro-frontends-teams-app {
   @include font-roboto;
-  color: #2A2A2A;
+  color: #2a2a2a;
 }
diff --git a/src/styles/main.vendor.scss b/src/styles/main.vendor.scss
new file mode 100644
index 00000000..2986d29d
--- /dev/null
+++ b/src/styles/main.vendor.scss
@@ -0,0 +1,4 @@
+// This file can import 3rd parties css/scss files globally
+// without applying CSS Modules
+
+@import "~react-redux-toastr/src/styles/index";

From 22ce5509c5c78ccd148ae52b9a249fca76fc188e Mon Sep 17 00:00:00 2001
From: maxceem <maxceem@gmail.com>
Date: Thu, 17 Dec 2020 14:01:42 +0200
Subject: [PATCH 02/23] docs: updated "Technology Stack"

---
 README.md | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/README.md b/README.md
index e22bfb1f..aa44f185 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,8 @@ This is a [single-spa](https://single-spa.js.org/) example React microapp.
 - Router via [Reach Router](https://reach.tech/router/)
 - CSS Modules with SCSS via [babel-plugin-react-css-modules](https://github.com/gajus/babel-plugin-react-css-modules)
 - [React Inline SVG](https://github.com/airbnb/babel-plugin-inline-react-svg)
+- We use **Redux Store** for storing page data if we need to edit it. Otherwise we can use local state.
+- [react-redux-toastr](https://www.npmjs.com/package/react-redux-toastr) for success/error popups in the bottom left corner.
 
 ## Config
 

From 0a9cda2dedf55c525371b96dcf97fc9a737015b9 Mon Sep 17 00:00:00 2001
From: maxceem <maxceem@gmail.com>
Date: Thu, 17 Dec 2020 14:12:23 +0200
Subject: [PATCH 03/23] feat: "Request an Extension" action

ref issue #26
---
 config/development.js                               |  6 ++++++
 config/production.js                                |  6 ++++++
 .../MyTeamsDetails/components/TeamMembers/index.jsx | 13 +++++++++++--
 .../MyTeamsList/components/TeamCard/index.jsx       |  6 ------
 src/utils/format.js                                 | 13 ++++++++++++-
 5 files changed, 35 insertions(+), 9 deletions(-)

diff --git a/config/development.js b/config/development.js
index 151a0de7..651de18a 100644
--- a/config/development.js
+++ b/config/development.js
@@ -8,6 +8,12 @@ module.exports = {
    * Email to report issues to
    */
   EMAIL_REPORT_ISSUE: "support+team-issue@topcoder-dev.com",
+
+  /**
+   * Email to request extension
+   */
+  EMAIL_REQUEST_EXTENSION: "customersuccess@topcoder-dev.com",
+
   API: {
     V5: "https://api.topcoder-dev.com/v5",
   },
diff --git a/config/production.js b/config/production.js
index 42c064b2..de80270a 100644
--- a/config/production.js
+++ b/config/production.js
@@ -8,6 +8,12 @@ module.exports = {
    * Email to report issues to
    */
   EMAIL_REPORT_ISSUE: "support+team-issue@topcoder.com",
+
+  /**
+   * Email to request extension
+   */
+  EMAIL_REQUEST_EXTENSION: "customersuccess@topcoder.com",
+
   API: {
     V5: "https://api.topcoder.com/v5",
   },
diff --git a/src/routes/MyTeamsDetails/components/TeamMembers/index.jsx b/src/routes/MyTeamsDetails/components/TeamMembers/index.jsx
index 26d8488b..6f325f38 100644
--- a/src/routes/MyTeamsDetails/components/TeamMembers/index.jsx
+++ b/src/routes/MyTeamsDetails/components/TeamMembers/index.jsx
@@ -16,7 +16,7 @@ import ActionsMenu from "components/ActionsMenu";
 import Button from "components/Button";
 import Pagination from "components/Pagination";
 import { DAY_FORMAT, TEAM_MEMBERS_PER_PAGE } from "constants";
-import { formatMoney, formatReportIssueUrl } from "utils/format";
+import { formatMoney, formatReportIssueUrl, formatRequestExtensionUrl } from "utils/format";
 import Input from "components/Input";
 import { skillShape } from "components/SkillsList";
 
@@ -153,7 +153,16 @@ const TeamMembers = ({ team }) => {
                             );
                           },
                         },
-                        { label: "Request an Extension", action: () => {} },
+                        {
+                          label: "Request an Extension",
+                          action: () => {
+                            window.open(
+                              formatRequestExtensionUrl(
+                                `Request extension for ${member.handle} on ${team.name}`
+                              )
+                            );
+                          },
+                        },
                       ]}
                     />
                   </div>
diff --git a/src/routes/MyTeamsList/components/TeamCard/index.jsx b/src/routes/MyTeamsList/components/TeamCard/index.jsx
index d8ea8b52..6c360719 100644
--- a/src/routes/MyTeamsList/components/TeamCard/index.jsx
+++ b/src/routes/MyTeamsList/components/TeamCard/index.jsx
@@ -27,12 +27,6 @@ const TeamCard = ({ team }) => {
       <div styleName="three-dots-menu">
         <ThreeDotsMenu
           options={[
-            { label: "Team Feedback", action: () => {} },
-            { label: "Team Invoices", action: () => {} },
-            { label: "Team Reports", action: () => {} },
-            { separator: true },
-            { label: "Add Team Member", action: () => {} },
-            { separator: true },
             {
               label: "Report an Issue",
               action: () => {
diff --git a/src/utils/format.js b/src/utils/format.js
index 3f2ad1f0..fef9e488 100644
--- a/src/utils/format.js
+++ b/src/utils/format.js
@@ -3,7 +3,7 @@
  */
 import _ from "lodash";
 import { RATE_TYPE } from "constants";
-import { EMAIL_REPORT_ISSUE } from "../../config";
+import { EMAIL_REPORT_ISSUE, EMAIL_REQUEST_EXTENSION } from "../../config";
 import moment from "moment";
 
 /**
@@ -120,3 +120,14 @@ export const formatFullName = (firstName, lastName) => {
 export const formatReportIssueUrl = (subject) => {
   return `mailto:${EMAIL_REPORT_ISSUE}?subject=${encodeURIComponent(subject)}`;
 };
+
+/**
+ * Format Request an Extension URL (mailto:)
+ *
+ * @param {string} subject email subject
+ *
+ * @returns {string} request an extension URL
+ */
+export const formatRequestExtensionUrl = (subject) => {
+  return `mailto:${EMAIL_REQUEST_EXTENSION}?subject=${encodeURIComponent(subject)}`;
+};

From 3aa1bf4d565a16b5007125179743b49a326e226d Mon Sep 17 00:00:00 2001
From: maxceem <maxceem@gmail.com>
Date: Fri, 25 Dec 2020 17:19:15 +0200
Subject: [PATCH 04/23] fix: skillMatched

don't use skillMatched from the server and calculate it client-side correctly
---
 src/components/SkillsList/index.jsx           |  8 +---
 src/components/SkillsSummary/index.jsx        | 20 +++++-----
 src/constants/index.js                        |  2 +-
 .../components/TeamMembers/index.jsx          | 10 +++--
 .../components/PositionCandidates/index.jsx   | 37 +++++++++++++------
 src/routes/PositionDetails/index.jsx          |  2 +-
 6 files changed, 46 insertions(+), 33 deletions(-)

diff --git a/src/components/SkillsList/index.jsx b/src/components/SkillsList/index.jsx
index 5718f1e8..da11f1a7 100644
--- a/src/components/SkillsList/index.jsx
+++ b/src/components/SkillsList/index.jsx
@@ -2,7 +2,6 @@
  * SkillsList
  *
  * Shows list of skills with "N more" link which is showing tooltip with a full list of skills.
- * If `showMatches = true` it marks matched and not matched required skills.
  */
 import React, { useCallback, useState } from "react";
 import PT from "prop-types";
@@ -13,11 +12,7 @@ import IconCross from "../../assets/images/icon-cross.svg";
 import { usePopper } from "react-popper";
 import OutsideClickHandler from "react-outside-click-handler";
 
-const SkillsList = ({
-  skills,
-  limit = 3,
-  showMatches = false,
-}) => {
+const SkillsList = ({ skills, limit = 3 }) => {
   const skillsToShow = skills.slice(0, limit);
   const skillsToHide = skills.slice(limit);
 
@@ -124,7 +119,6 @@ export const skillShape = PT.shape({
 SkillsList.propTypes = {
   skills: PT.arrayOf(skillShape),
   limit: PT.number,
-  showMatches: PT.bool,
 };
 
 export default SkillsList;
diff --git a/src/components/SkillsSummary/index.jsx b/src/components/SkillsSummary/index.jsx
index 66ae1e56..33faaaf3 100644
--- a/src/components/SkillsSummary/index.jsx
+++ b/src/components/SkillsSummary/index.jsx
@@ -10,18 +10,20 @@ import PercentageBar from "components/PercentageBar";
 import SkillsList from "components/SkillsList";
 import "./styles.module.scss";
 
-const SkillsSummary = ({ skills, skillMatched, limit }) => {
+const SkillsSummary = ({ skills, requiredSkills = [], limit }) => {
+  const skillsMatched = _.intersectionBy(skills, requiredSkills, "id");
+  const skillsMatchedRatio =
+    requiredSkills.length > 0
+      ? skillsMatched.length / requiredSkills.length
+      : 1;
+
   return (
     <div>
       <div styleName="percentage">
-        <PercentageBar ratio={skillMatched / 100} styleName="percentage-bar" />
-        {Math.round(skillMatched)}% skill matched
+        <PercentageBar ratio={skillsMatchedRatio} styleName="percentage-bar" />
+        {Math.round(skillsMatchedRatio * 100)}% skill matched
       </div>
-      <SkillsList
-        skills={skills}
-        limit={limit}
-        showMatches
-      />
+      <SkillsList skills={skills} limit={limit} />
     </div>
   );
 };
@@ -33,8 +35,8 @@ const skillShape = PT.shape({
 
 SkillsSummary.propTypes = {
   skills: PT.arrayOf(skillShape),
+  requiredSkills: PT.arrayOf(skillShape),
   limit: PT.number,
-  skillMatched: PT.number,
 };
 
 export default SkillsSummary;
diff --git a/src/constants/index.js b/src/constants/index.js
index 7e6e5784..5237b12e 100644
--- a/src/constants/index.js
+++ b/src/constants/index.js
@@ -110,7 +110,7 @@ export const CANDIDATE_STATUS_FILTERS = [
  * Candidates "sort by" values
  */
 export const CANDIDATES_SORT_BY = {
-  SKILL_MATCHED: "skillMatched",
+  SKILL_MATCHED: "skillsMatched",
   HANDLE: "handle",
 };
 
diff --git a/src/routes/MyTeamsDetails/components/TeamMembers/index.jsx b/src/routes/MyTeamsDetails/components/TeamMembers/index.jsx
index 6f325f38..83b55e31 100644
--- a/src/routes/MyTeamsDetails/components/TeamMembers/index.jsx
+++ b/src/routes/MyTeamsDetails/components/TeamMembers/index.jsx
@@ -16,7 +16,11 @@ import ActionsMenu from "components/ActionsMenu";
 import Button from "components/Button";
 import Pagination from "components/Pagination";
 import { DAY_FORMAT, TEAM_MEMBERS_PER_PAGE } from "constants";
-import { formatMoney, formatReportIssueUrl, formatRequestExtensionUrl } from "utils/format";
+import {
+  formatMoney,
+  formatReportIssueUrl,
+  formatRequestExtensionUrl,
+} from "utils/format";
 import Input from "components/Input";
 import { skillShape } from "components/SkillsList";
 
@@ -132,7 +136,7 @@ const TeamMembers = ({ team }) => {
                 <div styleName="table-cell cell-skills">
                   <SkillsSummary
                     skills={member.skills}
-                    skillMatched={member.skillMatched}
+                    requiredSkills={member.job.skills}
                   />
                 </div>
                 <div styleName="table-group-second-inner">
@@ -206,7 +210,7 @@ TeamMembers.propTypes = {
         firstName: PT.string,
         lastName: PT.string,
         skills: PT.arrayOf(skillShape),
-        skillMatched: PT.number,
+        skillsMatched: PT.number,
       })
     ),
   }),
diff --git a/src/routes/PositionDetails/components/PositionCandidates/index.jsx b/src/routes/PositionDetails/components/PositionCandidates/index.jsx
index 3fde4647..24e1248a 100644
--- a/src/routes/PositionDetails/components/PositionCandidates/index.jsx
+++ b/src/routes/PositionDetails/components/PositionCandidates/index.jsx
@@ -33,7 +33,7 @@ import { toastr } from "react-redux-toastr";
 const createSortCandidatesMethod = (sortBy) => (candidate1, candidate2) => {
   switch (sortBy) {
     case CANDIDATES_SORT_BY.SKILL_MATCHED:
-      return candidate2.skillMatched - candidate1.skillMatched;
+      return candidate2.skillsMatched - candidate1.skillsMatched;
     case CANDIDATES_SORT_BY.HANDLE:
       return new Intl.Collator().compare(
         candidate1.handle.toLowerCase(),
@@ -42,18 +42,31 @@ const createSortCandidatesMethod = (sortBy) => (candidate1, candidate2) => {
   }
 };
 
-const PositionCandidates = ({
-  candidates,
-  candidateStatus,
-  updateCandidate,
-}) => {
+/**
+ * Populates candidate objects with `skillsMatched` property
+ * which define the number of candidate skills that match position skills
+ *
+ * @param {object} position position
+ * @param {object} candidate candidate for position
+ */
+const populateSkillsMatched = (position, candidate) => ({
+  ...candidate,
+  skillsMatched: _.intersectionBy(position.skills, candidate.skills, "id"),
+});
+
+const PositionCandidates = ({ position, candidateStatus, updateCandidate }) => {
+  const { candidates } = position;
   const [sortBy, setSortBy] = useState(CANDIDATES_SORT_BY.SKILL_MATCHED);
   const filteredCandidates = useMemo(
     () =>
-      _.filter(candidates, { status: candidateStatus }).sort(
-        createSortCandidatesMethod(sortBy)
-      ),
-    [candidates, candidateStatus, sortBy]
+      _.chain(candidates)
+        .map((candidate) => populateSkillsMatched(position, candidate))
+        .filter({
+          status: candidateStatus,
+        })
+        .value()
+        .sort(createSortCandidatesMethod(sortBy)),
+    [candidates, candidateStatus, sortBy, position]
   );
 
   const onSortByChange = useCallback(
@@ -158,7 +171,7 @@ const PositionCandidates = ({
               <div styleName="table-cell cell-skills">
                 <SkillsSummary
                   skills={candidate.skills}
-                  skillMatched={candidate.skillMatched}
+                  requiredSkills={position.skills}
                   limit={7}
                 />
                 {candidate.resumeLink && (
@@ -221,7 +234,7 @@ const PositionCandidates = ({
 };
 
 PositionCandidates.propType = {
-  candidates: PT.array,
+  position: PT.object,
   candidateStatus: PT.oneOf(Object.values(CANDIDATE_STATUS)),
 };
 
diff --git a/src/routes/PositionDetails/index.jsx b/src/routes/PositionDetails/index.jsx
index b67e0b1b..13594078 100644
--- a/src/routes/PositionDetails/index.jsx
+++ b/src/routes/PositionDetails/index.jsx
@@ -46,7 +46,7 @@ const PositionDetails = ({ teamId, positionId }) => {
             }
           />
           <PositionCandidates
-            candidates={position.candidates}
+            position={position}
             candidateStatus={candidateStatus}
             updateCandidate={updateCandidate}
           />

From 2f87b4182fddacd5748e77da1281bf7d41de55a9 Mon Sep 17 00:00:00 2001
From: maxceem <maxceem@gmail.com>
Date: Fri, 25 Dec 2020 17:28:35 +0200
Subject: [PATCH 05/23] fix: filter resources by role and full name

---
 .../components/TeamMembers/index.jsx          | 59 ++++++++++---------
 1 file changed, 30 insertions(+), 29 deletions(-)

diff --git a/src/routes/MyTeamsDetails/components/TeamMembers/index.jsx b/src/routes/MyTeamsDetails/components/TeamMembers/index.jsx
index 83b55e31..88b850f1 100644
--- a/src/routes/MyTeamsDetails/components/TeamMembers/index.jsx
+++ b/src/routes/MyTeamsDetails/components/TeamMembers/index.jsx
@@ -30,31 +30,32 @@ const TeamMembers = ({ team }) => {
 
   const filteredMembers = useMemo(
     () =>
-      resources.filter((member) => {
-        const filterLowerCase = filter.toLowerCase();
-        const lookupFields = _.compact([
-          member.handle,
-          member.firstName,
-          member.lastName,
-          member.role,
-          ..._.map(member.skills, "name"),
-        ]).map((field) => field.toLowerCase());
+      resources
+        // populate resources with job data first
+        .map((member) => ({
+          ...member,
+          job: _.find(jobs, { id: member.jobId }) || {},
+        }))
+        // now we can filter resources
+        .filter((member) => {
+          const filterLowerCase = filter.toLowerCase().trim();
+          const lookupFields = _.compact([
+            member.handle,
+            member.firstName,
+            member.lastName,
+            `${member.firstName} ${member.lastName}`, // full name
+            member.job.description,
+            ..._.map(member.skills, "name"),
+          ]).map((field) => field.toLowerCase());
 
-        return _.some(
-          lookupFields,
-          (field) => field.indexOf(filterLowerCase) > -1
-        );
-      }),
-    [resources, filter]
+          return _.some(
+            lookupFields,
+            (field) => field.indexOf(filterLowerCase) > -1
+          );
+        }),
+    [resources, filter, jobs]
   );
 
-  const filteredMembersWithJobs = useMemo(() => {
-    return filteredMembers.map((member) => ({
-      ...member,
-      job: _.find(jobs, { id: member.jobId }) || {},
-    }));
-  }, [filteredMembers, jobs]);
-
   const onFilterChange = useCallback(
     (event) => {
       setFilter(event.target.value);
@@ -76,8 +77,8 @@ const TeamMembers = ({ team }) => {
   const pagesTotal = Math.ceil(filteredMembers.length / perPage);
 
   const pageMembers = useMemo(
-    () => filteredMembersWithJobs.slice((page - 1) * perPage, page * perPage),
-    [filteredMembersWithJobs, page, perPage]
+    () => filteredMembers.slice((page - 1) * perPage, page * perPage),
+    [filteredMembers, page, perPage]
   );
 
   const onPageClick = useCallback(
@@ -101,10 +102,10 @@ const TeamMembers = ({ team }) => {
         }
       />
       {resources.length === 0 && <div styleName="no-members">No members</div>}
-      {resources.length > 0 && filteredMembersWithJobs.length === 0 && (
+      {resources.length > 0 && filteredMembers.length === 0 && (
         <div styleName="no-members">No members matching filter</div>
       )}
-      {filteredMembersWithJobs.length > 0 && (
+      {filteredMembers.length > 0 && (
         <div styleName="table">
           {pageMembers.map((member) => (
             <div styleName="table-row" key={member.id}>
@@ -181,15 +182,15 @@ const TeamMembers = ({ team }) => {
           type="secondary"
           onClick={showMore}
           disabled={
-            filteredMembersWithJobs.length === 0 || // if no members to show
+            filteredMembers.length === 0 || // if no members to show
             page === pagesTotal // if we are already on the last page
           }
         >
           Show More
         </Button>
-        {filteredMembersWithJobs.length > 0 && (
+        {filteredMembers.length > 0 && (
           <Pagination
-            total={filteredMembersWithJobs.length}
+            total={filteredMembers.length}
             currentPage={page}
             perPage={perPage}
             onPageClick={onPageClick}

From 6b05bfd6401985db56f4cfbfefcbcb4d0b051434 Mon Sep 17 00:00:00 2001
From: yoution <zhuyan207@gmail.com>
Date: Wed, 30 Dec 2020 20:29:03 +0800
Subject: [PATCH 06/23] fix: issue #16

---
 package-lock.json                           |  7 +++
 package.json                                |  1 +
 src/routes/MyTeamsDetails/index.jsx         |  7 +--
 src/routes/MyTeamsList/index.jsx            |  7 +--
 src/routes/PositionDetails/actions/index.js | 12 +----
 src/services/requestInterceptor.js          | 43 +++++++++++++++++
 src/services/teams.js                       | 52 ++++-----------------
 7 files changed, 65 insertions(+), 64 deletions(-)
 create mode 100644 src/services/requestInterceptor.js

diff --git a/package-lock.json b/package-lock.json
index 939b1bc4..dc06ef85 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2725,6 +2725,13 @@
         "@types/testing-library__react": "^9.1.2"
       }
     },
+    "@topcoder-platform/tc-auth-lib": {
+      "version": "git+https://github.com/topcoder-platform/tc-auth-lib.git#68fdc22464810c51b703a33e529cdbd6d09437de",
+      "from": "git+https://github.com/topcoder-platform/tc-auth-lib.git#1.0.4",
+      "requires": {
+        "lodash": "^4.17.19"
+      }
+    },
     "@types/anymatch": {
       "version": "1.3.1",
       "resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz",
diff --git a/package.json b/package.json
index 3a16e2cc..872ec191 100644
--- a/package.json
+++ b/package.json
@@ -56,6 +56,7 @@
   "dependencies": {
     "@popperjs/core": "^2.5.4",
     "@reach/router": "^1.3.4",
+    "@topcoder-platform/tc-auth-lib": "git+https://github.com/topcoder-platform/tc-auth-lib.git#1.0.4",
     "axios": "^0.21.0",
     "classnames": "^2.2.6",
     "express": "^4.17.1",
diff --git a/src/routes/MyTeamsDetails/index.jsx b/src/routes/MyTeamsDetails/index.jsx
index 51f124d3..2f0d5c75 100644
--- a/src/routes/MyTeamsDetails/index.jsx
+++ b/src/routes/MyTeamsDetails/index.jsx
@@ -15,14 +15,9 @@ import TeamSummary from "./components/TeamSummary";
 import TeamMembers from "./components/TeamMembers";
 import TeamPositions from "./components/TeamPositions";
 import { useAsync } from "react-use";
-import {
-  getAuthUserTokens,
-} from "@topcoder/micro-frontends-navbar-app";
 
 const MyTeamsDetails = ({ teamId }) => {
-  const authUserTokens = useAsync(getAuthUserTokens);
-  const tokenV3 = authUserTokens.value ? authUserTokens.value.tokenV3 : null;
-  const [team, loadingError] = useData(getTeamById, tokenV3, teamId);
+  const [team, loadingError] = useData(getTeamById, teamId);
 
   return (
     <LayoutContainer>
diff --git a/src/routes/MyTeamsList/index.jsx b/src/routes/MyTeamsList/index.jsx
index 819f97c5..dfb6d320 100644
--- a/src/routes/MyTeamsList/index.jsx
+++ b/src/routes/MyTeamsList/index.jsx
@@ -12,14 +12,9 @@ import TeamCard from "./components/TeamCard";
 import TeamCardGrid from "./components/TeamCardGrid";
 import LoadingIndicator from "../../components/LoadingIndicator";
 import { useAsync } from "react-use";
-import {
-  getAuthUserTokens,
-} from "@topcoder/micro-frontends-navbar-app";
 
 const MyTeamsList = () => {
-  const authUserTokens = useAsync(getAuthUserTokens);
-  const tokenV3 = authUserTokens.value ? authUserTokens.value.tokenV3 : null;
-  const [myTeams, loadingError] = useData(getMyTeams, tokenV3);
+  const [myTeams, loadingError] = useData(getMyTeams);
 
   return (
     <LayoutContainer>
diff --git a/src/routes/PositionDetails/actions/index.js b/src/routes/PositionDetails/actions/index.js
index c5278a01..e7fdf31a 100644
--- a/src/routes/PositionDetails/actions/index.js
+++ b/src/routes/PositionDetails/actions/index.js
@@ -2,7 +2,6 @@
  * Position Details page actions
  */
 import { getPositionDetails, patchPositionCandidate } from "services/teams";
-import { getAuthUserTokens } from "@topcoder/micro-frontends-navbar-app";
 import { ACTION_TYPE } from "constants";
 
 /**
@@ -16,12 +15,7 @@ import { ACTION_TYPE } from "constants";
 export const loadPosition = (teamId, positionId) => ({
   type: ACTION_TYPE.LOAD_POSITION,
   payload: async () => {
-    const tokens = await getAuthUserTokens();
-    const response = await getPositionDetails(
-      tokens.tokenV3,
-      teamId,
-      positionId
-    );
+    const response = await getPositionDetails(teamId, positionId);
 
     return response.data;
   },
@@ -42,9 +36,7 @@ export const loadPosition = (teamId, positionId) => ({
 export const updateCandidate = (candidateId, partialCandidateData) => ({
   type: ACTION_TYPE.UPDATE_CANDIDATE,
   payload: async () => {
-    const tokens = await getAuthUserTokens();
     const response = await patchPositionCandidate(
-      tokens.tokenV3,
       candidateId,
       partialCandidateData
     );
@@ -61,4 +53,4 @@ export const updateCandidate = (candidateId, partialCandidateData) => ({
  */
 export const resetPositionState = () => ({
   type: ACTION_TYPE.RESET_POSITION_STATE,
-})
+});
diff --git a/src/services/requestInterceptor.js b/src/services/requestInterceptor.js
new file mode 100644
index 00000000..8adcb1f2
--- /dev/null
+++ b/src/services/requestInterceptor.js
@@ -0,0 +1,43 @@
+import axios from "axios";
+import store from "../store";
+import { getFreshToken, isTokenExpired } from "@topcoder-platform/tc-auth-lib";
+import { getAuthUserTokens } from "@topcoder/micro-frontends-navbar-app";
+
+export const getToken = () => {
+  return new Promise(async (resolve, reject) => {
+    const authUserTokens = await getAuthUserTokens();
+    const token = authUserTokens ? authUserTokens.tokenV3 : null;
+    if (token && !isTokenExpired(token)) {
+      return resolve(token);
+    } else {
+      return getFreshToken()
+        .then((token) => {
+          resolve(token);
+        })
+        .catch((err) => {
+          console.log(err);
+          reject(err);
+        });
+    }
+  });
+};
+
+export const axiosInstance = axios.create({
+  headers: {
+    "Content-Type": "application/json",
+  },
+});
+
+// request interceptor to pass auth token
+axiosInstance.interceptors.request.use((config) => {
+  return getToken()
+    .then((token) => {
+      config.headers["Authorization"] = `Bearer ${token}`;
+      return config;
+    })
+    .catch((err) => {
+      // TODO handle this error somehow
+      console.log(err);
+      return config;
+    });
+});
diff --git a/src/services/teams.js b/src/services/teams.js
index 65e158bb..eebc6c24 100644
--- a/src/services/teams.js
+++ b/src/services/teams.js
@@ -1,84 +1,52 @@
 /**
  * Topcoder TaaS Service
  */
-import axios from "axios";
+import { axiosInstance as axios } from "./requestInterceptor";
 import config from "../../config";
 
 /**
  * Get my teams.
  *
- * @param {string} tokenV3 login token
- *
  * @returns {Promise<object[]>} list of teams
  */
-export const getMyTeams = (tokenV3) => {
-  if (!tokenV3) {
-    return Promise.resolve({
-      data: null,
-    });
-  }
-  return axios.get(`${config.API.V5}/taas-teams`, {
-    headers: { Authorization: `Bearer ${tokenV3}` },
-  });
+export const getMyTeams = () => {
+  debugger;
+  return axios.get(`${config.API.V5}/taas-teams`);
 };
 
 /**
  * Get team by id.
  *
- * @param {string} tokenV3 login token
  * @param {string|number} teamId team id
  *
  * @returns {Promise<{}>} team object
  */
-export const getTeamById = (tokenV3, teamId) => {
-  if (!tokenV3) {
-    return Promise.resolve({
-      data: null,
-    });
-  }
-  return axios.get(`${config.API.V5}/taas-teams/${teamId}`, {
-    headers: { Authorization: `Bearer ${tokenV3}` },
-  });
+export const getTeamById = (teamId) => {
+  return axios.get(`${config.API.V5}/taas-teams/${teamId}`);
 };
 
 /**
  * Get team position details.
  *
- * @param {string} tokenV3 login token
  * @param {string|number} teamId team id
  * @param {string|number} positionId position id
  *
  * @returns {Promise<object{}>} job object
  */
-export const getPositionDetails = (tokenV3, teamId, positionId) => {
-  if (!tokenV3) {
-    return Promise.resolve({
-      data: null,
-    });
-  }
-  return axios.get(`${config.API.V5}/taas-teams/${teamId}/jobs/${positionId}`, {
-    headers: { Authorization: `Bearer ${tokenV3}` },
-  });
+export const getPositionDetails = (teamId, positionId) => {
+  return axios.get(`${config.API.V5}/taas-teams/${teamId}/jobs/${positionId}`);
 };
 
 /**
  * Patch Position Candidate
  *
- * @param {string} tokenV3 login token
  * @param {string} candidateId position candidate id
  *
  * @returns {Promise<object{}>} position candidate
  */
-export const patchPositionCandidate = (
-  tokenV3,
-  candidateId,
-  partialCandidateData
-) => {
+export const patchPositionCandidate = (candidateId, partialCandidateData) => {
   return axios.patch(
     `${config.API.V5}/jobCandidates/${candidateId}`,
-    partialCandidateData,
-    {
-      headers: { Authorization: `Bearer ${tokenV3}` },
-    }
+    partialCandidateData
   );
 };

From 5181c3f9a95bc160b44d148754b13f840bdc06d6 Mon Sep 17 00:00:00 2001
From: yoution <zhuyan207@gmail.com>
Date: Sun, 3 Jan 2021 17:09:45 +0800
Subject: [PATCH 07/23] fix: fix lint error #39

---
 package-lock.json                             |  7 -----
 package.json                                  |  1 -
 .../hooks/useTeamPositionsState.js            |  2 +-
 src/routes/PositionDetails/reducers/index.js  | 14 ++++------
 src/services/requestInterceptor.js            | 26 ++++++-------------
 src/services/teams.js                         |  1 -
 src/utils/format.js                           |  4 ++-
 7 files changed, 17 insertions(+), 38 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index dc06ef85..939b1bc4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2725,13 +2725,6 @@
         "@types/testing-library__react": "^9.1.2"
       }
     },
-    "@topcoder-platform/tc-auth-lib": {
-      "version": "git+https://github.com/topcoder-platform/tc-auth-lib.git#68fdc22464810c51b703a33e529cdbd6d09437de",
-      "from": "git+https://github.com/topcoder-platform/tc-auth-lib.git#1.0.4",
-      "requires": {
-        "lodash": "^4.17.19"
-      }
-    },
     "@types/anymatch": {
       "version": "1.3.1",
       "resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz",
diff --git a/package.json b/package.json
index 872ec191..3a16e2cc 100644
--- a/package.json
+++ b/package.json
@@ -56,7 +56,6 @@
   "dependencies": {
     "@popperjs/core": "^2.5.4",
     "@reach/router": "^1.3.4",
-    "@topcoder-platform/tc-auth-lib": "git+https://github.com/topcoder-platform/tc-auth-lib.git#1.0.4",
     "axios": "^0.21.0",
     "classnames": "^2.2.6",
     "express": "^4.17.1",
diff --git a/src/routes/PositionDetails/hooks/useTeamPositionsState.js b/src/routes/PositionDetails/hooks/useTeamPositionsState.js
index 6095d3f6..2f196abb 100644
--- a/src/routes/PositionDetails/hooks/useTeamPositionsState.js
+++ b/src/routes/PositionDetails/hooks/useTeamPositionsState.js
@@ -24,7 +24,7 @@ export const useTeamPositionsState = (teamId, positionId) => {
     // clear state when we leave the page
     return () => {
       dispatch(resetPositionState());
-    }
+    };
   }, [dispatch, teamId, positionId]);
 
   // bind actions to dispatch method
diff --git a/src/routes/PositionDetails/reducers/index.js b/src/routes/PositionDetails/reducers/index.js
index f522c503..4b01961c 100644
--- a/src/routes/PositionDetails/reducers/index.js
+++ b/src/routes/PositionDetails/reducers/index.js
@@ -76,14 +76,10 @@ const reducer = (state = initialState, action) => {
       });
 
     case ACTION_TYPE.UPDATE_CANDIDATE_SUCCESS:
-      return patchCandidateInState(
-        state,
-        action.meta.candidateId,
-        {
-          updating: false,
-          ...action.payload,
-        }
-      );
+      return patchCandidateInState(state, action.meta.candidateId, {
+        updating: false,
+        ...action.payload,
+      });
 
     case ACTION_TYPE.UPDATE_CANDIDATE_ERROR:
       return patchCandidateInState(state, action.meta.candidateId, {
@@ -92,7 +88,7 @@ const reducer = (state = initialState, action) => {
       });
 
     default:
-      return state
+      return state;
   }
 };
 
diff --git a/src/services/requestInterceptor.js b/src/services/requestInterceptor.js
index 8adcb1f2..471255f4 100644
--- a/src/services/requestInterceptor.js
+++ b/src/services/requestInterceptor.js
@@ -1,24 +1,16 @@
 import axios from "axios";
 import store from "../store";
-import { getFreshToken, isTokenExpired } from "@topcoder-platform/tc-auth-lib";
 import { getAuthUserTokens } from "@topcoder/micro-frontends-navbar-app";
 
 export const getToken = () => {
-  return new Promise(async (resolve, reject) => {
-    const authUserTokens = await getAuthUserTokens();
-    const token = authUserTokens ? authUserTokens.tokenV3 : null;
-    if (token && !isTokenExpired(token)) {
-      return resolve(token);
-    } else {
-      return getFreshToken()
-        .then((token) => {
-          resolve(token);
-        })
-        .catch((err) => {
-          console.log(err);
-          reject(err);
-        });
-    }
+  return new Promise((resolve, reject) => {
+    return getAuthUserTokens()
+      .then(({ tokenV3: token }) => {
+        return resolve(token);
+      })
+      .catch((err) => {
+        reject(err);
+      });
   });
 };
 
@@ -36,8 +28,6 @@ axiosInstance.interceptors.request.use((config) => {
       return config;
     })
     .catch((err) => {
-      // TODO handle this error somehow
-      console.log(err);
       return config;
     });
 });
diff --git a/src/services/teams.js b/src/services/teams.js
index eebc6c24..5278705c 100644
--- a/src/services/teams.js
+++ b/src/services/teams.js
@@ -10,7 +10,6 @@ import config from "../../config";
  * @returns {Promise<object[]>} list of teams
  */
 export const getMyTeams = () => {
-  debugger;
   return axios.get(`${config.API.V5}/taas-teams`);
 };
 
diff --git a/src/utils/format.js b/src/utils/format.js
index fef9e488..c6786d00 100644
--- a/src/utils/format.js
+++ b/src/utils/format.js
@@ -129,5 +129,7 @@ export const formatReportIssueUrl = (subject) => {
  * @returns {string} request an extension URL
  */
 export const formatRequestExtensionUrl = (subject) => {
-  return `mailto:${EMAIL_REQUEST_EXTENSION}?subject=${encodeURIComponent(subject)}`;
+  return `mailto:${EMAIL_REQUEST_EXTENSION}?subject=${encodeURIComponent(
+    subject
+  )}`;
 };

From 5596aadef097194aa82d1ff25255287ffc169daf Mon Sep 17 00:00:00 2001
From: yoution <zhuyan207@gmail.com>
Date: Sun, 3 Jan 2021 19:20:14 +0800
Subject: [PATCH 08/23] fix: issue #37

---
 package-lock.json                         |  5 ++
 package.json                              |  4 +-
 src/constants/index.js                    |  5 ++
 src/routes/MyTeamsList/index.jsx          | 72 ++++++++++++++++++++---
 src/routes/MyTeamsList/styles.module.scss | 33 +++++++++++
 src/services/teams.js                     | 12 +++-
 6 files changed, 119 insertions(+), 12 deletions(-)
 create mode 100644 src/routes/MyTeamsList/styles.module.scss

diff --git a/package-lock.json b/package-lock.json
index 939b1bc4..8563ac4d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16700,6 +16700,11 @@
       "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
       "dev": true
     },
+    "use-debounce": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-5.2.0.tgz",
+      "integrity": "sha512-lW4tbPsTnvPKYqOYXp5xZ7SP7No/ARLqqQqoyRKuSzP0HxR9arhSAhznXUZFoNPWDRij8fog+N6sYbjb8c3kzw=="
+    },
     "util": {
       "version": "0.11.1",
       "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz",
diff --git a/package.json b/package.json
index 3a16e2cc..bfd4a751 100644
--- a/package.json
+++ b/package.json
@@ -5,6 +5,7 @@
     "dev": "webpack-dev-server --port 8501 --host 0.0.0.0",
     "dev-https": "webpack-dev-server --https --port 8501 --host 0.0.0.0",
     "build": "webpack --mode=${APPMODE:-development} --env.config=${APPENV:-dev}",
+    "build:prod": "webpack --mode=${APPMODE:-production} --env.config=${APPENV:-prod}",
     "analyze": "webpack --mode=production --env.analyze=true",
     "lint": "eslint src --ext js",
     "format": "prettier --write \"./**\"",
@@ -74,7 +75,8 @@
     "redux": "^4.0.5",
     "redux-logger": "^3.0.6",
     "redux-promise-middleware": "^6.1.2",
-    "redux-thunk": "^2.3.0"
+    "redux-thunk": "^2.3.0",
+    "use-debounce": "^5.2.0"
   },
   "browserslist": [
     "last 1 version",
diff --git a/src/constants/index.js b/src/constants/index.js
index 5237b12e..98a0c5c4 100644
--- a/src/constants/index.js
+++ b/src/constants/index.js
@@ -12,6 +12,11 @@ export const DAY_FORMAT = "MM/DD/YYYY";
  */
 export const TEAM_MEMBERS_PER_PAGE = 5;
 
+/**
+ * How many teams  show per page by default
+ */
+export const TEAMS_PER_PAGE = 20;
+
 /**
  * How many position candidates show per page by default
  */
diff --git a/src/routes/MyTeamsList/index.jsx b/src/routes/MyTeamsList/index.jsx
index dfb6d320..7f7a3513 100644
--- a/src/routes/MyTeamsList/index.jsx
+++ b/src/routes/MyTeamsList/index.jsx
@@ -3,30 +3,84 @@
  *
  * Page for the list of teams.
  */
-import React from "react";
+import React, { useCallback, useState, useEffect } from "react";
+import _ from "lodash";
 import LayoutContainer from "components/LayoutContainer";
+import { useDebouncedCallback } from "use-debounce";
 import PageHeader from "components/PageHeader";
-import { useData } from "../../hooks/useData";
+import Input from "components/Input";
+import Pagination from "components/Pagination";
 import { getMyTeams } from "../../services/teams";
 import TeamCard from "./components/TeamCard";
 import TeamCardGrid from "./components/TeamCardGrid";
 import LoadingIndicator from "../../components/LoadingIndicator";
 import { useAsync } from "react-use";
+import { TEAMS_PER_PAGE } from "constants";
+import "./styles.module.scss";
 
 const MyTeamsList = () => {
-  const [myTeams, loadingError] = useData(getMyTeams);
+  // let [myTeams, loadingError] = useData(getMyTeams);
+  let [myTeams, setMyTeams] = useState();
+  const [filter, setFilter] = useState("");
+  const [loadingError, setLoadingError] = useState(false);
+  const [page, setPage] = useState(1);
+  const [total, setTotal] = useState(0);
+
+  const onFilterChange = useDebouncedCallback((value) => {
+    setFilter(value);
+    setPage(1);
+  }, 200);
+
+  useEffect(() => {
+    getMyTeams(filter, page, TEAMS_PER_PAGE)
+      .then((response) => {
+        setMyTeams(response.data);
+        setTotal(response.headers["x-total"]);
+      })
+      .catch((responseError) => {
+        setLoadingError(responseError);
+      });
+  }, [filter, page]);
+
+  const onPageClick = useCallback(
+    (newPage) => {
+      setPage(newPage);
+    },
+    [setPage]
+  );
 
   return (
     <LayoutContainer>
-      <PageHeader title="My Teams" />
+      <PageHeader
+        title="My Teams"
+        aside={
+          <Input
+            placeholder="Filter by team name"
+            styleName="filter-input"
+            onChange={(e) => onFilterChange.callback(e.target.value)}
+          />
+        }
+      />
       {!myTeams ? (
         <LoadingIndicator error={loadingError && loadingError.toString()} />
       ) : (
-        <TeamCardGrid>
-          {myTeams.map((team) => (
-            <TeamCard key={team.id} team={team} />
-          ))}
-        </TeamCardGrid>
+        <>
+          <TeamCardGrid>
+            {myTeams.map((team) => (
+              <TeamCard key={team.id} team={team} />
+            ))}
+          </TeamCardGrid>
+          {myTeams.length > 0 && (
+            <div styleName="pagination-wrapper">
+              <Pagination
+                total={total}
+                currentPage={page}
+                perPage={TEAMS_PER_PAGE}
+                onPageClick={onPageClick}
+              />
+            </div>
+          )}
+        </>
       )}
     </LayoutContainer>
   );
diff --git a/src/routes/MyTeamsList/styles.module.scss b/src/routes/MyTeamsList/styles.module.scss
new file mode 100644
index 00000000..5d77d644
--- /dev/null
+++ b/src/routes/MyTeamsList/styles.module.scss
@@ -0,0 +1,33 @@
+@import "styles/include";
+
+.filter-input {
+  width: 380px;
+  background-color: #FFFFFF;
+  border: 1px solid #AAAAAA;
+  border-radius: 6px;
+  box-sizing: border-box;
+  color: #2A2A2A;
+  font-size: 14px;
+  height: 40px;
+  line-height: 38px;
+  outline: none;
+  padding: 0 15px;
+
+  &::placeholder {
+    color: #AAAAAA;
+  }
+}
+
+
+.pagination-wrapper {
+  margin-top: 20px;
+  margin-right: 20px;
+  display: flex;
+  justify-content: flex-end;
+}
+
+@media (max-width: 650px) {
+  .filter-input {
+    width: 100%;
+  }
+}
diff --git a/src/services/teams.js b/src/services/teams.js
index 5278705c..b9121c46 100644
--- a/src/services/teams.js
+++ b/src/services/teams.js
@@ -6,11 +6,19 @@ import config from "../../config";
 
 /**
  * Get my teams.
+ * @param {string|number}  name    team name
+ * @param {number}         page    current page
+ * @param {number}         perPage perPage
  *
  * @returns {Promise<object[]>} list of teams
  */
-export const getMyTeams = () => {
-  return axios.get(`${config.API.V5}/taas-teams`);
+export const getMyTeams = (name, page = 1, perPage) => {
+  let query = `page=${page}&perPage=${perPage}`;
+  if (name) {
+    query += `&name=${name}`;
+  }
+
+  return axios.get(`${config.API.V5}/taas-teams?${query}`);
 };
 
 /**

From b924dc839835257af1d13e35d3dc33d2740a59de Mon Sep 17 00:00:00 2001
From: yoution <zhuyan207@gmail.com>
Date: Tue, 5 Jan 2021 14:31:53 +0800
Subject: [PATCH 09/23] fix: #36

---
 src/components/SkillsList/index.jsx           | 38 ++++++++++++++++---
 src/components/SkillsSummary/index.jsx        |  2 +-
 src/routes/PositionDetails/actions/index.js   |  2 +-
 .../hooks/useTeamPositionsState.js            |  2 +-
 src/routes/PositionDetails/reducers/index.js  | 14 +++----
 src/utils/format.js                           |  4 +-
 6 files changed, 44 insertions(+), 18 deletions(-)

diff --git a/src/components/SkillsList/index.jsx b/src/components/SkillsList/index.jsx
index da11f1a7..6f326aca 100644
--- a/src/components/SkillsList/index.jsx
+++ b/src/components/SkillsList/index.jsx
@@ -3,7 +3,7 @@
  *
  * Shows list of skills with "N more" link which is showing tooltip with a full list of skills.
  */
-import React, { useCallback, useState } from "react";
+import React, { useCallback, useState, useMemo } from "react";
 import PT from "prop-types";
 import _ from "lodash";
 import "./styles.module.scss";
@@ -12,13 +12,22 @@ import IconCross from "../../assets/images/icon-cross.svg";
 import { usePopper } from "react-popper";
 import OutsideClickHandler from "react-outside-click-handler";
 
-const SkillsList = ({ skills, limit = 3 }) => {
+const SkillsList = ({requiredSkills, skills, limit = 3 }) => {
   const skillsToShow = skills.slice(0, limit);
   const skillsToHide = skills.slice(limit);
 
+  // if has requiredSkills, show two columns, eles show only one column
+  const showMatches = !!requiredSkills
   const [isOpen, setIsOpen] = useState(false);
   const [referenceElement, setReferenceElement] = useState(null);
   const [popperElement, setPopperElement] = useState(null);
+
+  const otherSkills = useMemo(
+    () => {
+      return _.differenceBy(skills, requiredSkills, 'id' )
+    },
+    [requiredSkills, skills]
+  );
   const { styles, attributes } = usePopper(referenceElement, popperElement, {
     placement: "bottom",
     modifiers: [
@@ -91,11 +100,29 @@ const SkillsList = ({ skills, limit = 3 }) => {
                 {...attributes.popper}
               >
                 <div styleName="popover-content">
-                  {skills && (
+                  {requiredSkills && (
+                    <div styleName="skills-section">
+                      <div styleName="skills-title">Required Job Skills</div>
+                      <ul styleName="skills-list">
+                        {requiredSkills.map((skill) => (
+                          <li key={skill.id}>
+                            {showMatches &&
+                              (_.find(skills, { id: skill.id }) ? (
+                                <IconCheck />
+                              ) : (
+                                <IconCross />
+                              ))}{" "}
+                            {skill.name}
+                          </li>
+                        ))}
+                      </ul>
+                    </div>
+                  )}
+                  {otherSkills && (
                     <div styleName="skills-section">
-                      <div styleName="skills-title">Skills</div>
+                      <div styleName="skills-title">{showMatches ? 'Other User Skills': 'Required Skills'}</div>
                       <ul styleName="skills-list">
-                        {skills.map((skill) => (
+                        {otherSkills.map((skill) => (
                           <li key={skill.id}>{skill.name}</li>
                         ))}
                       </ul>
@@ -118,6 +145,7 @@ export const skillShape = PT.shape({
 
 SkillsList.propTypes = {
   skills: PT.arrayOf(skillShape),
+  requiredSkills: PT.arrayOf(skillShape),
   limit: PT.number,
 };
 
diff --git a/src/components/SkillsSummary/index.jsx b/src/components/SkillsSummary/index.jsx
index 33faaaf3..c20af3b9 100644
--- a/src/components/SkillsSummary/index.jsx
+++ b/src/components/SkillsSummary/index.jsx
@@ -23,7 +23,7 @@ const SkillsSummary = ({ skills, requiredSkills = [], limit }) => {
         <PercentageBar ratio={skillsMatchedRatio} styleName="percentage-bar" />
         {Math.round(skillsMatchedRatio * 100)}% skill matched
       </div>
-      <SkillsList skills={skills} limit={limit} />
+      <SkillsList skills={skills}  requiredSkills={requiredSkills} limit={limit} />
     </div>
   );
 };
diff --git a/src/routes/PositionDetails/actions/index.js b/src/routes/PositionDetails/actions/index.js
index c5278a01..0cd73876 100644
--- a/src/routes/PositionDetails/actions/index.js
+++ b/src/routes/PositionDetails/actions/index.js
@@ -61,4 +61,4 @@ export const updateCandidate = (candidateId, partialCandidateData) => ({
  */
 export const resetPositionState = () => ({
   type: ACTION_TYPE.RESET_POSITION_STATE,
-})
+});
diff --git a/src/routes/PositionDetails/hooks/useTeamPositionsState.js b/src/routes/PositionDetails/hooks/useTeamPositionsState.js
index 6095d3f6..2f196abb 100644
--- a/src/routes/PositionDetails/hooks/useTeamPositionsState.js
+++ b/src/routes/PositionDetails/hooks/useTeamPositionsState.js
@@ -24,7 +24,7 @@ export const useTeamPositionsState = (teamId, positionId) => {
     // clear state when we leave the page
     return () => {
       dispatch(resetPositionState());
-    }
+    };
   }, [dispatch, teamId, positionId]);
 
   // bind actions to dispatch method
diff --git a/src/routes/PositionDetails/reducers/index.js b/src/routes/PositionDetails/reducers/index.js
index f522c503..4b01961c 100644
--- a/src/routes/PositionDetails/reducers/index.js
+++ b/src/routes/PositionDetails/reducers/index.js
@@ -76,14 +76,10 @@ const reducer = (state = initialState, action) => {
       });
 
     case ACTION_TYPE.UPDATE_CANDIDATE_SUCCESS:
-      return patchCandidateInState(
-        state,
-        action.meta.candidateId,
-        {
-          updating: false,
-          ...action.payload,
-        }
-      );
+      return patchCandidateInState(state, action.meta.candidateId, {
+        updating: false,
+        ...action.payload,
+      });
 
     case ACTION_TYPE.UPDATE_CANDIDATE_ERROR:
       return patchCandidateInState(state, action.meta.candidateId, {
@@ -92,7 +88,7 @@ const reducer = (state = initialState, action) => {
       });
 
     default:
-      return state
+      return state;
   }
 };
 
diff --git a/src/utils/format.js b/src/utils/format.js
index fef9e488..c6786d00 100644
--- a/src/utils/format.js
+++ b/src/utils/format.js
@@ -129,5 +129,7 @@ export const formatReportIssueUrl = (subject) => {
  * @returns {string} request an extension URL
  */
 export const formatRequestExtensionUrl = (subject) => {
-  return `mailto:${EMAIL_REQUEST_EXTENSION}?subject=${encodeURIComponent(subject)}`;
+  return `mailto:${EMAIL_REQUEST_EXTENSION}?subject=${encodeURIComponent(
+    subject
+  )}`;
 };

From fcbf5d2100dd95198dc315db918d618264f1a580 Mon Sep 17 00:00:00 2001
From: maxceem <maxceem@gmail.com>
Date: Wed, 6 Jan 2021 10:06:39 +0200
Subject: [PATCH 10/23] refactor: improve skills component

---
 src/components/SkillsList/index.jsx | 28 +++++++++++++---------------
 1 file changed, 13 insertions(+), 15 deletions(-)

diff --git a/src/components/SkillsList/index.jsx b/src/components/SkillsList/index.jsx
index 6f326aca..edc64219 100644
--- a/src/components/SkillsList/index.jsx
+++ b/src/components/SkillsList/index.jsx
@@ -12,22 +12,19 @@ import IconCross from "../../assets/images/icon-cross.svg";
 import { usePopper } from "react-popper";
 import OutsideClickHandler from "react-outside-click-handler";
 
-const SkillsList = ({requiredSkills, skills, limit = 3 }) => {
+const SkillsList = ({ requiredSkills, skills, limit = 3 }) => {
   const skillsToShow = skills.slice(0, limit);
   const skillsToHide = skills.slice(limit);
 
   // if has requiredSkills, show two columns, eles show only one column
-  const showMatches = !!requiredSkills
+  const showMatches = !!requiredSkills;
   const [isOpen, setIsOpen] = useState(false);
   const [referenceElement, setReferenceElement] = useState(null);
   const [popperElement, setPopperElement] = useState(null);
 
-  const otherSkills = useMemo(
-    () => {
-      return _.differenceBy(skills, requiredSkills, 'id' )
-    },
-    [requiredSkills, skills]
-  );
+  const otherSkills = useMemo(() => {
+    return _.differenceBy(skills, requiredSkills, "id");
+  }, [requiredSkills, skills]);
   const { styles, attributes } = usePopper(referenceElement, popperElement, {
     placement: "bottom",
     modifiers: [
@@ -106,12 +103,11 @@ const SkillsList = ({requiredSkills, skills, limit = 3 }) => {
                       <ul styleName="skills-list">
                         {requiredSkills.map((skill) => (
                           <li key={skill.id}>
-                            {showMatches &&
-                              (_.find(skills, { id: skill.id }) ? (
-                                <IconCheck />
-                              ) : (
-                                <IconCross />
-                              ))}{" "}
+                            {_.find(skills, { id: skill.id }) ? (
+                              <IconCheck />
+                            ) : (
+                              <IconCross />
+                            )}{" "}
                             {skill.name}
                           </li>
                         ))}
@@ -120,7 +116,9 @@ const SkillsList = ({requiredSkills, skills, limit = 3 }) => {
                   )}
                   {otherSkills && (
                     <div styleName="skills-section">
-                      <div styleName="skills-title">{showMatches ? 'Other User Skills': 'Required Skills'}</div>
+                      <div styleName="skills-title">
+                        {showMatches ? "Other User Skills" : "Required Skills"}
+                      </div>
                       <ul styleName="skills-list">
                         {otherSkills.map((skill) => (
                           <li key={skill.id}>{skill.name}</li>

From d8f522cac7915a5eb5f5387cc09783d04515f365 Mon Sep 17 00:00:00 2001
From: maxceem <maxceem@gmail.com>
Date: Wed, 6 Jan 2021 10:20:21 +0200
Subject: [PATCH 11/23] refactor: fix formating

---
 src/components/SkillsSummary/index.jsx | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/components/SkillsSummary/index.jsx b/src/components/SkillsSummary/index.jsx
index c20af3b9..ceccbb62 100644
--- a/src/components/SkillsSummary/index.jsx
+++ b/src/components/SkillsSummary/index.jsx
@@ -23,7 +23,11 @@ const SkillsSummary = ({ skills, requiredSkills = [], limit }) => {
         <PercentageBar ratio={skillsMatchedRatio} styleName="percentage-bar" />
         {Math.round(skillsMatchedRatio * 100)}% skill matched
       </div>
-      <SkillsList skills={skills}  requiredSkills={requiredSkills} limit={limit} />
+      <SkillsList
+        skills={skills}
+        requiredSkills={requiredSkills}
+        limit={limit}
+      />
     </div>
   );
 };

From d935ee33b9b4065d56c423cf643505b902990ee5 Mon Sep 17 00:00:00 2001
From: yoution <zhuyan207@gmail.com>
Date: Wed, 6 Jan 2021 20:08:10 +0800
Subject: [PATCH 12/23] fix: issue #37

---
 package-lock.json                         |  8 +-------
 package.json                              |  4 +---
 src/routes/MyTeamsList/index.jsx          | 21 +++++++++++++--------
 src/routes/MyTeamsList/styles.module.scss |  3 +++
 src/services/requestInterceptor.js        | 16 ++--------------
 5 files changed, 20 insertions(+), 32 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 8563ac4d..422e1d0a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4680,8 +4680,7 @@
           "version": "3.0.0",
           "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
           "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "to-regex-range": {
           "version": "5.0.1",
@@ -16700,11 +16699,6 @@
       "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
       "dev": true
     },
-    "use-debounce": {
-      "version": "5.2.0",
-      "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-5.2.0.tgz",
-      "integrity": "sha512-lW4tbPsTnvPKYqOYXp5xZ7SP7No/ARLqqQqoyRKuSzP0HxR9arhSAhznXUZFoNPWDRij8fog+N6sYbjb8c3kzw=="
-    },
     "util": {
       "version": "0.11.1",
       "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz",
diff --git a/package.json b/package.json
index bfd4a751..3a16e2cc 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,6 @@
     "dev": "webpack-dev-server --port 8501 --host 0.0.0.0",
     "dev-https": "webpack-dev-server --https --port 8501 --host 0.0.0.0",
     "build": "webpack --mode=${APPMODE:-development} --env.config=${APPENV:-dev}",
-    "build:prod": "webpack --mode=${APPMODE:-production} --env.config=${APPENV:-prod}",
     "analyze": "webpack --mode=production --env.analyze=true",
     "lint": "eslint src --ext js",
     "format": "prettier --write \"./**\"",
@@ -75,8 +74,7 @@
     "redux": "^4.0.5",
     "redux-logger": "^3.0.6",
     "redux-promise-middleware": "^6.1.2",
-    "redux-thunk": "^2.3.0",
-    "use-debounce": "^5.2.0"
+    "redux-thunk": "^2.3.0"
   },
   "browserslist": [
     "last 1 version",
diff --git a/src/routes/MyTeamsList/index.jsx b/src/routes/MyTeamsList/index.jsx
index 7f7a3513..e173c2f1 100644
--- a/src/routes/MyTeamsList/index.jsx
+++ b/src/routes/MyTeamsList/index.jsx
@@ -6,7 +6,6 @@
 import React, { useCallback, useState, useEffect } from "react";
 import _ from "lodash";
 import LayoutContainer from "components/LayoutContainer";
-import { useDebouncedCallback } from "use-debounce";
 import PageHeader from "components/PageHeader";
 import Input from "components/Input";
 import Pagination from "components/Pagination";
@@ -14,24 +13,29 @@ import { getMyTeams } from "../../services/teams";
 import TeamCard from "./components/TeamCard";
 import TeamCardGrid from "./components/TeamCardGrid";
 import LoadingIndicator from "../../components/LoadingIndicator";
-import { useAsync } from "react-use";
+import { useDebounce } from "react-use";
 import { TEAMS_PER_PAGE } from "constants";
 import "./styles.module.scss";
 
 const MyTeamsList = () => {
-  // let [myTeams, loadingError] = useData(getMyTeams);
-  let [myTeams, setMyTeams] = useState();
+  let [myTeams, setMyTeams] = useState(null);
   const [filter, setFilter] = useState("");
+  const [tempFilter, setTempFilter] = React.useState('');
   const [loadingError, setLoadingError] = useState(false);
   const [page, setPage] = useState(1);
   const [total, setTotal] = useState(0);
+  const onFilterChange = (evt) => {
+    setTempFilter(evt.target.value)
+  }
 
-  const onFilterChange = useDebouncedCallback((value) => {
-    setFilter(value);
+  useDebounce((value) => {
+    console.log('xxxx', value)
+    setFilter(tempFilter);
     setPage(1);
-  }, 200);
+  }, 200, [tempFilter]);
 
   useEffect(() => {
+    setMyTeams(null);
     getMyTeams(filter, page, TEAMS_PER_PAGE)
       .then((response) => {
         setMyTeams(response.data);
@@ -57,10 +61,11 @@ const MyTeamsList = () => {
           <Input
             placeholder="Filter by team name"
             styleName="filter-input"
-            onChange={(e) => onFilterChange.callback(e.target.value)}
+            onChange={onFilterChange}
           />
         }
       />
+      {myTeams && myTeams.length === 0 && (<div styleName="empty">No teams found</div>)}
       {!myTeams ? (
         <LoadingIndicator error={loadingError && loadingError.toString()} />
       ) : (
diff --git a/src/routes/MyTeamsList/styles.module.scss b/src/routes/MyTeamsList/styles.module.scss
index 5d77d644..b2158822 100644
--- a/src/routes/MyTeamsList/styles.module.scss
+++ b/src/routes/MyTeamsList/styles.module.scss
@@ -18,6 +18,9 @@
   }
 }
 
+.empty {
+  text-align: center;
+}
 
 .pagination-wrapper {
   margin-top: 20px;
diff --git a/src/services/requestInterceptor.js b/src/services/requestInterceptor.js
index 471255f4..ec369c2b 100644
--- a/src/services/requestInterceptor.js
+++ b/src/services/requestInterceptor.js
@@ -2,18 +2,6 @@ import axios from "axios";
 import store from "../store";
 import { getAuthUserTokens } from "@topcoder/micro-frontends-navbar-app";
 
-export const getToken = () => {
-  return new Promise((resolve, reject) => {
-    return getAuthUserTokens()
-      .then(({ tokenV3: token }) => {
-        return resolve(token);
-      })
-      .catch((err) => {
-        reject(err);
-      });
-  });
-};
-
 export const axiosInstance = axios.create({
   headers: {
     "Content-Type": "application/json",
@@ -22,8 +10,8 @@ export const axiosInstance = axios.create({
 
 // request interceptor to pass auth token
 axiosInstance.interceptors.request.use((config) => {
-  return getToken()
-    .then((token) => {
+  return getAuthUserTokens()
+    .then(({ tokenV3: token }) => {
       config.headers["Authorization"] = `Bearer ${token}`;
       return config;
     })

From 945065f29dc6793dff055e169b1203ceaf83c31b Mon Sep 17 00:00:00 2001
From: maxceem <maxceem@gmail.com>
Date: Thu, 7 Jan 2021 00:01:13 +0200
Subject: [PATCH 13/23] refactor: debounce filter

---
 src/constants/index.js           |  5 +++++
 src/routes/MyTeamsList/index.jsx | 31 +++++++++++++++++++------------
 2 files changed, 24 insertions(+), 12 deletions(-)

diff --git a/src/constants/index.js b/src/constants/index.js
index 98a0c5c4..e65c089b 100644
--- a/src/constants/index.js
+++ b/src/constants/index.js
@@ -22,6 +22,11 @@ export const TEAMS_PER_PAGE = 20;
  */
 export const POSITION_CANDIDATES_PER_PAGE = 5;
 
+/**
+ * Input debounce delay (ms)
+ */
+export const INPUT_DEBOUNCE_DELAY = 200;
+
 /**
  * Position statuses
  */
diff --git a/src/routes/MyTeamsList/index.jsx b/src/routes/MyTeamsList/index.jsx
index e173c2f1..8db13129 100644
--- a/src/routes/MyTeamsList/index.jsx
+++ b/src/routes/MyTeamsList/index.jsx
@@ -16,27 +16,32 @@ import LoadingIndicator from "../../components/LoadingIndicator";
 import { useDebounce } from "react-use";
 import { TEAMS_PER_PAGE } from "constants";
 import "./styles.module.scss";
+import { INPUT_DEBOUNCE_DELAY } from "constants/";
 
 const MyTeamsList = () => {
   let [myTeams, setMyTeams] = useState(null);
-  const [filter, setFilter] = useState("");
-  const [tempFilter, setTempFilter] = React.useState('');
+  const [debouncedFilter, setDebouncedFilter] = useState("");
+  const [filter, setFilter] = React.useState("");
   const [loadingError, setLoadingError] = useState(false);
   const [page, setPage] = useState(1);
   const [total, setTotal] = useState(0);
+
   const onFilterChange = (evt) => {
-    setTempFilter(evt.target.value)
-  }
+    setFilter(evt.target.value);
+  };
 
-  useDebounce((value) => {
-    console.log('xxxx', value)
-    setFilter(tempFilter);
-    setPage(1);
-  }, 200, [tempFilter]);
+  useDebounce(
+    () => {
+      setDebouncedFilter(filter);
+      setPage(1);
+    },
+    INPUT_DEBOUNCE_DELAY,
+    [filter]
+  );
 
   useEffect(() => {
     setMyTeams(null);
-    getMyTeams(filter, page, TEAMS_PER_PAGE)
+    getMyTeams(debouncedFilter, page, TEAMS_PER_PAGE)
       .then((response) => {
         setMyTeams(response.data);
         setTotal(response.headers["x-total"]);
@@ -44,7 +49,7 @@ const MyTeamsList = () => {
       .catch((responseError) => {
         setLoadingError(responseError);
       });
-  }, [filter, page]);
+  }, [debouncedFilter, page]);
 
   const onPageClick = useCallback(
     (newPage) => {
@@ -65,7 +70,9 @@ const MyTeamsList = () => {
           />
         }
       />
-      {myTeams && myTeams.length === 0 && (<div styleName="empty">No teams found</div>)}
+      {myTeams && myTeams.length === 0 && (
+        <div styleName="empty">No teams found</div>
+      )}
       {!myTeams ? (
         <LoadingIndicator error={loadingError && loadingError.toString()} />
       ) : (

From cd8ad9c298c799653cd50e448a5fe176e7738d4d Mon Sep 17 00:00:00 2001
From: maxceem <maxceem@gmail.com>
Date: Thu, 7 Jan 2021 00:03:12 +0200
Subject: [PATCH 14/23] feat: filter teams by substring

---
 src/services/teams.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/services/teams.js b/src/services/teams.js
index b9121c46..7b8739c2 100644
--- a/src/services/teams.js
+++ b/src/services/teams.js
@@ -15,7 +15,7 @@ import config from "../../config";
 export const getMyTeams = (name, page = 1, perPage) => {
   let query = `page=${page}&perPage=${perPage}`;
   if (name) {
-    query += `&name=${name}`;
+    query += `&name=*${name}*`; // wrap with asterisks to search by substrings
   }
 
   return axios.get(`${config.API.V5}/taas-teams?${query}`);

From a2657fcbfdb27884dcd6149c8e7ab004fc14d98c Mon Sep 17 00:00:00 2001
From: maxceem <maxceem@gmail.com>
Date: Thu, 7 Jan 2021 17:32:10 +0200
Subject: [PATCH 15/23] feat: use "title" for jobs

---
 src/routes/MyTeamsDetails/components/TeamMembers/index.jsx  | 6 ++----
 .../MyTeamsDetails/components/TeamPositions/index.jsx       | 4 ++--
 src/routes/PositionDetails/index.jsx                        | 2 +-
 3 files changed, 5 insertions(+), 7 deletions(-)

diff --git a/src/routes/MyTeamsDetails/components/TeamMembers/index.jsx b/src/routes/MyTeamsDetails/components/TeamMembers/index.jsx
index 88b850f1..9443651c 100644
--- a/src/routes/MyTeamsDetails/components/TeamMembers/index.jsx
+++ b/src/routes/MyTeamsDetails/components/TeamMembers/index.jsx
@@ -44,7 +44,7 @@ const TeamMembers = ({ team }) => {
             member.firstName,
             member.lastName,
             `${member.firstName} ${member.lastName}`, // full name
-            member.job.description,
+            member.job.title,
             ..._.map(member.skills, "name"),
           ]).map((field) => field.toLowerCase());
 
@@ -119,9 +119,7 @@ const TeamMembers = ({ team }) => {
                   />
                 </div>
                 <div styleName="table-group-first-inner">
-                  <div styleName="table-cell cell-role">
-                    {member.job.description}
-                  </div>
+                  <div styleName="table-cell cell-role">{member.job.title}</div>
                   <div styleName="table-cell cell-date">
                     {moment(member.startDate).format(DAY_FORMAT)} -{" "}
                     {moment(member.endDate).format(DAY_FORMAT)}
diff --git a/src/routes/MyTeamsDetails/components/TeamPositions/index.jsx b/src/routes/MyTeamsDetails/components/TeamPositions/index.jsx
index 91c90ab1..26db9e0b 100644
--- a/src/routes/MyTeamsDetails/components/TeamPositions/index.jsx
+++ b/src/routes/MyTeamsDetails/components/TeamPositions/index.jsx
@@ -28,7 +28,7 @@ const TeamPositions = ({ teamId, positions }) => {
             <div styleName="table-row" key={index}>
               <div styleName="table-group-first">
                 <div styleName="table-cell cell-skills">
-                  <strong>{position.description}</strong>
+                  <strong>{position.title}</strong>
                   <SkillsList skills={position.skills} limit={5} />
                 </div>
                 <div styleName="table-group-first-inner">
@@ -70,7 +70,7 @@ TeamPositions.propTypes = {
   teamId: PT.string,
   positions: PT.arrayOf(
     PT.shape({
-      description: PT.string,
+      title: PT.string,
       customerRate: PT.number,
       rateType: PT.oneOf(Object.values(RATE_TYPE)),
       skills: PT.arrayOf(skillShape),
diff --git a/src/routes/PositionDetails/index.jsx b/src/routes/PositionDetails/index.jsx
index 13594078..4c30df09 100644
--- a/src/routes/PositionDetails/index.jsx
+++ b/src/routes/PositionDetails/index.jsx
@@ -35,7 +35,7 @@ const PositionDetails = ({ teamId, positionId }) => {
       ) : (
         <>
           <PageHeader
-            title={position.description}
+            title={position.title}
             backTo={`/taas/myteams/${teamId}`}
             aside={
               <CandidatesStatusFilter

From 820d8ca5c78571b4a2e00192ee71e410b5c0667c Mon Sep 17 00:00:00 2001
From: yoution <zhuyan207@gmail.com>
Date: Fri, 8 Jan 2021 13:56:17 +0800
Subject: [PATCH 16/23] fix: error message for #44

---
 src/components/Authentication/index.jsx   | 39 +++++++++++++++++++++++
 src/components/LoadingIndicator/index.jsx |  5 +--
 src/root.component.jsx                    |  6 ++--
 src/routes/MyTeamsDetails/index.jsx       |  5 +--
 src/routes/MyTeamsList/index.jsx          |  5 +--
 src/routes/PositionDetails/index.jsx      |  3 +-
 6 files changed, 53 insertions(+), 10 deletions(-)
 create mode 100644 src/components/Authentication/index.jsx

diff --git a/src/components/Authentication/index.jsx b/src/components/Authentication/index.jsx
new file mode 100644
index 00000000..2335d674
--- /dev/null
+++ b/src/components/Authentication/index.jsx
@@ -0,0 +1,39 @@
+/**
+ * Authentication 
+ *
+ * wrap component for authentication 
+ */
+import React, { useCallback, useState, useEffect } from "react";
+import { getAuthUserTokens, login } from "@topcoder/micro-frontends-navbar-app";
+
+export default function Authentication(Component) {
+
+  const  AuthenticatedComponent = (props) => {
+    let [isLoggedIn, setIsLoggedIn] = useState(null);
+
+    useEffect(() => {
+      if (props.auth) {
+        getAuthUserTokens()
+          .then(({ tokenV3 }) => {
+            if (!!tokenV3) {
+              setIsLoggedIn(!!tokenV3)
+            } else {
+              login()
+            }
+          })
+      }
+    }, [props.auth]);
+
+    return (
+      <div>
+        {
+          (!props.auth || props.auth && isLoggedIn === true)
+            ? <Component { ...props}/>
+            : null
+        }
+      </div>
+    )
+  }
+
+  return AuthenticatedComponent
+}
diff --git a/src/components/LoadingIndicator/index.jsx b/src/components/LoadingIndicator/index.jsx
index a5f70dd1..6d7e180d 100644
--- a/src/components/LoadingIndicator/index.jsx
+++ b/src/components/LoadingIndicator/index.jsx
@@ -4,17 +4,18 @@
  * Optionally shows error.
  */
 import React from "react";
+import _ from "lodash";
 import PT from "prop-types";
 import "./styles.module.scss";
 
 const LoadingIndicator = ({ error }) => {
   return (
-    <div styleName="loading-indicator">{!error ? "Loading..." : error}</div>
+    <div styleName="loading-indicator">{!error ? "Loading..." : _.get(error, 'response.data.message') || error}</div>
   );
 };
 
 LoadingIndicator.propTypes = {
-  error: PT.string,
+  error: PT.object,
 };
 
 export default LoadingIndicator;
diff --git a/src/root.component.jsx b/src/root.component.jsx
index dd6bd948..eed88770 100644
--- a/src/root.component.jsx
+++ b/src/root.component.jsx
@@ -14,9 +14,9 @@ export default function Root() {
     <div className={styles['topcoder-micro-frontends-teams-app']}>
       <Provider store={store}>
         <Router>
-          <MyTeamsList path="/taas/myteams" />
-          <MyTeamsDetails path="/taas/myteams/:teamId" />
-          <PositionDetails path="/taas/myteams/:teamId/positions/:positionId" />
+          <MyTeamsList path="/taas/myteams" auth/>
+          <MyTeamsDetails path="/taas/myteams/:teamId" auth />
+          <PositionDetails path="/taas/myteams/:teamId/positions/:positionId" auth/>
         </Router>
 
         {/* Global config for Toastr popups */}
diff --git a/src/routes/MyTeamsDetails/index.jsx b/src/routes/MyTeamsDetails/index.jsx
index 2f0d5c75..444690b4 100644
--- a/src/routes/MyTeamsDetails/index.jsx
+++ b/src/routes/MyTeamsDetails/index.jsx
@@ -14,6 +14,7 @@ import LoadingIndicator from "components/LoadingIndicator";
 import TeamSummary from "./components/TeamSummary";
 import TeamMembers from "./components/TeamMembers";
 import TeamPositions from "./components/TeamPositions";
+import Authentication from '../../components/Authentication'
 import { useAsync } from "react-use";
 
 const MyTeamsDetails = ({ teamId }) => {
@@ -22,7 +23,7 @@ const MyTeamsDetails = ({ teamId }) => {
   return (
     <LayoutContainer>
       {!team ? (
-        <LoadingIndicator error={loadingError && loadingError.toString()} />
+        <LoadingIndicator error={loadingError} />
       ) : (
         <>
           <PageHeader title={team.name} backTo="/taas/myteams" />
@@ -39,4 +40,4 @@ MyTeamsDetails.propTypes = {
   teamId: PT.string,
 };
 
-export default MyTeamsDetails;
+export default Authentication(MyTeamsDetails);
diff --git a/src/routes/MyTeamsList/index.jsx b/src/routes/MyTeamsList/index.jsx
index 8db13129..1ae10f9e 100644
--- a/src/routes/MyTeamsList/index.jsx
+++ b/src/routes/MyTeamsList/index.jsx
@@ -13,6 +13,7 @@ import { getMyTeams } from "../../services/teams";
 import TeamCard from "./components/TeamCard";
 import TeamCardGrid from "./components/TeamCardGrid";
 import LoadingIndicator from "../../components/LoadingIndicator";
+import Authentication from '../../components/Authentication'
 import { useDebounce } from "react-use";
 import { TEAMS_PER_PAGE } from "constants";
 import "./styles.module.scss";
@@ -74,7 +75,7 @@ const MyTeamsList = () => {
         <div styleName="empty">No teams found</div>
       )}
       {!myTeams ? (
-        <LoadingIndicator error={loadingError && loadingError.toString()} />
+        <LoadingIndicator error={loadingError} />
       ) : (
         <>
           <TeamCardGrid>
@@ -98,4 +99,4 @@ const MyTeamsList = () => {
   );
 };
 
-export default MyTeamsList;
+export default Authentication(MyTeamsList);
diff --git a/src/routes/PositionDetails/index.jsx b/src/routes/PositionDetails/index.jsx
index 4c30df09..8a994206 100644
--- a/src/routes/PositionDetails/index.jsx
+++ b/src/routes/PositionDetails/index.jsx
@@ -9,6 +9,7 @@ import LayoutContainer from "components/LayoutContainer";
 import LoadingIndicator from "components/LoadingIndicator";
 import PageHeader from "components/PageHeader";
 import { CANDIDATE_STATUS } from "constants";
+import Authentication from '../../components/Authentication'
 import PositionCandidates from "./components/PositionCandidates";
 import CandidatesStatusFilter from "./components/CandidatesStatusFilter";
 import { useTeamPositionsState } from "./hooks/useTeamPositionsState";
@@ -61,4 +62,4 @@ PositionDetails.propTypes = {
   positionId: PT.string,
 };
 
-export default PositionDetails;
+export default Authentication(PositionDetails);

From 32ffdd4cb822f2493ac7c0c6c93a9f5cd3f35101 Mon Sep 17 00:00:00 2001
From: yoution <zhuyan207@gmail.com>
Date: Sat, 9 Jan 2021 22:21:12 +0800
Subject: [PATCH 17/23] fix: issue #44

---
 src/components/Authentication/index.jsx   | 39 ---------------------
 src/components/LoadingIndicator/index.jsx |  4 ++-
 src/hoc/withAuthentication.js             | 42 +++++++++++++++++++++++
 src/root.component.jsx                    | 11 +++---
 src/routes/MyTeamsDetails/index.jsx       |  4 +--
 src/routes/MyTeamsList/index.jsx          |  4 +--
 src/routes/PositionDetails/index.jsx      |  4 +--
 7 files changed, 58 insertions(+), 50 deletions(-)
 delete mode 100644 src/components/Authentication/index.jsx
 create mode 100644 src/hoc/withAuthentication.js

diff --git a/src/components/Authentication/index.jsx b/src/components/Authentication/index.jsx
deleted file mode 100644
index 2335d674..00000000
--- a/src/components/Authentication/index.jsx
+++ /dev/null
@@ -1,39 +0,0 @@
-/**
- * Authentication 
- *
- * wrap component for authentication 
- */
-import React, { useCallback, useState, useEffect } from "react";
-import { getAuthUserTokens, login } from "@topcoder/micro-frontends-navbar-app";
-
-export default function Authentication(Component) {
-
-  const  AuthenticatedComponent = (props) => {
-    let [isLoggedIn, setIsLoggedIn] = useState(null);
-
-    useEffect(() => {
-      if (props.auth) {
-        getAuthUserTokens()
-          .then(({ tokenV3 }) => {
-            if (!!tokenV3) {
-              setIsLoggedIn(!!tokenV3)
-            } else {
-              login()
-            }
-          })
-      }
-    }, [props.auth]);
-
-    return (
-      <div>
-        {
-          (!props.auth || props.auth && isLoggedIn === true)
-            ? <Component { ...props}/>
-            : null
-        }
-      </div>
-    )
-  }
-
-  return AuthenticatedComponent
-}
diff --git a/src/components/LoadingIndicator/index.jsx b/src/components/LoadingIndicator/index.jsx
index 6d7e180d..96faef90 100644
--- a/src/components/LoadingIndicator/index.jsx
+++ b/src/components/LoadingIndicator/index.jsx
@@ -10,7 +10,9 @@ import "./styles.module.scss";
 
 const LoadingIndicator = ({ error }) => {
   return (
-    <div styleName="loading-indicator">{!error ? "Loading..." : _.get(error, 'response.data.message') || error}</div>
+    <div styleName="loading-indicator">
+      {!error ? "Loading..." : _.get(error, "response.data.message") || error}
+    </div>
   );
 };
 
diff --git a/src/hoc/withAuthentication.js b/src/hoc/withAuthentication.js
new file mode 100644
index 00000000..0385666b
--- /dev/null
+++ b/src/hoc/withAuthentication.js
@@ -0,0 +1,42 @@
+/**
+ * Authentication
+ *
+ * wrap component for authentication
+ */
+import React, { useCallback, useState, useEffect } from "react";
+import { getAuthUserTokens, login } from "@topcoder/micro-frontends-navbar-app";
+import LoadingIndicator from "../components/LoadingIndicator";
+
+export default function withAuthentication(Component) {
+  const AuthenticatedComponent = (props) => {
+    let [isLoggedIn, setIsLoggedIn] = useState(null);
+    let [authError, setAuthError] = useState(false);
+
+    useEffect(() => {
+      if (props.auth) {
+        getAuthUserTokens()
+          .then(({ tokenV3 }) => {
+            if (!!tokenV3) {
+              setIsLoggedIn(!!tokenV3);
+            } else {
+              login();
+            }
+          })
+          .catch((err) => {
+            setAuthError(err);
+          });
+      }
+    }, [props.auth]);
+
+    return (
+      <>
+        {authError && <LoadingIndicator error={authError.toString()} />}
+        {!props.auth || (props.auth && isLoggedIn === true) ? (
+          <Component {...props} />
+        ) : null}
+      </>
+    );
+  };
+
+  return AuthenticatedComponent;
+}
diff --git a/src/root.component.jsx b/src/root.component.jsx
index eed88770..6ae1658e 100644
--- a/src/root.component.jsx
+++ b/src/root.component.jsx
@@ -4,19 +4,22 @@ import { Router } from "@reach/router";
 import MyTeamsList from "./routes/MyTeamsList";
 import MyTeamsDetails from "./routes/MyTeamsDetails";
 import PositionDetails from "./routes/PositionDetails";
-import ReduxToastr from 'react-redux-toastr'
+import ReduxToastr from "react-redux-toastr";
 import store from "./store";
 import "./styles/main.vendor.scss";
 import styles from "./styles/main.module.scss";
 
 export default function Root() {
   return (
-    <div className={styles['topcoder-micro-frontends-teams-app']}>
+    <div className={styles["topcoder-micro-frontends-teams-app"]}>
       <Provider store={store}>
         <Router>
-          <MyTeamsList path="/taas/myteams" auth/>
+          <MyTeamsList path="/taas/myteams" auth />
           <MyTeamsDetails path="/taas/myteams/:teamId" auth />
-          <PositionDetails path="/taas/myteams/:teamId/positions/:positionId" auth/>
+          <PositionDetails
+            path="/taas/myteams/:teamId/positions/:positionId"
+            auth
+          />
         </Router>
 
         {/* Global config for Toastr popups */}
diff --git a/src/routes/MyTeamsDetails/index.jsx b/src/routes/MyTeamsDetails/index.jsx
index 444690b4..c8e258d1 100644
--- a/src/routes/MyTeamsDetails/index.jsx
+++ b/src/routes/MyTeamsDetails/index.jsx
@@ -14,7 +14,7 @@ import LoadingIndicator from "components/LoadingIndicator";
 import TeamSummary from "./components/TeamSummary";
 import TeamMembers from "./components/TeamMembers";
 import TeamPositions from "./components/TeamPositions";
-import Authentication from '../../components/Authentication'
+import withAuthentication from "../../hoc/withAuthentication";
 import { useAsync } from "react-use";
 
 const MyTeamsDetails = ({ teamId }) => {
@@ -40,4 +40,4 @@ MyTeamsDetails.propTypes = {
   teamId: PT.string,
 };
 
-export default Authentication(MyTeamsDetails);
+export default withAuthentication(MyTeamsDetails);
diff --git a/src/routes/MyTeamsList/index.jsx b/src/routes/MyTeamsList/index.jsx
index 1ae10f9e..afc92e73 100644
--- a/src/routes/MyTeamsList/index.jsx
+++ b/src/routes/MyTeamsList/index.jsx
@@ -13,7 +13,7 @@ import { getMyTeams } from "../../services/teams";
 import TeamCard from "./components/TeamCard";
 import TeamCardGrid from "./components/TeamCardGrid";
 import LoadingIndicator from "../../components/LoadingIndicator";
-import Authentication from '../../components/Authentication'
+import withAuthentication from "../../hoc/withAuthentication";
 import { useDebounce } from "react-use";
 import { TEAMS_PER_PAGE } from "constants";
 import "./styles.module.scss";
@@ -99,4 +99,4 @@ const MyTeamsList = () => {
   );
 };
 
-export default Authentication(MyTeamsList);
+export default withAuthentication(MyTeamsList);
diff --git a/src/routes/PositionDetails/index.jsx b/src/routes/PositionDetails/index.jsx
index 8a994206..cb9d3ae6 100644
--- a/src/routes/PositionDetails/index.jsx
+++ b/src/routes/PositionDetails/index.jsx
@@ -9,7 +9,7 @@ import LayoutContainer from "components/LayoutContainer";
 import LoadingIndicator from "components/LoadingIndicator";
 import PageHeader from "components/PageHeader";
 import { CANDIDATE_STATUS } from "constants";
-import Authentication from '../../components/Authentication'
+import withAuthentication from "../../hoc/withAuthentication";
 import PositionCandidates from "./components/PositionCandidates";
 import CandidatesStatusFilter from "./components/CandidatesStatusFilter";
 import { useTeamPositionsState } from "./hooks/useTeamPositionsState";
@@ -62,4 +62,4 @@ PositionDetails.propTypes = {
   positionId: PT.string,
 };
 
-export default Authentication(PositionDetails);
+export default withAuthentication(PositionDetails);

From b6ff645904794bf9aa82415f172ec328023cff9c Mon Sep 17 00:00:00 2001
From: maxceem <maxceem@gmail.com>
Date: Sun, 10 Jan 2021 11:22:05 +0200
Subject: [PATCH 18/23] refactor: remove "auth"

ref issue #44
---
 src/components/LoadingIndicator/index.jsx |  4 ++-
 src/hoc/withAuthentication.js             | 36 +++++++++++------------
 src/root.component.jsx                    |  9 ++----
 3 files changed, 23 insertions(+), 26 deletions(-)

diff --git a/src/components/LoadingIndicator/index.jsx b/src/components/LoadingIndicator/index.jsx
index 96faef90..c6668f08 100644
--- a/src/components/LoadingIndicator/index.jsx
+++ b/src/components/LoadingIndicator/index.jsx
@@ -11,7 +11,9 @@ import "./styles.module.scss";
 const LoadingIndicator = ({ error }) => {
   return (
     <div styleName="loading-indicator">
-      {!error ? "Loading..." : _.get(error, "response.data.message") || error}
+      {!error
+        ? "Loading..."
+        : _.get(error, "response.data.message", error.toString())}
     </div>
   );
 };
diff --git a/src/hoc/withAuthentication.js b/src/hoc/withAuthentication.js
index 0385666b..56a2fcec 100644
--- a/src/hoc/withAuthentication.js
+++ b/src/hoc/withAuthentication.js
@@ -3,7 +3,7 @@
  *
  * wrap component for authentication
  */
-import React, { useCallback, useState, useEffect } from "react";
+import React, { useState, useEffect } from "react";
 import { getAuthUserTokens, login } from "@topcoder/micro-frontends-navbar-app";
 import LoadingIndicator from "../components/LoadingIndicator";
 
@@ -13,27 +13,25 @@ export default function withAuthentication(Component) {
     let [authError, setAuthError] = useState(false);
 
     useEffect(() => {
-      if (props.auth) {
-        getAuthUserTokens()
-          .then(({ tokenV3 }) => {
-            if (!!tokenV3) {
-              setIsLoggedIn(!!tokenV3);
-            } else {
-              login();
-            }
-          })
-          .catch((err) => {
-            setAuthError(err);
-          });
-      }
-    }, [props.auth]);
+      getAuthUserTokens()
+        .then(({ tokenV3 }) => {
+          if (!!tokenV3) {
+            setIsLoggedIn(!!tokenV3);
+          } else {
+            login();
+          }
+        })
+        .catch(setAuthError);
+    }, []);
 
     return (
       <>
-        {authError && <LoadingIndicator error={authError.toString()} />}
-        {!props.auth || (props.auth && isLoggedIn === true) ? (
-          <Component {...props} />
-        ) : null}
+        {/* Show loading indicator until we know if user is logged-in or no.
+            In we got error during this process, show error */}
+        {isLoggedIn === null && <LoadingIndicator error={authError} />}
+
+        {/* Show component only if user is logged-in */}
+        {isLoggedIn === true ? <Component {...props} /> : null}
       </>
     );
   };
diff --git a/src/root.component.jsx b/src/root.component.jsx
index 6ae1658e..b48ea5cd 100644
--- a/src/root.component.jsx
+++ b/src/root.component.jsx
@@ -14,12 +14,9 @@ export default function Root() {
     <div className={styles["topcoder-micro-frontends-teams-app"]}>
       <Provider store={store}>
         <Router>
-          <MyTeamsList path="/taas/myteams" auth />
-          <MyTeamsDetails path="/taas/myteams/:teamId" auth />
-          <PositionDetails
-            path="/taas/myteams/:teamId/positions/:positionId"
-            auth
-          />
+          <MyTeamsList path="/taas/myteams" />
+          <MyTeamsDetails path="/taas/myteams/:teamId" />
+          <PositionDetails path="/taas/myteams/:teamId/positions/:positionId" />
         </Router>
 
         {/* Global config for Toastr popups */}

From 124d317865ab60db2c893b29ca73964a6e2e40c1 Mon Sep 17 00:00:00 2001
From: yoution <zhuyan207@gmail.com>
Date: Sun, 10 Jan 2021 19:13:22 +0800
Subject: [PATCH 19/23] fix: issue #44

---
 src/hoc/withAuthentication.js | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/src/hoc/withAuthentication.js b/src/hoc/withAuthentication.js
index 56a2fcec..785dcdcb 100644
--- a/src/hoc/withAuthentication.js
+++ b/src/hoc/withAuthentication.js
@@ -13,15 +13,20 @@ export default function withAuthentication(Component) {
     let [authError, setAuthError] = useState(false);
 
     useEffect(() => {
+      // prevent page redirecting to login page when unmount
+      let isUnmount = false;
       getAuthUserTokens()
         .then(({ tokenV3 }) => {
           if (!!tokenV3) {
             setIsLoggedIn(!!tokenV3);
-          } else {
+          } else if (!isUnmount) {
             login();
           }
         })
         .catch(setAuthError);
+      return () => {
+        isUnmount = true;
+      };
     }, []);
 
     return (

From 4a4773319e9a52d2427639edd05f8a4e42f6d4c9 Mon Sep 17 00:00:00 2001
From: maxceem <maxceem@gmail.com>
Date: Sun, 10 Jan 2021 14:24:01 +0200
Subject: [PATCH 20/23] fix: use "resume" instead of "resumeLink"

---
 .../PositionDetails/components/PositionCandidates/index.jsx   | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/routes/PositionDetails/components/PositionCandidates/index.jsx b/src/routes/PositionDetails/components/PositionCandidates/index.jsx
index 24e1248a..2a840849 100644
--- a/src/routes/PositionDetails/components/PositionCandidates/index.jsx
+++ b/src/routes/PositionDetails/components/PositionCandidates/index.jsx
@@ -174,8 +174,8 @@ const PositionCandidates = ({ position, candidateStatus, updateCandidate }) => {
                   requiredSkills={position.skills}
                   limit={7}
                 />
-                {candidate.resumeLink && (
-                  <a href={`${candidate.resumeLink}`} styleName="resume-link">
+                {candidate.resume && (
+                  <a href={`${candidate.resume}`} styleName="resume-link">
                     <IconResume />
                     Download Resume
                   </a>

From 80bf6f46717c0d4897a1899d6363a99d95eb859c Mon Sep 17 00:00:00 2001
From: maxceem <maxceem@gmail.com>
Date: Mon, 11 Jan 2021 10:30:33 +0200
Subject: [PATCH 21/23] fix: date range formatting

Cover the cases when not all dates are defined.
---
 .../components/TeamMembers/index.jsx             |  7 +++----
 .../components/TeamPositions/index.jsx           | 12 +++---------
 .../MyTeamsList/components/TeamCard/index.jsx    | 12 ++++--------
 src/utils/format.js                              | 16 ++++++++++++++++
 4 files changed, 26 insertions(+), 21 deletions(-)

diff --git a/src/routes/MyTeamsDetails/components/TeamMembers/index.jsx b/src/routes/MyTeamsDetails/components/TeamMembers/index.jsx
index 9443651c..945995fb 100644
--- a/src/routes/MyTeamsDetails/components/TeamMembers/index.jsx
+++ b/src/routes/MyTeamsDetails/components/TeamMembers/index.jsx
@@ -7,7 +7,6 @@ import React, { useState, useCallback, useMemo } from "react";
 import PT from "prop-types";
 import "./styles.module.scss";
 import _ from "lodash";
-import moment from "moment";
 import User from "components/User";
 import CardHeader from "components/CardHeader";
 // import Rating from "components/Rating";
@@ -15,8 +14,9 @@ import SkillsSummary from "components/SkillsSummary";
 import ActionsMenu from "components/ActionsMenu";
 import Button from "components/Button";
 import Pagination from "components/Pagination";
-import { DAY_FORMAT, TEAM_MEMBERS_PER_PAGE } from "constants";
+import { TEAM_MEMBERS_PER_PAGE } from "constants";
 import {
+  formatDateRange,
   formatMoney,
   formatReportIssueUrl,
   formatRequestExtensionUrl,
@@ -121,8 +121,7 @@ const TeamMembers = ({ team }) => {
                 <div styleName="table-group-first-inner">
                   <div styleName="table-cell cell-role">{member.job.title}</div>
                   <div styleName="table-cell cell-date">
-                    {moment(member.startDate).format(DAY_FORMAT)} -{" "}
-                    {moment(member.endDate).format(DAY_FORMAT)}
+                    {formatDateRange(member.startDate, member.endDate)}
                   </div>
                   <div styleName="table-cell cell-money">
                     {member.customerRate && member.customerRate > 0
diff --git a/src/routes/MyTeamsDetails/components/TeamPositions/index.jsx b/src/routes/MyTeamsDetails/components/TeamPositions/index.jsx
index 26db9e0b..f69f4f88 100644
--- a/src/routes/MyTeamsDetails/components/TeamPositions/index.jsx
+++ b/src/routes/MyTeamsDetails/components/TeamPositions/index.jsx
@@ -5,17 +5,12 @@
  */
 import React from "react";
 import PT from "prop-types";
-import moment from "moment";
 import CardHeader from "components/CardHeader";
 import SkillsList, { skillShape } from "components/SkillsList";
 import Button from "components/Button";
-import {
-  DAY_FORMAT,
-  POSITION_STATUS,
-  POSITION_STATUS_TO_TEXT,
-  RATE_TYPE,
-} from "constants";
+import { POSITION_STATUS, POSITION_STATUS_TO_TEXT, RATE_TYPE } from "constants";
 import "./styles.module.scss";
+import { formatDateRange } from "utils/format";
 
 const TeamPositions = ({ teamId, positions }) => {
   return (
@@ -33,8 +28,7 @@ const TeamPositions = ({ teamId, positions }) => {
                 </div>
                 <div styleName="table-group-first-inner">
                   <div styleName="table-cell cell-date">
-                    {moment(position.startDate).format(DAY_FORMAT)} -{" "}
-                    {moment(position.endDate).format(DAY_FORMAT)}
+                    {formatDateRange(position.startDate, position.endDate)}
                   </div>
                   <div styleName="table-cell cell-money">
                     {/* Hide rate as we don't have data for it */}
diff --git a/src/routes/MyTeamsList/components/TeamCard/index.jsx b/src/routes/MyTeamsList/components/TeamCard/index.jsx
index 6c360719..f4c9e9ac 100644
--- a/src/routes/MyTeamsList/components/TeamCard/index.jsx
+++ b/src/routes/MyTeamsList/components/TeamCard/index.jsx
@@ -14,6 +14,7 @@ import IconClock from "../../../../assets/images/icon-clock.svg";
 import IconMoney from "../../../../assets/images/icon-money.svg";
 import IconPeople from "../../../../assets/images/icon-people.svg";
 import {
+  formatDateRange,
   formatMoney,
   formatRemainingTimeForTeam,
   formatReportIssueUrl,
@@ -45,14 +46,9 @@ const TeamCard = ({ team }) => {
 
       <div styleName="data-items">
         <DataItem title="Start - End Date" icon={<IconCalendar />}>
-          {team.startDate && team.endDate ? (
-            <>
-              {moment(team.startDate).format(DAY_FORMAT)} -{" "}
-              {moment(team.endDate).format(DAY_FORMAT)}
-            </>
-          ) : (
-            <>TBD</>
-          )}
+          {team.startDate && team.endDate
+            ? formatDateRange(team.startDate, team.endDate)
+            : "TBD"}
         </DataItem>
 
         <DataItem title="Time Remaining" icon={<IconClock />}>
diff --git a/src/utils/format.js b/src/utils/format.js
index c6786d00..edf98638 100644
--- a/src/utils/format.js
+++ b/src/utils/format.js
@@ -5,6 +5,7 @@ import _ from "lodash";
 import { RATE_TYPE } from "constants";
 import { EMAIL_REPORT_ISSUE, EMAIL_REQUEST_EXTENSION } from "../../config";
 import moment from "moment";
+import { DAY_FORMAT } from "constants/";
 
 /**
  * Formats number with base word in singular or plural form depend on the number.
@@ -133,3 +134,18 @@ export const formatRequestExtensionUrl = (subject) => {
     subject
   )}`;
 };
+
+/**
+ * Form date range
+ *
+ * @param {string|Date|moment.Moment} startDate start date
+ * @param {string|Date|moment.Moment} endDate   end date
+ *
+ * @returns {string} formatted date range
+ */
+export const formatDateRange = (startDate, endDate) => {
+  const startDateStr = startDate ? moment(startDate).format(DAY_FORMAT) : "";
+  const endDateStr = endDate ? moment(endDate).format(DAY_FORMAT) : "";
+
+  return `${startDateStr} - ${endDateStr}`;
+};

From 2b58cf0765b1fb6f05852714b1ce5a8f95a99020 Mon Sep 17 00:00:00 2001
From: maxceem <maxceem@gmail.com>
Date: Mon, 11 Jan 2021 10:42:05 +0200
Subject: [PATCH 22/23] feat: redirect from "/taas" to "/taas/myteams"

ref issue #44
---
 src/root.component.jsx | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/root.component.jsx b/src/root.component.jsx
index b48ea5cd..834234fb 100644
--- a/src/root.component.jsx
+++ b/src/root.component.jsx
@@ -1,6 +1,6 @@
 import React from "react";
 import { Provider } from "react-redux";
-import { Router } from "@reach/router";
+import { Router, Redirect } from "@reach/router";
 import MyTeamsList from "./routes/MyTeamsList";
 import MyTeamsDetails from "./routes/MyTeamsDetails";
 import PositionDetails from "./routes/PositionDetails";
@@ -14,6 +14,7 @@ export default function Root() {
     <div className={styles["topcoder-micro-frontends-teams-app"]}>
       <Provider store={store}>
         <Router>
+          <Redirect from="/taas" to="/taas/myteams" exact />
           <MyTeamsList path="/taas/myteams" />
           <MyTeamsDetails path="/taas/myteams/:teamId" />
           <PositionDetails path="/taas/myteams/:teamId/positions/:positionId" />

From 3f1d9cb7c2ae8f1fdb99671cbd6234e91d20525f Mon Sep 17 00:00:00 2001
From: maxceem <maxceem@gmail.com>
Date: Mon, 11 Jan 2021 10:46:52 +0200
Subject: [PATCH 23/23] feat: open resume in a new page

---
 .../PositionDetails/components/PositionCandidates/index.jsx | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/routes/PositionDetails/components/PositionCandidates/index.jsx b/src/routes/PositionDetails/components/PositionCandidates/index.jsx
index 2a840849..6b893b39 100644
--- a/src/routes/PositionDetails/components/PositionCandidates/index.jsx
+++ b/src/routes/PositionDetails/components/PositionCandidates/index.jsx
@@ -175,7 +175,11 @@ const PositionCandidates = ({ position, candidateStatus, updateCandidate }) => {
                   limit={7}
                 />
                 {candidate.resume && (
-                  <a href={`${candidate.resume}`} styleName="resume-link">
+                  <a
+                    href={`${candidate.resume}`}
+                    styleName="resume-link"
+                    target="_blank"
+                  >
                     <IconResume />
                     Download Resume
                   </a>