diff --git a/.eslintrc b/.eslintrc index 5c36732..a998e96 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,4 +1,12 @@ { - "extends": ["react-important-stuff", "plugin:prettier/recommended"], - "parser": "babel-eslint" + "extends": [ + "eslint:recommended", + "plugin:react/recommended", + "react-important-stuff", + "plugin:prettier/recommended" + ], + "parser": "babel-eslint", + "rules": { + "no-unused-vars": "warn" + } } diff --git a/README.md b/README.md index 2529e55..fbf648e 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ This is a [single-spa](https://single-spa.js.org/) example React microapp. ## Config -For available variables config which depend on the running environment (`APPENV=dev` or `APPENV=prod`), please refer to `config/development.js` and `config/production.js`. +For available variables config which depend on the running environment (`APPENV=dev` or `APPENV=prod`), please refer to `config/dev.js` and `config/prod.js`. For application constants which don't depend on the running environment use `src/constants/index.js`. @@ -59,14 +59,14 @@ Make sure you have [Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli ## How to run Locally for Development -TaaS App is done using Single SPA micro-frontend architecture https://single-spa.js.org/. So to start it, we would also have to run Frame App and Navbar App. Here I would show the steps to run locally everything we need. +TaaS Admin App is done using Single SPA micro-frontend architecture https://single-spa.js.org/. So to start it, we would also have to run Frame App and Navbar App. Here I would show the steps to run locally everything we need. ### Local Authentication First of all, to authenticate locally we have to run a local authentication service. - Clone this repository into `taas-admin-app`. -- Inside the folder `taas-admin-app/local/auth-local` run `npm start`. +- Inside the folder `taas-admin-app/auth-local` run `npm start`. - You would need npm 5+ for it. This would start a local sever on port 5000 which could be used for local Authentication. ### Local Domain @@ -115,7 +115,7 @@ Some config files are using domain `local.topcoder-dev.com`. You can change it t cd micro-frontends-navbar-app ``` - Update in file `micro-frontends-navbar-app/blob/dev/config/dev.js` values for `ACCOUNTS_APP_CONNECTOR` and `AUTH` to `http://localhost:5000` so Navbar app which handles authentication uses our local Authentication service. + Update in file `micro-frontends-navbar-app/config/dev.js` values for `ACCOUNTS_APP_CONNECTOR` and `AUTH` to `http://localhost:5000` so Navbar app which handles authentication uses our local Authentication service. ```sh # inside folder "micro-frontends-navbar-app" run: @@ -128,31 +128,31 @@ Some config files are using domain `local.topcoder-dev.com`. You can change it t # this host navbar app as http://localhost:3001/navbar/topcoder-micro-frontends-navbar-app.js ``` -3. Run **TaaS** micro-app: +3. Run **TaaS Admin** micro-app: ```sh - # inside folder "taas-app" run: + # inside folder "taas-admin-app" run: nvm use # or make sure to use Node 10 npm i # to install dependencies npm run dev - # this host TaaS App as http://localhost:8501/taas-app/topcoder-micro-frontends-teams.js + # this hosts TaaS Admin App as http://localhost:8502/taas-admin-app/topcoder-micro-frontends-taas-admin-app.js ``` -4. Now we have to update the `micro-frontends-frame` app to show our local version of TaaS App, instead of remote one. Update file `micro-frontends-frame/config/micro-frontends-config-local.json`: +4. Now we have to update the `micro-frontends-frame` app to show our local version of TaaS Admin App, instead of remote one. Update file `micro-frontends-frame/config/micro-frontends-config-local.json`: ```js // replace line - "@topcoder/micro-frontends-teams": "https://platform.topcoder-dev.com/taas-app/topcoder-micro-frontends-teams.js", + "@topcoder/micro-frontends-taas-admin-app": "https://platform.topcoder-dev.com/taas-admin-app/topcoder-micro-frontends-taas-admin-app.js", // with line: - "@topcoder/micro-frontends-teams": "http://localhost:8501/taas-app/topcoder-micro-frontends-teams.js", + "@topcoder/micro-frontends-taas-admin-app": "http://localhost:8502/taas-admin-app/topcoder-micro-frontends-taas-admin-app.js", ``` -- Now open in the browser http://localhost:8080/taas/myteams. +- Now open in the browser http://localhost:8080/taas-admin. - If you are not logged-in yet, you should be redirected to the login page. -- If you cannot see the application and redirect doesn't happen, make sure that file "http://local.topcoder-dev.com:8501/taas-app/topcoder-micro-frontends-teams.js" is loaded successfully in the Network tab. +- If you cannot see the application and redirect doesn't happen, make sure that file "http://local.topcoder-dev.com:8502/taas-admin-app/topcoder-micro-frontends-taas-admin-app.js" is loaded successfully in the Network tab. Congratulations, you successfully run the project. If you had some issue, please, try to go through README of https://github.com/topcoder-platform/micro-frontends-frame and https://github.com/topcoder-platform/micro-frontends-navbar-app. diff --git a/babel.config.js b/babel.config.js index 676353c..0ae0dd5 100644 --- a/babel.config.js +++ b/babel.config.js @@ -46,6 +46,7 @@ module.exports = function (api) { // "module-resolver", // { // alias: { + // assets: "./src/assets", // styles: "./src/styles", // components: "./src/components", // hooks: "./src/hooks", diff --git a/config/dev.js b/config/dev.js index 2cba008..04cc1f2 100644 --- a/config/dev.js +++ b/config/dev.js @@ -1,16 +1,6 @@ module.exports = { - /** - * URL of Topcoder Community Website - */ - TOPCODER_COMMUNITY_WEBSITE_URL: "https://topcoder-dev.com", - - /** - * URL of Topcoder Connect Website - */ - CONNECT_WEBSITE_URL: "https://connect.topcoder-dev.com", - API: { V5: "https://api.topcoder-dev.com/v5", - V3: "https://api.topcoder-dev.com/v3", }, + PLATFORM_WEBSITE_URL: "https://platform.topcoder-dev.com", }; diff --git a/config/index.js b/config/index.js index 49f591b..8511e16 100644 --- a/config/index.js +++ b/config/index.js @@ -1,14 +1,10 @@ /* global process */ module.exports = (() => { - const env = process.env.APPENV || "dev"; + const appEnv = process.env.APPENV === "prod" ? "prod" : "dev"; - console.log(`APPENV: "${env}"`); + // eslint-disable-next-line no-console + console.log(`APPENV: "${appEnv}"`); - // for security reason don't let to require any arbitrary file defined in process.env - if (["prod", "dev"].indexOf(env) < 0) { - return require("./dev"); - } - - return require("./" + env); + return require(`./${appEnv}`); })(); diff --git a/config/prod.js b/config/prod.js index 21cdc37..7d0f7c4 100644 --- a/config/prod.js +++ b/config/prod.js @@ -1,16 +1,6 @@ module.exports = { - /** - * URL of Topcoder Community Website - */ - TOPCODER_COMMUNITY_WEBSITE_URL: "https://topcoder.com", - - /** - * URL of Topcoder Connect Website - */ - CONNECT_WEBSITE_URL: "https://connect.topcoder.com", - API: { V5: "https://api.topcoder.com/v5", - V3: "https://api.topcoder.com/v3", }, + PLATFORM_WEBSITE_URL: "https://platform.topcoder.com", }; diff --git a/package-lock.json b/package-lock.json index 6634b49..3cfcece 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3822,6 +3822,18 @@ "es-abstract": "^1.17.4" } }, + "array.prototype.flatmap": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.2.4.tgz", + "integrity": "sha512-r9Z0zYoxqHz60vvQbWEdXIEtCwHF0yxaWfno9qzXeNHvfyl3BZqygmGzb84dsubyaXLH4husF+NFgMSdpZhk2Q==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1", + "function-bind": "^1.1.1" + } + }, "arrify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", @@ -6419,6 +6431,47 @@ "prettier-linter-helpers": "^1.0.0" } }, + "eslint-plugin-react": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.23.2.tgz", + "integrity": "sha512-AfjgFQB+nYszudkxRkTFu0UR1zEQig0ArVMPloKhxwlwkzaw/fBiH0QWcBBhZONlXqQC51+nfqFrkn4EzHcGBw==", + "dev": true, + "requires": { + "array-includes": "^3.1.3", + "array.prototype.flatmap": "^1.2.4", + "doctrine": "^2.1.0", + "has": "^1.0.3", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.0.4", + "object.entries": "^1.1.3", + "object.fromentries": "^2.0.4", + "object.values": "^1.1.3", + "prop-types": "^15.7.2", + "resolve": "^2.0.0-next.3", + "string.prototype.matchall": "^4.0.4" + }, + "dependencies": { + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "resolve": { + "version": "2.0.0-next.3", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.3.tgz", + "integrity": "sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q==", + "dev": true, + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } + } + } + }, "eslint-plugin-react-hooks": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-2.5.1.tgz", @@ -8425,6 +8478,17 @@ "ipaddr.js": "^1.9.0" } }, + "internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + } + }, "interpret": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", @@ -13172,6 +13236,18 @@ "has": "^1.0.3" } }, + "object.fromentries": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.4.tgz", + "integrity": "sha512-EsFBshs5RUUpQEY1D4q/m59kMfz4YJvxuNCJcv/jWwOJr34EaVnG11ZrZa0UHB3wnzV1wx8m58T4hQL8IuNXlQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.2", + "has": "^1.0.3" + } + }, "object.getownpropertydescriptors": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.2.tgz", @@ -15393,6 +15469,17 @@ "dev": true, "optional": true }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, "signal-exit": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", @@ -15970,6 +16057,48 @@ "strip-ansi": "^5.1.0" } }, + "string.prototype.matchall": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.5.tgz", + "integrity": "sha512-Z5ZaXO0svs0M2xd/6By3qpeKpLKd9mO4v4q3oMEQrk8Ck4xOD5d5XeBOOjGrmVZZ/AHB1S0CgG4N5r1G9N3E2Q==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.2", + "get-intrinsic": "^1.1.1", + "has-symbols": "^1.0.2", + "internal-slot": "^1.0.3", + "regexp.prototype.flags": "^1.3.1", + "side-channel": "^1.0.4" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.3.tgz", + "integrity": "sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.2", + "is-callable": "^1.2.3", + "is-negative-zero": "^2.0.1", + "is-regex": "^1.1.3", + "is-string": "^1.0.6", + "object-inspect": "^1.10.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.4", + "string.prototype.trimstart": "^1.0.4", + "unbox-primitive": "^1.0.1" + } + } + } + }, "string.prototype.trimend": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", @@ -16744,6 +16873,17 @@ } } }, + "url-loader": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-4.1.1.tgz", + "integrity": "sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==", + "dev": true, + "requires": { + "loader-utils": "^2.0.0", + "mime-types": "^2.1.27", + "schema-utils": "^3.0.0" + } + }, "url-parse": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.1.tgz", diff --git a/package.json b/package.json index f236d9b..00665f0 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "eslint-config-prettier": "^6.7.0", "eslint-config-react-important-stuff": "^2.0.0", "eslint-plugin-prettier": "^3.1.1", + "eslint-plugin-react": "^7.23.2", "file-loader": "^6.2.0", "identity-obj-proxy": "^3.0.0", "jest": "^25.2.7", @@ -93,6 +94,7 @@ "single-spa-react": "^2.14.0", "style-loader": "^2.0.0", "systemjs-webpack-interop": "^2.1.2", + "url-loader": "^4.1.1", "webpack": "^4.41.2", "webpack-cli": "^3.3.10", "webpack-config-single-spa-react": "^1.0.3", diff --git a/server.js b/server.js index ff83f95..f0dfc56 100644 --- a/server.js +++ b/server.js @@ -22,7 +22,7 @@ app.get("/", (req, res) => { res.send("alive"); }); -const PORT = process.env.PORT || 8501; +const PORT = process.env.PORT || 8502; app.listen(PORT, () => { console.log(`App is hosted on port ${PORT}.`); // eslint-disable-line no-console }); diff --git a/src/assets/images/icon-arrow-down-small.svg b/src/assets/images/icon-arrow-down-small.svg index 02b6b6c..795e5c8 100644 --- a/src/assets/images/icon-arrow-down-small.svg +++ b/src/assets/images/icon-arrow-down-small.svg @@ -1,7 +1,19 @@ <?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 25.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - viewBox="0 0 11 7" enable-background="new 0 0 11 7" xml:space="preserve"> -<polygon id="path-1_1_" fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" points="5.5,6.9 0.1,1.5 1.5,0.1 5.5,4.1 9.5,0.1 - 10.9,1.5 "/> +<svg + version="1.1" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + x="0px" + y="0px" + viewBox="0 0 11 7" + enable-background="new 0 0 11 7" + xml:space="preserve" +> + <polygon + fill-rule="evenodd" + clip-rule="evenodd" + fill="#137D60" + points="5.5,6.9 0.1,1.5 1.5,0.1 5.5,4.1 9.5,0.1 + 10.9,1.5 " + /> </svg> diff --git a/src/assets/images/icon-arrow-down.svg b/src/assets/images/icon-arrow-down.svg index f1280a4..5618a0a 100644 --- a/src/assets/images/icon-arrow-down.svg +++ b/src/assets/images/icon-arrow-down.svg @@ -1,10 +1,22 @@ <?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 25.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - viewBox="0 0 15 9" enable-background="new 0 0 15 9" xml:space="preserve"> -<path id="arrow" fill-rule="evenodd" clip-rule="evenodd" fill="#137D60" d="M6.7914,8.2883L0.7067,2.1772 +<svg + version="1.1" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + x="0px" + y="0px" + viewBox="0 0 15 9" + enable-background="new 0 0 15 9" + xml:space="preserve" +> + <path + fill-rule="evenodd" + clip-rule="evenodd" + fill="#137D60" + d="M6.7914,8.2883L0.7067,2.1772 C0.312,1.7808,0.3011,1.1434,0.6818,0.7337C1.044,0.3439,1.6537,0.3216,2.0434,0.6838c0.0091,0.0085,0.0181,0.0171,0.0269,0.026 L7.5,6.163l0,0l5.4297-5.4533c0.3754-0.3771,0.9855-0.3784,1.3625-0.003c0.0088,0.0088,0.0175,0.0178,0.026,0.0269 c0.3808,0.4097,0.3698,1.0471-0.0249,1.4435L8.2086,8.2883C7.819,8.6797,7.1858,8.681,6.7944,8.2913 - C6.7934,8.2903,6.7924,8.2893,6.7914,8.2883z"/> + C6.7934,8.2903,6.7924,8.2893,6.7914,8.2883z" + /> </svg> diff --git a/src/assets/images/icon-arrow-left-green.png b/src/assets/images/icon-arrow-left-green.png deleted file mode 100644 index 87d176d..0000000 Binary files a/src/assets/images/icon-arrow-left-green.png and /dev/null differ diff --git a/src/assets/images/icon-arrow-left.svg b/src/assets/images/icon-arrow-left.svg index abfb337..3f7c835 100644 --- a/src/assets/images/icon-arrow-left.svg +++ b/src/assets/images/icon-arrow-left.svg @@ -1,7 +1,19 @@ <?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 25.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - viewBox="0 0 8 12" enable-background="new 0 0 8 12" xml:space="preserve"> -<polygon id="path-1_1_" fill-rule="evenodd" clip-rule="evenodd" fill="#137D60" points="6.325,11.775 7.45,10.725 2.725,6 - 7.45,1.275 6.325,0.225 0.55,6 "/> +<svg + version="1.1" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + x="0px" + y="0px" + viewBox="0 0 8 12" + enable-background="new 0 0 8 12" + xml:space="preserve" +> + <polygon + fill-rule="evenodd" + clip-rule="evenodd" + fill="#137D60" + points="6.325,11.775 7.45,10.725 2.725,6 + 7.45,1.275 6.325,0.225 0.55,6 " + /> </svg> diff --git a/src/assets/images/icon-arrow-right-green.png b/src/assets/images/icon-arrow-right-green.png deleted file mode 100644 index dfa18cc..0000000 Binary files a/src/assets/images/icon-arrow-right-green.png and /dev/null differ diff --git a/src/assets/images/icon-arrow-right.svg b/src/assets/images/icon-arrow-right.svg index 226d94e..8f6f601 100644 --- a/src/assets/images/icon-arrow-right.svg +++ b/src/assets/images/icon-arrow-right.svg @@ -1,7 +1,19 @@ <?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 25.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - viewBox="0 0 8 12" enable-background="new 0 0 8 12" xml:space="preserve"> -<polygon id="path-1_1_" fill-rule="evenodd" clip-rule="evenodd" fill="#137D60" points="1.675,11.775 0.55,10.725 5.275,6 - 0.55,1.275 1.675,0.225 7.45,6 "/> +<svg + version="1.1" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + x="0px" + y="0px" + viewBox="0 0 8 12" + enable-background="new 0 0 8 12" + xml:space="preserve" +> + <polygon + fill-rule="evenodd" + clip-rule="evenodd" + fill="#137D60" + points="1.675,11.775 0.55,10.725 5.275,6 + 0.55,1.275 1.675,0.225 7.45,6 " + /> </svg> diff --git a/src/assets/images/icon-magnifier.svg b/src/assets/images/icon-magnifier.svg index 4660b6d..06ae4d6 100644 --- a/src/assets/images/icon-magnifier.svg +++ b/src/assets/images/icon-magnifier.svg @@ -1,6 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> <svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> - <title>7A98E5AD-FECB-4DD2-9CB2-207FDF43A1DA</title> <defs> <path d="M12.7,11.2298137 C13.6,10.0372671 14.1,8.64596273 14.1,7.05590062 C14.1,3.18012422 11,0 7.1,0 C3.2,0 0,3.18012422 0,7.05590062 C0,10.931677 3.2,14.1118012 7.1,14.1118012 C8.7,14.1118012 10.2,13.6149068 11.3,12.7204969 L14.3,15.7018634 C14.5,15.9006211 14.8,16 15,16 C15.2,16 15.5,15.9006211 15.7,15.7018634 C16.1,15.3043478 16.1,14.7080745 15.7,14.310559 L12.7,11.2298137 Z M7.1,12.0248447 C4.3,12.0248447 2,9.83850932 2,7.05590062 C2,4.27329193 4.3,1.98757764 7.1,1.98757764 C9.9,1.98757764 12.2,4.27329193 12.2,7.05590062 C12.2,9.83850932 9.9,12.0248447 7.1,12.0248447 L7.1,12.0248447 Z" id="path-1"></path> </defs> @@ -18,4 +17,4 @@ </g> </g> </g> -</svg> \ No newline at end of file +</svg> diff --git a/src/assets/images/icon-menu-item-feelancers.svg b/src/assets/images/icon-menu-item-feelancers.svg index c52005a..76e32d3 100644 --- a/src/assets/images/icon-menu-item-feelancers.svg +++ b/src/assets/images/icon-menu-item-feelancers.svg @@ -1,23 +1,49 @@ <?xml version="1.0" encoding="utf-8"?> -<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve"> -<path fill="#555555" d="M4.5,9c-1.6569,0-3-1.3431-3-3s1.3431-3,3-3s3,1.3431,3,3c0,0.7956-0.3161,1.5587-0.8787,2.1213 - C6.0587,8.6839,5.2956,9,4.5,9z M4.5,4.5C3.6716,4.5,3,5.1716,3,6s0.6716,1.5,1.5,1.5S6,6.8284,6,6S5.3284,4.5,4.5,4.5z"/> -<path fill="#555555" d="M3,22.5c-0.378-0.0006-0.6967-0.282-0.744-0.657L1.588,16.5H0.75C0.3358,16.5,0,16.1642,0,15.75 +<svg + version="1.1" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + x="0px" + y="0px" + viewBox="0 0 24 24" + enable-background="new 0 0 24 24" + xml:space="preserve" +> + <path + fill="#555555" + d="M4.5,9c-1.6569,0-3-1.3431-3-3s1.3431-3,3-3s3,1.3431,3,3c0,0.7956-0.3161,1.5587-0.8787,2.1213 + C6.0587,8.6839,5.2956,9,4.5,9z M4.5,4.5C3.6716,4.5,3,5.1716,3,6s0.6716,1.5,1.5,1.5S6,6.8284,6,6S5.3284,4.5,4.5,4.5z" + /> + <path + fill="#555555" + d="M3,22.5c-0.378-0.0006-0.6967-0.282-0.744-0.657L1.588,16.5H0.75C0.3358,16.5,0,16.1642,0,15.75 V13.5C0.0028,11.0159,2.0159,9.0028,4.5,9c0.4142,0,0.75,0.3358,0.75,0.75S4.9142,10.5,4.5,10.5c-1.6569,0-3,1.3431-3,3V15h0.75 - c0.378,0.0006,0.6967,0.282,0.744,0.657L3.662,21H6c0.4142,0,0.75,0.3358,0.75,0.75S6.4142,22.5,6,22.5H3z"/> -<path fill="#555555" d="M19.5,9c-1.6569,0-3-1.3431-3-3s1.3431-3,3-3s3,1.3431,3,3 + c0.378,0.0006,0.6967,0.282,0.744,0.657L3.662,21H6c0.4142,0,0.75,0.3358,0.75,0.75S6.4142,22.5,6,22.5H3z" + /> + <path + fill="#555555" + d="M19.5,9c-1.6569,0-3-1.3431-3-3s1.3431-3,3-3s3,1.3431,3,3 c0,0.7956-0.3161,1.5587-0.8787,2.1213C21.0587,8.6839,20.2956,9,19.5,9z M19.5,4.5C18.6716,4.5,18,5.1716,18,6s0.6716,1.5,1.5,1.5 - S21,6.8284,21,6S20.3284,4.5,19.5,4.5z"/> -<path fill="#555555" d="M18,22.5c-0.4142,0-0.75-0.3358-0.75-0.75S17.5858,21,18,21h2.338l0.668-5.343 + S21,6.8284,21,6S20.3284,4.5,19.5,4.5z" + /> + <path + fill="#555555" + d="M18,22.5c-0.4142,0-0.75-0.3358-0.75-0.75S17.5858,21,18,21h2.338l0.668-5.343 c0.0473-0.375,0.366-0.6564,0.744-0.657h0.75v-1.5c0-1.6569-1.3431-3-3-3c-0.4142,0-0.75-0.3358-0.75-0.75S19.0858,9,19.5,9 c2.4841,0.0028,4.4972,2.0159,4.5,4.5v2.25c0,0.4142-0.3358,0.75-0.75,0.75h-0.838l-0.668,5.343 - C21.6967,22.218,21.378,22.4994,21,22.5H18z"/> -<path fill="#555555" d="M12,7.5c-2.0711,0-3.75-1.6789-3.75-3.75S9.9289,0,12,0s3.75,1.6789,3.75,3.75 + C21.6967,22.218,21.378,22.4994,21,22.5H18z" + /> + <path + fill="#555555" + d="M12,7.5c-2.0711,0-3.75-1.6789-3.75-3.75S9.9289,0,12,0s3.75,1.6789,3.75,3.75 C15.7478,5.8202,14.0702,7.4978,12,7.5L12,7.5z M12,1.5c-1.2426,0-2.25,1.0074-2.25,2.25S10.7574,6,12,6s2.25-1.0074,2.25-2.25 - C14.2489,2.5078,13.2422,1.5011,12,1.5L12,1.5z"/> -<path fill="#555555" d="M9.75,24C9.3629,24.0034,9.0373,23.7104,9,23.325L8.321,16.5H6.75 + C14.2489,2.5078,13.2422,1.5011,12,1.5L12,1.5z" + /> + <path + fill="#555555" + d="M9.75,24C9.3629,24.0034,9.0373,23.7104,9,23.325L8.321,16.5H6.75 C6.3358,16.5,6,16.1642,6,15.75V13.5c0-3.3137,2.6863-6,6-6s6,2.6863,6,6v2.25c0,0.4142-0.3358,0.75-0.75,0.75h-1.571L15,23.325 c-0.0373,0.3854-0.3629,0.6784-0.75,0.675H9.75z M13.571,22.5l0.683-6.825C14.2912,15.2912,14.6144,14.9987,15,15h1.5v-1.5 - c0-2.4853-2.0147-4.5-4.5-4.5s-4.5,2.0147-4.5,4.5V15H9c0.3856-0.0013,0.7088,0.2912,0.746,0.675l0.682,6.825H13.571z"/> + c0-2.4853-2.0147-4.5-4.5-4.5s-4.5,2.0147-4.5,4.5V15H9c0.3856-0.0013,0.7088,0.2912,0.746,0.675l0.682,6.825H13.571z" + /> </svg> diff --git a/src/assets/images/icon-menu-item-workperiods.svg b/src/assets/images/icon-menu-item-workperiods.svg index 045bcb3..4078d17 100644 --- a/src/assets/images/icon-menu-item-workperiods.svg +++ b/src/assets/images/icon-menu-item-workperiods.svg @@ -1,11 +1,66 @@ <?xml version="1.0" encoding="utf-8"?> -<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - viewBox="0 0 20 22" enable-background="new 0 0 20 22" xml:space="preserve"> -<path fill="none" stroke="#06D6A0" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" d="M4,3.6111 +<svg + version="1.1" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + x="0px" + y="0px" + viewBox="0 0 20 22" + enable-background="new 0 0 20 22" + xml:space="preserve" +> + <path + fill="none" + stroke="#06D6A0" + stroke-width="1.5" + stroke-linecap="round" + stroke-linejoin="round" + d="M4,3.6111 h12.2222c1.2887,0,2.3333,1.0447,2.3333,2.3333v12.2222c0,1.2887-1.0447,2.3333-2.3333,2.3333H4 - c-1.2887,0-2.3333-1.0447-2.3333-2.3333V5.9444C1.6667,4.6558,2.7113,3.6111,4,3.6111z"/> -<line fill="none" stroke="#06D6A0" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" x1="14.3333" y1="1.5" x2="14.3333" y2="5.7222"/> -<line fill="none" stroke="#06D6A0" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" x1="5.8889" y1="1.5" x2="5.8889" y2="5.7222"/> -<line fill="none" stroke="#06D6A0" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" x1="1.6667" y1="9.9444" x2="18.5556" y2="9.9444"/> -<line fill="none" stroke="#06D6A0" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" x1="8" y1="15.2222" x2="12.2222" y2="15.2222"/> + c-1.2887,0-2.3333-1.0447-2.3333-2.3333V5.9444C1.6667,4.6558,2.7113,3.6111,4,3.6111z" + /> + <line + fill="none" + stroke="#06D6A0" + stroke-width="1.5" + stroke-linecap="round" + stroke-linejoin="round" + x1="14.3333" + y1="1.5" + x2="14.3333" + y2="5.7222" + /> + <line + fill="none" + stroke="#06D6A0" + stroke-width="1.5" + stroke-linecap="round" + stroke-linejoin="round" + x1="5.8889" + y1="1.5" + x2="5.8889" + y2="5.7222" + /> + <line + fill="none" + stroke="#06D6A0" + stroke-width="1.5" + stroke-linecap="round" + stroke-linejoin="round" + x1="1.6667" + y1="9.9444" + x2="18.5556" + y2="9.9444" + /> + <line + fill="none" + stroke="#06D6A0" + stroke-width="1.5" + stroke-linecap="round" + stroke-linejoin="round" + x1="8" + y1="15.2222" + x2="12.2222" + y2="15.2222" + /> </svg> diff --git a/src/assets/images/icon-tick-white-bg-green.png b/src/assets/images/icon-tick-white-bg-green.png deleted file mode 100644 index 82c777b..0000000 Binary files a/src/assets/images/icon-tick-white-bg-green.png and /dev/null differ diff --git a/src/assets/images/icon-tick-white.svg b/src/assets/images/icon-tick-white.svg index 448b389..35d4235 100644 --- a/src/assets/images/icon-tick-white.svg +++ b/src/assets/images/icon-tick-white.svg @@ -1,6 +1,20 @@ <?xml version="1.0" encoding="utf-8"?> -<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - viewBox="0 0 13 10" enable-background="new 0 0 13 10" xml:space="preserve"> -<polyline fill="none" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" points="1.413,4.465 3.889,6.941 - 9.413,1.417 "/> +<svg + version="1.1" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + x="0px" + y="0px" + viewBox="0 0 13 10" + enable-background="new 0 0 13 10" + xml:space="preserve" +> + <polyline + fill="none" + stroke="#FFFFFF" + stroke-width="2" + stroke-linecap="round" + points="1.413,4.465 3.889,6.941 + 9.413,1.417 " + /> </svg> diff --git a/src/components/Button/index.jsx b/src/components/Button/index.jsx index 85f4a04..18f73b2 100644 --- a/src/components/Button/index.jsx +++ b/src/components/Button/index.jsx @@ -9,7 +9,7 @@ import styles from "./styles.module.scss"; * @param {Object} props component properties * @param {Object} props.children button text * @param {string} [props.className] class name added to root element - * @param {'primary'|'primary-dark'} [props.color] button color + * @param {'primary'|'primary-dark'|'primary-light'} [props.color] button color * @param {boolean} [props.isSelected] if button is selected * @param {string} [props.name] button name * @param {(e: any) => void} props.onClick function called when button is clicked @@ -58,6 +58,7 @@ Button.propTypes = { children: PT.node, className: PT.string, color: PT.oneOf(["primary"]), + isSelected: PT.bool, name: PT.string, onClick: PT.func, size: PT.oneOf(["medium"]), diff --git a/src/components/Button/styles.module.scss b/src/components/Button/styles.module.scss index c758bf3..14164c5 100644 --- a/src/components/Button/styles.module.scss +++ b/src/components/Button/styles.module.scss @@ -43,7 +43,12 @@ &.primary { border-color: $primary-color; - color: $primary-color; + color: $primary-text-color; + } + + &.primary-light { + border-color: $primary-light-color; + color: $primary-light-text-color; } &.primary-dark { @@ -62,6 +67,11 @@ background-color: $primary-color; } + &.primary-light { + border-color: $primary-light-color; + background-color: $primary-light-color; + } + &.primary-dark { border-color: $primary-dark-color; background-color: $primary-dark-color; diff --git a/src/components/Checkbox/index.jsx b/src/components/Checkbox/index.jsx index 50e55a2..28d00df 100644 --- a/src/components/Checkbox/index.jsx +++ b/src/components/Checkbox/index.jsx @@ -48,7 +48,9 @@ const Checkbox = ({ Checkbox.propTypes = { checked: PT.bool, + className: PT.string, name: PT.string.isRequired, + size: PT.oneOf(["medium", "small"]), onChange: PT.func.isRequired, option: PT.shape({ value: PT.string.isRequired, diff --git a/src/components/ContentBlock/styles.module.scss b/src/components/ContentBlock/styles.module.scss index 7cd756f..7b15d7f 100644 --- a/src/components/ContentBlock/styles.module.scss +++ b/src/components/ContentBlock/styles.module.scss @@ -1,5 +1,7 @@ @import "styles/mixins"; .container { + margin-bottom: 50px; + border-radius: 8px; background-color: #fff; } diff --git a/src/components/IconWrapper/index.jsx b/src/components/IconWrapper/index.jsx index 1e7be4f..6b99cb3 100644 --- a/src/components/IconWrapper/index.jsx +++ b/src/components/IconWrapper/index.jsx @@ -1,4 +1,5 @@ import React, { memo } from "react"; +import PT from "prop-types"; import cn from "classnames"; import styles from "./styles.module.scss"; @@ -15,4 +16,8 @@ const IconWrapper = (props) => ( <span {...props} className={cn(styles.iconWrapper, props.className)} /> ); +IconWrapper.propTypes = { + className: PT.string, +}; + export default memo(IconWrapper); diff --git a/src/components/Icons/ArrowDown/index.jsx b/src/components/Icons/ArrowDown/index.jsx index 98b3060..4afc8ff 100644 --- a/src/components/Icons/ArrowDown/index.jsx +++ b/src/components/Icons/ArrowDown/index.jsx @@ -2,16 +2,20 @@ import React from "react"; import PT from "prop-types"; import cn from "classnames"; import IconWrapper from "components/IconWrapper"; +import IconArrowDown from "../../../assets/images/icon-arrow-down.svg"; import styles from "./styles.module.scss"; /** + * Displays a wide dark green arrow pointing down. * * @param {Object} props component props * @param {string} [props.className] class name added to root element * @returns {JSX.Element} */ const ArrowDown = ({ className }) => ( - <IconWrapper className={cn(styles.arrow, className)}>{jsx}</IconWrapper> + <IconWrapper className={cn(styles.arrow, className)}> + <IconArrowDown /> + </IconWrapper> ); ArrowDown.propTypes = { @@ -19,28 +23,3 @@ ArrowDown.propTypes = { }; export default ArrowDown; - -// This JSX will never change so it's alright to create it only once. -const jsx = ( - <svg - version="1.1" - xmlns="http://www.w3.org/2000/svg" - xmlnsXlink="http://www.w3.org/1999/xlink" - x="0px" - y="0px" - viewBox="0 0 15 9" - enableBackground="new 0 0 15 9" - xmlSpace="preserve" - > - <path - fillRule="evenodd" - clipRule="evenodd" - fill="#137D60" - d="M6.7914,8.2883L0.7067,2.1772 - C0.312,1.7808,0.3011,1.1434,0.6818,0.7337C1.044,0.3439,1.6537,0.3216,2.0434,0.6838c0.0091,0.0085,0.0181,0.0171,0.0269,0.026 - L7.5,6.163l0,0l5.4297-5.4533c0.3754-0.3771,0.9855-0.3784,1.3625-0.003c0.0088,0.0088,0.0175,0.0178,0.026,0.0269 - c0.3808,0.4097,0.3698,1.0471-0.0249,1.4435L8.2086,8.2883C7.819,8.6797,7.1858,8.681,6.7944,8.2913 - C6.7934,8.2903,6.7924,8.2893,6.7914,8.2883z" - /> - </svg> -); diff --git a/src/components/Icons/ArrowLeft/index.jsx b/src/components/Icons/ArrowLeft/index.jsx index add5974..8a14d8b 100644 --- a/src/components/Icons/ArrowLeft/index.jsx +++ b/src/components/Icons/ArrowLeft/index.jsx @@ -2,16 +2,20 @@ import React from "react"; import PT from "prop-types"; import cn from "classnames"; import IconWrapper from "components/IconWrapper"; +import IconArrowLeft from "../../../assets/images/icon-arrow-left.svg"; import styles from "./styles.module.scss"; /** + * Displays a green arrow pointing to the left. * * @param {Object} props component props * @param {string} [props.className] class name added to root element * @returns {JSX.Element} */ const ArrowLeft = ({ className }) => ( - <IconWrapper className={cn(styles.arrow, className)}>{jsx}</IconWrapper> + <IconWrapper className={cn(styles.arrow, className)}> + <IconArrowLeft /> + </IconWrapper> ); ArrowLeft.propTypes = { @@ -19,25 +23,3 @@ ArrowLeft.propTypes = { }; export default ArrowLeft; - -// This JSX will never change so it's alright to create it only once. -const jsx = ( - <svg - version="1.1" - xmlns="http://www.w3.org/2000/svg" - xmlnsXlink="http://www.w3.org/1999/xlink" - x="0px" - y="0px" - viewBox="0 0 8 12" - enableBackground="new 0 0 8 12" - xmlSpace="preserve" - > - <polygon - fillRule="evenodd" - clipRule="evenodd" - fill="#137D60" - points="6.325,11.775 7.45,10.725 2.725,6 - 7.45,1.275 6.325,0.225 0.55,6 " - /> - </svg> -); diff --git a/src/components/Icons/ArrowRight/index.jsx b/src/components/Icons/ArrowRight/index.jsx index c3e5f19..0001253 100644 --- a/src/components/Icons/ArrowRight/index.jsx +++ b/src/components/Icons/ArrowRight/index.jsx @@ -2,16 +2,20 @@ import React from "react"; import PT from "prop-types"; import cn from "classnames"; import IconWrapper from "components/IconWrapper"; +import IconArrowRight from "../../../assets/images/icon-arrow-right.svg"; import styles from "./styles.module.scss"; /** + * Displays a green arrow pointing to the right. * * @param {Object} props component props * @param {string} [props.className] class name added to root element * @returns {JSX.Element} */ const ArrowRight = ({ className }) => ( - <IconWrapper className={cn(styles.arrow, className)}>{jsx}</IconWrapper> + <IconWrapper className={cn(styles.arrow, className)}> + <IconArrowRight /> + </IconWrapper> ); ArrowRight.propTypes = { @@ -19,25 +23,3 @@ ArrowRight.propTypes = { }; export default ArrowRight; - -// This JSX will never change so it's alright to create it only once. -const jsx = ( - <svg - version="1.1" - xmlns="http://www.w3.org/2000/svg" - xmlnsXlink="http://www.w3.org/1999/xlink" - x="0px" - y="0px" - viewBox="0 0 8 12" - enableBackground="new 0 0 8 12" - xmlSpace="preserve" - > - <polygon - fillRule="evenodd" - clipRule="evenodd" - fill="#137D60" - points="1.675,11.775 0.55,10.725 5.275,6 - 0.55,1.275 1.675,0.225 7.45,6 " - /> - </svg> -); diff --git a/src/components/Icons/ArrowSmall/index.jsx b/src/components/Icons/ArrowSmall/index.jsx index dc9d633..6a41ab2 100644 --- a/src/components/Icons/ArrowSmall/index.jsx +++ b/src/components/Icons/ArrowSmall/index.jsx @@ -2,14 +2,15 @@ import React from "react"; import PT from "prop-types"; import cn from "classnames"; import IconWrapper from "components/IconWrapper"; +import IconArrowDownSmall from "../../../assets/images/icon-arrow-down-small.svg"; import styles from "./styles.module.scss"; /** - * Displays small down-arrow. + * Displays a small narrow arrow pointing down or up. * * @param {Object} props component props * @param {string} [props.className] class name added to root element - * @param {() => void} props.onClick click handler + * @param {() => void} [props.onClick] click handler * @param {boolean} props.isActive whether the icon is in active state * @param {'up'|'down'} [props.direction] arrow direction * @returns {JSX.Element} @@ -21,33 +22,15 @@ const ArrowSmall = ({ className, onClick, isActive, direction = "down" }) => ( [styles.isActive]: isActive, })} > - {jsx} + <IconArrowDownSmall /> </IconWrapper> ); ArrowSmall.propTypes = { className: PT.string, + isActive: PT.bool.isRequired, + direction: PT.oneOf(["down", "up"]), + onClick: PT.func, }; export default ArrowSmall; - -const jsx = ( - <svg - version="1.1" - xmlns="http://www.w3.org/2000/svg" - xmlnsXlink="http://www.w3.org/1999/xlink" - x="0px" - y="0px" - viewBox="0 0 11 7" - enableBackground="new 0 0 11 7" - xmlSpace="preserve" - > - <polygon - fillRule="evenodd" - clipRule="evenodd" - fill="#FFFFFF" - points="5.5,6.9 0.1,1.5 1.5,0.1 5.5,4.1 9.5,0.1 - 10.9,1.5 " - /> - </svg> -); diff --git a/src/components/Icons/ArrowSmall/styles.module.scss b/src/components/Icons/ArrowSmall/styles.module.scss index df32d79..4751dd2 100644 --- a/src/components/Icons/ArrowSmall/styles.module.scss +++ b/src/components/Icons/ArrowSmall/styles.module.scss @@ -4,7 +4,9 @@ svg { display: block; width: 100%; + height: auto; + path, polygon { fill: #aaa; } @@ -12,6 +14,7 @@ &.isActive { svg { + path, polygon { fill: #000; } diff --git a/src/components/Icons/Freelancers/index.jsx b/src/components/Icons/Freelancers/index.jsx index 8880c39..62bc3a8 100644 --- a/src/components/Icons/Freelancers/index.jsx +++ b/src/components/Icons/Freelancers/index.jsx @@ -2,9 +2,11 @@ import React from "react"; import PT from "prop-types"; import cn from "classnames"; import IconWrapper from "components/IconWrapper"; +import IconFreelancers from "../../../assets/images/icon-menu-item-feelancers.svg"; import styles from "./styles.module.scss"; /** + * Displays a "people" icon used in navigation menu. * * @param {Object} props component props * @param {string} [props.className] class name added to root element @@ -15,7 +17,7 @@ const Freelancers = ({ className, isActive = false }) => ( <IconWrapper className={cn(styles.container, className, { [styles.isActive]: isActive })} > - {jsx} + <IconFreelancers /> </IconWrapper> ); @@ -25,55 +27,3 @@ Freelancers.propTypes = { }; export default Freelancers; - -// This JSX will never change so it's alright to create it only once. -const jsx = ( - <svg - version="1.1" - xmlns="http://www.w3.org/2000/svg" - xmlnsXlink="http://www.w3.org/1999/xlink" - x="0px" - y="0px" - viewBox="0 0 24 24" - enableBackground="new 0 0 24 24" - xmlSpace="preserve" - > - <path - fill="#555555" - d="M4.5,9c-1.6569,0-3-1.3431-3-3s1.3431-3,3-3s3,1.3431,3,3c0,0.7956-0.3161,1.5587-0.8787,2.1213 - C6.0587,8.6839,5.2956,9,4.5,9z M4.5,4.5C3.6716,4.5,3,5.1716,3,6s0.6716,1.5,1.5,1.5S6,6.8284,6,6S5.3284,4.5,4.5,4.5z" - /> - <path - fill="#555555" - d="M3,22.5c-0.378-0.0006-0.6967-0.282-0.744-0.657L1.588,16.5H0.75C0.3358,16.5,0,16.1642,0,15.75 - V13.5C0.0028,11.0159,2.0159,9.0028,4.5,9c0.4142,0,0.75,0.3358,0.75,0.75S4.9142,10.5,4.5,10.5c-1.6569,0-3,1.3431-3,3V15h0.75 - c0.378,0.0006,0.6967,0.282,0.744,0.657L3.662,21H6c0.4142,0,0.75,0.3358,0.75,0.75S6.4142,22.5,6,22.5H3z" - /> - <path - fill="#555555" - d="M19.5,9c-1.6569,0-3-1.3431-3-3s1.3431-3,3-3s3,1.3431,3,3 - c0,0.7956-0.3161,1.5587-0.8787,2.1213C21.0587,8.6839,20.2956,9,19.5,9z M19.5,4.5C18.6716,4.5,18,5.1716,18,6s0.6716,1.5,1.5,1.5 - S21,6.8284,21,6S20.3284,4.5,19.5,4.5z" - /> - <path - fill="#555555" - d="M18,22.5c-0.4142,0-0.75-0.3358-0.75-0.75S17.5858,21,18,21h2.338l0.668-5.343 - c0.0473-0.375,0.366-0.6564,0.744-0.657h0.75v-1.5c0-1.6569-1.3431-3-3-3c-0.4142,0-0.75-0.3358-0.75-0.75S19.0858,9,19.5,9 - c2.4841,0.0028,4.4972,2.0159,4.5,4.5v2.25c0,0.4142-0.3358,0.75-0.75,0.75h-0.838l-0.668,5.343 - C21.6967,22.218,21.378,22.4994,21,22.5H18z" - /> - <path - fill="#555555" - d="M12,7.5c-2.0711,0-3.75-1.6789-3.75-3.75S9.9289,0,12,0s3.75,1.6789,3.75,3.75 - C15.7478,5.8202,14.0702,7.4978,12,7.5L12,7.5z M12,1.5c-1.2426,0-2.25,1.0074-2.25,2.25S10.7574,6,12,6s2.25-1.0074,2.25-2.25 - C14.2489,2.5078,13.2422,1.5011,12,1.5L12,1.5z" - /> - <path - fill="#555555" - d="M9.75,24C9.3629,24.0034,9.0373,23.7104,9,23.325L8.321,16.5H6.75 - C6.3358,16.5,6,16.1642,6,15.75V13.5c0-3.3137,2.6863-6,6-6s6,2.6863,6,6v2.25c0,0.4142-0.3358,0.75-0.75,0.75h-1.571L15,23.325 - c-0.0373,0.3854-0.3629,0.6784-0.75,0.675H9.75z M13.571,22.5l0.683-6.825C14.2912,15.2912,14.6144,14.9987,15,15h1.5v-1.5 - c0-2.4853-2.0147-4.5-4.5-4.5s-4.5,2.0147-4.5,4.5V15H9c0.3856-0.0013,0.7088,0.2912,0.746,0.675l0.682,6.825H13.571z" - /> - </svg> -); diff --git a/src/components/Icons/WorkPeriods/index.jsx b/src/components/Icons/WorkPeriods/index.jsx index bb20c75..a29fe09 100644 --- a/src/components/Icons/WorkPeriods/index.jsx +++ b/src/components/Icons/WorkPeriods/index.jsx @@ -2,9 +2,11 @@ import React from "react"; import PT from "prop-types"; import cn from "classnames"; import IconWrapper from "components/IconWrapper"; +import IconWorkPeriods from "../../../assets/images/icon-menu-item-workperiods.svg"; import styles from "./styles.module.scss"; /** + * Displays a "calendar" icon used in navigation menu. * * @param {Object} props component props * @param {string} [props.className] class name added to root element @@ -15,7 +17,7 @@ const WorkPeriods = ({ className, isActive = false }) => ( <IconWrapper className={cn(styles.container, className, { [styles.isActive]: isActive })} > - {jsx} + <IconWorkPeriods /> </IconWrapper> ); @@ -25,72 +27,3 @@ WorkPeriods.propTypes = { }; export default WorkPeriods; - -// This JSX will never change so it's alright to create it only once. -const jsx = ( - <svg - version="1.1" - xmlns="http://www.w3.org/2000/svg" - xmlnsXlink="http://www.w3.org/1999/xlink" - x="0px" - y="0px" - viewBox="0 0 20 22" - enableBackground="new 0 0 20 22" - xmlSpace="preserve" - > - <path - fill="none" - stroke="#06D6A0" - strokeWidth="1.5" - strokeLinecap="round" - strokeLinejoin="round" - d="M4,3.6111 - h12.2222c1.2887,0,2.3333,1.0447,2.3333,2.3333v12.2222c0,1.2887-1.0447,2.3333-2.3333,2.3333H4 - c-1.2887,0-2.3333-1.0447-2.3333-2.3333V5.9444C1.6667,4.6558,2.7113,3.6111,4,3.6111z" - /> - <line - fill="none" - stroke="#06D6A0" - strokeWidth="1.5" - strokeLinecap="round" - strokeLinejoin="round" - x1="14.3333" - y1="1.5" - x2="14.3333" - y2="5.7222" - /> - <line - fill="none" - stroke="#06D6A0" - strokeWidth="1.5" - strokeLinecap="round" - strokeLinejoin="round" - x1="5.8889" - y1="1.5" - x2="5.8889" - y2="5.7222" - /> - <line - fill="none" - stroke="#06D6A0" - strokeWidth="1.5" - strokeLinecap="round" - strokeLinejoin="round" - x1="1.6667" - y1="9.9444" - x2="18.5556" - y2="9.9444" - /> - <line - fill="none" - stroke="#06D6A0" - strokeWidth="1.5" - strokeLinecap="round" - strokeLinejoin="round" - x1="8" - y1="15.2222" - x2="12.2222" - y2="15.2222" - /> - </svg> -); diff --git a/src/components/IntegerField/index.jsx b/src/components/IntegerField/index.jsx index 761eb46..2fde8c9 100644 --- a/src/components/IntegerField/index.jsx +++ b/src/components/IntegerField/index.jsx @@ -1,10 +1,10 @@ -import React, { useCallback } from "react"; +import React from "react"; import PT from "prop-types"; import cn from "classnames"; import styles from "./styles.module.scss"; /** - * Display input field for inputing numbers with plus and minus buttons. + * Displays input field for inputing integer numbers with plus and minus buttons. * * @param {Object} props component properties * @param {string} [props.className] class name to be added to root element @@ -39,6 +39,8 @@ const IntegerField = ({ IntegerField.propTypes = { className: PT.string, name: PT.string.isRequired, + maxValue: PT.number, + minValue: PT.string, onChange: PT.func.isRequired, value: PT.number.isRequired, }; diff --git a/src/components/LoadingIndicator/index.jsx b/src/components/LoadingIndicator/index.jsx index 694c20d..f51dbd0 100644 --- a/src/components/LoadingIndicator/index.jsx +++ b/src/components/LoadingIndicator/index.jsx @@ -1,13 +1,15 @@ -/** - * LoadingIndicator - * - * Optionally shows error. - */ import React from "react"; import get from "lodash/get"; import PT from "prop-types"; import styles from "./styles.module.scss"; +/** + * Displays "Loading..." message or an error. + * + * @param {Object} props component properties + * @param {Object} [props.error] error object + * @returns {JSX.Element} + */ const LoadingIndicator = ({ error }) => ( <div className={styles.loadingIndicator}> {!error diff --git a/src/components/NavLink/index.jsx b/src/components/NavLink/index.jsx index aed80fb..096c8bd 100644 --- a/src/components/NavLink/index.jsx +++ b/src/components/NavLink/index.jsx @@ -1,4 +1,4 @@ -import React, { useCallback } from "react"; +import React from "react"; import PT from "prop-types"; import cn from "classnames"; import { Link, useMatch } from "@reach/router"; @@ -30,10 +30,10 @@ const NavLink = ({ className, exact = false, icon: Icon, label, path }) => { ); }; -NavLink.ptopTypes = { +NavLink.propTypes = { className: PT.string, exact: PT.bool, - icon: PT.node.isRequired, + icon: PT.func.isRequired, label: PT.string.isRequired, path: PT.string.isRequired, }; diff --git a/src/components/Page/styles.module.scss b/src/components/Page/styles.module.scss index 8e72dc0..6fecdb9 100644 --- a/src/components/Page/styles.module.scss +++ b/src/components/Page/styles.module.scss @@ -1,11 +1,24 @@ @import "styles/mixins"; +@import "styles/variables"; .container { - flex: 1 0 auto; + position: relative; + z-index: 0; display: flex; flex-direction: column; + @include roboto-regular; + font-size: 14px; + line-height: (22/14); + color: $text-color; + background-color: $page-bg-color; @include desktop { flex-direction: row; } + + *, + *::before, + *::after { + box-sizing: border-box; + } } diff --git a/src/components/PageTitle/index.jsx b/src/components/PageTitle/index.jsx index 5ba2fdf..b2db721 100644 --- a/src/components/PageTitle/index.jsx +++ b/src/components/PageTitle/index.jsx @@ -17,7 +17,7 @@ const PageTitle = ({ className, text: title }) => ( PageTitle.propTypes = { className: PT.string, - title: PT.string.isRequired, + text: PT.string.isRequired, }; export default PageTitle; diff --git a/src/components/Pagination/index.jsx b/src/components/Pagination/index.jsx index 412d35a..a56209d 100644 --- a/src/components/Pagination/index.jsx +++ b/src/components/Pagination/index.jsx @@ -1,6 +1,8 @@ import React, { useCallback } from "react"; import PT from "prop-types"; import cn from "classnames"; +import IconArrowLeft from "components/Icons/ArrowLeft"; +import IconArrowRight from "components/Icons/ArrowRight"; import SelectField from "components/SelectField"; import styles from "./styles.module.scss"; import Button from "components/Button"; @@ -12,6 +14,8 @@ import Button from "components/Button"; * @param {string} [props.className] class name added to root element * @param {string} [props.pageSizeClassName] class name for page size select * @param {string} props.id id for input element + * @param {string} [props.label] label displayed to the left of page size dropdown + * @param {string} [props.pageSizeLabel] label displayed at the top of page size dropdown * @param {(v: number) => void} props.onPageNumberClick function called when page button is clicked * @param {() => void} props.onPageSizeChange function called when page size is changed * @param {Object} props.pageSizeOptions page size options object @@ -22,6 +26,8 @@ const Pagination = ({ className, pageSizeClassName, id, + label, + pageSizeLabel, onPageNumberClick, onPageSizeChange, pageSizeOptions, @@ -48,7 +54,7 @@ const Pagination = ({ size="small" value={pageNumber - 1} > - <span className={styles.iconArrowLeft} /> + <IconArrowLeft className={styles.iconArrowLeft} /> <span className={styles.buttonLabel}>Previous</span> </Button> ); @@ -78,15 +84,16 @@ const Pagination = ({ value={pageNumber + 1} > <span className={styles.buttonLabel}>Next</span> - <span className={styles.iconArrowRight} /> + <IconArrowRight className={styles.iconArrowRight} /> </Button> ); } return ( <div className={cn(styles.pagination, className)}> - <span className={styles.label}>Records/Page</span> + {label && <span className={styles.label}>{label}</span>} <SelectField id={id} + label={pageSizeLabel} className={cn(styles.pageSize, pageSizeClassName)} onChange={onPageSizeChange} options={pageSizeOptions} @@ -100,9 +107,13 @@ const Pagination = ({ Pagination.propTypes = { className: PT.string, + pageSizeClassName: PT.string, + id: PT.string.isRequired, + label: PT.string, + pageSizeLabel: PT.string, onPageNumberClick: PT.func.isRequired, onPageSizeChange: PT.func.isRequired, - options: PT.arrayOf( + pageSizeOptions: PT.arrayOf( PT.shape({ value: PT.oneOfType([PT.number, PT.string]).isRequired, label: PT.string.isRequired, diff --git a/src/components/Pagination/styles.module.scss b/src/components/Pagination/styles.module.scss index 9085bef..788ce2a 100644 --- a/src/components/Pagination/styles.module.scss +++ b/src/components/Pagination/styles.module.scss @@ -21,7 +21,7 @@ .pageButtons { position: relative; display: flex; - margin-left: auto; + margin-left: 20px; @include desktop { margin-left: 40px; @@ -30,14 +30,18 @@ .pageButton { flex: 0 0 auto; - margin-right: 10px; + margin-left: 10px; + + &:first-child { + margin-left: 0; + } } .buttonPrev { + flex: 0 0 auto; // position: absolute; // top: auto; // right: 100%; - margin-right: 10px; } .buttonNext { @@ -58,22 +62,15 @@ display: inline-block; width: 7px; height: 12px; - background-size: 7px 12px; - background-position: center; - background-repeat: no-repeat; } .iconArrowLeft { - background-image: url("./../../assets/images/icon-arrow-left-green.png"); - @include desktop { margin-right: 3px; } } .iconArrowRight { - background-image: url("./../../assets/images/icon-arrow-right-green.png"); - @include desktop { margin-left: 3px; } diff --git a/src/components/SearchField/index.jsx b/src/components/SearchField/index.jsx index 08ed45d..66b10f9 100644 --- a/src/components/SearchField/index.jsx +++ b/src/components/SearchField/index.jsx @@ -4,7 +4,7 @@ import cn from "classnames"; import styles from "./styles.module.scss"; /** - * Displays search input field field. + * Displays search input field. * * @param {Object} props component properties * @param {string} [props.className] class name added to root element @@ -25,9 +25,12 @@ const SearchField = ({ placeholder, value, }) => { - const onInputChange = useCallback((event) => { - onChange(event.target.value); - }, []); + const onInputChange = useCallback( + (event) => { + onChange(event.target.value); + }, + [onChange] + ); return ( <div className={cn(styles.container, styles[size], className)}> @@ -47,6 +50,8 @@ const SearchField = ({ SearchField.propTypes = { className: PT.string, + id: PT.string.isRequired, + size: PT.oneOf(["medium", "small"]), name: PT.string.isRequired, onChange: PT.func.isRequired, placeholder: PT.string, diff --git a/src/components/SelectField/index.jsx b/src/components/SelectField/index.jsx index 8e51d06..31d8587 100644 --- a/src/components/SelectField/index.jsx +++ b/src/components/SelectField/index.jsx @@ -6,6 +6,11 @@ import IconArrowDown from "components/Icons/ArrowDown"; import { getOptionByValue } from "utils/misc"; import styles from "./styles.module.scss"; +/** + * Custom dropdown indicator. + * + * @returns {JSX.Element} + */ const DropdownIndicator = () => ( <IconArrowDown className={styles.dropdownIndicator} /> ); @@ -18,7 +23,7 @@ const selectComponents = { DropdownIndicator, IndicatorSeparator: () => null }; * @param {Object} props component properties * @param {string} [props.className] class name to be added to root element * @param {string} props.id control's id - * @param {string} [props.label] control' label + * @param {string} [props.label] control's label * @param {(v: string) => void} props.onChange on change handler * @param {Object} props.options options for dropdown * @param {'medium'|'small'} [props.size] control's size @@ -45,9 +50,12 @@ const SelectField = ({ setIsOpen(false); }, []); - const onOptionChange = useCallback((option) => { - onChange(option.value); - }, []); + const onOptionChange = useCallback( + (option) => { + onChange(option.value); + }, + [onChange] + ); return ( <div @@ -84,6 +92,7 @@ SelectField.propTypes = { className: PT.string, id: PT.string.isRequired, label: PT.string, + size: PT.oneOf(["medium", "small"]), onChange: PT.func.isRequired, options: PT.arrayOf( PT.shape({ diff --git a/src/components/Sidebar/index.jsx b/src/components/Sidebar/index.jsx index 4c6ed80..1424f71 100644 --- a/src/components/Sidebar/index.jsx +++ b/src/components/Sidebar/index.jsx @@ -1,4 +1,4 @@ -import React, { useCallback } from "react"; +import React from "react"; import PT from "prop-types"; import cn from "classnames"; import NavMenu from "components/NavMenu"; @@ -8,7 +8,7 @@ import Freelancers from "components/Icons/Freelancers"; import { APP_BASE_PATH } from "../../constants"; /** - * Displays challenges' and gigs' menu and challenge filters. + * Positions sidebar contents like filters and displays navigation menu. * * @param {Object} props component properties * @param {Object} props.children component children @@ -16,10 +16,10 @@ import { APP_BASE_PATH } from "../../constants"; * @returns {JSX.Element} */ const Sidebar = ({ className, children }) => ( - <div className={cn(styles.container, className)}> + <aside className={cn(styles.container, className)}> <NavMenu className={styles.menu} items={NAV_ITEMS} /> {children} - </div> + </aside> ); Sidebar.propTypes = { @@ -28,7 +28,11 @@ Sidebar.propTypes = { }; const NAV_ITEMS = [ - { icon: WorkPeriods, label: "Working Periods", path: APP_BASE_PATH }, + { + icon: WorkPeriods, + label: "Working Periods", + path: `${APP_BASE_PATH}/work-periods`, + }, { icon: Freelancers, label: "Freelancers", diff --git a/src/components/Sidebar/styles.module.scss b/src/components/Sidebar/styles.module.scss index b9e07e7..fb525f7 100644 --- a/src/components/Sidebar/styles.module.scss +++ b/src/components/Sidebar/styles.module.scss @@ -3,14 +3,18 @@ .container { padding: 27px 24px; - width: $sidebar-width; background-color: #fff; @include desktop { + flex: 0 0 auto; padding: 27px 20px; + width: $sidebar-width; + min-height: calc(100vh - 60px); } } .menu { - margin-bottom: 45px; + margin-bottom: 20px; + border-bottom: 1px solid #e9e9e9; + padding-bottom: 20px; } diff --git a/src/components/SidebarSection/index.jsx b/src/components/SidebarSection/index.jsx index 64d9741..0cfaa0a 100644 --- a/src/components/SidebarSection/index.jsx +++ b/src/components/SidebarSection/index.jsx @@ -5,7 +5,7 @@ import SidebarSectionLabel from "components/SidebarSectionLabel"; import styles from "./styles.module.scss"; /** - * Displays sidebar's section. + * Displays sidebar's section with optional label. * * @param {Object} props component properties * @param {Object} props.children component children diff --git a/src/components/SidebarSectionLabel/index.jsx b/src/components/SidebarSectionLabel/index.jsx index a82915b..6bde427 100644 --- a/src/components/SidebarSectionLabel/index.jsx +++ b/src/components/SidebarSectionLabel/index.jsx @@ -4,7 +4,7 @@ import cn from "classnames"; import styles from "./styles.module.scss"; /** - * Displays sidebar label. + * Displays sidebar section's label. * * @param {Object} props component props * @param {string} [props.className] class name added to root element diff --git a/src/components/SortingControl/index.jsx b/src/components/SortingControl/index.jsx index f33ae91..812ac8b 100644 --- a/src/components/SortingControl/index.jsx +++ b/src/components/SortingControl/index.jsx @@ -48,6 +48,7 @@ const SortingControl = ({ className, onChange, sortBy, value }) => { SortingControl.propTypes = { className: PT.string, onChange: PT.func.isRequired, + sortBy: PT.string.isRequired, value: PT.oneOf([SORT_ORDER.ASC, SORT_ORDER.DESC, null, undefined]) .isRequired, }; diff --git a/src/components/TextField/index.jsx b/src/components/TextField/index.jsx deleted file mode 100644 index 4786d2f..0000000 --- a/src/components/TextField/index.jsx +++ /dev/null @@ -1,18 +0,0 @@ -import React, { useCallback } from "react"; -import styles from "./styles.module.scss"; - -const TextField = ({ className, onChange, placeholder, value }) => { - return ( - <div className={styles.textField}> - <input - type="text" - onChange={(event) => { - onChange(event.target.value); - }} - value={value} - /> - </div> - ); -}; - -export default TextField; diff --git a/src/components/TextField/styles.module.scss b/src/components/TextField/styles.module.scss deleted file mode 100644 index 1423310..0000000 --- a/src/components/TextField/styles.module.scss +++ /dev/null @@ -1,3 +0,0 @@ -.textField { - position: relative; -} diff --git a/src/components/WeekPicker/index.jsx b/src/components/WeekPicker/index.jsx index bdbc98c..8bc860b 100644 --- a/src/components/WeekPicker/index.jsx +++ b/src/components/WeekPicker/index.jsx @@ -1,4 +1,4 @@ -import React, { forwardRef, useCallback, useRef, useState } from "react"; +import React, { forwardRef, useCallback, useRef } from "react"; import PT from "prop-types"; import cn from "classnames"; import DatePicker from "react-datepicker"; @@ -9,14 +9,16 @@ import "react-datepicker/dist/react-datepicker.css"; import styles from "./styles.module.scss"; /** + * Displays a control which allows to select a specific date using dropdown calendar + * or "next" and "previous" buttons. This control is used for week selection. * * @param {Object} props - * @param {string} [props.className] - * @param {Object} props.startDate - * @param {Object} props.endDate - * @param {() => void} props.onWeekSelect - * @param {() => void} props.onNextWeekSelect - * @param {() => void} props.onPreviousWeekSelect + * @param {string} [props.className] class name to be added to root element + * @param {Object} props.startDate momentjs object describing selection's start date + * @param {Object} props.endDate momentjs object describing selection's end date + * @param {() => void} props.onWeekSelect function called on date select + * @param {() => void} props.onNextWeekSelect function called on next week button click + * @param {() => void} props.onPreviousWeekSelect function called on previous week button click * @returns {JSX.Element} */ const WeekPicker = ({ @@ -27,41 +29,46 @@ const WeekPicker = ({ onNextWeekSelect, onPreviousWeekSelect, }) => { - const onDateChange = useCallback((date) => { - onWeekSelect(date); - }, []); + const isOpenRef = useRef(false); + const wasInputClickedRef = useRef(false); - const onBtnPrevClick = useCallback( - (event) => { - event.stopPropagation(); - event.preventDefault(); - onPreviousWeekSelect(); - }, - [onPreviousWeekSelect] - ); + const onCalendarOpen = useCallback(() => { + isOpenRef.current = true; + wasInputClickedRef.current = false; + }, []); - const onBtnNextClick = useCallback( - (event) => { - event.stopPropagation(); - event.preventDefault(); - onNextWeekSelect(); - }, - [onNextWeekSelect] - ); + const onCalendarClose = useCallback(() => { + isOpenRef.current = false; + }, []); - const CustomInput = forwardRef(({ value, onClick }, ref) => ( + // @ts-ignore + const CustomInput = forwardRef(({ onClick }, ref) => ( <div ref={ref} className={styles.customInput} - onClick={onClick} + onMouseDown={() => { + if (isOpenRef.current) { + wasInputClickedRef.current = true; + } + }} + onClick={() => { + if (!isOpenRef.current && !wasInputClickedRef.current) { + onClick(); + } + wasInputClickedRef.current = false; + }} tabIndex={0} role="button" > <Button - color="primary-dark" size="small" style="circle" - onClick={onBtnPrevClick} + onClick={(event) => { + wasInputClickedRef.current = false; + event.stopPropagation(); + event.preventDefault(); + onPreviousWeekSelect(); + }} > <IconArrowLeft /> </Button> @@ -69,23 +76,34 @@ const WeekPicker = ({ {formatDateRange(startDate, endDate)} </span> <Button - color="primary-dark" size="small" style="circle" - onClick={onBtnNextClick} + onClick={(event) => { + wasInputClickedRef.current = false; + event.stopPropagation(); + event.preventDefault(); + onNextWeekSelect(); + }} > <IconArrowRight /> </Button> </div> )); + CustomInput.displayName = "CustomInput"; + CustomInput.propTypes = { + // @ts-ignore + onClick: PT.func, + }; return ( <div className={cn(styles.container, className)}> <DatePicker - selected={new Date()} + selected={startDate.toDate()} startDate={startDate.toDate()} endDate={endDate.toDate()} - onChange={onDateChange} + onChange={onWeekSelect} + onCalendarOpen={onCalendarOpen} + onCalendarClose={onCalendarClose} showMonthDropdown showYearDropdown formatWeekDay={formatWeekDay} diff --git a/src/components/WeekPicker/styles.module.scss b/src/components/WeekPicker/styles.module.scss index 8002f7f..e2a9779 100644 --- a/src/components/WeekPicker/styles.module.scss +++ b/src/components/WeekPicker/styles.module.scss @@ -14,27 +14,52 @@ 0 10px 14px 0 rgba(21, 21, 22, 0.3); } + :global(.react-datepicker-popper[data-placement^="bottom"]) { + margin-top: 0px; + } + + :global(.react-datepicker__triangle) { + display: none; + } + :global(.react-datepicker__month-container) { background: transparent; } + :global(.react-datepicker__month) { + margin: 0 7px 12px; + } + :global(.react-datepicker__day-names) { + display: flex; + margin: 8px 7px 0; background: transparent; } :global(.react-datepicker__day-name) { - width: 59px; - font-size: 16px; - line-height: 39px; + flex: 0 0 auto; + margin: 0; + width: 61px; @include roboto-medium; + font-size: 16px; + line-height: 36px; + text-align: center; background: transparent; + user-select: none; + } + + :global(.react-datepicker__week) { + display: flex; } :global(.react-datepicker__day) { - width: 59px; + flex: 0 0 auto; + margin: 0; + width: 61px; font-size: 16px; - line-height: 39px; + line-height: 44px; @include roboto-regular; + user-select: none; } :global(.react-datepicker__day--selected) { @@ -52,12 +77,13 @@ } :global(.react-datepicker__day--today) { - margin: 2px 12px; + margin: 4px 13px 5px; border-radius: 999px; + padding-top: 1px; width: 35px; height: 35px; - line-height: 35px; - background: $primary-color; + line-height: 34px; + background: $primary-light-color; color: #fff; } @@ -94,7 +120,7 @@ :global(.react-datepicker__header__dropdown) { position: relative; - padding: 12px 0; + padding: 12px 0 9px; &::after { content: ""; @@ -124,6 +150,7 @@ border: none; width: 15px; height: 9px; + background-position: center; background-repeat: no-repeat; background-size: 15px 9px; background-image: url("./../../assets/images/icon-arrow-down.svg"); @@ -176,6 +203,7 @@ background-color: #fff; cursor: pointer; @include roboto-regular; + user-select: none; &:hover { background-color: #f5f5f5; @@ -193,12 +221,23 @@ display: none; } - :global(.react-datepicker__month-container) { - float: none; + :global(.react-datepicker__navigation--years-upcoming), + :global(.react-datepicker__navigation--years-previous) { + display: inline-block; + width: 11px; + height: 7px; + background-position: center; + background-repeat: no-repeat; + background-size: 11px 7px; + background-image: url("./../../assets/images/icon-arrow-down-small.svg"); } - :global(.react-datepicker__month) { - margin-bottom: 12px; + :global(.react-datepicker__navigation--years-upcoming) { + transform: rotate(180deg); + } + + :global(.react-datepicker__month-container) { + float: none; } :global(.react-datepicker__current-month) { @@ -214,7 +253,7 @@ padding: 5px 4px; height: 40px; border-radius: 999px; - background: $primary-dark-color; + background: $primary-color; } .btnPrev { @@ -236,4 +275,5 @@ line-height: 26px; color: #fff; white-space: nowrap; + cursor: pointer; } diff --git a/src/config/development.js b/src/config/development.js deleted file mode 100644 index 04cc1f2..0000000 --- a/src/config/development.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - API: { - V5: "https://api.topcoder-dev.com/v5", - }, - PLATFORM_WEBSITE_URL: "https://platform.topcoder-dev.com", -}; diff --git a/src/config/index.js b/src/config/index.js deleted file mode 100644 index 18f4eb6..0000000 --- a/src/config/index.js +++ /dev/null @@ -1,11 +0,0 @@ -/* global process */ - -module.exports = (() => { - const appEnv = - process.env.APPENV === "production" ? "production" : "development"; - - // eslint-disable-next-line no-console - console.log(`APPENV: "${appEnv}"`); - - return require(`./${appEnv}`); -})(); diff --git a/src/config/production.js b/src/config/production.js deleted file mode 100644 index 7d0f7c4..0000000 --- a/src/config/production.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - API: { - V5: "https://api.topcoder.com/v5", - }, - PLATFORM_WEBSITE_URL: "https://platform.topcoder.com", -}; diff --git a/src/constants/index.js b/src/constants/index.js index 1fb84fd..12e064e 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -1,3 +1,7 @@ +import { PLATFORM_WEBSITE_URL } from "../../config"; + +export { PLATFORM_WEBSITE_URL }; + export const APP_BASE_PATH = "/taas-admin"; export const TAAS_BASE_PATH = "/taas/teams"; diff --git a/src/constants/workPeriods.js b/src/constants/workPeriods.js index d80a3c4..a27aded 100644 --- a/src/constants/workPeriods.js +++ b/src/constants/workPeriods.js @@ -1,5 +1,5 @@ // @ts-ignore -import { API } from "../config"; +import { API } from "../../config"; import * as API_PAYMENT_STATUS from "./workPeriods/apiPaymentStatus"; import * as API_SORT_BY from "./workPeriods/apiSortBy"; import * as SORT_BY from "./workPeriods/sortBy"; @@ -59,6 +59,7 @@ export const PAYMENT_STATUS_LABELS = { [PAYMENT_STATUS.PAID]: "Paid", [PAYMENT_STATUS.PENDING]: "Pending", [PAYMENT_STATUS.IN_PROGRESS]: "In Progress", + [PAYMENT_STATUS.UNDEFINED]: "Undefined", }; export const PAYMENT_STATUS_MAP = { @@ -71,7 +72,7 @@ export const PAYMENT_STATUS_MAP = { export const API_PAYMENT_STATUS_MAP = (function () { const obj = {}; for (let key in PAYMENT_STATUS_MAP) { - if (PAYMENT_STATUS_MAP.hasOwnProperty(key)) { + if (Object.prototype.hasOwnProperty.call(PAYMENT_STATUS_MAP, key)) { obj[PAYMENT_STATUS_MAP[key]] = key; } } diff --git a/src/hoc/withAuthentication/index.js b/src/hoc/withAuthentication/index.js index efe9063..2508017 100644 --- a/src/hoc/withAuthentication/index.js +++ b/src/hoc/withAuthentication/index.js @@ -20,7 +20,7 @@ import LoadingIndicator from "components/LoadingIndicator"; import { authUserSuccess, authUserError } from "./actions"; export default function withAuthentication(Component) { - return (props) => { + return function AuthenticatedComponent(props) { const dispatch = useDispatch(); const { isLoggedIn, authError } = useSelector((state) => state.authUser); /* @@ -33,7 +33,7 @@ export default function withAuthentication(Component) { if (!isLoggedIn) { getAuthUserTokens() .then(({ tokenV3 }) => { - if (!!tokenV3) { + if (tokenV3) { const tokenData = decodeToken(tokenV3); dispatch( authUserSuccess( @@ -50,7 +50,7 @@ export default function withAuthentication(Component) { return () => { isUnmount = true; }; - }, [isLoggedIn]); + }, [isLoggedIn, dispatch]); return ( <> diff --git a/src/root.component.jsx b/src/root.component.jsx index aaaa7c8..6be922c 100644 --- a/src/root.component.jsx +++ b/src/root.component.jsx @@ -1,7 +1,7 @@ -import React, { useEffect } from "react"; +import React, { useLayoutEffect } from "react"; import { Provider } from "react-redux"; -import { Router } from "@reach/router"; -import ReduxToastr from "react-redux-toastr"; +import { Router, Redirect } from "@reach/router"; +// import ReduxToastr from "react-redux-toastr"; import store from "store"; import { disableSidebarForRoute } from "@topcoder/micro-frontends-navbar-app"; import WorkPeriods from "routes/WorkPeriods"; @@ -10,14 +10,19 @@ import { APP_BASE_PATH } from "./constants"; import "styles/global.scss"; export default function Root() { - useEffect(() => { + useLayoutEffect(() => { disableSidebarForRoute(`${APP_BASE_PATH}/*`); }, []); return ( <Provider store={store}> <Router> - <WorkPeriods path={APP_BASE_PATH} /> + <Redirect + from={APP_BASE_PATH} + to={`${APP_BASE_PATH}/work-periods`} + exact + /> + <WorkPeriods path={`${APP_BASE_PATH}/work-periods`} /> <Freelancers path={`${APP_BASE_PATH}/freelancers`} /> </Router> {/* <ReduxToastr diff --git a/src/routes/Freelancers/index.jsx b/src/routes/Freelancers/index.jsx index 760d2e6..e0694e2 100644 --- a/src/routes/Freelancers/index.jsx +++ b/src/routes/Freelancers/index.jsx @@ -7,6 +7,11 @@ import Page from "components/Page"; import PageTitle from "components/PageTitle"; import Sidebar from "components/Sidebar"; +/** + * Displays route component for Freelancers' route. + * + * @returns {JSX.Element} + */ const Freelancers = () => ( <Page> <Sidebar></Sidebar> diff --git a/src/routes/WorkPeriods/components/PaymentStatus/index.jsx b/src/routes/WorkPeriods/components/PaymentStatus/index.jsx index 47cb067..0bb1ad3 100644 --- a/src/routes/WorkPeriods/components/PaymentStatus/index.jsx +++ b/src/routes/WorkPeriods/components/PaymentStatus/index.jsx @@ -5,10 +5,11 @@ import { formatPaymentStatus } from "utils/formatters"; import styles from "./styles.module.scss"; /** + * Displays payment status for working period. * * @param {Object} props component properties - * @param {string} [props.className] - * @param {string} props.status + * @param {string} [props.className] class name to be added to root element + * @param {string} props.status one of PAYMENT_STATUS enum constants * @returns {JSX.Element} */ const PaymentStatus = ({ className, status }) => ( diff --git a/src/routes/WorkPeriods/components/PaymentStatus/styles.module.scss b/src/routes/WorkPeriods/components/PaymentStatus/styles.module.scss index 58b449a..139f939 100644 --- a/src/routes/WorkPeriods/components/PaymentStatus/styles.module.scss +++ b/src/routes/WorkPeriods/components/PaymentStatus/styles.module.scss @@ -1,7 +1,13 @@ +@import "styles/mixins"; + .container { display: inline-block; padding: 3px 8px 1px; + @include roboto-medium; + font-size: 12px; line-height: 16px; + letter-spacing: 0.5px; + white-space: nowrap; border-radius: 5px; background: #aaa; color: #fff; @@ -24,5 +30,5 @@ } .undefined { - background: #000; + background: #2a2a2a; } diff --git a/src/routes/WorkPeriods/components/PeriodCount/index.jsx b/src/routes/WorkPeriods/components/PeriodCount/index.jsx index 01f4f19..8e6dc95 100644 --- a/src/routes/WorkPeriods/components/PeriodCount/index.jsx +++ b/src/routes/WorkPeriods/components/PeriodCount/index.jsx @@ -6,7 +6,7 @@ import { getWorkPeriodsTotalCount } from "store/selectors/workPeriods"; import styles from "./styles.module.scss"; /** - * Displays the total number of working periods. + * Displays the total number of working periods for current filters. * * @param {Object} props component properties * @param {string} [props.className] class name to be added to root element diff --git a/src/routes/WorkPeriods/components/Filters/index.jsx b/src/routes/WorkPeriods/components/PeriodFilters/index.jsx similarity index 57% rename from src/routes/WorkPeriods/components/Filters/index.jsx rename to src/routes/WorkPeriods/components/PeriodFilters/index.jsx index 17dcacf..fff1eb6 100644 --- a/src/routes/WorkPeriods/components/Filters/index.jsx +++ b/src/routes/WorkPeriods/components/PeriodFilters/index.jsx @@ -3,14 +3,14 @@ import { useDispatch, useSelector } from "react-redux"; import debounce from "lodash/debounce"; import PT from "prop-types"; import cn from "classnames"; -import SidebarSection from "components/SidebarSection"; +// import SidebarSection from "components/SidebarSection"; import Button from "components/Button"; -import CheckboxList from "components/CheckboxList"; -import { PAYMENT_STATUS } from "constants/workPeriods"; +// import CheckboxList from "components/CheckboxList"; +// import { PAYMENT_STATUS } from "constants/workPeriods"; import { getWorkPeriodsFilters } from "store/selectors/workPeriods"; import { resetWorkPeriodsFilters, - setWorkPeriodsPaymentStatuses, + // setWorkPeriodsPaymentStatuses, setWorkPeriodsUserHandle, } from "store/actions/workPeriods"; import { loadWorkPeriodsPage as loadWorkingPeriodsPage } from "store/thunks/workPeriods"; @@ -19,45 +19,52 @@ import styles from "./styles.module.scss"; import SearchField from "components/SearchField"; /** - * Displays challenges' and gigs' menu and challenge filters. + * Displays working periods' filters like user handle search control or + * payment status checkboxes. * * @param {Object} props component properties * @param {string} [props.className] optional class name added to root element * @returns {JSX.Element} */ -const Filters = ({ className }) => { +const PeriodFilters = ({ className }) => { const dispatch = useDispatch(); const filters = useSelector(getWorkPeriodsFilters); - const { paymentStatuses, userHandle } = filters; + const { /*paymentStatuses,*/ userHandle } = filters; - const onUserHandleChange = useCallback((value) => { - dispatch(setWorkPeriodsUserHandle(value)); - }, []); + const onUserHandleChange = useCallback( + (value) => { + dispatch(setWorkPeriodsUserHandle(value)); + }, + [dispatch] + ); - const onPaymentStatusesChange = useCallback((statuses) => { - dispatch(setWorkPeriodsPaymentStatuses(statuses)); - }, []); + // const onPaymentStatusesChange = useCallback( + // (statuses) => { + // dispatch(setWorkPeriodsPaymentStatuses(statuses)); + // }, + // [dispatch] + // ); const onClearFilter = useCallback(() => { dispatch(resetWorkPeriodsFilters()); - }, []); + }, [dispatch]); const loadWorkingPeriodsFirstPage = useCallback( debounce( () => { dispatch(loadWorkingPeriodsPage(1)); }, - 200, + 300, { leading: false } ), - [] + [dispatch] ); - // Load challenges' first page when any filter option changes. + // Load working periods' first page when any filter option changes. useUpdateEffect(loadWorkingPeriodsFirstPage, [filters]); return ( - <form className={styles.container} action="#"> + <form className={cn(styles.container, className)} action="#"> <div className={styles.handleSection}> <SearchField id="topcoder-handle" @@ -67,21 +74,16 @@ const Filters = ({ className }) => { value={userHandle} /> </div> - <SidebarSection label="Payment Status"> + {/* <SidebarSection label="Payment Status"> <CheckboxList name="payment_status[]" onChange={onPaymentStatusesChange} options={PAYMENT_STATUS_OPTIONS} value={paymentStatuses} /> - </SidebarSection> + </SidebarSection> */} <div className={styles.buttons}> - <Button - className={styles.button} - size="small" - color="primary-dark" - onClick={onClearFilter} - > + <Button className={styles.button} size="small" onClick={onClearFilter}> Clear Filter </Button> </div> @@ -89,14 +91,14 @@ const Filters = ({ className }) => { ); }; -Filters.propTypes = { +PeriodFilters.propTypes = { className: PT.string, }; -const PAYMENT_STATUS_OPTIONS = [ - { value: PAYMENT_STATUS.PENDING, label: "Pending" }, - { value: PAYMENT_STATUS.PAID, label: "Paid" }, - { value: PAYMENT_STATUS.IN_PROGRESS, label: "In Progress" }, -]; +// const PAYMENT_STATUS_OPTIONS = [ +// { value: PAYMENT_STATUS.PENDING, label: "Pending" }, +// { value: PAYMENT_STATUS.PAID, label: "Paid" }, +// { value: PAYMENT_STATUS.IN_PROGRESS, label: "In Progress" }, +// ]; -export default Filters; +export default PeriodFilters; diff --git a/src/routes/WorkPeriods/components/Filters/styles.module.scss b/src/routes/WorkPeriods/components/PeriodFilters/styles.module.scss similarity index 90% rename from src/routes/WorkPeriods/components/Filters/styles.module.scss rename to src/routes/WorkPeriods/components/PeriodFilters/styles.module.scss index eb51f82..c68620a 100644 --- a/src/routes/WorkPeriods/components/Filters/styles.module.scss +++ b/src/routes/WorkPeriods/components/PeriodFilters/styles.module.scss @@ -1,7 +1,7 @@ @import "styles/mixins"; .handleSection { - margin: 47px 0 26px; + margin: 0 0 26px; } .buttons { diff --git a/src/routes/WorkPeriods/components/PeriodItem/index.jsx b/src/routes/WorkPeriods/components/PeriodItem/index.jsx index cb1209d..fad0ef3 100644 --- a/src/routes/WorkPeriods/components/PeriodItem/index.jsx +++ b/src/routes/WorkPeriods/components/PeriodItem/index.jsx @@ -1,21 +1,34 @@ import React, { memo, useCallback } from "react"; import PT from "prop-types"; -import cn from "classnames"; import Checkbox from "components/Checkbox"; import IntegerField from "components/IntegerField"; import PaymentStatus from "../PaymentStatus"; import { formatUserHandleLink } from "utils/formatters"; import styles from "./styles.module.scss"; +/** + * Displays the working period data row to be used in PeriodList component. + * + * @param {Object} props component properties + * @param {boolean} props.isSelected whether the item is selected + * @param {Object} props.item object describing a working period + * @param {(v: string) => void} props.onToggle function called when working period checkbox is clicked + * @param {(v: { periodId: string, workingDays: number }) => void} props.onWorkingDaysChange + * function called when the number of working days is changed + * @returns {JSX.Element} + */ const PeriodItem = ({ isSelected, item, onToggle, onWorkingDaysChange }) => { - const onToggleItem = useCallback((event) => { - onToggle(event.target.value); - }, []); + const onToggleItem = useCallback( + (event) => { + onToggle(event.target.value); + }, + [onToggle] + ); const onDaysChange = useCallback( (workingDays) => { onWorkingDaysChange({ periodId: item.id, workingDays }); }, - [item] + [item, onWorkingDaysChange] ); return ( <tr className={styles.container}> @@ -30,14 +43,18 @@ const PeriodItem = ({ isSelected, item, onToggle, onWorkingDaysChange }) => { </td> <td className={styles.userHandle}> <span> - <a href={formatUserHandleLink(item.projectId, item.id)}> + <a + href={formatUserHandleLink(item.projectId, item.rbId)} + target="_blank" + rel="noreferrer" + > {item.userHandle} </a> </span> </td> - <td>{item.projectId}</td> - <td>{item.startDate}</td> - <td>{item.endDate}</td> + <td className={styles.teamName}>{item.projectId}</td> + <td className={styles.startDate}>{item.startDate}</td> + <td className={styles.endDate}>{item.endDate}</td> <td>{currencyFormatter.format(item.weeklyRate)}</td> <td> <PaymentStatus status={item.paymentStatus} /> @@ -61,6 +78,8 @@ PeriodItem.propTypes = { isSelected: PT.bool.isRequired, item: PT.shape({ id: PT.oneOfType([PT.number, PT.string]).isRequired, + rbId: PT.string.isRequired, + projectId: PT.oneOfType([PT.number, PT.string]).isRequired, userHandle: PT.string.isRequired, teamName: PT.oneOfType([PT.number, PT.string]).isRequired, startDate: PT.string.isRequired, @@ -69,6 +88,8 @@ PeriodItem.propTypes = { paymentStatus: PT.string.isRequired, workingDays: PT.number.isRequired, }), + onToggle: PT.func.isRequired, + onWorkingDaysChange: PT.func.isRequired, }; const currencyFormatter = new Intl.NumberFormat("en-US", { diff --git a/src/routes/WorkPeriods/components/PeriodItem/styles.module.scss b/src/routes/WorkPeriods/components/PeriodItem/styles.module.scss index b63f9c3..9cf123f 100644 --- a/src/routes/WorkPeriods/components/PeriodItem/styles.module.scss +++ b/src/routes/WorkPeriods/components/PeriodItem/styles.module.scss @@ -7,7 +7,7 @@ background: #fff; } - &:nth-child(even) { + &:nth-child(odd) { td { background: #f9f9f9; } @@ -15,13 +15,14 @@ } td.toggle { - padding: 12px 20px 12px 30px; + padding: 12px 20px 12px 15px; line-height: 15px; } .userHandle { a { @include roboto-bold; + color: #0d61bf; } span { display: block; @@ -32,8 +33,14 @@ td.toggle { } } -.teamName { +td.teamName { @include roboto-medium; + white-space: nowrap; +} + +td.startDate, +td.endDate { + white-space: nowrap; } td.workingDays { diff --git a/src/routes/WorkPeriods/components/PeriodList/index.jsx b/src/routes/WorkPeriods/components/PeriodList/index.jsx index e80b45d..5cb6f88 100644 --- a/src/routes/WorkPeriods/components/PeriodList/index.jsx +++ b/src/routes/WorkPeriods/components/PeriodList/index.jsx @@ -15,10 +15,10 @@ import { import styles from "./styles.module.scss"; /** - * Displays the main content of the challenges' page. + * Displays the list of the working periods with column headers. * * @param {Object} props component properties - * @param {string} [props.className] + * @param {string} [props.className] class name to be added to root element * @returns {JSX.Element} */ const PeriodList = ({ className }) => { @@ -26,31 +26,42 @@ const PeriodList = ({ className }) => { const periodsSelected = useSelector(getWorkPeriodsSelected); const dispatch = useDispatch(); - const onTogglePeriod = useCallback((periodId) => { - dispatch(toggleWorkPeriod(periodId)); - }, []); + const onTogglePeriod = useCallback( + (periodId) => { + dispatch(toggleWorkPeriod(periodId)); + }, + [dispatch] + ); - const onWorkingDaysChange = useCallback((payload) => { - dispatch(setWorkPeriodWorkingDays(payload)); - }, []); + const onWorkingDaysChange = useCallback( + (payload) => { + dispatch(setWorkPeriodWorkingDays(payload)); + }, + [dispatch] + ); return ( - <table className={cn(styles.container, className)}> - <thead> - <PeriodListHead /> - </thead> - <tbody> - {periods.map((period) => ( - <PeriodItem - key={period.id} - isSelected={period.id in periodsSelected} - item={period} - onToggle={onTogglePeriod} - onWorkingDaysChange={onWorkingDaysChange} - /> - ))} - </tbody> - </table> + <div className={cn(styles.container, className)}> + <table className={styles.table}> + <thead> + <PeriodListHead /> + </thead> + <tbody> + <tr> + <td colSpan={8} className={styles.listTopMargin}></td> + </tr> + {periods.map((period) => ( + <PeriodItem + key={period.id} + isSelected={period.id in periodsSelected} + item={period} + onToggle={onTogglePeriod} + onWorkingDaysChange={onWorkingDaysChange} + /> + ))} + </tbody> + </table> + </div> ); }; diff --git a/src/routes/WorkPeriods/components/PeriodList/styles.module.scss b/src/routes/WorkPeriods/components/PeriodList/styles.module.scss index 78ef9e8..9bb7222 100644 --- a/src/routes/WorkPeriods/components/PeriodList/styles.module.scss +++ b/src/routes/WorkPeriods/components/PeriodList/styles.module.scss @@ -1,5 +1,24 @@ @import "styles/mixins"; .container { + position: relative; + padding: 0 20px 0 15px; +} + +.table { width: 100%; } + +.listTopMargin { + padding: 0 0 7px; + background: #fff !important; + + &::before { + content: ""; + display: block; + height: 1px; + margin-left: -15px; + margin-right: -20px; + background: #e9e9e9; + } +} diff --git a/src/routes/WorkPeriods/components/PeriodListHead/index.jsx b/src/routes/WorkPeriods/components/PeriodListHead/index.jsx index c9b2f8f..175618b 100644 --- a/src/routes/WorkPeriods/components/PeriodListHead/index.jsx +++ b/src/routes/WorkPeriods/components/PeriodListHead/index.jsx @@ -4,7 +4,6 @@ import cn from "classnames"; import Checkbox from "components/Checkbox"; import SortingControl from "components/SortingControl"; import { SORT_BY } from "constants/workPeriods"; -import { useUpdateEffect } from "utils/hooks"; import { getWorkPeriodsIsSelectedVisible, getWorkPeriodsSorting, @@ -13,27 +12,35 @@ import { setWorkPeriodsSorting, toggleWorkingPeriodsVisible, } from "store/actions/workPeriods"; -import { loadWorkPeriodsPage } from "store/thunks/workPeriods"; import styles from "./styles.module.scss"; +/** + * Displays working period list column heads with sorting controls to be used + * in PeriodList component. + * + * @returns {JSX.Element} + */ const PeriodListHead = () => { const sorting = useSelector(getWorkPeriodsSorting); const isSelectedVisible = useSelector(getWorkPeriodsIsSelectedVisible); const dispatch = useDispatch(); const { criteria, order } = sorting; - const onSortingChange = useCallback((sorting) => { - dispatch(setWorkPeriodsSorting(sorting)); - }, []); + const onSortingChange = useCallback( + (sorting) => { + dispatch(setWorkPeriodsSorting(sorting)); + }, + [dispatch] + ); const onToggleVisible = useCallback(() => { dispatch(toggleWorkingPeriodsVisible()); - }, []); + }, [dispatch]); return ( <tr className={styles.container}> <th> - <div className={styles.colHeadSelect}> + <div className={styles.colHead}> <Checkbox size="small" name={"visible_periods_selected"} diff --git a/src/routes/WorkPeriods/components/PeriodListHead/styles.module.scss b/src/routes/WorkPeriods/components/PeriodListHead/styles.module.scss index 3a2e9d1..5ba0b69 100644 --- a/src/routes/WorkPeriods/components/PeriodListHead/styles.module.scss +++ b/src/routes/WorkPeriods/components/PeriodListHead/styles.module.scss @@ -5,24 +5,52 @@ text-align: left; background: #f4f4f4; + &:first-child, &:last-child { .colHead { - padding-left: 10px; - padding-right: 10px; + position: relative; + + &::before { + content: ""; + display: block; + position: absolute; + top: 0; + bottom: 0; + background: #f4f4f4; + } } } - } -} -.colHeadSelect { - padding: 9px 20px 9px 30px; + &:first-child { + .colHead { + padding: 9px 20px 9px 15px; + + &::before { + left: -15px; + width: 15px; + } + } + } + + &:last-child { + .colHead { + padding: 12px 10px; + + &::before { + right: -20px; + width: 20px; + } + } + } + } } .colHead { display: flex; justify-content: flex-start; align-items: center; - padding: 9px 17px; + padding: 12px 17px; + height: 40px; } .colLabel { diff --git a/src/routes/WorkPeriods/components/PeriodWeekPicker/index.jsx b/src/routes/WorkPeriods/components/PeriodWeekPicker/index.jsx index ca36054..09dbec4 100644 --- a/src/routes/WorkPeriods/components/PeriodWeekPicker/index.jsx +++ b/src/routes/WorkPeriods/components/PeriodWeekPicker/index.jsx @@ -1,28 +1,36 @@ -/* eslint-disable react-hooks/exhaustive-deps */ import React, { useCallback } from "react"; import { useSelector, useDispatch } from "react-redux"; -import cn from "classnames"; +import PT from "prop-types"; import moment from "moment"; import WeekPicker from "components/WeekPicker"; import { getWorkPeriodsDateRange } from "store/selectors/workPeriods"; -import styles from "./styles.module.scss"; import { setWorkPeriodsDateRange } from "store/actions/workPeriods"; +/** + * Displays working periods' week picker. + * + * @param {Object} props component properties + * @param {string} [props.className] class name to be added to root element + * @returns {JSX.Element} + */ const PeriodWeekPicker = ({ className }) => { const [startDate, endDate] = useSelector(getWorkPeriodsDateRange); const dispatch = useDispatch(); - const onWeekSelect = useCallback((date) => { - dispatch(setWorkPeriodsDateRange(moment(date))); - }, []); + const onWeekSelect = useCallback( + (date) => { + dispatch(setWorkPeriodsDateRange(moment(date))); + }, + [dispatch] + ); const onNextWeekSelect = useCallback(() => { dispatch(setWorkPeriodsDateRange(startDate.clone().add(1, "week"))); - }, [startDate]); + }, [startDate, dispatch]); const onPreviousWeekSelect = useCallback(() => { dispatch(setWorkPeriodsDateRange(startDate.clone().add(-1, "week"))); - }, [startDate]); + }, [startDate, dispatch]); return ( <WeekPicker @@ -36,4 +44,8 @@ const PeriodWeekPicker = ({ className }) => { ); }; +PeriodWeekPicker.propTypes = { + className: PT.string, +}; + export default PeriodWeekPicker; diff --git a/src/routes/WorkPeriods/components/Periods/index.jsx b/src/routes/WorkPeriods/components/Periods/index.jsx index d35225e..3f971da 100644 --- a/src/routes/WorkPeriods/components/Periods/index.jsx +++ b/src/routes/WorkPeriods/components/Periods/index.jsx @@ -5,23 +5,34 @@ import PeriodList from "../PeriodList"; import { getWorkPeriodsError, getWorkPeriodsIsLoading, + getWorkPeriodsPagination, getWorkPeriodsSorting, - getWorkPeriodsTotalCount, } from "store/selectors/workPeriods"; import { loadWorkPeriodsPage } from "store/thunks/workPeriods"; +import { useUpdateEffect } from "utils/hooks"; +/** + * Displays working periods' list or a "Loading..." message or an error message. + * + * @returns {JSX.Element} + */ const Periods = () => { + const pagination = useSelector(getWorkPeriodsPagination); const sorting = useSelector(getWorkPeriodsSorting); - const count = useSelector(getWorkPeriodsTotalCount); const error = useSelector(getWorkPeriodsError); const isLoading = useSelector(getWorkPeriodsIsLoading); const dispatch = useDispatch(); // Load working periods' first page once when page loads and then - // only if sorting changes. + // only if page size or sorting changes. useEffect(() => { dispatch(loadWorkPeriodsPage(1)); - }, [sorting]); + }, [dispatch, pagination.pageSize, sorting]); + + // Load working periods' new page if page number changes. + useUpdateEffect(() => { + dispatch(loadWorkPeriodsPage()); + }, [dispatch, pagination.pageNumber]); return ( <> @@ -30,7 +41,7 @@ const Periods = () => { {!isLoading && error && ( <ContentMessage type="error">{error}</ContentMessage> )} - {!isLoading && !error && !count && ( + {!isLoading && !error && !pagination.totalCount && ( <ContentMessage>No resource bookings found.</ContentMessage> )} </> diff --git a/src/routes/WorkPeriods/components/PeriodsPagination/index.jsx b/src/routes/WorkPeriods/components/PeriodsPagination/index.jsx index 40e7175..cec8594 100644 --- a/src/routes/WorkPeriods/components/PeriodsPagination/index.jsx +++ b/src/routes/WorkPeriods/components/PeriodsPagination/index.jsx @@ -1,5 +1,5 @@ -/* eslint-disable react-hooks/exhaustive-deps */ import React, { useCallback } from "react"; +import PT from "prop-types"; import cn from "classnames"; import { useDispatch, useSelector } from "react-redux"; import Pagination from "components/Pagination"; @@ -8,12 +8,10 @@ import { setWorkPeriodsPageNumber, setWorkPeriodsPageSize, } from "store/actions/workPeriods"; -import { loadWorkPeriodsPage } from "store/thunks/workPeriods"; -import { useUpdateEffect } from "utils/hooks"; import styles from "./styles.module.scss"; /** - * Displays challenges' pagination and a menu to choose page size. + * Displays working periods' pagination and a menu to choose page size. * * @param {Object} props component properties * @param {string} [props.className] class name added to root element @@ -24,31 +22,24 @@ const PeriodsPagination = ({ className, id }) => { const pagination = useSelector(getWorkPeriodsPagination); const dispatch = useDispatch(); - const onPageNumberClick = useCallback((pageNumber) => { - dispatch(setWorkPeriodsPageNumber(+pageNumber)); - }, []); - - const onPageSizeChange = useCallback((pageSize) => { - dispatch(setWorkPeriodsPageSize(+pageSize)); - }, []); - - const loadWorkPeriodsFirstPage = useCallback(() => { - dispatch(loadWorkPeriodsPage(1)); - }, []); - - const loadWorkPeriodsNewPage = useCallback(() => { - dispatch(loadWorkPeriodsPage()); - }, []); - - // Load challenges' first page if page size changes. - useUpdateEffect(loadWorkPeriodsFirstPage, [pagination.pageSize]); + const onPageNumberClick = useCallback( + (pageNumber) => { + dispatch(setWorkPeriodsPageNumber(+pageNumber)); + }, + [dispatch] + ); - // Load challenges' new page if page number changes. - useUpdateEffect(loadWorkPeriodsNewPage, [pagination.pageNumber]); + const onPageSizeChange = useCallback( + (pageSize) => { + dispatch(setWorkPeriodsPageSize(+pageSize)); + }, + [dispatch] + ); return ( <Pagination id={id} + label={"Records/Page"} className={cn(styles.pagination, className)} pageSizeClassName={styles.pageSize} pagination={pagination} @@ -59,6 +50,11 @@ const PeriodsPagination = ({ className, id }) => { ); }; +PeriodsPagination.propTypes = { + className: PT.string, + id: PT.string.isRequired, +}; + export default PeriodsPagination; const PAGE_SIZE_OPTIONS = [ diff --git a/src/routes/WorkPeriods/index.jsx b/src/routes/WorkPeriods/index.jsx index 4458380..3bb972f 100644 --- a/src/routes/WorkPeriods/index.jsx +++ b/src/routes/WorkPeriods/index.jsx @@ -7,22 +7,27 @@ import ContentBlock from "components/ContentBlock"; import Button from "components/Button"; import Page from "components/Page"; import PageTitle from "components/PageTitle"; -import Filters from "./components/Filters"; +import PeriodFilters from "./components/PeriodFilters"; import Periods from "./components/Periods"; import PeriodCount from "./components/PeriodCount"; import PeriodsPagination from "./components/PeriodsPagination"; import styles from "./styles.module.scss"; import PeriodWeekPicker from "./components/PeriodWeekPicker"; +/** + * Displays route component for Working Days' route. + * + * @returns {JSX.Element} + */ const WorkPeriods = () => ( <Page className={styles.container}> <Sidebar> - <Filters /> + <PeriodFilters /> </Sidebar> <Content> <ContentHeader className={styles.contentHeader}> <PageTitle text="Working Periods" /> - <Button color="primary-dark" variant="contained" onClick={() => {}}> + <Button variant="contained" onClick={() => {}}> Process Day </Button> </ContentHeader> @@ -31,7 +36,7 @@ const WorkPeriods = () => ( <PeriodCount className={styles.periodCount} /> <PeriodWeekPicker className={styles.periodWeekPicker} /> <PeriodsPagination - className={styles.periodPagination} + className={styles.periodsPagination} id="periods-pagination-top" /> </div> diff --git a/src/routes/WorkPeriods/styles.module.scss b/src/routes/WorkPeriods/styles.module.scss index 33a3210..bedfd27 100644 --- a/src/routes/WorkPeriods/styles.module.scss +++ b/src/routes/WorkPeriods/styles.module.scss @@ -22,9 +22,12 @@ } .periodCount { - } .periodWeekPicker { margin-left: 40px; } + +.periodsPagination { + margin-left: 40px; +} diff --git a/src/store/reducers/workPeriods.js b/src/store/reducers/workPeriods.js index 019f079..25b1089 100644 --- a/src/store/reducers/workPeriods.js +++ b/src/store/reducers/workPeriods.js @@ -165,7 +165,10 @@ const actionHandlers = { for (let i = 0, len = oldPeriods.length; i < len; i++) { let period = oldPeriods[i]; if (period.id === periodId) { - period = { ...period, workingDays: Math.max(workingDays, 0) }; + period = { + ...period, + workingDays: Math.min(Math.max(workingDays, 0), 5), + }; } periods.push(period); } @@ -175,16 +178,22 @@ const actionHandlers = { }; }, [ACTION_TYPE.WP_TOGGLE_PERIOD]: (state, periodId) => { + let isSelectedPeriodsAll = state.isSelectedPeriodsAll; + let isSelectedPeriodsVisible = state.isSelectedPeriodsVisible; const periodsSelected = { ...state.periodsSelected }; const isSelected = !periodsSelected[periodId]; if (isSelected) { periodsSelected[periodId] = true; } else { + isSelectedPeriodsAll = false; + isSelectedPeriodsVisible = false; delete periodsSelected[periodId]; } return { ...state, periodsSelected, + isSelectedPeriodsAll, + isSelectedPeriodsVisible, }; }, [ACTION_TYPE.WP_TOGGLE_PERIODS_ALL]: (state) => { diff --git a/src/store/thunks/workPeriods.js b/src/store/thunks/workPeriods.js index fe80837..cd2b1a7 100644 --- a/src/store/thunks/workPeriods.js +++ b/src/store/thunks/workPeriods.js @@ -1,5 +1,4 @@ import axios from "axios"; -import moment from "moment"; import * as actions from "store/actions/workPeriods"; import * as selectors from "store/selectors/workPeriods"; import * as services from "services/workPeriods"; @@ -16,13 +15,12 @@ import { replaceItems, } from "utils/misc"; import { normalizePeriodItems } from "utils/workPeriods"; -import { USER_HANDLE } from "constants/workPeriods/apiSortBy"; /** - * Thunk that loads the specified challenges' page. If page number is not + * Thunk that loads the specified working periods' page. If page number is not * provided the current page number from current state is used. All relevant - * challenge parameters are loaded from the current state to construct a request - * query. + * working period filters are loaded from the current state to construct + * a request query. * * @param {number} [pageNumber] page number to load * @returns {function} @@ -44,7 +42,7 @@ export const loadWorkPeriodsPage = const sortOrder = sorting.order; const sortBy = SORT_BY_MAP[sorting.criteria] || API_SORT_BY.USER_HANDLE; - const [startDate, endDate] = filters.dateRange; + const [startDate] = filters.dateRange; const paymentStatuses = replaceItems( Object.keys(filters.paymentStatuses), PAYMENT_STATUS_MAP @@ -68,19 +66,19 @@ export const loadWorkPeriodsPage = // paymentStatuses, }); dispatch(actions.loadWorkPeriodsPagePending(cancelSource, pageNumber)); - let totalCount, periods, pageCount, pageSize; + let totalCount, periods, pageCount; try { const response = await promise; - ({ totalCount, pageNumber, pageCount, pageSize } = + ({ totalCount, pageNumber, pageCount } = extractResponsePagination(response)); const data = extractResponseData(response); periods = normalizePeriodItems(data); } catch (error) { + // If request was cancelled by the next call to loadWorkPeriodsPage + // there's nothing more to do. if (!axios.isCancel(error)) { dispatch(actions.loadWorkPeriodsPageError(error.toString())); } - // If request was cancelled by the next call to loadWorkPeriodsPage - // there's nothing more to do. return; } dispatch( diff --git a/src/styles/global.scss b/src/styles/global.scss index 441dd50..9a0cbff 100644 --- a/src/styles/global.scss +++ b/src/styles/global.scss @@ -1,4 +1 @@ -// @import 'fonts'; -@import "normalize"; -@import "layout"; @import "toastr"; diff --git a/src/styles/layout.scss b/src/styles/layout.scss deleted file mode 100644 index 66db9c3..0000000 --- a/src/styles/layout.scss +++ /dev/null @@ -1,31 +0,0 @@ -html, -body { - height: 100%; -} - -body { - display: flex; - flex-direction: column; - line-height: (22/14); - overflow-x: auto; - overflow-y: scroll; - - > nav { - flex: 0 0 auto; - } -} - -#single-spa-main { - flex: 1 0 auto; - display: flex; - - > div { - flex: 1 0 auto; - display: flex; - - > div:first-child { - flex: 1 0 auto; - display: flex; - } - } -} diff --git a/src/styles/normalize.scss b/src/styles/normalize.scss deleted file mode 100644 index e47847f..0000000 --- a/src/styles/normalize.scss +++ /dev/null @@ -1,23 +0,0 @@ -@import "mixins"; -@import "variables"; - -html { - background-color: $page-bg-color !important; -} - -#single-spa-main *, -#single-spa-main *::before, -#single-spa-main *::after { - box-sizing: border-box; -} - -#single-spa-main { - @include roboto-regular; - font-size: 14px; - line-height: (22/14); - color: $text-color; -} - -strong { - font-weight: 700; -} diff --git a/src/styles/variables/_colors.scss b/src/styles/variables/_colors.scss index 19030eb..8a9b503 100644 --- a/src/styles/variables/_colors.scss +++ b/src/styles/variables/_colors.scss @@ -1,8 +1,11 @@ -$primary-color: #0ab88a; -$primary-dark-color: #137d60; -$primary-dark-text-color: #229174; +$primary-color: #137d60; +$primary-text-color: #229174; +$primary-light-color: #0ab88a; +$primary-light-text-color: #0ab88a; // currently not used, cen be changed +$primary-dark-color: #137d60; // currently not used, cen be changed +$primary-dark-text-color: #137d60; // currently not used, cen be changed $text-color: #2a2a2a; $page-bg-color: #f4f5f6; $control-border-color: #aaa; -$checkbox-bg-color: $primary-color; +$checkbox-bg-color: $primary-light-color; diff --git a/src/topcoder-micro-frontends-taas-admin-app.js b/src/topcoder-micro-frontends-taas-admin-app.js index ae388ad..948ab39 100644 --- a/src/topcoder-micro-frontends-taas-admin-app.js +++ b/src/topcoder-micro-frontends-taas-admin-app.js @@ -8,7 +8,7 @@ const lifecycles = singleSpaReact({ React, ReactDOM, rootComponent: Root, - errorBoundary(err, info, props) { + errorBoundary(/*err, info, props*/) { // Customize the root error boundary for your microfrontend here. return null; }, diff --git a/src/utils/formatters.js b/src/utils/formatters.js index f5d3ecb..ecad6ab 100644 --- a/src/utils/formatters.js +++ b/src/utils/formatters.js @@ -1,22 +1,26 @@ // @ts-ignore -import { PLATFORM_WEBSITE_URL } from "../config"; -import { TAAS_BASE_PATH } from "../constants"; +import { PAYMENT_STATUS_LABELS } from "constants/workPeriods"; +import { PLATFORM_WEBSITE_URL, TAAS_BASE_PATH } from "../constants"; const rxWhitespace = /\s+/; /** * Formats payment status. * - * @param {string} status payment status + * @param {string} status payment status as defined by PAYMENT_STATUS enum constant * @returns {string} */ export function formatPaymentStatus(status) { - let words = status.split(rxWhitespace); - for (let i = 0, len = words.length; i < len; i++) { - let word = words[i]; - words[i] = word.slice(0, 1).toUpperCase() + word.slice(1).toLowerCase(); + let paymentStatus = PAYMENT_STATUS_LABELS[status]; + if (!paymentStatus) { + let words = status.split(rxWhitespace); + for (let i = 0, len = words.length; i < len; i++) { + let word = words[i]; + words[i] = word.slice(0, 1).toUpperCase() + word.slice(1).toLowerCase(); + } + paymentStatus = words.join(" "); } - return words.join(" "); + return paymentStatus; } /** diff --git a/src/utils/hooks.js b/src/utils/hooks.js index 8200488..82e138f 100644 --- a/src/utils/hooks.js +++ b/src/utils/hooks.js @@ -16,5 +16,5 @@ export const useUpdateEffect = (effect, deps) => { isMountedRef.current = true; } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [effect, ...deps]); + }, deps); }; diff --git a/src/utils/misc.js b/src/utils/misc.js index 0031391..f931c62 100644 --- a/src/utils/misc.js +++ b/src/utils/misc.js @@ -58,7 +58,7 @@ export function replaceItems(array, map) { export function updateOptionMap(oldOptions, newOptions) { oldOptions = { ...oldOptions }; for (let key in newOptions) { - if (newOptions.hasOwnProperty(key)) { + if (Object.prototype.hasOwnProperty.call(newOptions, key)) { let value = newOptions[key]; if (value) { oldOptions[key] = true; diff --git a/src/utils/workPeriods.js b/src/utils/workPeriods.js index fecca9a..d09f3b0 100644 --- a/src/utils/workPeriods.js +++ b/src/utils/workPeriods.js @@ -24,7 +24,7 @@ export function normalizePeriodItems(items) { paymentStatus: paymentStatus ? API_PAYMENT_STATUS_MAP[paymentStatus] || paymentStatus.toUpperCase() : PAYMENT_STATUS.UNDEFINED, - workingDays: daysWorked === null ? 5 : +daysWorked || 5, + workingDays: daysWorked === null ? 5 : +daysWorked || 0, }); } return periods; diff --git a/webpack.config.js b/webpack.config.js index 64fb056..afbbf1b 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -63,18 +63,21 @@ module.exports = (webpackConfigEnv) => { }, ], }, - // { - // test: /\.svg$/, - // exclude: /node_modules/, - // use: { - // loader: "babel-loader", - // }, - // }, + { + test: /\.svg$/, + exclude: /node_modules/, + use: { + loader: "babel-loader", + }, + }, { test: /\.(gif|jpg|png|svg)$/, include: /.*assets[/\\]images.+/, use: { - loader: "file-loader", + loader: "url-loader", + options: { + limit: 4096, + }, }, }, ],